@ts-ghost/core-api
Version:
TypeScript utilities to build type-safe queries and fetchers for the Ghost API based on Zod schemas.
1 lines • 99.1 kB
Source Map (JSON)
{"version":3,"sources":["../src/schemas/authors.ts","../src/schemas/shared.ts","../src/schemas/pages.ts","../src/schemas/tags.ts","../src/schemas/posts.ts","../src/schemas/settings.ts","../src/schemas/tiers.ts","../src/schemas/email.ts","../src/schemas/offers.ts","../src/schemas/members.ts","../src/schemas/newsletter.ts","../src/schemas/subscriptions.ts","../src/schemas/site.ts","../src/fetchers/browse-fetcher.ts","../src/fetchers/formats.ts","../src/helpers/masks.ts","../src/fetchers/read-fetcher.ts","../src/fetchers/basic-fetcher.ts","../src/fetchers/mutation-fetcher.ts","../src/fetchers/delete-fetcher.ts","../src/api-composer.ts","../src/helpers/browse-params.ts","../src/helpers/fields.ts","../src/helpers/http-client.ts","../src/helpers/debug.ts"],"sourcesContent":["import { z } from \"zod\";\n\nimport { ghostIdentitySchema, ghostMetadataSchema } from \"./shared\";\n\nexport const baseAuthorsSchema = z.object({\n ...ghostIdentitySchema.shape,\n ...ghostMetadataSchema.shape,\n name: z.string(),\n profile_image: z.string().nullable(),\n cover_image: z.string().nullable(),\n bio: z.string().nullable(),\n website: z.string().nullable(),\n location: z.string().nullable(),\n facebook: z.string().nullable(),\n twitter: z.string().nullable(),\n count: z\n .object({\n posts: z.number(),\n })\n .optional(),\n url: z.string().nullish(),\n});\n","import { z } from \"zod\";\n\nimport type { HTTPClient } from \"../helpers/http-client\";\n\nexport const ghostIdentitySchema = z.object({\n slug: z.string(),\n id: z.string(),\n});\n\nexport const ghostIdentityInputSchema = z.object({\n slug: z.string().optional(),\n id: z.string().optional(),\n email: z.email().optional(),\n});\n\nexport type GhostIdentityInput = z.output<typeof ghostIdentityInputSchema>;\n\nexport type GhostIdentity = z.infer<typeof ghostIdentitySchema>;\n\nexport const ghostMetaSchema = z.object({\n pagination: z.object({\n pages: z.number(),\n page: z.number(),\n limit: z.union([z.number(), z.literal(\"all\")]),\n total: z.number(),\n prev: z.number().nullable(),\n next: z.number().nullable(),\n }),\n});\n\nexport type GhostMeta = z.infer<typeof ghostMetaSchema>;\n\nexport const ghostExcerptSchema = z.object({\n excerpt: z.string().optional(),\n custom_excerpt: z.string().optional(),\n});\n\nexport const ghostCodeInjectionSchema = z.object({\n codeinjection_head: z.string().nullable(),\n codeinjection_foot: z.string().nullable(),\n});\n\nexport const ghostFacebookSchema = z.object({\n og_image: z.string().nullable(),\n og_title: z.string().nullable(),\n og_description: z.string().nullable(),\n});\n\nexport const ghostTwitterSchema = z.object({\n twitter_image: z.string().nullable(),\n twitter_title: z.string().nullable(),\n twitter_description: z.string().nullable(),\n});\n\nexport const ghostSocialMediaSchema = z.object({\n ...ghostFacebookSchema.shape,\n ...ghostTwitterSchema.shape,\n});\n\nexport const ghostMetadataSchema = z.object({\n meta_title: z.string().nullable(),\n meta_description: z.string().nullable(),\n});\n\nexport const ghostVisibilitySchema = z.union([\n z.literal(\"public\"),\n z.literal(\"members\"),\n z.literal(\"none\"),\n z.literal(\"internal\"),\n z.literal(\"paid\"),\n z.literal(\"tiers\"),\n]);\n\nexport const apiVersionsSchema = z\n .string()\n .startsWith(\"v5.\")\n .or(z.string().startsWith(\"v6.\"))\n .default(\"v6.0\");\nexport type TAPIVersion<V> = V extends \"v5.0\" | `v5.${infer Minor}`\n ? `v5.${Minor}`\n : V extends \"v6.0\" | `v6.${infer Minor}`\n ? `v6.${Minor}`\n : never;\nexport type APIVersions = z.infer<typeof apiVersionsSchema>;\n\nexport const contentAPICredentialsSchema = z.object({\n key: z.string().regex(/[0-9a-f]{26}/, { message: \"'key' must have 26 hex characters\" }),\n version: apiVersionsSchema,\n url: z.string().url(),\n});\n\nexport type ContentAPICredentials = z.infer<typeof contentAPICredentialsSchema>;\n\nexport type APIResource =\n | \"pages\"\n | \"posts\"\n | \"settings\"\n | \"authors\"\n | \"tiers\"\n | \"tags\"\n | \"members\"\n | \"site\"\n | \"offers\"\n | \"users\"\n | \"newsletters\"\n | \"webhooks\"\n | \"themes\"\n | \"files\"\n | \"images\";\n\nexport type APIEndpoint = \"admin\" | \"content\";\n\nexport type APICredentials = {\n key: string;\n version: APIVersions;\n url: string;\n};\n\nexport type GhostRequestConfig = {\n endpoint: APIEndpoint;\n resource: APIResource;\n httpClient: HTTPClient;\n};\n\nexport const adminAPICredentialsSchema = z.object({\n key: z.string().regex(/[0-9a-f]{24}:[0-9a-f]{64}/, {\n message:\n \"'key' must have the following format {A}:{B}, where A is 24 hex characters and B is 64 hex characters\",\n }),\n version: apiVersionsSchema,\n url: z.string().url(),\n});\n\nexport const slugOrIdSchema = z.union([z.object({ slug: z.string() }), z.object({ id: z.string() })]);\nexport const emailOrIdSchema = z.union([\n z.object({ email: z.string().email() }),\n z.object({ id: z.string() }),\n]);\nexport const identitySchema = z.union([\n z.object({ email: z.string().email() }),\n z.object({ id: z.string() }),\n z.object({ slug: z.string() }),\n]);\n\nexport type AdminAPICredentials = z.infer<typeof adminAPICredentialsSchema>;\n","import { z } from \"zod\";\n\nimport { baseAuthorsSchema } from \"./authors\";\nimport {\n ghostCodeInjectionSchema,\n ghostIdentitySchema,\n ghostMetadataSchema,\n ghostSocialMediaSchema,\n ghostVisibilitySchema,\n} from \"./shared\";\nimport { baseTagsSchema } from \"./tags\";\n\nconst postsAuthorSchema = baseAuthorsSchema.extend({\n url: z.string().nullish(),\n});\n\nexport const basePagesSchema = z.object({\n ...ghostIdentitySchema.shape,\n ...ghostMetadataSchema.shape,\n title: z.string(),\n html: z.string().nullish(),\n plaintext: z.string().nullish(),\n comment_id: z.string().nullable(),\n feature_image: z.string().nullable(),\n feature_image_alt: z.string().nullable(),\n feature_image_caption: z.string().nullable(),\n featured: z.boolean(),\n custom_excerpt: z.string().nullable(),\n ...ghostCodeInjectionSchema.shape,\n ...ghostSocialMediaSchema.shape,\n visibility: ghostVisibilitySchema,\n custom_template: z.string().nullable(),\n canonical_url: z.string().nullable(),\n authors: z.array(postsAuthorSchema).optional(),\n tags: z.array(baseTagsSchema).optional(),\n primary_author: postsAuthorSchema.nullish(),\n primary_tag: baseTagsSchema.nullish(),\n url: z.string(),\n excerpt: z.string().nullish(),\n reading_time: z.number().optional().default(0),\n created_at: z.string(),\n updated_at: z.string().nullish(),\n published_at: z.string().nullable(),\n email_subject: z.string().nullish(),\n is_page: z.boolean().default(true),\n});\n","import { z } from \"zod\";\n\nimport {\n ghostCodeInjectionSchema,\n ghostIdentitySchema,\n ghostMetadataSchema,\n ghostSocialMediaSchema,\n ghostVisibilitySchema,\n} from \"./shared\";\n\nexport const baseTagsSchema = z.object({\n ...ghostIdentitySchema.shape,\n ...ghostMetadataSchema.shape,\n ...ghostCodeInjectionSchema.shape,\n ...ghostSocialMediaSchema.shape,\n name: z.string(),\n description: z.string().nullable(),\n feature_image: z.string().nullable(),\n visibility: ghostVisibilitySchema,\n canonical_url: z.string().nullable(),\n accent_color: z.string().nullable(),\n url: z.string(),\n created_at: z.string().nullish(),\n updated_at: z.string().nullish(),\n count: z\n .object({\n posts: z.number(),\n })\n .optional(),\n});\n","import { z } from \"zod\";\n\nimport { baseAuthorsSchema } from \"./authors\";\nimport {\n ghostCodeInjectionSchema,\n ghostIdentitySchema,\n ghostMetadataSchema,\n ghostSocialMediaSchema,\n ghostVisibilitySchema,\n} from \"./shared\";\nimport { baseTagsSchema } from \"./tags\";\n\nconst postsAuthorSchema = baseAuthorsSchema.extend({\n url: z.string().nullish(),\n});\nexport const basePostsSchema = z.object({\n ...ghostIdentitySchema.shape,\n ...ghostMetadataSchema.shape,\n title: z.string(),\n html: z.string().nullish(),\n plaintext: z.string().nullish(),\n comment_id: z.string().nullable(),\n feature_image: z.string().nullable(),\n feature_image_alt: z.string().nullable(),\n feature_image_caption: z.string().nullable(),\n featured: z.boolean(),\n custom_excerpt: z.string().nullable(),\n ...ghostCodeInjectionSchema.shape,\n ...ghostSocialMediaSchema.shape,\n visibility: ghostVisibilitySchema,\n custom_template: z.string().nullable(),\n canonical_url: z.string().nullable(),\n authors: z.array(postsAuthorSchema).optional(),\n tags: z.array(baseTagsSchema).optional(),\n primary_author: postsAuthorSchema.nullish(),\n primary_tag: baseTagsSchema.nullish(),\n url: z.string(),\n excerpt: z.string().nullable(),\n reading_time: z.number().optional().default(0),\n created_at: z.string(),\n updated_at: z.string().nullish(),\n published_at: z.string().nullable(),\n email_subject: z.string().nullish(),\n is_page: z.boolean().default(false),\n});\n","import { z } from \"zod\";\n\nexport const baseSettingsSchema = z.object({\n title: z.string(),\n description: z.string(),\n logo: z.string().nullable(),\n icon: z.string().nullable(),\n accent_color: z.string().nullable(),\n cover_image: z.string().nullable(),\n facebook: z.string().nullable(),\n twitter: z.string().nullable(),\n lang: z.string(),\n timezone: z.string(),\n codeinjection_head: z.string().nullable(),\n codeinjection_foot: z.string().nullable(),\n navigation: z.array(\n z.object({\n label: z.string(),\n url: z.string(),\n })\n ),\n secondary_navigation: z.array(\n z.object({\n label: z.string(),\n url: z.string(),\n })\n ),\n meta_title: z.string().nullable(),\n meta_description: z.string().nullable(),\n og_image: z.string().nullable(),\n og_title: z.string().nullable(),\n og_description: z.string().nullable(),\n twitter_image: z.string().nullable(),\n twitter_title: z.string().nullable(),\n twitter_description: z.string().nullable(),\n members_support_address: z.string(),\n url: z.string(),\n});\n","import { z } from \"zod\";\n\nimport { ghostIdentitySchema, ghostVisibilitySchema } from \"./shared\";\n\nexport const baseTiersSchema = z.object({\n ...ghostIdentitySchema.shape,\n name: z.string(),\n description: z.string().nullable(),\n active: z.boolean(),\n type: z.union([z.literal(\"free\"), z.literal(\"paid\")]),\n welcome_page_url: z.string().nullable(),\n created_at: z.string(),\n updated_at: z.string().nullable(),\n stripe_prices: z\n .array(z.number())\n .optional()\n .transform((v) => (v?.length ? v : [])),\n monthly_price: z\n .number()\n .nullable()\n .optional()\n .transform((v) => (v ? v : null)),\n yearly_price: z\n .number()\n .nullable()\n .optional()\n .transform((v) => (v ? v : null)),\n benefits: z.array(z.string()).optional(),\n visibility: ghostVisibilitySchema,\n currency: z.string().nullish(),\n trial_days: z.number().default(0),\n});\n","import { z } from \"zod\";\n\nexport const baseEmailSchema = z.object({\n id: z.string(),\n uuid: z.string(),\n status: z.string(),\n recipient_filter: z.string(),\n error: z.string().nullish(),\n error_data: z.any().nullable(),\n email_count: z.number(),\n delivered_count: z.number(),\n opened_count: z.number(),\n failed_count: z.number(),\n subject: z.string(),\n from: z.string(),\n reply_to: z.string().nullable(),\n source: z.string(), // lexical format\n html: z.string().nullable(),\n plaintext: z.string().nullable(),\n track_opens: z.boolean(),\n submitted_at: z.string(),\n created_at: z.string(),\n updated_at: z.string(),\n});\n","import { z } from \"zod\";\n\nexport const baseOffersSchema = z.object({\n id: z.string(),\n name: z.string().meta({ description: \"Internal name for an offer, must be unique\" }).default(\"\"),\n code: z\n .string()\n .meta({ description: \"Shortcode for the offer, for example: https://yoursite.com/black-friday\" }),\n display_title: z.string().meta({ description: \"Name displayed in the offer window\" }).nullish(),\n display_description: z.string().meta({ description: \"Text displayed in the offer window\" }).nullish(),\n type: z.union([z.literal(\"percent\"), z.literal(\"fixed\"), z.literal(\"trial\")]),\n cadence: z.union([z.literal(\"month\"), z.literal(\"year\")]),\n amount: z.number().meta({\n description: `Offer discount amount, as a percentage or fixed value as set in type. \n Amount is always denoted by the smallest currency unit \n (e.g., 100 cents instead of $1.00 in USD)`,\n }),\n duration: z\n .union([z.literal(\"once\"), z.literal(\"forever\"), z.literal(\"repeating\"), z.literal(\"trial\")])\n .meta({\n description: \"once/forever/repeating. repeating duration is only available when cadence is month\",\n }),\n duration_in_months: z\n .number()\n .meta({ description: \"Number of months offer should be repeated when duration is repeating\" })\n .nullish(),\n currency_restriction: z\n .boolean()\n .meta({\n description:\n \"Denotes whether the offer `currency` is restricted. If so, changing the currency invalidates the offer\",\n })\n .nullish(),\n currency: z\n .string()\n .meta({\n description: \"fixed type offers only - specifies tier's currency as three letter ISO currency code\",\n })\n .nullish(),\n status: z.union([z.literal(\"active\"), z.literal(\"archived\")]).meta({\n description: \"active or archived - denotes if the offer is active or archived\",\n }),\n redemption_count: z\n .number()\n .meta({ description: \"Number of times the offer has been redeemed\" })\n .nullish(),\n tier: z\n .object({\n id: z.string(),\n name: z.string().nullish(),\n })\n .meta({ description: \"Tier on which offer is applied\" }),\n});\n","import { z } from \"zod\";\n\nimport { baseNewsletterSchema } from \"./newsletter\";\nimport { baseSubscriptionsSchema } from \"./subscriptions\";\n\nexport const baseMembersSchema = z.object({\n id: z.string(),\n email: z.string().meta({ description: \"The email address of the member\" }),\n name: z.string().meta({ description: \"The name of the member\" }).nullable(),\n note: z.string().meta({ description: \"(nullable) A note about the member\" }).nullish(),\n geolocation: z.string().meta({ description: \"(nullable) The geolocation of the member\" }).nullish(),\n created_at: z.string().meta({ description: \"The date and time the member was created\" }),\n updated_at: z\n .string()\n .meta({ description: \"(nullable) The date and time the member was last updated\" })\n .nullish(),\n labels: z.array(\n z\n .object({\n id: z.string().meta({ description: \"The ID of the label\" }),\n name: z.string().meta({ description: \"The name of the label\" }),\n slug: z.string().meta({ description: \"The slug of the label\" }),\n created_at: z.string().meta({ description: \"The date and time the label was created\" }),\n updated_at: z\n .string()\n .meta({ description: \"(nullable) The date and time the label was last updated\" })\n .nullish(),\n })\n .meta({ description: \"The labels associated with the member\" }),\n ),\n subscriptions: z\n .array(baseSubscriptionsSchema)\n .meta({ description: \"The subscriptions associated with the member\" }),\n avatar_image: z.string().meta({ description: \"The URL of the member's avatar image\" }),\n email_count: z.number().meta({ description: \"The number of emails sent to the member\" }),\n email_opened_count: z.number().meta({ description: \"The number of emails opened by the member\" }),\n email_open_rate: z.number().meta({ description: \"(nullable) The open rate of the member\" }).nullish(),\n status: z.string().meta({ description: \"The status of the member\" }),\n last_seen_at: z\n .string()\n .meta({ description: \"(nullable) The date and time the member was last seen\" })\n .nullish(),\n newsletters: z.array(baseNewsletterSchema),\n});\n","import { z } from \"zod\";\n\nimport { ghostIdentitySchema } from \"./shared\";\n\nexport const baseNewsletterSchema = z.object({\n ...ghostIdentitySchema.shape,\n name: z.string().meta({ description: \"Public name for the newsletter\" }),\n description: z.string().meta({ description: \"(nullable) Public description of the newsletter\" }).nullish(),\n sender_name: z.string().meta({ description: \"(nullable) The sender name of the emails\" }).nullish(),\n sender_email: z\n .string()\n .meta({ description: \"(nullable) The email from which to send emails. Requires validation.\" })\n .nullish(),\n sender_reply_to: z.string().meta({\n description:\n \"The reply-to email address for sent emails. Can be either newsletter (= use sender_email) or support (use support email from Portal settings).\",\n }),\n status: z.union([z.literal(\"active\"), z.literal(\"archived\")]).meta({\n description: \"active or archived - denotes if the newsletter is active or archived\",\n }),\n visibility: z.union([z.literal(\"public\"), z.literal(\"members\")]),\n subscribe_on_signup: z.boolean().meta({\n description: \"true/false. Whether members should automatically subscribe to this newsletter on signup\",\n }),\n sort_order: z.number().meta({ description: \"The order in which newsletters are displayed in the Portal\" }),\n header_image: z\n .string()\n .meta({\n description: \"(nullable) Path to an image to show at the top of emails. Recommended size 1200x600\",\n })\n .nullish(),\n show_header_icon: z.boolean().meta({ description: \"true/false. Show the site icon in emails\" }),\n show_header_title: z.boolean().meta({ description: \"true/false. Show the site name in emails\" }),\n title_font_category: z.union([z.literal(\"serif\"), z.literal(\"sans_serif\")]).meta({\n description: \"Title font style. Either serif or sans_serif\",\n }),\n title_alignment: z.string().nullish(),\n show_feature_image: z\n .boolean()\n .meta({ description: \"true/false. Show the post's feature image in emails\" }),\n body_font_category: z.union([z.literal(\"serif\"), z.literal(\"sans_serif\")]).meta({\n description: \"Body font style. Either serif or sans_serif\",\n }),\n footer_content: z\n .string()\n .meta({\n description:\n \"(nullable) Extra information or legal text to show in the footer of emails. Should contain valid HTML.\",\n })\n .nullish(),\n show_badge: z.boolean().meta({\n description:\n \"true/false. Show you’re a part of the indie publishing movement by adding a small Ghost badge in the footer\",\n }),\n created_at: z.string(),\n updated_at: z.string().nullish(),\n show_header_name: z.boolean().meta({ description: \"true/false. Show the newsletter name in emails\" }),\n uuid: z.string(),\n});\n","import { z } from \"zod\";\n\nimport { baseOffersSchema } from \"./offers\";\nimport { baseTiersSchema } from \"./tiers\";\n\nexport const baseSubscriptionsSchema = z.object({\n id: z.string().meta({ description: \"Stripe subscription ID sub_XXXX\" }),\n customer: z\n .object({\n id: z.string(),\n name: z.string().nullable(),\n email: z.string(),\n })\n .meta({ description: \"Stripe customer attached to the subscription\" }),\n status: z.string().meta({ description: \"Subscription status\" }),\n start_date: z.string().meta({ description: \"Subscription start date\" }),\n default_payment_card_last4: z.string().meta({ description: \"Last 4 digits of the card\" }).nullable(),\n cancel_at_period_end: z.boolean().meta({\n description: \"If the subscription should be canceled or renewed at period end\",\n }),\n cancellation_reason: z.string().meta({ description: \"Reason for subscription cancellation\" }).nullable(),\n current_period_end: z.string().meta({ description: \"Subscription end date\" }),\n price: z.object({\n id: z.string().meta({ description: \"Stripe price ID\" }),\n price_id: z.string().meta({ description: \"Ghost price ID\" }),\n nickname: z.string().meta({ description: \"Price nickname\" }),\n amount: z.number().meta({ description: \"Price amount\" }),\n interval: z.string().meta({ description: \"Price interval\" }),\n type: z.string().meta({ description: \"Price type\" }),\n currency: z.string().meta({ description: \"Price currency\" }),\n }),\n tier: baseTiersSchema.nullish(),\n offer: baseOffersSchema.nullish(),\n});\n","import { z } from \"zod\";\n\nexport const baseSiteSchema = z.object({\n title: z.string(),\n description: z.string(),\n logo: z.string().nullable(),\n version: z.string(),\n url: z.string(),\n});\n","import { z, ZodRawShape } from \"zod\";\n\nimport { BrowseParamsSchema } from \"../helpers/browse-params\";\nimport { DebugOption } from \"../helpers/debug\";\nimport { sanitizeFormatMask, sanitizeMask } from \"../helpers/masks\";\nimport type { HTTPClient } from \"../helpers/http-client\";\nimport { ghostMetaSchema, type APIResource } from \"../schemas/shared\";\nimport type { Exactly, Mask, NoUnrecognizedKeys } from \"../utils\";\nimport { contentFormats, type ContentFormats } from \"./formats\";\n\nexport class BrowseFetcher<\n const Resource extends APIResource = any,\n Params extends BrowseParamsSchema = any,\n Fields extends Mask<OutputShape> = any,\n BaseShape extends ZodRawShape = any,\n OutputShape extends ZodRawShape = any,\n IncludeShape extends ZodRawShape = any,\n> {\n protected _urlParams: Record<string, string> = {};\n protected _urlSearchParams: URLSearchParams | undefined = undefined;\n protected _includeFields: (keyof IncludeShape)[] = [];\n\n constructor(\n protected resource: Resource,\n protected config: {\n schema: z.ZodObject<BaseShape>;\n output: z.ZodObject<OutputShape>;\n include: z.ZodObject<IncludeShape>;\n },\n private _params: {\n browseParams?: Params;\n include?: (keyof IncludeShape)[];\n fields?: Fields;\n formats?: string[];\n } = { browseParams: {} as Params, include: [], fields: {} as NoUnrecognizedKeys<Fields, OutputShape> },\n protected httpClient: HTTPClient,\n ) {\n this._buildUrlParams();\n }\n\n /**\n * Lets you choose output format for the content of Post and Pages resources\n * The choices are html, mobiledoc or plaintext. It will transform the output of the fetcher to a new shape\n * with the selected formats required.\n *\n * @param formats html, mobiledoc or plaintext\n * @returns A new Fetcher with the fixed output shape and the formats specified\n */\n public formats<Formats extends Mask<Pick<OutputShape, ContentFormats>>>(\n formats: NoUnrecognizedKeys<Formats, OutputShape>,\n ) {\n const requiredFormats = sanitizeFormatMask(this.config.output, formats);\n const params = {\n ...this._params,\n formats: Object.keys(requiredFormats).filter((key) => contentFormats.includes(key)),\n };\n return new BrowseFetcher(\n this.resource,\n {\n schema: this.config.schema,\n output: this.config.output.required(requiredFormats as Exactly<Formats, Formats>),\n include: this.config.include,\n },\n params,\n this.httpClient,\n );\n }\n\n /**\n * Let's you include special keys into the Ghost API Query to retrieve complimentary info\n * The available keys are defined by the Resource include schema, will not care about unknown keys.\n * Returns a new Fetcher with an Output shape modified with the include keys required.\n *\n * @param include Include specific keys from the include shape\n * @returns A new Fetcher with the fixed output shape and the formats specified\n */\n public include<Includes extends Mask<IncludeShape>>(include: NoUnrecognizedKeys<Includes, IncludeShape>) {\n const parsedInclude = this.config.include.parse(include);\n const params = {\n ...this._params,\n include: Object.keys(parsedInclude),\n };\n const requiredIncludeKeys = sanitizeMask(this.config.output, parsedInclude, { excludeDotNotation: true });\n\n return new BrowseFetcher(\n this.resource,\n {\n schema: this.config.schema,\n output: this.config.output.required(requiredIncludeKeys as Exactly<Includes, Includes>),\n include: this.config.include,\n },\n params,\n this.httpClient,\n );\n }\n\n /**\n * Let's you strip the output to only the specified keys of your choice that are in the config Schema\n * Will not care about unknown keys and return a new Fetcher with an Output shape with only the selected keys.\n *\n * @param fields Any keys from the resource Schema\n * @returns A new Fetcher with the fixed output shape having only the selected Fields\n */\n public fields<Fields extends Mask<OutputShape>>(fields: NoUnrecognizedKeys<Fields, OutputShape>) {\n const pickedFields = sanitizeMask(this.config.output, fields);\n const newOutput = this.config.output.pick(pickedFields as Exactly<Fields, Fields>);\n return new BrowseFetcher(\n this.resource,\n {\n schema: this.config.schema,\n output: newOutput,\n include: this.config.include,\n },\n this._params,\n this.httpClient,\n );\n }\n\n public getResource() {\n return this.resource;\n }\n\n public getParams() {\n return this._params;\n }\n\n public getOutputFields() {\n return this.config.output.keyof().options as string[];\n }\n\n public getURLSearchParams() {\n return this._urlSearchParams;\n }\n\n public getIncludes() {\n return this._params?.include || [];\n }\n\n public getFormats() {\n return this._params?.formats || [];\n }\n\n private _buildUrlParams() {\n const inputKeys = this.config.schema.keyof().options as string[];\n const outputKeys = this.config.output.keyof().options as string[];\n this._urlParams = {\n ...this._urlBrowseParams(),\n };\n\n if (inputKeys.length !== outputKeys.length && outputKeys.length > 0) {\n this._urlParams.fields = outputKeys.filter((key) => key !== \"count\").join(\",\");\n }\n if (this._params.include && this._params.include.length > 0) {\n this._urlParams.include = this._params.include.join(\",\");\n }\n if (this._params.formats && this._params.formats.length > 0) {\n this._urlParams.formats = this._params.formats.join(\",\");\n }\n this._urlSearchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(this._urlParams)) {\n this._urlSearchParams.append(key, value);\n }\n }\n\n private _urlBrowseParams() {\n let urlBrowseParams: { filter?: string; page?: string; order?: string; limit?: string } = {};\n if (this._params.browseParams === undefined) return {};\n const { limit, page, ...params } = this._params.browseParams;\n urlBrowseParams = {\n ...params,\n };\n if (limit) {\n urlBrowseParams.limit = limit.toString();\n }\n if (page) {\n urlBrowseParams.page = page.toString();\n }\n return urlBrowseParams;\n }\n\n private _getResultSchema() {\n return z.discriminatedUnion(\"success\", [\n z.object({\n success: z.literal(true),\n meta: ghostMetaSchema,\n data: z.array(this.config.output),\n }),\n z.object({\n success: z.literal(false),\n errors: z.array(\n z.object({\n type: z.string(),\n message: z.string(),\n }),\n ),\n status: z.number(),\n }),\n ]);\n }\n\n public async fetch(options?: RequestInit & DebugOption) {\n const resultSchema = this._getResultSchema();\n const { data: result, status } = (await this.httpClient.fetchWithStatus({\n resource: this.resource,\n searchParams: this._urlSearchParams,\n options,\n })) as { data: any; status: number };\n let data: any = {};\n if (result.errors) {\n data.success = false;\n data.errors = result.errors;\n data.status = status;\n } else {\n data = {\n success: true,\n meta: result.meta || {\n pagination: {\n pages: 0,\n page: 0,\n limit: 15,\n total: 0,\n prev: null,\n next: null,\n },\n },\n data: result[this.resource],\n };\n }\n return resultSchema.parse(data);\n }\n\n public async paginate(options?: RequestInit & DebugOption) {\n if (!this._params.browseParams?.page) {\n this._params.browseParams = {\n ...this._params.browseParams,\n page: 1,\n } as Params;\n this._buildUrlParams();\n }\n\n const resultSchema = this._getResultSchema();\n const { data: result, status } = (await this.httpClient.fetchWithStatus({\n resource: this.resource,\n searchParams: this._urlSearchParams,\n options,\n })) as { data: any; status: number };\n let data: any = {};\n if (result.errors) {\n data.success = false;\n data.errors = result.errors;\n data.status = status;\n } else {\n data = {\n success: true,\n meta: result.meta || {\n pagination: {\n pages: 0,\n page: 0,\n limit: 15,\n total: 0,\n prev: null,\n next: null,\n },\n },\n data: result[this.resource],\n };\n }\n const response: {\n current: z.infer<typeof resultSchema>;\n next: BrowseFetcher<Resource, Params, Fields, BaseShape, OutputShape, IncludeShape> | undefined;\n } = {\n current: resultSchema.parse(data),\n next: undefined,\n };\n if (response.current.success === false) return response;\n const { meta } = response.current;\n if (meta.pagination.pages <= 1 || meta.pagination.page === meta.pagination.pages) return response;\n const params = {\n ...this._params,\n browseParams: {\n ...this._params.browseParams,\n page: meta.pagination.page + 1,\n },\n };\n const next = new BrowseFetcher(this.resource, this.config, params, this.httpClient);\n response.next = next;\n return response;\n }\n}\n","export type ContentFormats = \"html\" | \"mobiledoc\" | \"plaintext\" | \"lexical\";\nexport const contentFormats = [\"html\", \"mobiledoc\", \"plaintext\", \"lexical\"];\n","import { z } from \"zod\";\n\nimport { contentFormats } from \"../fetchers/formats\";\n\ntype RuntimeMask = Record<string, unknown>;\n\nconst getKnownSchemaKeys = <Shape extends z.ZodRawShape>(schema: z.ZodObject<Shape>) => {\n return new Set(schema.keyof().options as string[]);\n};\n\nexport const sanitizeMask = <Shape extends z.ZodRawShape>(\n schema: z.ZodObject<Shape>,\n mask: RuntimeMask,\n options: { excludeDotNotation?: boolean } = {},\n) => {\n const knownKeys = getKnownSchemaKeys(schema);\n\n return Object.fromEntries(\n Object.entries(mask).filter(\n ([key]) => knownKeys.has(key) && (!options.excludeDotNotation || !key.includes(\".\")),\n ),\n );\n};\n\nexport const sanitizeFormatMask = <Shape extends z.ZodRawShape>(schema: z.ZodObject<Shape>, mask: RuntimeMask) => {\n const knownKeys = getKnownSchemaKeys(schema);\n\n return Object.fromEntries(Object.entries(mask).filter(([key]) => knownKeys.has(key) && contentFormats.includes(key)));\n};\n","import { z, ZodRawShape } from \"zod\";\n\nimport { DebugOption } from \"../helpers/debug\";\nimport type { HTTPClient } from \"../helpers/http-client\";\nimport { sanitizeFormatMask, sanitizeMask } from \"../helpers/masks\";\nimport { type APIResource, type GhostIdentityInput } from \"../schemas/shared\";\nimport type { Exactly, Mask, NoUnrecognizedKeys } from \"../utils\";\nimport { contentFormats, type ContentFormats } from \"./formats\";\n\nexport class ReadFetcher<\n const Resource extends APIResource = any,\n Fields extends Mask<OutputShape> = any,\n BaseShape extends ZodRawShape = any,\n OutputShape extends ZodRawShape = any,\n IncludeShape extends ZodRawShape = any,\n> {\n protected _urlParams: Record<string, string> = {};\n protected _urlSearchParams: URLSearchParams | undefined = undefined;\n protected _pathnameIdentity: string | undefined = undefined;\n protected _includeFields: (keyof IncludeShape)[] = [];\n\n constructor(\n protected resource: Resource,\n protected config: {\n schema: z.ZodObject<BaseShape>;\n output: z.ZodObject<OutputShape>;\n include: z.ZodObject<IncludeShape>;\n },\n private _params: {\n identity: GhostIdentityInput;\n include?: (keyof IncludeShape)[];\n fields?: Fields;\n formats?: string[];\n },\n protected httpClient: HTTPClient,\n ) {\n this._buildUrlParams();\n }\n\n /**\n * Lets you choose output format for the content of Post and Pages resources\n * The choices are html, mobiledoc or plaintext. It will transform the output of the fetcher to a new shape\n * with the selected formats required.\n *\n * @param formats html, mobiledoc or plaintext\n * @returns A new Fetcher with the fixed output shape and the formats specified\n */\n public formats<Formats extends Mask<Pick<OutputShape, ContentFormats>>>(\n formats: NoUnrecognizedKeys<Formats, OutputShape>,\n ) {\n const requiredFormats = sanitizeFormatMask(this.config.output, formats);\n const newOutput = this.config.output.required(requiredFormats as Exactly<Formats, Formats>);\n const params = {\n ...this._params,\n formats: Object.keys(requiredFormats).filter((key) => contentFormats.includes(key)),\n };\n return new ReadFetcher(\n this.resource,\n {\n schema: this.config.schema,\n output: newOutput,\n include: this.config.include,\n },\n params,\n this.httpClient,\n );\n }\n\n /**\n * Let's you include special keys into the Ghost API Query to retrieve complimentary info\n * The available keys are defined by the Resource include schema, will not care about unknown keys.\n * Returns a new Fetcher with an Output shape modified with the include keys required.\n *\n * @param include Include specific keys from the include shape\n * @returns A new Fetcher with the fixed output shape and the formats specified\n */\n public include<Includes extends Mask<IncludeShape>>(include: NoUnrecognizedKeys<Includes, IncludeShape>) {\n const parsedInclude = this.config.include.parse(include);\n const params = {\n ...this._params,\n include: Object.keys(parsedInclude),\n };\n const requiredIncludeKeys = sanitizeMask(this.config.output, parsedInclude, { excludeDotNotation: true });\n return new ReadFetcher(\n this.resource,\n {\n schema: this.config.schema,\n output: this.config.output.required(requiredIncludeKeys as Exactly<Includes, Includes>),\n include: this.config.include,\n },\n params,\n this.httpClient,\n );\n }\n\n /**\n * Let's you strip the output to only the specified keys of your choice that are in the config Schema\n * Will not care about unknown keys and return a new Fetcher with an Output shape with only the selected keys.\n *\n * @param fields Any keys from the resource Schema\n * @returns A new Fetcher with the fixed output shape having only the selected Fields\n */\n public fields<Fields extends Mask<OutputShape>>(fields: NoUnrecognizedKeys<Fields, OutputShape>) {\n const pickedFields = sanitizeMask(this.config.output, fields);\n const newOutput = this.config.output.pick(pickedFields as Exactly<Fields, Fields>);\n return new ReadFetcher(\n this.resource,\n {\n schema: this.config.schema,\n output: newOutput,\n include: this.config.include,\n },\n this._params,\n this.httpClient,\n );\n }\n\n public getResource() {\n return this.resource;\n }\n\n public getParams() {\n return this._params;\n }\n\n public getOutputFields() {\n return this.config.output.keyof().options as string[];\n }\n\n public getIncludes() {\n return this._params?.include || [];\n }\n\n public getFormats() {\n return this._params?.formats || [];\n }\n\n private _buildUrlParams() {\n const inputKeys = this.config.schema.keyof().options as string[];\n const outputKeys = this.config.output.keyof().options as string[];\n\n if (inputKeys.length !== outputKeys.length && outputKeys.length > 0) {\n this._urlParams.fields = outputKeys.join(\",\");\n }\n if (this._params.include && this._params.include.length > 0) {\n this._urlParams.include = this._params.include.join(\",\");\n }\n if (this._params.formats && this._params.formats.length > 0) {\n this._urlParams.formats = this._params.formats.join(\",\");\n }\n\n if (this._params.identity.id) {\n this._pathnameIdentity = `${this._params.identity.id}`;\n } else if (this._params.identity.slug) {\n this._pathnameIdentity = `slug/${this._params.identity.slug}`;\n } else if (this._params.identity.email) {\n this._pathnameIdentity = `email/${this._params.identity.email}`;\n } else {\n throw new Error(\"Identity is not defined\");\n }\n this._urlSearchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(this._urlParams)) {\n this._urlSearchParams.append(key, value);\n }\n }\n\n public async fetch(options?: RequestInit & DebugOption) {\n const res = z.discriminatedUnion(\"success\", [\n z.object({\n success: z.literal(true),\n data: this.config.output,\n }),\n z.object({\n success: z.literal(false),\n errors: z.array(\n z.object({\n type: z.string(),\n message: z.string(),\n }),\n ),\n status: z.number(),\n }),\n ]);\n const { data: result, status } = (await this.httpClient.fetchWithStatus({\n resource: this.resource,\n pathnameIdentity: this._pathnameIdentity,\n searchParams: this._urlSearchParams,\n options,\n })) as { data: any; status: number };\n let data: any = {};\n if (result.errors) {\n data.success = false;\n data.errors = result.errors;\n data.status = status;\n } else {\n data = {\n success: true,\n data: result[this.resource][0],\n };\n }\n return res.parse(data);\n }\n}\n","import { z, ZodTypeAny } from \"zod\";\n\nimport { DebugOption } from \"../helpers/debug\";\nimport type { HTTPClient } from \"../helpers/http-client\";\nimport type { APIResource } from \"../schemas/shared\";\n\nexport class BasicFetcher<const Resource extends APIResource = any, OutputShape extends ZodTypeAny = any> {\n constructor(\n protected resource: Resource,\n protected config: {\n output: OutputShape;\n },\n protected httpClient: HTTPClient,\n ) {}\n\n public getResource() {\n return this.resource;\n }\n\n public async fetch(options?: RequestInit & DebugOption) {\n const res = z.discriminatedUnion(\"success\", [\n z.object({\n success: z.literal(true),\n data: this.config.output,\n }),\n z.object({\n success: z.literal(false),\n errors: z.array(\n z.object({\n type: z.string(),\n message: z.string(),\n }),\n ),\n status: z.number(),\n }),\n ]);\n const { data: result, status } = (await this.httpClient.fetchWithStatus({\n options,\n resource: this.resource,\n })) as { data: any; status: number };\n let data: any = {};\n if (result.errors) {\n data.success = false;\n data.errors = result.errors;\n data.status = status;\n } else {\n data = {\n success: true,\n data: result[this.resource],\n };\n }\n return res.parse(data);\n }\n}\n","import { z } from \"zod\";\nimport * as z4 from \"zod/v4/core\";\n\nimport { DebugOption } from \"../helpers/debug\";\nimport { HTTPClient } from \"../helpers/http-client\";\nimport type { APIResource } from \"../schemas/shared\";\n\ntype ExplicitObjectKeys<T> = {\n [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K];\n};\n\nexport class MutationFetcher<\n const Resource extends APIResource = any,\n OutputShape extends z4.$ZodType = any,\n ParamsShape extends z4.$ZodType = any,\n const HTTPVerb extends \"POST\" | \"PUT\" = \"POST\",\n> {\n protected _urlParams: Record<string, string> = {};\n protected _urlSearchParams: URLSearchParams | undefined = undefined;\n protected _pathnameIdentity: string | undefined = undefined;\n\n constructor(\n protected resource: Resource,\n protected config: {\n output: OutputShape;\n paramsShape?: ParamsShape;\n },\n private _params: ({ id?: string } & ExplicitObjectKeys<z4.output<ParamsShape>>) | undefined,\n protected _options: {\n method: HTTPVerb;\n body: Record<string, unknown>;\n },\n protected httpClient: HTTPClient,\n ) {\n this._buildUrlParams();\n }\n\n public getResource() {\n return this.resource;\n }\n\n public getParams() {\n return this._params;\n }\n\n private _buildUrlParams() {\n if (this._params) {\n for (const [key, value] of Object.entries(this._params)) {\n if (key !== \"id\") {\n this._urlParams[key] = value;\n }\n }\n }\n\n this._urlSearchParams = new URLSearchParams();\n if (this._params?.id) {\n this._pathnameIdentity = `${this._params.id}`;\n }\n for (const [key, value] of Object.entries(this._urlParams)) {\n this._urlSearchParams.append(key, value);\n }\n }\n\n public async submit(options?: RequestInit & DebugOption) {\n const schema = z.discriminatedUnion(\"success\", [\n z.object({\n success: z.literal(true),\n data: this.config.output,\n }),\n z.object({\n success: z.literal(false),\n errors: z.array(\n z.object({\n type: z.string(),\n message: z.string(),\n context: z.string().nullish(),\n }),\n ),\n status: z.number(),\n }),\n ]);\n // Ghost API is expecting a JSON object with a key that matches the resource name\n // e.g. { posts: [ { title: \"Hello World\" } ] }\n // https://ghost.org/docs/api/v3/content/#create-a-post\n // body is also an array of objects, so we need to wrap it in another array\n // but Ghost will throw an error if given more than 1 item in the array.\n const createData = {\n [this.resource]: [this._options.body],\n };\n const { data: response, status } = (await this.httpClient.fetchWithStatus({\n resource: this.resource,\n searchParams: this._urlSearchParams,\n pathnameIdentity: this._pathnameIdentity,\n options: { ...options, method: this._options.method, body: JSON.stringify(createData) },\n })) as { data: any; status: number };\n let result: any = {};\n if (response.errors) {\n result.success = false;\n result.errors = response.errors;\n result.status = status;\n } else {\n result = {\n success: true,\n data: response[this.resource][0],\n };\n }\n return schema.parse(result);\n }\n}\n","import { z } from \"zod\";\n\nimport { DebugOption } from \"../helpers/debug\";\nimport type { HTTPClient } from \"../helpers/http-client\";\nimport type { APIResource } from \"../schemas/shared\";\n\nexport class DeleteFetcher<const Resource extends APIResource = any> {\n protected _pathnameIdentity: string | undefined = undefined;\n\n constructor(\n protected resource: Resource,\n private _params: { id: string },\n protected httpClient: HTTPClient,\n ) {\n this._buildPathnameIdentity();\n }\n\n public getResource() {\n return this.resource;\n }\n\n public getParams() {\n return this._params;\n }\n\n private _buildPathnameIdentity() {\n if (!this._params.id) {\n throw new Error(\"Missing id in params\");\n }\n this._pathnameIdentity = this._params.id;\n }\n\n public async submit(options?: RequestInit & DebugOption) {\n const schema = z.discriminatedUnion(\"success\", [\n z.object({\n success: z.literal(true),\n }),\n z.object({\n success: z.literal(false),\n errors: z.array(\n z.object({\n type: z.string(),\n message: z.string(),\n context: z.string().nullish(),\n }),\n ),\n status: z.number(),\n }),\n ]);\n let result: any = {};\n try {\n const response = await this.httpClient.fetchRawResponse({\n resource: this.resource,\n pathnameIdentity: this._pathnameIdentity,\n options: {\n ...options,\n method: \"DELETE\",\n },\n });\n if (response.status === 204) {\n result = {\n success: true,\n };\n } else {\n const res = await response.json();\n if (res.errors) {\n result.success = false;\n result.errors = res.errors;\n result.status = response.status;\n }\n }\n } catch (e) {\n result = {\n success: false,\n errors: [\n {\n type: \"FetchError\",\n message: (e as Error).toString(),\n },\n ],\n status: 0,\n };\n }\n return schema.parse(result);\n }\n}\n","import { z, ZodObject as ZodObjectV4, ZodRawShape } from \"zod\";\nimport * as z4 from \"zod/v4/core\";\n\nimport { DeleteFetcher } from \"./fetchers\";\nimport { BrowseFetcher } from \"./fetchers/browse-fetcher\";\nimport { MutationFetcher } from \"./fetchers/mutation-fetcher\";\nimport { ReadFetcher } from \"./fetchers/read-fetcher\";\nimport { parseBrowseParams, type BrowseParams } from \"./helpers/browse-params\";\nimport type { HTTPClientFactory } from \"./helpers/http-client\";\nimport type { APIResource } from \"./schemas\";\nimport type { IsAny } from \"./utils\";\n\nfunction isZodObject(schema: z4.$ZodType): schema is ZodObjectV4<any> {\n return (schema as ZodObjectV4<any>).partial !== undefined;\n}\n\n/**\n * API Composer contains all methods, pick and choose.\n */\nexport class APIComposer<\n const Resource extends APIResource = any,\n Shape extends ZodRawShape = any,\n IdentityShape extends z4.$ZodType<{ slug?: string; id?: string; email?: string }> = any,\n IncludeShape extends ZodRawShape = any,\n CreateSchema extends z4.$ZodType = any,\n CreateOptions extends z4.$ZodType = any,\n UpdateSchema extends z4.$ZodObject = any,\n UpdateOptions extends z4.$ZodObject = any,\n> {\n constructor(\n protected resource: Resource,\n protected config: {\n schema: z.ZodObject<Shape>;\n identitySchema: IdentityShape;\n include: z.ZodObject<IncludeShape>;\n createSchema?: CreateSchema;\n createOptionsSchema?: CreateOptions;\n updateSchema?: UpdateSchema;\n updateOptionsSchema?: UpdateOptions;\n },\n protected httpClientFactory: HTTPClientFactory,\n ) {}\n\n /**\n * Browse function that accepts browse params order, filter, page and limit. Will return an instance\n * of BrowseFetcher class.\n */\n public browse<\n const OrderStr extends string,\n const FilterStr extends string,\n const P extends {\n order?: OrderStr;\n limit?: number | \"all\";\n page?: number | string;\n filter?: FilterStr;\n },\n >(options?: BrowseParams<P, Shape & IncludeShape>) {\n return new BrowseFetcher(\n this.resource,\n {\n schema: this.config.schema,\n output: this.config.schema,\n include: this.config.include,\n },\n {\n browseParams:\n (options && parseBrowseParams(options, this.config.schema, this.config.include)) || undefined,\n },\n this.httpClientFactory.create(),\n );\n }\n\n /**\n * Read function that accepts Identify fields like id, slug or email. Will return an instance\n * of ReadFetcher class.\n */\n public read(options: z4.infer<IdentityShape>) {\n return new ReadFetcher(\n this.resource,\n {\n schema: this.config.schema,\n output: this.config.schema,\n include: this.config.include,\n },\n {\n identity: z.parse(this.config.identitySchema, options),\n },\n this.httpClientFactory.create(),\n );\n }\n\n public async add(data: z4.input<CreateSchema>, options?: z4.infer<CreateOptions>) {\n if (!this.config.createSchema) {\n throw new Error(\"No createSchema defined\");\n }\n const parsedData = z.parse(this.config.createSchema, data);\n const parsedOptions =\n this.config.createOptionsSchema && options\n ? z.parse(this.config.createOptionsSchema, options)\n : undefined;\n const fetcher = new MutationFetcher(\n this.resource,\n {\n output: this.config.schema,\n paramsShape: this.config.createOptionsSchema,\n },\n parsedOptions as ({ id?: string } & z4.output<CreateOptions>) | undefined,\n { method: \"POST\", body: parsedData as Record<string, unknown> },\n this.httpClientFactory.create(),\n );\n return fetcher.submit();\n }\n\n public async edit(\n id: string,\n data: IsAny<UpdateSchema> extends true ? Partial<z4.input<CreateSchema>> : z4.input<UpdateSchema>,\n options?: z4.infer<UpdateOptions>,\n ) {\n let updateSchema: z4.$ZodObject | undefined = this.config.updateSchema;\n if (!this.config.updateSchema && this.config.createSchema && isZodObject(this.config.createSchema)) {\n updateSchema = this.config.createSchema.partial();\n }\n if (!updateSchema) {\n throw new Error(\"No updateSchema defined\");\n }\n const cleanId = z.string().min(1