diff --git a/components/Layout.tsx b/components/Layout.tsx index c426737..cdafb77 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -1,12 +1,7 @@ import { MDXProvider } from "@mdx-js/react"; -import Head from "next/head"; -import { useState } from "react"; import React from "react"; -import { GlobalHotKeys } from "react-hotkeys"; import slugify from "../lib/slugify"; -import Footer from "./Footer"; -import Header from "./Header"; import { A, Blockquote, @@ -23,12 +18,8 @@ import { Pre, Ul, } from "./Markdown"; -import Sidebar from "./Sidebar/Sidebar"; import TableOfContents from "./TableOfContents"; - -const keyMap = { - TRIGGER_SEARCH: "/", -}; +import Scaffolding from "./Scaffolding"; const extractAnchorFromHeader = (child: React.ReactElement) => ({ url: "#" + slugify(child.props.children), @@ -46,14 +37,6 @@ const extractAnchorFromEndpoint = (child: React.ReactElement) => ({ }); export default function Layout({ meta, children }: any) { - const [searchOpen, setSearchOpen] = useState(false); - const [sidebarOpen, setSidebarOpen] = useState(false); - - const title = - meta && meta.title - ? `${meta.title} • Buttondown documentation` - : "Buttondown documentation"; - const anchors = React.Children.toArray(children) .filter( (child: any) => @@ -67,71 +50,30 @@ export default function Layout({ meta, children }: any) { }); return ( - <> - { - e?.preventDefault(); - setSearchOpen(true); - }, - }} - /> -
- - {title} - - {meta && meta.description && ( - - )} - - - - - -
-
-
-
-
- - {children} - -
- -
-
-
-
+ +
+ + {children} +
- + +
); } diff --git a/components/MarkdownString.tsx b/components/MarkdownString.tsx new file mode 100644 index 0000000..5597255 --- /dev/null +++ b/components/MarkdownString.tsx @@ -0,0 +1,10 @@ +import remark from "remark"; +import remarkHtml from "remark-html"; + +const render = (markdown: string): string => { + return remark().use(remarkHtml).processSync(markdown).toString(); +}; + +export default function MarkdownString({ text }: { text: string }) { + return
; +} diff --git a/components/MonospacedSpan.tsx b/components/MonospacedSpan.tsx new file mode 100644 index 0000000..e71361b --- /dev/null +++ b/components/MonospacedSpan.tsx @@ -0,0 +1,3 @@ +export default function MonospacedSpan(s: string | JSX.Element) { + return {s}; +} diff --git a/components/Pill.tsx b/components/Pill.tsx index e9d205f..2b92da2 100644 --- a/components/Pill.tsx +++ b/components/Pill.tsx @@ -2,9 +2,11 @@ import { ReactNode } from "react"; import classNames from "../lib/classNames"; // This should match the Pill in `buttondown-app`. + +export type Variant = "success" | "info" | "error" | "warning"; type Props = { children: ReactNode; - variant: "success" | "info" | "error" | "warning"; + variant: Variant; }; const Pill = (props: Props) => ( diff --git a/components/Scaffolding.tsx b/components/Scaffolding.tsx new file mode 100644 index 0000000..a2bbceb --- /dev/null +++ b/components/Scaffolding.tsx @@ -0,0 +1,67 @@ +import Head from "next/head"; +import { useState } from "react"; +import React from "react"; +import { GlobalHotKeys } from "react-hotkeys"; + +import Footer from "./Footer"; +import Header from "./Header"; +import Sidebar from "./Sidebar/Sidebar"; + +const keyMap = { + TRIGGER_SEARCH: "/", +}; + +export default function Layout({ meta, children }: any) { + const [searchOpen, setSearchOpen] = useState(false); + const [sidebarOpen, setSidebarOpen] = useState(false); + + const title = + meta && meta.title + ? `${meta.title} • Buttondown documentation` + : "Buttondown documentation"; + + return ( + <> + { + e?.preventDefault(); + setSearchOpen(true); + }, + }} + /> +
+ + {title} + + {meta && meta.description && ( + + )} + + + + + +
+
+
+
{children}
+
+
+
+
+ + ); +} diff --git a/components/Sidebar/NavigationHierarchy.tsx b/components/Sidebar/NavigationHierarchy.tsx index b9cc9ec..56994a5 100644 --- a/components/Sidebar/NavigationHierarchy.tsx +++ b/components/Sidebar/NavigationHierarchy.tsx @@ -167,6 +167,7 @@ const NAVIGATION: Array = [ { name: "Images", href: "/api-reference/images" }, { name: "Newsletters", href: "/api-reference/newsletters" }, { name: "Exports", href: "/api-reference/exports" }, + { name: "Bulk actions", href: "/api-reference/bulk-actions" }, ], }, { diff --git a/components/Table.tsx b/components/Table.tsx index 2104865..e114861 100644 --- a/components/Table.tsx +++ b/components/Table.tsx @@ -8,7 +8,7 @@ export type Column = { }; export type Row = { - [key: string]: string; + [key: string]: string | boolean | undefined; }; type TableProps = { diff --git a/components/TableOfContents.tsx b/components/TableOfContents.tsx index aab8a6f..017dcbf 100644 --- a/components/TableOfContents.tsx +++ b/components/TableOfContents.tsx @@ -1,7 +1,7 @@ type Anchor = { depth: Number; text: string; - url: string; + url?: string; }; type Props = { @@ -18,12 +18,12 @@ export default function TableOfContents({ anchors }: Props) {
)} {anchors.map((anchor, i) => { - return anchor.text === "FAQs" ? ( + return anchor.text === "FAQs" || anchor.url === undefined ? (
- FAQs + {anchor.text}
) : (
= { title: string; - method: string; + method: Method; path: string; beta?: boolean; }; -export default function Endpoint({ title, method, path, beta }: Props) { +export default function Endpoint({ + title, + method, + path, + beta, +}: Props) { return ( <> -

{title}

+

{title}

{beta && ( <> @@ -19,7 +25,9 @@ export default function Endpoint({ title, method, path, beta }: Props) { )}
-        {method} → https://api.buttondown.email{path}
+        $ curl -X{" "}
+        {method.toLocaleUpperCase()} https://api.buttondown.email/v1
+        {path}
       
); diff --git a/components/api/EndpointDescription.tsx b/components/api/EndpointDescription.tsx new file mode 100644 index 0000000..943386c --- /dev/null +++ b/components/api/EndpointDescription.tsx @@ -0,0 +1,90 @@ +import Endpoint from "./Endpoint"; +import ParametersTable, { Parameter } from "./ParametersTable"; +import PathTable from "./PathTable"; +import OpenAPI from "../../lib/openapi/openapi.json"; +import { Method, Route, Operation } from "../../lib/openapi/types"; +import { extractRefFromType, extractOperation } from "../../lib/openapi/utils"; + +const extractRef = ( + operation: Operation> +): string | undefined => { + const body = operation.requestBody; + if (body === undefined) { + return undefined; + } + if ("application/json" in body.content) { + return body.content["application/json"].schema.$ref; + } + if ("multipart/form-data" in body.content) { + return body.content["multipart/form-data"].schema.$ref; + } + return undefined; +}; + +const extractParameters = ( + operation: Operation> +): Parameter[] => { + if (operation.parameters.length > 0) { + return operation.parameters + .filter((parameter) => parameter.in !== "path") + .map((parameter) => { + const type = + "type" in parameter.schema + ? parameter.schema.type + : extractRefFromType(parameter.schema.$ref); + return { + parameter: parameter.name, + type: type || "unknown", + description: "", + optional: !parameter.required, + }; + }); + } + const parameters = extractRef(operation); + const ref = parameters !== undefined ? extractRefFromType(parameters) : null; + const schema = ref !== null ? OpenAPI.components.schemas[ref] : null; + if (schema === null) { + return []; + } + if ("properties" in schema) { + return Object.keys(schema.properties).map((parameter: any) => { + const qualifiedParameter = (schema.properties as any)[parameter] as { + type: string; + description: string; + }; + return { + parameter, + type: qualifiedParameter.type, + description: qualifiedParameter.description, + optional: + "required" in schema ? !schema.required.includes(parameter) : true, + }; + }); + } + return []; +}; + +export default function EndpointDescription({ + path, +}: { + path: R; +}) { + const informationForPath = OpenAPI.paths[path]; + const methods = Object.keys(informationForPath) as Method[]; + + return ( + <> + {methods.map((method) => { + const operation = extractOperation(path, method); + const parameters = extractParameters(operation); + return ( +
+ + {parameters.length > 0 && } + +
+ ); + })} + + ); +} diff --git a/components/api/ObjectDescription.tsx b/components/api/ObjectDescription.tsx index 3fbb208..e228f74 100644 --- a/components/api/ObjectDescription.tsx +++ b/components/api/ObjectDescription.tsx @@ -1,9 +1,26 @@ -import { Code } from "../Markdown"; -import Table, { Row } from "../Table"; +import { Code, H1, H4, P } from "../Markdown"; +import Table from "../Table"; +import { extractRefFromType } from "../../lib/openapi/utils"; +import EnumTable from "./openapi/EnumTable"; +import OpenAPI from "../../lib/openapi/openapi.json"; +import OpenAPIFixtures from "../../lib/openapi/fixtures.json"; +import EndpointDescription from "./EndpointDescription"; +import MarkdownString from "../MarkdownString"; +import { + Enum, + Object as OpenAPIObject, + Route, + ObjectDescription as OpenAPIObjectDescription, + Fixture, +} from "../../lib/openapi/types"; +import MonospacedSpan from "../MonospacedSpan"; +import TableOfContents from "../TableOfContents"; -function MonospacedSpan(s: string) { - return {s}; -} +type Props = { + name: OpenAPIObject; + enums: Array; + endpoints: Array; +}; type Field = { field: string; @@ -11,32 +28,116 @@ type Field = { description: string; }; -type Props = { - example: string; - fields: Array; +const getFixtures = (name: keyof typeof OpenAPIFixtures): Fixture[] => { + const fixture = OpenAPIFixtures[name]; + return fixture; }; -export default function ObjectDescription({ example, fields }: Props) { +export default function ObjectDescription({ name, enums, endpoints }: Props) { + const schema = OpenAPI.components.schemas[name] as OpenAPIObjectDescription; + // @ts-ignore + const fields = Object.entries(schema.properties).map(([key, property]) => { + return { + field: key, + // @ts-ignore + type: property.type || property["$ref"], + // @ts-ignore + description: property.description, + }; + }); + + const fixtures = getFixtures(name); + return ( -
- {example} - -
- - MonospacedSpan(c.field), - }, - { - title: "type", - component: (c: Field) => MonospacedSpan(c.type), - }, - { title: "description" }, - ]} - content={fields} + <> +
+
+
+

{schema.title}

+ + {fixtures.length > 0 && + fixtures.map((fixture) => { + return ( + <> +

{fixture.description}

+ + {JSON.stringify(fixture.object, null, 4)} + + + ); + })} +
+ +
MonospacedSpan(c.field), + }, + { + title: "type", + component: (c: Field) => + MonospacedSpan( + extractRefFromType(c.type) ? ( + + {extractRefFromType(c.type)} + + ) : ( + c.type + ) + ), + }, + { title: "description" }, + ]} + content={fields} + /> + + {enums.map((e) => { + return ( + + ); + })} + + {endpoints.map((e) => { + return ( +
+ +
+ ); + })} + + + 0 ? { text: "Enums", depth: 0 } : null, + ...enums.map((e) => { + return { + text: e, + depth: 1, + url: `#${e}`, + }; + }), + endpoints.length > 0 ? { text: "Endpoints", depth: 0 } : null, + ...endpoints.map((e) => { + return { + text: e, + depth: 1, + url: `#${e}`, + }; + }), + ] + .filter((s) => s !== null) + .map((s) => s as { text: string; depth: number; url?: string })} /> - + ); } diff --git a/components/api/ParametersTable.tsx b/components/api/ParametersTable.tsx index 1ec90bb..7e06a85 100644 --- a/components/api/ParametersTable.tsx +++ b/components/api/ParametersTable.tsx @@ -2,18 +2,10 @@ import { CheckCircleIcon } from "@heroicons/react/outline"; import { H3 } from "../Markdown"; import Table, { Row } from "../Table"; -import remark from "remark"; -import remarkHtml from "remark-html"; +import MonospacedSpan from "../MonospacedSpan"; +import MarkdownString from "../MarkdownString"; -function MonospacedSpan(s: string) { - return {s}; -} - -function RawHTML(s: string) { - return
; -} - -function CheckMark(s: string) { +function CheckMark(s: boolean) { return ( s && ( ; }; -const renderMarkdown = (markdown: string): string => { - return remark().use(remarkHtml).processSync(markdown).toString(); -}; - export default function ParametersTable({ content }: Props) { return ( <> @@ -56,17 +45,17 @@ export default function ParametersTable({ content }: Props) { }, { title: "description", - component: (c: Parameter) => RawHTML(c.description), + component: (c: Parameter) => + MarkdownString({ text: c.description }), }, { title: "optional", alignment: "right", - component: CheckMark, + component: (c: Parameter) => CheckMark(c.optional), }, ]} content={content.map((row) => ({ ...row, - description: renderMarkdown(row.description), }))} /> diff --git a/components/api/PathTable.tsx b/components/api/PathTable.tsx new file mode 100644 index 0000000..2205740 --- /dev/null +++ b/components/api/PathTable.tsx @@ -0,0 +1,61 @@ +import ResponsesTable from "./ResponsesTable"; +import { extractBackingFixtureFromRef } from "../../lib/openapi/utils"; +import { H3 } from "../Markdown"; +import EnumTable from "./openapi/EnumTable"; +import { RequestBody } from "../../lib/openapi/types"; +import OpenAPI from "../../lib/openapi/openapi.json"; + +const extractSchemaFromContent = ( + content: RequestBody["content"] +): keyof typeof OpenAPI.components.schemas | undefined => { + if (content) { + if ("application/json" in content) { + const schema = content["application/json"].schema; + if (schema) { + const potentialRef = schema["$ref"].split("/").pop(); + if (potentialRef) { + return potentialRef as keyof typeof OpenAPI.components.schemas; + } + } + } + } +}; + +export default function PathTable({ content }: any) { + const responses = Object.keys(content).map((key) => ({ + Status: key, + description: content[key].description, + fixture: extractSchemaFromContent(content[key].content), + })); + + const responseErrors = responses + .map((response) => + response.fixture !== undefined + ? extractBackingFixtureFromRef(response.fixture) + : undefined + ) + .map((fixture) => { + if (fixture === undefined) { + return undefined; + } + if (fixture.type === "ErrorMessage") { + return fixture.value; + } + return undefined; + }); + + const responseError = responseErrors.find((error) => error !== undefined); + + if (responseError) { + return ( + <> + +

Error codes

+ + + + + ); + } + return ; +} diff --git a/components/api/ResponsesTable.tsx b/components/api/ResponsesTable.tsx index db07276..7bdd02e 100644 --- a/components/api/ResponsesTable.tsx +++ b/components/api/ResponsesTable.tsx @@ -1,6 +1,8 @@ import classNames from "../../lib/classNames"; import { Code, H3 } from "../Markdown"; -import Table, { Row } from "../Table"; +import Table from "../Table"; +import OpenAPIFixtures from "../../lib/openapi/fixtures.json"; +import { extractBackingFixtureFromRef } from "../../lib/openapi/utils"; function ResponseCodeBadge(text: string) { return ( @@ -19,18 +21,30 @@ function ResponseCodeBadge(text: string) { ); } -function SampleResponse(text: any) { - return ( - - {JSON.stringify(text["Sample Response"], null, 4)} - - ); +const fixtureForRef = (ref: string) => { + const fixtureInformation = extractBackingFixtureFromRef(ref); + if (fixtureInformation.type === "Page") { + const pageRef = OpenAPIFixtures[fixtureInformation.value][0].object; + return { + results: [pageRef], + count: 1, + }; + } else if (fixtureInformation.type === "ErrorMessage") { + return OpenAPIFixtures["ErrorMessage"][0].object; + } else { + return OpenAPIFixtures[fixtureInformation.value][0].object; + } +}; + +function SampleResponse(fixtureName: string) { + const relevantText = fixtureForRef(fixtureName); + return {JSON.stringify(relevantText, null, 4)}; } type Response = { Status: string; description: string; - "Sample Response": string; + fixture: string | undefined; }; type Props = { @@ -48,7 +62,15 @@ export default function ResponsesTable({ content }: Props) { component: (c: Response) => ResponseCodeBadge(c.Status), }, { title: "Description", key: "description" }, - { title: "Sample Response", component: SampleResponse }, + { + title: "Sample Response", + component: (c: Response) => + c.fixture ? ( + SampleResponse(c.fixture) + ) : ( + {"{}"} + ), + }, ]} content={content} /> diff --git a/components/api/openapi/EnumTable.tsx b/components/api/openapi/EnumTable.tsx new file mode 100644 index 0000000..fe07225 --- /dev/null +++ b/components/api/openapi/EnumTable.tsx @@ -0,0 +1,35 @@ +import OpenAPIEnums from "../../../lib/openapi/enums.json"; +import Table, { Row } from "../../Table"; +import Pill, { Variant } from "../../Pill"; + +type Props = { + enum: keyof typeof OpenAPIEnums; +}; + +export default function EnumTable({ enum: enumName }: Props) { + const valueToSpec = OpenAPIEnums[enumName]; + const rows = Object.entries(valueToSpec).map(([key, value]) => ({ + type: key, + ...value, + })); + return ( +
( + {row.name || row.type} + ), + }, + { + title: "identifier", + component: (row: Row) => ( + {row.type} + ), + }, + { title: "description" }, + ]} + content={rows} + /> + ); +} diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..64e5853 --- /dev/null +++ b/lib/README.md @@ -0,0 +1 @@ +# openapi diff --git a/lib/openapi/enums.json b/lib/openapi/enums.json new file mode 100644 index 0000000..ee337ea --- /dev/null +++ b/lib/openapi/enums.json @@ -0,0 +1,336 @@ +{ + "UpdateTagErrorCode": { + "name_already_exists": { + "variant": "error", + "name": "Name already exists", + "description": "The name of the tag already exists" + } + }, + "CreateNewsletterErrorCode": { + "username_already_exists": { + "variant": "error", + "name": "Username already exists", + "description": "A newsletter already exists with that username" + } + }, + "SubscriberSource": { + "api": { + "name": "API", + "variant": "info" + }, + "import": { + "variant": "info" + }, + "organic": { + "variant": "info" + }, + "admin": { + "variant": "info" + }, + "user": { + "variant": "info" + } + }, + "UpdateSubscriberErrorCode": { + "invalid_tag": { + "variant": "error", + "name": "Invalid tag" + } + }, + "ListSubscribersErrorCode": { + "invalid_tag": { + "variant": "error", + "name": "Invalid tag" + } + }, + "EmailCreationErrorCode": { + "subject_invalid": { + "variant": "error", + "name": "Invalid subject" + }, + "email_duplicate": { + "variant": "error", + "name": "Duplicate email" + }, + "email_invalid": { + "variant": "error", + "name": "Invalid email" + } + }, + "EmailStatus": { + "draft": { + "variant": "info", + "name": "Draft" + }, + "about_to_send": { + "variant": "info", + "name": "About to send" + }, + "scheduled": { + "variant": "info", + "name": "Scheduled" + }, + "in_flight": { + "variant": "info", + "name": "In flight" + }, + "imported": { + "variant": "info", + "name": "Imported" + }, + "deleted": { + "variant": "warning", + "name": "Deleted" + }, + "sent": { + "variant": "success", + "name": "Sent" + }, + "errored": { + "variant": "error", + "name": "Errored" + } + }, + "EmailType": { + "public": { + "variant": "info", + "name": "Public", + "description": "Public emails are sent out to all of your subscribers and are available in your web archives." + }, + "private": { + "variant": "info", + "name": "Private", + "description": "Private emails are sent out to all of your subscribers but are not viewable in your web archives." + }, + "premium": { + "variant": "info", + "name": "Premium", + "description": "Premium emails are sent out to only paying subscribers (including those with gift subscriptions or on a free trial), and only premium subscribers can view them in online archives." + }, + "free": { + "variant": "info", + "name": "Free", + "description": "Free emails are sent out only to subscribers who are not paying for your newsletter (so you can send specific emails to convince them to pay, for instance!)" + } + }, + "ExportCollection": { + "subscribers": { + "variant": "info", + "name": "Subscribers" + }, + "emails": { + "variant": "info", + "name": "Emails" + }, + "scheduled_emails": { + "variant": "info", + "name": "Scheduled emails" + }, + "drafts": { + "variant": "info", + "name": "Drafts" + }, + "unsubscribers": { + "variant": "info", + "name": "Unsubscribers" + }, + "events": { + "variant": "info", + "name": "Events" + }, + "referrals": { + "variant": "info", + "name": "Referrals" + } + }, + "ExportStatus": { + "not_started": { + "variant": "info", + "name": "Not started", + "description": "The export has not yet started" + }, + "in_progress": { + "variant": "info", + "name": "In progress", + "description": "The export is currently being processed" + }, + "ready": { + "variant": "success", + "description": "The export has completed" + }, + "error": { + "variant": "error", + "description": "The export was unable to be completed. Buttondown is looking into it." + } + }, + "EventType": { + "subscriber.created": { + "name": "subscriber.created", + "description": "Whenever a new subscriber is created" + }, + "subscriber.unsubscribed": { + "name": "subscriber.unsubscribed", + "description": "When a subscriber has manually unsubscribed from your newsletter" + }, + "subscriber.confirmed": { + "name": "subscriber.confirmed", + "description": "When a subscriber has confirmed that they are enrolled in your newsletter. (For newsletters without double opt-in, this event is created immediately after `subscriber.created`.)" + }, + "subscriber.trial_started": { + "name": "subscriber.trial_started", + "description": "When the trial has started for a subscriber. (For newsletters with automatic free trials, this will be created immediately after `subscriber.confirmed`.)" + }, + "subscriber.trial_ended": { + "name": "subscriber.trial_ended", + "description": "When the trial has ended for a subscriber." + }, + "subscriber.paid": { + "name": "subscriber.paid", + "description": "When a subscriber has enrolled in your newsletter's paid offering" + }, + "subscriber.churned": { + "name": "subscriber.churned", + "description": "When a subscriber has unenrolled from your newsletter's paid offering" + }, + "subscriber.updated": { + "name": "subscriber.updated", + "description": "When a subscriber's notes or metadata has changed" + }, + "email.created": { + "name": "email.created", + "description": "Whenever a new email is created and begins delivery. Note that event happens immediately before the emails themselves are sent." + }, + "email.sent": { + "name": "email.sent", + "description": "Whenever an email has finished its delivery. Note that event happens immediately after all emails have been sent, but some email events may not have finished processing." + } + }, + "BulkActionStatus": { + "not_started": { + "variant": "info", + "name": "Not started", + "description": "The bulk action has not yet started" + }, + "in_progress": { + "variant": "info", + "name": "In progress", + "description": "The bulk action is currently being processed" + }, + "processed": { + "variant": "success", + "description": "The bulk action has completed" + }, + "failed": { + "variant": "error", + "description": "The bulk action was unable to be completed. Buttondown is looking into it." + } + }, + "BulkActionType": { + "apply_tags": { + "variant": "info", + "name": "Apply tags", + "description": "This action requires two additional parameters within `metadata`: `tag_id` (the ID of the tag which you'd like to either add or remove to the list of subscribers) and `action` (which can be either `add` or `remove`)." + }, + "apply_metadata": { + "variant": "info", + "name": "Apply metadata (to subscribers)", + "description": "This action requires two additional parameters within `metadata`: `action` (which can be either `add`, `set`, or `remove`), and `metadata` (the value of the metadata which you'd like to either add or remove to the list of subscribers)." + }, + "ban_subscribers": { + "variant": "info", + "name": "Ban subscribers" + }, + "delete_emails": { + "variant": "info", + "name": "Delete emails" + }, + "delete_subscribers": { + "variant": "info", + "name": "Delete subscribers" + }, + "delete_tags": { + "variant": "info", + "name": "Delete tags" + }, + "reactivate_subscribers": { + "variant": "info", + "name": "Reactivate subscribers" + }, + "replay_events": { + "variant": "info", + "name": "Replay events" + }, + "resubscribe_subscribers": { + "variant": "info", + "name": "Resubscribe subscribers" + }, + "send_emails": { + "variant": "info", + "name": "Send emails", + "description": "This action requires one additional parameter within `metadata`: `email_id` (the ID of the email which you'd like to send)." + }, + "send_reminders": { + "variant": "info", + "name": "Send reminders" + }, + "update_email_types": { + "variant": "info", + "name": "Update email types" + }, + "unsubscribe_subscribers": { + "variant": "info", + "name": "Unsubscribe subscribers" + } + }, + "SubscriberType": { + "regular": { + "variant": "success", + "description": "normal subscribers who have not unsubscribed or deactivated in any way" + }, + "premium": { + "variant": "success", + "description": "subscribers with active premium subscriptions" + }, + "trialed": { + "variant": "info", + "description": "subscribers that are temporarily receiving a premium subscription to your newsletter" + }, + "unpaid": { + "variant": "info", + "description": "subscribers who have not yet purchased a subscription to your newsletter" + }, + "gifted": { + "variant": "info", + "description": "subscribers that have been gifted free premium subscriptions" + }, + "churning": { + "variant": "warning", + "description": "subscribers who have elected to not renew their subscription to your newsletter and will become unpaid subscribers at the end of their current billing period" + }, + "unactivated": { + "variant": "warning", + "description": "subscribers who have not yet confirmed their email or opted in" + }, + "paused": { + "variant": "warning", + "description": "subscribers that are on a temporary hold from their premium subscription, but are still subscribed to your newsletter" + }, + "unsubscribed": { + "variant": "error", + "description": "subscribers that have voluntarily unsubscribed from your newsletter" + }, + "spammy": { + "variant": "error", + "description": "subscribers that have been deemed spammy by Buttondown's automated systems" + }, + "removed": { + "variant": "error", + "description": "subscribers who have been explicitly removed by the newsletter (notably, this does not mean unsubscribers: use /v1/unsubscribers for that!)" + }, + "past_due": { + "variant": "error", + "name": "Past due", + "description": "subscribers who technically have active paid subscriptions, but have not paid their invoices in time" + } + } +} diff --git a/lib/openapi/fixtures.json b/lib/openapi/fixtures.json new file mode 100644 index 0000000..287788b --- /dev/null +++ b/lib/openapi/fixtures.json @@ -0,0 +1,142 @@ +{ + "Subscriber": [ + { + "description": "Basic subscriber", + "object": { + "creation_date": "2019-08-24T14:15:22Z", + "email": "user@example.com", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "metadata": { + "foo": "bar" + }, + "notes": "", + "referrer_url": "http://jmduke.com/", + "secondary_id": 3, + "source": "api", + "subscriber_type": "regular", + "tags": [], + "utm_campaign": "", + "utm_medium": "", + "utm_source": "" + } + } + ], + "Export": [ + { + "description": "Basic export", + "object": { + "creation_date": "2019-08-24T14:15:22Z", + "completion_date": "2019-08-24T15:15:22Z", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "url": "s3://buttondown/path-to-export", + "status": "completed" + } + } + ], + "Analytics": [ + { + "description": "Basic analytics response", + "object": { + "recipients": 100, + "deliveries": 99, + "opens": 50, + "clicks": 25, + "temporary_failures": 1, + "permanent_failures": 2, + "unsubscriptions": 3, + "complaints": 1 + } + } + ], + "Email": [ + { + "description": "Basic email", + "object": { + "creation_date": "2019-08-24T14:15:22Z", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "body": "Lorem ipsum yadda yadda", + "subject": "This is my first email on Buttondown!", + "excluded_tags": [], + "included_tags": [], + "email_type": "public", + "metadata": {}, + "secondary_id": 3, + "external_url": "https://buttondown.email/jmduke/my-first-email" + } + } + ], + "Newsletter": [ + { + "description": "Basic newsletter", + "object": { + "api_key": "7f819f8f-8220-4dcd-b7e3-37c81ead8b7a", + "creation_date": "2019-08-24T14:15:22Z", + "description": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", + "username": "string" + } + } + ], + "Image": [ + { + "description": "Basic image", + "object": { + "creation_date": "2019-08-24T14:15:22Z", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "image": "string" + } + } + ], + "Tag": [ + { + "description": "Basic tag", + "object": { + "color": "string", + "creation_date": "2019-08-24T14:15:22Z", + "description": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string" + } + } + ], + "BulkAction": [ + { + "description": "Basic bulk action", + "object": { + "creation_date": "2019-08-24T14:15:22Z", + "completion_date": "2019-08-24T14:17:22Z", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "completed", + "type": "delete_subscribers", + "metadata": { + "ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"] + } + } + }, + { + "description": "Bulk action to apply tags (which has a slightly different payload)", + "object": { + "creation_date": "2019-08-24T14:15:22Z", + "completion_date": "2019-08-24T14:17:22Z", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "completed", + "type": "apply_tags", + "metadata": { + "tag": "4931-4f5c-9b5b-2c9f6c9f6c9f", + "action": "add", + "ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"] + } + } + } + ], + "ErrorMessage": [ + { + "description": "Basic error", + "object": { + "code": "something_went_wrong", + "detail": "Your call is very important to us." + } + } + ] +} diff --git a/lib/openapi/openapi.json b/lib/openapi/openapi.json new file mode 100644 index 0000000..2ac5e1e --- /dev/null +++ b/lib/openapi/openapi.json @@ -0,0 +1,2332 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "NinjaAPI", + "version": "1.0.0", + "description": "" + }, + "paths": { + "/exports": { + "post": { + "operationId": "api_views_exports_create_export", + "summary": "Create Export", + "parameters": [], + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Export" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Payload", + "default": { + "collections": [ + "subscribers", + "emails", + "scheduled_emails", + "drafts", + "unsubscribers", + "events", + "referrals" + ] + }, + "allOf": [ + { + "$ref": "#/components/schemas/ExportInput" + } + ] + } + } + }, + "required": false + }, + "security": [ + { + "GlobalAuth": [] + } + ] + }, + "get": { + "operationId": "api_views_exports_list_exports", + "summary": "List Exports", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_Export_" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/exports/{pk}": { + "get": { + "operationId": "api_views_exports_retrieve_export", + "summary": "Retrieve Export", + "parameters": [ + { + "in": "path", + "name": "pk", + "schema": { + "title": "Pk", + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Export" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/tags": { + "post": { + "operationId": "api_views_subscriber_tags_create_tag", + "summary": "Create Tag", + "parameters": [], + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tag" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagInput" + } + } + }, + "required": true + }, + "security": [ + { + "GlobalAuth": [] + } + ] + }, + "get": { + "operationId": "api_views_subscriber_tags_list_tags", + "summary": "List Tags", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_Tag_" + } + } + } + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/tags/{tag_id}": { + "get": { + "operationId": "api_views_subscriber_tags_retrieve_tag", + "summary": "Retrieve Tag", + "parameters": [ + { + "in": "path", + "name": "tag_id", + "schema": { + "title": "Tag Id", + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tag" + } + } + } + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + }, + "patch": { + "operationId": "api_views_subscriber_tags_update_tag", + "summary": "Update Tag", + "parameters": [ + { + "in": "path", + "name": "tag_id", + "schema": { + "title": "Tag Id", + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tag" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage_UpdateTagErrorCode_" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagUpdateInput" + } + } + }, + "required": true + }, + "security": [ + { + "GlobalAuth": [] + } + ] + }, + "delete": { + "operationId": "api_views_subscriber_tags_delete_tag", + "summary": "Delete Tag", + "parameters": [ + { + "in": "path", + "name": "tag_id", + "schema": { + "title": "Tag Id", + "type": "string" + }, + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/ping": { + "get": { + "operationId": "api_views_ping_ping", + "summary": "Ping", + "parameters": [], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/images": { + "post": { + "operationId": "api_views_images_create_image", + "summary": "Create Image", + "parameters": [], + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Image" + } + } + } + } + }, + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "title": "FileParams", + "type": "object", + "properties": { + "image": { + "title": "Image", + "type": "string", + "format": "binary" + } + }, + "required": ["image"] + } + } + }, + "required": true + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/images/{image_id}": { + "delete": { + "operationId": "api_views_images_delete_image", + "summary": "Delete Image", + "parameters": [ + { + "in": "path", + "name": "image_id", + "schema": { + "title": "Image Id", + "type": "string" + }, + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/emails": { + "post": { + "operationId": "api_views_emails_create_email", + "summary": "Create Email", + "parameters": [], + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Email" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage_EmailCreationErrorCode_" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailInput" + } + } + }, + "required": true + }, + "security": [ + { + "GlobalAuth": [] + } + ] + }, + "get": { + "operationId": "api_views_emails_list_emails", + "summary": "List Emails", + "parameters": [ + { + "in": "query", + "name": "status", + "schema": { + "default": ["about_to_send", "in_flight", "sent"], + "type": "array", + "items": { + "$ref": "#/components/schemas/EmailStatus" + } + }, + "required": false + }, + { + "in": "query", + "name": "included_tags", + "schema": { + "title": "Included Tags", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "required": false + }, + { + "in": "query", + "name": "excluded_tags", + "schema": { + "title": "Excluded Tags", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "required": false + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_Email_" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/emails/{pk}": { + "get": { + "operationId": "api_views_emails_retrieve_email", + "summary": "Retrieve Email", + "parameters": [ + { + "in": "path", + "name": "pk", + "schema": { + "title": "Pk", + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Email" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/emails/{pk}/analytics": { + "get": { + "operationId": "api_views_emails_retrieve_email_analytics", + "summary": "Retrieve Email Analytics", + "parameters": [ + { + "in": "path", + "name": "pk", + "schema": { + "title": "Pk", + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Analytics" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/subscribers": { + "post": { + "operationId": "api_views_subscribers_create_subscriber", + "summary": "Create Subscriber", + "parameters": [], + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Subscriber" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriberInput" + } + } + }, + "required": true + }, + "security": [ + { + "GlobalAuth": [] + } + ] + }, + "get": { + "operationId": "api_views_subscribers_list_subscribers", + "summary": "List Subscribers", + "parameters": [ + { + "in": "query", + "name": "type", + "schema": { + "$ref": "#/components/schemas/SubscriberType" + }, + "required": false + }, + { + "in": "query", + "name": "email", + "schema": { + "title": "Email", + "default": "", + "type": "string" + }, + "required": false + }, + { + "in": "query", + "name": "tag", + "schema": { + "title": "Tag", + "default": "", + "type": "string" + }, + "required": false + }, + { + "in": "query", + "name": "-tag", + "schema": { + "title": "-Tag", + "type": "string" + }, + "required": false + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_Subscriber_" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage_ListSubscribersErrorCode_" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/subscribers/{pk}": { + "get": { + "operationId": "api_views_subscribers_retrieve_subscriber", + "summary": "Retrieve Subscriber", + "parameters": [ + { + "in": "path", + "name": "pk", + "schema": { + "title": "Pk", + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Subscriber" + } + } + } + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + }, + "delete": { + "operationId": "api_views_subscribers_delete_subscriber", + "summary": "Delete Subscriber", + "parameters": [ + { + "in": "path", + "name": "pk", + "schema": { + "title": "Pk", + "type": "string" + }, + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + }, + "patch": { + "operationId": "api_views_subscribers_update_subscriber", + "summary": "Update Subscriber", + "parameters": [ + { + "in": "path", + "name": "pk", + "schema": { + "title": "Pk", + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Subscriber" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage_UpdateSubscriberErrorCode_" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriberUpdateInput" + } + } + }, + "required": true + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/subscribers/{pk}/send-reminder": { + "post": { + "operationId": "api_views_subscribers_send_reminder", + "summary": "Send Reminder", + "parameters": [ + { + "in": "path", + "name": "pk", + "schema": { + "title": "Pk", + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/subscribers/{pk}/emails/{email_pk}": { + "post": { + "operationId": "api_views_subscribers_send_email_to", + "summary": "Send Email To", + "parameters": [ + { + "in": "path", + "name": "pk", + "schema": { + "title": "Pk", + "type": "string" + }, + "required": true + }, + { + "in": "path", + "name": "email_pk", + "schema": { + "title": "Email Pk", + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/newsletters": { + "get": { + "operationId": "api_views_newsletters_list_newsletters", + "summary": "List Newsletters", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_Newsletter_" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + }, + "post": { + "operationId": "api_views_newsletters_create_newsletter", + "summary": "Create Newsletter", + "parameters": [], + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Newsletter" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage_CreateNewsletterErrorCode_" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewsletterInput" + } + } + }, + "required": true + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/newsletters/{pk}": { + "patch": { + "operationId": "api_views_newsletters_update_newsletter", + "summary": "Update Newsletter", + "parameters": [ + { + "in": "path", + "name": "pk", + "schema": { + "title": "Pk", + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Newsletter" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewsletterUpdateInput" + } + } + }, + "required": true + }, + "security": [ + { + "GlobalAuth": [] + } + ] + }, + "delete": { + "operationId": "api_views_newsletters_delete_newsletter", + "summary": "Delete Newsletter", + "parameters": [ + { + "in": "path", + "name": "pk", + "schema": { + "title": "Pk", + "type": "string" + }, + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/bulk_actions": { + "post": { + "operationId": "api_views_bulk_actions_create_bulk_action", + "summary": "Create Bulk Action", + "parameters": [], + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkAction" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkActionInput" + } + } + }, + "required": true + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + }, + "/bulk_actions/{pk}": { + "get": { + "operationId": "api_views_bulk_actions_retrieve_bulk_action", + "summary": "Retrieve Bulk Action", + "parameters": [ + { + "in": "path", + "name": "pk", + "schema": { + "title": "Pk", + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkAction" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "security": [ + { + "GlobalAuth": [] + } + ] + } + } + }, + "components": { + "schemas": { + "ExportStatus": { + "title": "Status", + "description": "\n Represents the status of an export.\n\n No action is required to move from one state or another; Buttondown\n internally handles the transitions, and exposing the status is for\n observability purposes only.\n ", + "enum": ["error", "in_progress", "not_started", "ready"], + "type": "string" + }, + "Export": { + "title": "Export", + "description": "Some software applications may want programmatic access to their newsletter exports.\nThis assists with a few niche use cases, such as regular backups or data ingestion\n(into a data warehouse), or post-publishing processes that hinge on email events.\n\nIn general, you probably won't _need_ to use this endpoint unless you\n _absolutely_ need to use this endpoint.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string", + "format": "uuid" + }, + "creation_date": { + "title": "Creation Date", + "type": "string", + "format": "date-time" + }, + "status": { + "$ref": "#/components/schemas/ExportStatus" + }, + "url": { + "title": "Url", + "type": "string" + }, + "completion_date": { + "title": "Completion Date", + "type": "string", + "format": "date-time" + } + }, + "required": ["id", "creation_date", "status"] + }, + "ErrorMessage": { + "title": "ErrorMessage", + "type": "object", + "properties": { + "code": { + "title": "Code" + }, + "detail": { + "title": "Detail", + "type": "string" + }, + "metadata": { + "title": "Metadata", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["detail"] + }, + "ExportCollection": { + "title": "Collection", + "description": "\n A group of data that can be exported in an export.\n ", + "enum": [ + "subscribers", + "emails", + "scheduled_emails", + "drafts", + "unsubscribers", + "events", + "referrals" + ], + "type": "string" + }, + "ExportInput": { + "title": "ExportInput", + "type": "object", + "properties": { + "collections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExportCollection" + } + } + }, + "required": ["collections"] + }, + "Page_Export_": { + "title": "Page[Export]", + "type": "object", + "properties": { + "results": { + "title": "Results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Export" + } + }, + "next": { + "title": "Next", + "type": "string" + }, + "previous": { + "title": "Previous", + "type": "string" + }, + "count": { + "title": "Count", + "type": "integer" + } + }, + "required": ["results", "count"] + }, + "Tag": { + "title": "Tag", + "description": "Tags are a way to organize your subscribers. You can create, update, and\ndelete tags via the API. You can also list all tags for a given newsletter.\n\nTags don't have any strict functionality on their own, but you can send emails\nto subscribers with a given tag (or to all subscribers _without_ a given tag.)", + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string", + "format": "uuid" + }, + "name": { + "title": "Name", + "type": "string" + }, + "color": { + "title": "Color", + "type": "string" + }, + "description": { + "title": "Description", + "type": "string" + }, + "secondary_id": { + "title": "Secondary Id", + "type": "integer" + }, + "creation_date": { + "title": "Creation Date", + "type": "string", + "format": "date-time" + } + }, + "required": ["id", "name", "color", "secondary_id", "creation_date"] + }, + "TagInput": { + "title": "TagInput", + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "color": { + "title": "Color", + "type": "string" + }, + "description": { + "title": "Description", + "type": "string" + } + }, + "required": ["name", "color"] + }, + "Page_Tag_": { + "title": "Page[Tag]", + "type": "object", + "properties": { + "results": { + "title": "Results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Tag" + } + }, + "next": { + "title": "Next", + "type": "string" + }, + "previous": { + "title": "Previous", + "type": "string" + }, + "count": { + "title": "Count", + "type": "integer" + } + }, + "required": ["results", "count"] + }, + "UpdateTagErrorCode": { + "title": "UpdateTagErrorCode", + "description": "\n A potential error code that can be returned when updating a tag.\n ", + "enum": ["name_already_exists"], + "type": "string" + }, + "ErrorMessage_UpdateTagErrorCode_": { + "title": "ErrorMessage[UpdateTagErrorCode]", + "type": "object", + "properties": { + "code": { + "$ref": "#/components/schemas/UpdateTagErrorCode" + }, + "detail": { + "title": "Detail", + "type": "string" + }, + "metadata": { + "title": "Metadata", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["code", "detail"] + }, + "TagUpdateInput": { + "title": "TagUpdateInput", + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "color": { + "title": "Color", + "type": "string" + }, + "description": { + "title": "Description", + "type": "string" + } + } + }, + "Image": { + "title": "Image", + "description": "Images are, well, images! Buttondown allows you to upload images to its secure\nS3 bucket and do with them what you will. This is sort of an odd duck of an\nAPI, to be sure, but if you want to be able to do things like draft\nand send emails completely on your iPad you need a surefire way of creating images.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string", + "format": "uuid" + }, + "creation_date": { + "title": "Creation Date", + "type": "string", + "format": "date-time" + }, + "image": { + "title": "Image", + "type": "string" + } + }, + "required": ["id", "creation_date", "image"] + }, + "EmailType": { + "title": "Type", + "description": "\n Represents the audience of an email, and to whom it is visible both in the initial\n email and in online archives.\n ", + "enum": ["public", "private", "premium", "free", "archival", "hidden"], + "type": "string" + }, + "EmailStatus": { + "title": "Status", + "description": "\n Represents the state of an email.\n\n No action is required to move from one state or another; Buttondown\n internally handles the transitions, and exposing the status is for\n observability purposes only.\n ", + "enum": [ + "draft", + "about_to_send", + "scheduled", + "in_flight", + "deleted", + "errored", + "sent", + "imported" + ], + "type": "string" + }, + "Email": { + "title": "Email", + "description": "Emails are what you're for here on Buttondown at the end of the day, right?\nCreating an email via the API is just like creating one in the interface;\nit will instantly trigger sending actual emails,\nbased on the tags and email type you provide.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string", + "format": "uuid" + }, + "included_tags": { + "title": "Included Tags", + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "excluded_tags": { + "title": "Excluded Tags", + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "publish_date": { + "title": "Publish Date", + "type": "string", + "format": "date-time" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "body": { + "title": "Body", + "type": "string" + }, + "secondary_id": { + "title": "Secondary Id", + "type": "integer" + }, + "email_type": { + "$ref": "#/components/schemas/EmailType" + }, + "slug": { + "title": "Slug", + "type": "string" + }, + "external_url": { + "title": "External Url", + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/EmailStatus" + }, + "metadata": { + "title": "Metadata", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "id", + "subject", + "body", + "email_type", + "slug", + "external_url", + "status" + ] + }, + "EmailCreationErrorCode": { + "title": "EmailCreationErrorCode", + "description": "\n Represents the type of error that occurred when creating an email.\n\n Human-readable error messages are provided in the `detail` field of the response;\n these values are meant to be parseable by code or client logic.\n ", + "enum": ["subject_invalid", "email_duplicate", "email_invalid"], + "type": "string" + }, + "ErrorMessage_EmailCreationErrorCode_": { + "title": "ErrorMessage[EmailCreationErrorCode]", + "type": "object", + "properties": { + "code": { + "$ref": "#/components/schemas/EmailCreationErrorCode" + }, + "detail": { + "title": "Detail", + "type": "string" + }, + "metadata": { + "title": "Metadata", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["code", "detail"] + }, + "EmailInput": { + "title": "EmailInput", + "type": "object", + "properties": { + "included_tags": { + "title": "Included Tags", + "default": [], + "type": "array", + "items": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "string" + } + ] + } + }, + "excluded_tags": { + "title": "Excluded Tags", + "default": [], + "type": "array", + "items": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "string" + } + ] + } + }, + "publish_date": { + "title": "Publish Date", + "type": "string", + "format": "date-time" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "body": { + "title": "Body", + "default": "", + "type": "string" + }, + "email_type": { + "default": "public", + "allOf": [ + { + "$ref": "#/components/schemas/EmailType" + } + ] + }, + "status": { + "default": "about_to_send", + "allOf": [ + { + "$ref": "#/components/schemas/EmailStatus" + } + ] + }, + "metadata": { + "title": "Metadata", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["subject"] + }, + "Page_Email_": { + "title": "Page[Email]", + "type": "object", + "properties": { + "results": { + "title": "Results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Email" + } + }, + "next": { + "title": "Next", + "type": "string" + }, + "previous": { + "title": "Previous", + "type": "string" + }, + "count": { + "title": "Count", + "type": "integer" + } + }, + "required": ["results", "count"] + }, + "Analytics": { + "title": "Analytics", + "type": "object", + "properties": { + "recipients": { + "title": "Recipients", + "type": "integer" + }, + "deliveries": { + "title": "Deliveries", + "type": "integer" + }, + "opens": { + "title": "Opens", + "type": "integer" + }, + "clicks": { + "title": "Clicks", + "type": "integer" + }, + "temporary_failures": { + "title": "Temporary Failures", + "type": "integer" + }, + "permanent_failures": { + "title": "Permanent Failures", + "type": "integer" + }, + "unsubscriptions": { + "title": "Unsubscriptions", + "type": "integer" + }, + "complaints": { + "title": "Complaints", + "type": "integer" + } + }, + "required": [ + "recipients", + "deliveries", + "opens", + "clicks", + "temporary_failures", + "permanent_failures", + "unsubscriptions", + "complaints" + ] + }, + "SubscriberType": { + "title": "Type", + "description": "\n Represents the state of a subscriber and what emails they\n should or should not be receiving. This type is meant to be fully expressive\n so as to consolidate the logic of determining what emails a subscriber should\n receive into a single place.\n ", + "enum": [ + "regular", + "premium", + "churning", + "past_due", + "gifted", + "unpaid", + "unactivated", + "unsubscribed", + "spammy", + "removed", + "trialed", + "disabled", + "paused" + ], + "type": "string" + }, + "SubscriberSource": { + "title": "Source", + "description": "\n Represents the original provenance of a subscriber. This value is not exposed\n to subscribers; it's only used for internal tracking purposes and governs some\n of the behavior of the subscriber (i.e. whether or not to require double\n opt-in.)\n ", + "enum": ["api", "import", "organic", "user", "admin"], + "type": "string" + }, + "Subscriber": { + "title": "Subscriber", + "description": "Subscribers are the main way you collect email addresses and\nrecipients on Buttondown. They're what you see on your\n[subscribers page](https://buttondown.email/subscribers).", + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string", + "format": "uuid" + }, + "email": { + "title": "Email", + "type": "string" + }, + "notes": { + "title": "Notes", + "default": "", + "type": "string" + }, + "metadata": { + "title": "Metadata", + "default": {}, + "type": "object" + }, + "tags": { + "title": "Tags", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "referrer_url": { + "title": "Referrer Url", + "default": "", + "type": "string" + }, + "creation_date": { + "title": "Creation Date", + "type": "string", + "format": "date-time" + }, + "secondary_id": { + "title": "Secondary Id", + "type": "integer" + }, + "subscriber_type": { + "$ref": "#/components/schemas/SubscriberType" + }, + "source": { + "$ref": "#/components/schemas/SubscriberSource" + }, + "utm_campaign": { + "title": "Utm Campaign", + "type": "string" + }, + "utm_medium": { + "title": "Utm Medium", + "type": "string" + }, + "utm_source": { + "title": "Utm Source", + "type": "string" + } + }, + "required": [ + "id", + "email", + "creation_date", + "secondary_id", + "subscriber_type", + "source", + "utm_campaign", + "utm_medium", + "utm_source" + ] + }, + "SubscriberInput": { + "title": "SubscriberInput", + "type": "object", + "properties": { + "email": { + "title": "Email", + "type": "string" + }, + "notes": { + "title": "Notes", + "default": "", + "type": "string" + }, + "metadata": { + "title": "Metadata", + "default": {}, + "type": "object" + }, + "tags": { + "title": "Tags", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "referrer_url": { + "title": "Referrer Url", + "default": "", + "type": "string" + } + }, + "required": ["email"] + }, + "Page_Subscriber_": { + "title": "Page[Subscriber]", + "type": "object", + "properties": { + "results": { + "title": "Results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Subscriber" + } + }, + "next": { + "title": "Next", + "type": "string" + }, + "previous": { + "title": "Previous", + "type": "string" + }, + "count": { + "title": "Count", + "type": "integer" + } + }, + "required": ["results", "count"] + }, + "ListSubscribersErrorCode": { + "title": "ListSubscribersErrorCode", + "description": "\n Represents the type of error that occurred when listing subscribers.\n\n Human-readable error messages are provided in the `detail` field of the response;\n these values are meant to be parseable by code or client logic.\n ", + "enum": ["invalid_tag"], + "type": "string" + }, + "ErrorMessage_ListSubscribersErrorCode_": { + "title": "ErrorMessage[ListSubscribersErrorCode]", + "type": "object", + "properties": { + "code": { + "$ref": "#/components/schemas/ListSubscribersErrorCode" + }, + "detail": { + "title": "Detail", + "type": "string" + }, + "metadata": { + "title": "Metadata", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["code", "detail"] + }, + "UpdateSubscriberErrorCode": { + "title": "UpdateSubscriberErrorCode", + "description": "\n Represents the type of error that occurred when updating a subscriber.\n\n Human-readable error messages are provided in the `detail` field of the response;\n these values are meant to be parseable by code or client logic.\n ", + "enum": [ + "email_already_exists", + "email_invalid", + "subscriber_type_invalid" + ], + "type": "string" + }, + "ErrorMessage_UpdateSubscriberErrorCode_": { + "title": "ErrorMessage[UpdateSubscriberErrorCode]", + "type": "object", + "properties": { + "code": { + "$ref": "#/components/schemas/UpdateSubscriberErrorCode" + }, + "detail": { + "title": "Detail", + "type": "string" + }, + "metadata": { + "title": "Metadata", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["code", "detail"] + }, + "SubscriberUpdateInput": { + "title": "SubscriberUpdateInput", + "type": "object", + "properties": { + "email": { + "title": "Email", + "type": "string" + }, + "notes": { + "title": "Notes", + "type": "string" + }, + "metadata": { + "title": "Metadata", + "type": "object" + }, + "tags": { + "title": "Tags", + "type": "array", + "items": { + "type": "string" + } + }, + "referrer_url": { + "title": "Referrer Url", + "default": "", + "type": "string" + }, + "subscriber_type": { + "$ref": "#/components/schemas/SubscriberType" + } + } + }, + "Newsletter": { + "title": "Newsletter", + "description": "You will likely not need to interact with your newsletter settings\nprogrammatically, but if you do, this is the endpoint for you. You can\ncreate, update, and list newsletters via the API; this is ideal for\nintegrating with Buttondown as a headless email or newsletter provider\n(e.g. for a SaaS product.)", + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string", + "format": "uuid" + }, + "username": { + "title": "Username", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "description": { + "title": "Description", + "type": "string" + }, + "creation_date": { + "title": "Creation Date", + "type": "string", + "format": "date-time" + }, + "api_key": { + "title": "Api Key", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "id", + "username", + "name", + "description", + "creation_date", + "api_key" + ] + }, + "Page_Newsletter_": { + "title": "Page[Newsletter]", + "type": "object", + "properties": { + "results": { + "title": "Results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Newsletter" + } + }, + "next": { + "title": "Next", + "type": "string" + }, + "previous": { + "title": "Previous", + "type": "string" + }, + "count": { + "title": "Count", + "type": "integer" + } + }, + "required": ["results", "count"] + }, + "CreateNewsletterErrorCode": { + "title": "CreateNewsletterErrorCode", + "description": "\n Represents the type of error that occurred when creating a newsletter.\n\n Human-readable error messages are provided in the `detail` field of the response;\n these values are meant to be parseable by code or client logic.\n ", + "enum": ["username_already_exists"], + "type": "string" + }, + "ErrorMessage_CreateNewsletterErrorCode_": { + "title": "ErrorMessage[CreateNewsletterErrorCode]", + "type": "object", + "properties": { + "code": { + "$ref": "#/components/schemas/CreateNewsletterErrorCode" + }, + "detail": { + "title": "Detail", + "type": "string" + }, + "metadata": { + "title": "Metadata", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["code", "detail"] + }, + "NewsletterInput": { + "title": "NewsletterInput", + "type": "object", + "properties": { + "username": { + "title": "Username", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "description": { + "title": "Description", + "type": "string" + } + }, + "required": ["username", "name", "description"] + }, + "NewsletterUpdateInput": { + "title": "NewsletterUpdateInput", + "type": "object", + "properties": { + "username": { + "title": "Username", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "description": { + "title": "Description", + "type": "string" + } + } + }, + "BulkActionStatus": { + "title": "Status", + "description": "\n Represents the status of a bulk action.\n\n No action is required to move from one state or another; Buttondown\n internally handles the transitions, and exposing the status is for\n observability purposes only.\n ", + "enum": ["not_started", "in_progress", "processed", "failed"], + "type": "string" + }, + "BulkActionType": { + "title": "Type", + "description": "\n Represents the action being performed on a bulk of objects.\n\n (Not to be coy, but these names should be self-explanatory.)\n ", + "enum": [ + "apply_tags", + "apply_metadata", + "ban_subscribers", + "delete_emails", + "delete_subscribers", + "delete_tags", + "reactivate_subscribers", + "replay_events", + "resubscribe_subscribers", + "send_emails", + "send_reminders", + "update_email_types", + "unsubscribe_subscribers" + ], + "type": "string" + }, + "BulkAction": { + "title": "BulkAction", + "description": "A bulk action represents, well, a bulk action. It is used to perform\nactions on a large number of objects at once. For example, you can\nuse it to delete a large number of emails, or to unsubscribe a large\nnumber of subscribers. The actions within a bulk action are processed\nserially by Buttondown; this should be considered an ergonomic way to\nbatch API calls across the network rather than a net-new piece of functionality\nin of itself.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string", + "format": "uuid" + }, + "creation_date": { + "title": "Creation Date", + "type": "string", + "format": "date-time" + }, + "status": { + "$ref": "#/components/schemas/BulkActionStatus" + }, + "type": { + "$ref": "#/components/schemas/BulkActionType" + }, + "completion_date": { + "title": "Completion Date", + "type": "string", + "format": "date-time" + }, + "metadata": { + "title": "Metadata", + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "object" + } + ] + } + } + }, + "required": ["id", "creation_date", "status", "type", "metadata"] + }, + "BulkActionInput": { + "title": "BulkActionInput", + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/BulkActionType" + }, + "metadata": { + "title": "Metadata", + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "object" + } + ] + } + } + }, + "required": ["type", "metadata"] + } + }, + "securitySchemes": { + "GlobalAuth": { + "type": "http", + "scheme": "token" + } + } + } +} diff --git a/lib/openapi/types.ts b/lib/openapi/types.ts new file mode 100644 index 0000000..c895afd --- /dev/null +++ b/lib/openapi/types.ts @@ -0,0 +1,89 @@ +import OpenAPI from "./openapi.json"; +import OpenAPIEnums from "./enums.json"; +import OpenAPIFixtures from "./fixtures.json"; +import { Variant } from "../../components/Pill"; + +type KeysOfType = keyof { + [P in keyof T as V extends keyof T[P] ? P : never]: any; +}; + +// An OpenAPI spec has many `Routes` (e.g. /v1/subscribers). +// Every route has one or more `Methods` (e.g. GET, POST, PUT, DELETE). +// An `Operation` is a combination of a `Route` and a `Method`, with multiple characteristics: +// - `parameters` (e.g. query parameters, path parameters, etc.) +// - `requestBody` (e.g. the body of a POST request) +// - `responses` (e.g. the response body of a GET request) +// - `security` (e.g. the authentication requirements of a request) +// - `summary` (e.g. a short description of the operation) +// Some of those Operations may return `Objects`, which are top-level objects in the OpenAPI spec. +// Those Objects may have `Enums` as properties. +export type Route = keyof typeof OpenAPI.paths; +export type Method = keyof typeof OpenAPI.paths[R] & string; +export type Content = { + schema: { + $ref: string; + }; +}; +export type RequestBody = { + content: + | { + "application/json": Content; + } + | { + "multipart/form-data": Content; + }; +}; +export type Parameter = { + in: "path" | "query"; + name: string; + schema: + | { + title: string; + type: string; + description?: string; + } + | { + $ref: string; + }; + required: boolean; +}; +export type Operation< + R extends Route, + M extends Method +> = typeof OpenAPI.paths[R][M] & { + operationId: string; + summary: string; + requestBody: RequestBody; + parameters: Parameter[]; + responses: { + [key in string]: { + description: string; + content: { + "application/json": Content; + }; + }; + }; +}; +export type Enum = keyof typeof OpenAPI.components.schemas & + keyof typeof OpenAPIEnums; +export type EnumDescription = { + description: string; + name: string; + variant: Variant; +}; +export type Object = KeysOfType< + typeof OpenAPI.components.schemas, + "properties" +> & + KeysOfType & + keyof typeof OpenAPIFixtures; + +export type ObjectDescription = Pick< + typeof OpenAPI["components"]["schemas"][Object], + "description" | "title" | "properties" +>; + +export type Fixture = { + description: string; + object: any; +}; diff --git a/lib/openapi/utils.ts b/lib/openapi/utils.ts new file mode 100644 index 0000000..a418c04 --- /dev/null +++ b/lib/openapi/utils.ts @@ -0,0 +1,72 @@ +import OpenAPI from "./openapi.json"; +import OpenAPIEnums from "./enums.json"; +import OpenAPIFixtures from "./fixtures.json"; +import { Method, Operation, Route } from "./types"; + +export const extractRefFromType = ( + type: string +): keyof typeof OpenAPI.components.schemas | null => { + // If #/components/schemas/ is present, extract the name of the schema + const match = type.match(/#\/components\/schemas\/(.*)/); + if (match) { + const ref = match[1]; + return ref as keyof typeof OpenAPI.components.schemas; + } + return null; +}; + +type BackingFixture = + | { + type: "ErrorMessage"; + value: keyof typeof OpenAPIEnums; + } + | { + type: "Page"; + value: keyof typeof OpenAPIFixtures; + } + | { + type: "Object"; + value: keyof typeof OpenAPIFixtures; + }; + +export const extractBackingFixtureFromRef = (ref: string): BackingFixture => { + // Pages are wrapped like so: "Page_FOO_". + // We want to extract 'FOO'. + if (ref.startsWith("Page_")) { + const pageName = ref.split("_")[1]; + return { + type: "Page", + value: pageName as keyof typeof OpenAPIFixtures, + }; + } + + // Error messages are wrapped like so: "ErrorMessage_FOO_". + // We want to extract 'FOO'. + if (ref.startsWith("ErrorMessage_")) { + const errorMessageName = ref.split("_")[1]; + return { + type: "ErrorMessage", + value: errorMessageName as keyof typeof OpenAPIEnums, + }; + } + + return { + type: "Object", + value: ref as keyof typeof OpenAPIFixtures, + }; +}; + +export const extractOperation = ( + route: R, + method: Method +): Operation> => { + return OpenAPI.paths[route][method] as Operation>; +}; + +export const extractRouteInformation = >( + route: R, + method: M +) => { + const operation = extractOperation(route, method); + return operation.summary; +}; diff --git a/package-lock.json b/package-lock.json index 646677f..a228079 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,7 @@ "remark-html": "^15.0.1", "remark-mdx": "^1.6.22", "tailwindcss": "^3.0.8", - "typescript": "^4.3.5" + "typescript": "^4.9.4" } }, "node_modules/@babel/code-frame": { @@ -8547,9 +8547,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001355", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001355.tgz", - "integrity": "sha512-Sd6pjJHF27LzCB7pT7qs+kuX2ndurzCzkpJl6Qct7LPSZ9jn0bkOA8mdgMgmqnQAWLVOOGjLpc+66V57eLtb1g==", + "version": "1.0.30001436", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001436.tgz", + "integrity": "sha512-ZmWkKsnC2ifEPoWUvSAIGyOYwT+keAaaWPHiQ9DfMqS1t6tfuyFYoWR78TeZtznkEQ64+vGXH9cZrElwR2Mrxg==", "funding": [ { "type": "opencollective", @@ -22150,9 +22150,9 @@ } }, "node_modules/typescript": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", - "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -30488,9 +30488,9 @@ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" }, "caniuse-lite": { - "version": "1.0.30001355", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001355.tgz", - "integrity": "sha512-Sd6pjJHF27LzCB7pT7qs+kuX2ndurzCzkpJl6Qct7LPSZ9jn0bkOA8mdgMgmqnQAWLVOOGjLpc+66V57eLtb1g==" + "version": "1.0.30001436", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001436.tgz", + "integrity": "sha512-ZmWkKsnC2ifEPoWUvSAIGyOYwT+keAaaWPHiQ9DfMqS1t6tfuyFYoWR78TeZtznkEQ64+vGXH9cZrElwR2Mrxg==" }, "capture-exit": { "version": "2.0.0", @@ -41165,9 +41165,9 @@ } }, "typescript": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", - "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", "dev": true }, "ua-parser-js": { diff --git a/package.json b/package.json index 4f57246..3cd3652 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,6 @@ "remark-html": "^15.0.1", "remark-mdx": "^1.6.22", "tailwindcss": "^3.0.8", - "typescript": "^4.3.5" + "typescript": "^4.9.4" } } diff --git a/pages/api-reference/bulk-actions.tsx b/pages/api-reference/bulk-actions.tsx new file mode 100644 index 0000000..54ae946 --- /dev/null +++ b/pages/api-reference/bulk-actions.tsx @@ -0,0 +1,14 @@ +import ObjectDescription from "../../components/api/ObjectDescription"; +import Scaffolding from "../../components/Scaffolding"; + +export default function BulkActions() { + return ( + + + + ); +} diff --git a/pages/api-reference/emails.tsx b/pages/api-reference/emails.tsx new file mode 100644 index 0000000..86e9512 --- /dev/null +++ b/pages/api-reference/emails.tsx @@ -0,0 +1,14 @@ +import ObjectDescription from "../../components/api/ObjectDescription"; +import Scaffolding from "../../components/Scaffolding"; + +export default function Emails() { + return ( + + + + ); +} diff --git a/pages/api-reference/emails/index.mdx b/pages/api-reference/emails/index.mdx deleted file mode 100644 index 7c8b68b..0000000 --- a/pages/api-reference/emails/index.mdx +++ /dev/null @@ -1,189 +0,0 @@ ---- -title: Emails -description: An API reference for the 'email' object in Buttondown ---- - -import Layout from "../../../components/Layout"; -import Table from "../../../components/Table"; -import ResponsesTable from "../../../components/api/ResponsesTable"; -import ObjectDescription from "../../../components/api/ObjectDescription"; -import ParametersTable from "../../../components/api/ParametersTable"; -import ClosedBetaNotice from "../../../components/api/ClosedBetaNotice"; -import Endpoint from "../../../components/api/Endpoint"; -import DATA from "./metadata.json"; -import { PAGE_PARAMETER } from "../../../lib/parameters"; - -export const meta = { - title: DATA.title, -}; - -export default ({ children }) => {children}; - -# Emails - -Emails are what you're for here on Buttondown at the end of the day, right? Creating an email via the API is just like creating one in the interface; it will instantly trigger sending actual emails, based on the tags and email type you provide. - -## The email object - -An email looks like this: - -EmailType }, - { field: "body", type: "string" }, - { field: "included_tags", type: "array | array" }, - { field: "excluded_tags", type: "array | array" }, - { field: "metadata", type: "map" }, - ]} -/> - - - -### Email types - -Email types govern the visibility of an email, both in the sense of to whom it is originally delivered and whom can view it -after it's been sent out. By default, all emails are public. - -
{s.type}, - }, - { title: "description" }, - ]} - content={[ - { - type: "public", - description: - "Public emails are sent out to all of your subscribers and are available in your web archives.", - }, - { - type: "private", - description: - "Private emails are sent out to all of your subscribers but are not viewable in your web archives.", - }, - { - type: "premium", - description: - "Premium emails are sent out to only paying subscribers (including those with gift subscriptions or on a free trial), and only premium subscribers can view them in online archives.", - }, - { - type: "free", - description: - "Free emails are sent out only to subscribers who are not paying for your newsletter (so you can send specific emails to convince them to pay, for instance!)", - }, - ]} -/> - - - | array", - description: - "If included, the response will be filtered down to emails whose `included_tags` include _all_ of the elements in the array. (This means that supplying two tags will only return emails which contain both tags, not either tag.)", - example: "", - optional: true, - }, - { - parameter: "excluded_tags", - type: "array | array", - description: - "If included, the response will be filtered down to emails whose `excluded_tags` include _all_ of the elements in the array. (This means that supplying two tags will only return emails which contain both tags, not either tag.)", - example: "", - optional: true, - }, - ]} -/> - - ---- - - - | array", - description: "", - optional: true, - }, - { - parameter: "included_tags", - type: "array | array", - description: "", - optional: true, - }, - { - parameter: "metadata", - type: "map", - description: "", - optional: true, - }, - ]} -/> - - ---- - -"} -/> - diff --git a/pages/api-reference/emails/metadata.json b/pages/api-reference/emails/metadata.json deleted file mode 100644 index 66a4594..0000000 --- a/pages/api-reference/emails/metadata.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "title": "Email", - "noun": "email", - "endpoint": "/v1/emails", - "object": { - "creation_date": "2019-08-24T14:15:22Z", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "body": "Lorem ipsum yadda yadda", - "subject": "This is my first email on Buttondown!", - "excluded_tags": [], - "included_tags": [], - "email_type": "public", - "metadata": {}, - "secondary_id": 3, - "external_url": "https://buttondown.email/jmduke/my-first-email" - } -} diff --git a/pages/api-reference/event_types.json b/pages/api-reference/event_types.json deleted file mode 100644 index 08a000f..0000000 --- a/pages/api-reference/event_types.json +++ /dev/null @@ -1,42 +0,0 @@ -[ - { - "name": "subscriber.created", - "description": "Whenever a new subscriber is created" - }, - { - "name": "subscriber.unsubscribed", - "description": "When a subscriber has manually unsubscribed from your newsletter" - }, - { - "name": "subscriber.confirmed", - "description": "When a subscriber has confirmed that they are enrolled in your newsletter. (For newsletters without double opt-in, this event is created immediately after `subscriber.created`.)" - }, - { - "name": "subscriber.trial_started", - "description": "When the trial has started for a subscriber. (For newsletters with automatic free trials, this will be created immediately after `subscriber.confirmed`.)" - }, - { - "name": "subscriber.trial_ended", - "description": "When the trial has ended for a subscriber." - }, - { - "name": "subscriber.paid", - "description": "When a subscriber has enrolled in your newsletter's paid offering" - }, - { - "name": "subscriber.churned", - "description": "When a subscriber has unenrolled from your newsletter's paid offering" - }, - { - "name": "subscriber.updated", - "description": "When a subscriber's notes or metadata has changed" - }, - { - "name": "email.created", - "description": "Whenever a new email is created and begins delivery. Note that event happens immediately before the emails themselves are sent." - }, - { - "name": "email.sent", - "description": "Whenever an email has finished its delivery. Note that event happens immediately after all emails have been sent, but some email events may not have finished processing." - } -] diff --git a/pages/api-reference/events-and-webhooks.mdx b/pages/api-reference/events-and-webhooks.mdx index f47d991..0ac8b97 100644 --- a/pages/api-reference/events-and-webhooks.mdx +++ b/pages/api-reference/events-and-webhooks.mdx @@ -5,7 +5,7 @@ description: How to get a full lens of your Buttondown newsletter through struct import Layout from "../../components/Layout"; import Table from "../../components/Table"; -import EVENT_TYPES from "./event_types.json"; +import OpenAPIEnums from "../../lib/openapi/enums.json"; export const meta = { title: "Events & webhooks", @@ -33,5 +33,5 @@ The payload of most events are fairly simple, and look something like this:
diff --git a/pages/api-reference/exports.tsx b/pages/api-reference/exports.tsx new file mode 100644 index 0000000..77ca82d --- /dev/null +++ b/pages/api-reference/exports.tsx @@ -0,0 +1,14 @@ +import ObjectDescription from "../../components/api/ObjectDescription"; +import Scaffolding from "../../components/Scaffolding"; + +export default function Exports() { + return ( + + + + ); +} diff --git a/pages/api-reference/exports/index.mdx b/pages/api-reference/exports/index.mdx deleted file mode 100644 index cc82f86..0000000 --- a/pages/api-reference/exports/index.mdx +++ /dev/null @@ -1,110 +0,0 @@ ---- -title: Exports -description: An API reference for the 'export' object in Buttondown ---- - -import Layout from "../../../components/Layout"; -import ResponsesTable from "../../../components/api/ResponsesTable"; -import ObjectDescription from "../../../components/api/ObjectDescription"; -import ParametersTable from "../../../components/api/ParametersTable"; -import Endpoint from "../../../components/api/Endpoint"; -import DATA from "./metadata.json"; - -import { PAGE_PARAMETER } from '../../../lib/parameters'; -export const meta = { - title: DATA.title, -}; - -export const serializedExport = JSON.stringify(DATA.object, null, 4); - -export default ({ children }) => {children}; - -# Exports - -Some software applications may want programmatic access to their newsletter exports. This assists with a few niche -use cases, such as regular backups or data ingestion (into a data warehouse), or post-publishing processes that hinge -on email events. In general, you probably won't _need_ to use this endpoint unless you _absolutely_ need to use this endpoint. - -## The export object - -An export looks like this: - - - - - - - ---- - - - - - ---- - -"} -/> - - ---- - -"} -/> - diff --git a/pages/api-reference/exports/metadata.json b/pages/api-reference/exports/metadata.json deleted file mode 100644 index a6c4eb5..0000000 --- a/pages/api-reference/exports/metadata.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "title": "Export", - "noun": "export", - "endpoint": "/v1/exports", - "object": { - "creation_date": "2019-08-24T14:15:22Z", - "completion_date": "2019-08-24T15:15:22Z", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "url": "s3://buttondown/path-to-export", - "status": "completed" - } -} diff --git a/pages/api-reference/images.tsx b/pages/api-reference/images.tsx new file mode 100644 index 0000000..c02e198 --- /dev/null +++ b/pages/api-reference/images.tsx @@ -0,0 +1,14 @@ +import ObjectDescription from "../../components/api/ObjectDescription"; +import Scaffolding from "../../components/Scaffolding"; + +export default function Images() { + return ( + + + + ); +} diff --git a/pages/api-reference/images/index.mdx b/pages/api-reference/images/index.mdx deleted file mode 100644 index 8504c0f..0000000 --- a/pages/api-reference/images/index.mdx +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: Images -description: An API reference for the 'image' object in Buttondown ---- - -import Layout from "../../../components/Layout"; -import ResponsesTable from "../../../components/api/ResponsesTable"; -import ObjectDescription from "../../../components/api/ObjectDescription"; -import ParametersTable from "../../../components/api/ParametersTable"; -import Endpoint from "../../../components/api/Endpoint"; -import DATA from "./metadata.json"; -import { PAGE_PARAMETER } from '../../../lib/parameters'; - -export const meta = { - title: DATA.title, -}; - -export const serializedImage = JSON.stringify(DATA.object, null, 4); - -export default ({ children }) => {children}; - -# Images - -Images are, well, images! Buttondown allows you to upload images to its secure [S3](https://aws.amazon.com/s3/) bucket and do with them what you will. This is sort of an odd duck of an API, to be sure, but if you want to be able to do things like draft and send emails completely on your iPad you need a surefire way of creating images. - -## The image object - -An image looks like this: - - - - - - - ---- - - -", - description: "", - optional: false, - }, - ]} -/> - - ---- - -"} -/> - - ---- - -"} -/> - diff --git a/pages/api-reference/images/metadata.json b/pages/api-reference/images/metadata.json deleted file mode 100644 index dc43f66..0000000 --- a/pages/api-reference/images/metadata.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "title": "Image", - "noun": "image", - "endpoint": "/v1/images", - "object": { - "creation_date": "2019-08-24T14:15:22Z", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "image": "string" - } -} diff --git a/pages/api-reference/newsletters.tsx b/pages/api-reference/newsletters.tsx new file mode 100644 index 0000000..ffdf7f3 --- /dev/null +++ b/pages/api-reference/newsletters.tsx @@ -0,0 +1,14 @@ +import ObjectDescription from "../../components/api/ObjectDescription"; +import Scaffolding from "../../components/Scaffolding"; + +export default function Newsletters() { + return ( + + + + ); +} diff --git a/pages/api-reference/newsletters/index.mdx b/pages/api-reference/newsletters/index.mdx deleted file mode 100644 index 76d3530..0000000 --- a/pages/api-reference/newsletters/index.mdx +++ /dev/null @@ -1,152 +0,0 @@ ---- -title: Newsletters -description: An API reference for the 'newsletter' object in Buttondown ---- - -import Layout from "../../../components/Layout"; -import ResponsesTable from "../../../components/api/ResponsesTable"; -import ObjectDescription from "../../../components/api/ObjectDescription"; -import ParametersTable from "../../../components/api/ParametersTable"; -import ClosedBetaNotice from "../../../components/api/ClosedBetaNotice"; -import Endpoint from "../../../components/api/Endpoint"; -import DATA from "./metadata.json"; -import { PAGE_PARAMETER } from '../../../lib/parameters'; - -export const meta = { - title: DATA.title, -}; - -export default ({ children }) => {children}; - -# Newsletters - -## The newsletter object - -A newsletter looks like this: - - - - - - - ---- - - - - - ---- - -"} - beta -/> - - ---- - -"} - beta -/> - - ---- - -"} - beta -/> - diff --git a/pages/api-reference/newsletters/metadata.json b/pages/api-reference/newsletters/metadata.json deleted file mode 100644 index 51c289e..0000000 --- a/pages/api-reference/newsletters/metadata.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "title": "Newsletter", - "noun": "newsletter", - "endpoint": "/v1/newsletters", - "object": { - "api_key": "7f819f8f-8220-4dcd-b7e3-37c81ead8b7a", - "creation_date": "2019-08-24T14:15:22Z", - "description": "string", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "name": "string", - "username": "string" - } -} diff --git a/pages/api-reference/subscribers.tsx b/pages/api-reference/subscribers.tsx new file mode 100644 index 0000000..8e80a88 --- /dev/null +++ b/pages/api-reference/subscribers.tsx @@ -0,0 +1,19 @@ +import ObjectDescription from "../../components/api/ObjectDescription"; +import Scaffolding from "../../components/Scaffolding"; + +export default function Subscribers() { + return ( + + + + ); +} diff --git a/pages/api-reference/subscribers/index.mdx b/pages/api-reference/subscribers/index.mdx deleted file mode 100644 index ceeaf0b..0000000 --- a/pages/api-reference/subscribers/index.mdx +++ /dev/null @@ -1,405 +0,0 @@ ---- -title: Subscribers -description: An API reference for the 'subscriber' object in Buttondown ---- - -import Layout from "../../../components/Layout"; -import Table from "../../../components/Table"; -import Pill from "../../../components/Pill"; -import ObjectDescription from "../../../components/api/ObjectDescription"; -import ParametersTable from "../../../components/api/ParametersTable"; -import ResponsesTable from "../../../components/api/ResponsesTable"; -import Endpoint from "../../../components/api/Endpoint"; -import ClosedBetaNotice from "../../../components/api/ClosedBetaNotice"; -import { MDXProvider } from "@mdx-js/react"; -import { H2, H1, P, H3, H4, Pre, Code } from "../../../components/Markdown"; -import { CheckCircleIcon } from "@heroicons/react/outline"; -import DATA from "./metadata.json"; -import { PAGE_PARAMETER } from "../../../lib/parameters"; - -export const custom = () =>

; - -export default ({ children }) => {children}; -export const meta = { - title: "Subscribers", -}; - -# Subscribers - -Subscribers are the main way you collect email addresses and recipients on Buttondown. -They're what you see on your [subscribers page](https://buttondown.email/subscribers). - -## The subscriber object - -A subscriber looks like this: - -SubscriberType, - }, - { field: "source", type: "string" }, - { field: "tags", type: "array" }, - { field: "utm_campaign", type: "string" }, - { field: "utm_medium", type: "string" }, - { field: "utm_source", type: "string" }, - ]} -/> - ---- - -## Listing subscribers - - - - - - - -#### Subscriber types - -

( - {row.name || row.type} - ), - }, - { - title: "identifier", - component: (s) => {s.type}, - }, - { title: "description" }, - ]} - content={[ - { - variant: "success", - type: "regular", - description: - "normal subscribers who have not unsubscribed or deactivated in any way", - }, - { - variant: "success", - type: "premium", - description: "subscribers with active premium subscriptions", - }, - { - variant: "info", - type: "trialed", - description: - "subscribers that are temporarily receiving a premium subscription to your newsletter", - }, - { - variant: "info", - type: "unpaid", - description: - "subscribers who have not yet purchased a subscription to your newsletter", - }, - { - variant: "info", - type: "gifted", - description: - "subscribers that have been gifted free premium subscriptions", - }, - { - variant: "warning", - type: "churning", - description: - "subscribers who have elected to not renew their subscription to your newsletter and will become unpaid subscribers at the end of their current billing period", - }, - { - variant: "warning", - type: "unactivated", - description: - "subscribers who have not yet confirmed their email or opted in", - }, - { - variant: "warning", - type: "paused", - description: - "subscribers that are on a temporary hold from their premium subscription, but are still subscribed to your newsletter", - }, - { - variant: "error", - type: "unsubscribed", - description: - "subscribers that have voluntarily unsubscribed from your newsletter", - }, - { - variant: "error", - type: "spammy", - description: - "subscribers that have been deemed spammy by Buttondown's automated systems", - }, - { - variant: "error", - type: "removed", - description: - "subscribers who have been explicitly removed by the newsletter (notably, this does not mean unsubscribers: use /v1/unsubscribers for that!)", - }, - { - variant: "error", - type: "past_due", - name: "Past due", - description: - "subscribers who technically have active paid subscriptions, but have not paid their invoices in time", - }, - ]} -/> - - - ---- - - - -If Buttondown cannot create a new subscriber with the email address you've provided, there are a few likely reasons why. They're enumerated below: - -- A subscriber with that email has already been unsubscribed. -- That email address (justin@gmail.com) is already subscribed. -- That email address (justin@gmail.com) is already subscribed, but has not confirmed their email. -- That email address (justin@gmail.com) is already subscribed, but has not provided payment. - - | array", - description: "", - optional: true, - }, - ]} -/> - - - ---- - - - - | array", - description: "", - optional: true, - }, - ]} -/> - - - ---- - - - - - ---- - - - - ---- - - - - - ---- - - - - diff --git a/pages/api-reference/subscribers/metadata.json b/pages/api-reference/subscribers/metadata.json deleted file mode 100644 index 4334572..0000000 --- a/pages/api-reference/subscribers/metadata.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "title": "Subscriber", - "noun": "subscriber", - "endpoint": "/v1/subscribers", - "object": { - "creation_date": "2019-08-24T14:15:22Z", - "email": "user@example.com", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "metadata": { - "foo": "bar" - }, - "notes": "", - "referrer_url": "http://jmduke.com/", - "secondary_id": 3, - "source": "api", - "subscriber_type": "regular", - "tags": [], - "utm_campaign": "", - "utm_medium": "", - "utm_source": "" - } -} diff --git a/pages/api-reference/tags.tsx b/pages/api-reference/tags.tsx new file mode 100644 index 0000000..4a7f666 --- /dev/null +++ b/pages/api-reference/tags.tsx @@ -0,0 +1,14 @@ +import ObjectDescription from "../../components/api/ObjectDescription"; +import Scaffolding from "../../components/Scaffolding"; + +export default function Tags() { + return ( + + + + ); +} diff --git a/pages/api-reference/tags/index.mdx b/pages/api-reference/tags/index.mdx deleted file mode 100644 index de72238..0000000 --- a/pages/api-reference/tags/index.mdx +++ /dev/null @@ -1,123 +0,0 @@ ---- -title: Tags -description: An API reference for the 'tag' object in Buttondown ---- - -import Layout from "../../../components/Layout"; -import ResponsesTable from "../../../components/api/ResponsesTable"; -import ObjectDescription from "../../../components/api/ObjectDescription"; -import ParametersTable from "../../../components/api/ParametersTable"; -import Endpoint from "../../../components/api/Endpoint"; -import DATA from "./metadata.json"; -import {PAGE_PARAMETER} from '../../../lib/parameters'; - -export const meta = { - title: DATA.title, -}; - -export default ({ children }) => {children}; - -# Tags - -## The tag object - -A tag looks like this: - - - - - - - ---- - - - - - ---- - -"} -/> - - ---- - -"} -/> - diff --git a/pages/api-reference/tags/metadata.json b/pages/api-reference/tags/metadata.json deleted file mode 100644 index 1f7797f..0000000 --- a/pages/api-reference/tags/metadata.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "title": "Tag", - "noun": "tag", - "endpoint": "/v1/tags", - "object": { - "color": "string", - "creation_date": "2019-08-24T14:15:22Z", - "description": "string", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "name": "string" - } -} diff --git a/pages/getting-started/sending-your-first-email.mdx b/pages/getting-started/sending-your-first-email.mdx index 8420292..0e906aa 100644 --- a/pages/getting-started/sending-your-first-email.mdx +++ b/pages/getting-started/sending-your-first-email.mdx @@ -47,7 +47,7 @@ There are many ways to apply a tag to a subscriber, once you've created them: - Customizing your [subscription form](https://blog.buttondown.email/2019/07/17/customizing-your-subscription-form-to-tag-new-subscribers) - Manually tagging them -- Setting them via the [API](https://api.buttondown.email/v1/schema) +- Setting them via the [API](https://api.buttondown.email/v1/docs) ## Mail merges diff --git a/public/schema.yaml b/public/schema.yaml index 0e1b741..0196199 100644 --- a/public/schema.yaml +++ b/public/schema.yaml @@ -19,8 +19,8 @@ components: subject: type: string required: - - subject - - body + - subject + - body type: object Email: properties: @@ -28,10 +28,10 @@ components: type: string email_type: enum: - - public - - private - - premium - - promoted + - public + - private + - premium + - promoted type: string excluded_tags: nullable: true @@ -39,7 +39,8 @@ components: external_url: format: uri maxLength: 200 - pattern: "^(?:[a-z0-9\\.\\-\\+]*)://(?:[^\\s:@/]+(?::[^\\s:@/]*)?@)?(?:(?:25[0-5]|2[0-4]\\\ + pattern: + "^(?:[a-z0-9\\.\\-\\+]*)://(?:[^\\s:@/]+(?::[^\\s:@/]*)?@)?(?:(?:25[0-5]|2[0-4]\\\ d|[0-1]?\\d?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}|\\[[0-9a-f:\\\ .]+\\]|([a-z\xA1-\uFFFF0-9](?:[a-z\xA1-\uFFFF0-9-]{0,61}[a-z\xA1-\uFFFF\ 0-9])?(?:\\.(?!-)[a-z\xA1-\uFFFF0-9-]{1,63}(?