diff --git a/docs/src/app/docs/core-api/page.mdx b/docs/src/app/docs/core-api/page.mdx index d91d4222..d4540a87 100644 --- a/docs/src/app/docs/core-api/page.mdx +++ b/docs/src/app/docs/core-api/page.mdx @@ -43,7 +43,7 @@ All these methods like `read` and `browse` gives you back the appropriate `Fetch ### Instantiation ```ts -import { z } from "zod/v3"; +import { z } from "zod"; import { APIComposer, HTTPClient, type HTTPClientOptions } from "@ts-ghost/core-api"; const credentials: HTTPClientOptions = { @@ -109,7 +109,7 @@ After instantiation you can use the `APIComposer` to build your queries with 2 a The `browse` and `read` methods accept a config object with 2 properties: `input` and an `output`. These params mimic the way Ghost API Content is built but with the power of Zod and TypeScript they are type-safe here. ```ts -import { z } from "zod/v3"; +import { z } from "zod"; import { APIComposer, HTTPClient, type HTTPClientOptions } from "@ts-ghost/core-api"; const credentials: HTTPClientOptions = { diff --git a/packages/ts-ghost-admin-api/src/admin-api.ts b/packages/ts-ghost-admin-api/src/admin-api.ts index cabd6617..6aaf446a 100644 --- a/packages/ts-ghost-admin-api/src/admin-api.ts +++ b/packages/ts-ghost-admin-api/src/admin-api.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { adminAPICredentialsSchema, APIComposer, @@ -7,6 +7,7 @@ import { baseSiteSchema, baseTagsSchema, BasicFetcher, + DebugOption, emailOrIdSchema, HTTPClientFactory, slugOrIdSchema, @@ -31,6 +32,7 @@ export class TSGhostAdminAPI; diff --git a/packages/ts-ghost-admin-api/src/schemas/members.ts b/packages/ts-ghost-admin-api/src/schemas/members.ts index 74077cc9..7ff322fa 100644 --- a/packages/ts-ghost-admin-api/src/schemas/members.ts +++ b/packages/ts-ghost-admin-api/src/schemas/members.ts @@ -1,64 +1,65 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { baseMembersSchema } from "@ts-ghost/core-api"; export const adminMembersCreateSchema = z.object({ - email: z.string({ description: "The email address of the member" }).email(), - name: z.string({ description: "The name of the member" }).optional(), - note: z.string({ description: "(nullable) A note about the member" }).optional(), - geolocation: z.string({ description: "(nullable) The geolocation of the member" }).optional(), + email: z.email().meta({ description: "The email address of the member" }), + name: z.string().meta({ description: "The name of the member" }).optional(), + note: z.string().meta({ description: "(nullable) A note about the member" }).optional(), + geolocation: z.string().meta({ description: "(nullable) The geolocation of the member" }).optional(), labels: z .array( z.union([ z.object({ - id: z.string({ description: "The ID of the label" }), + id: z.string().meta({ description: "The ID of the label" }), }), z.object({ - name: z.string({ description: "The name of the label" }), + name: z.string().meta({ description: "The name of the label" }), }), z.object({ - slug: z.string({ description: "The slug of the label" }), + slug: z.string().meta({ description: "The slug of the label" }), }), ]), - { description: "The labels associated with the member" }, ) + .meta({ description: "The labels associated with the member" }) .optional(), products: z .array( z.union([ z.object({ - id: z.string({ description: "The ID of the subscription" }), + id: z.string().meta({ description: "The ID of the subscription" }), }), z.object({ - name: z.string({ description: "The name of the subscription" }), + name: z.string().meta({ description: "The name of the subscription" }), }), z.object({ - slug: z.string({ description: "The slug of the subscription" }), + slug: z.string().meta({ description: "The slug of the subscription" }), }), ]), - { - description: `The products associated with the member, they correspond to a stripe product. - If given the member status will be 'comped' as given away a subscription.`, - }, ) + .meta({ + description: `The products associated with the member, they correspond to a stripe product. + If given the member status will be 'comped' as given away a subscription.`, + }) .optional(), // newsletters and subscribed exclude each other. `subscribed` newsletters: z .array( z.union([ z.object({ - id: z.string({ description: "The ID of the newsletter" }), + id: z.string().meta({ description: "The ID of the newsletter" }), }), z.object({ - name: z.string({ description: "The name of the newsletter" }), + name: z.string().meta({ description: "The name of the newsletter" }), }), ]), - { - description: `Specifing newsletter to subscribe to via id or name, incompatible with the \`subscribed\` property`, - }, ) + .meta({ + description: `Specifying newsletter to subscribe to via id or name, incompatible with the \`subscribed\` property`, + }) .optional(), subscribed: z - .boolean({ + .boolean() + .meta({ description: "Will subscribe the user to the default Newsletter, incompatible with the `newsletters` property", }) @@ -81,9 +82,12 @@ export const adminMembersSchema = baseMembersSchema.merge( newsletters: z.array( z.object({ id: z.string(), - name: z.string({ description: "Public name for the newsletter" }), - description: z.string({ description: "(nullable) Public description of the newsletter" }).nullish(), - status: z.union([z.literal("active"), z.literal("archived")], { + name: z.string().meta({ description: "Public name for the newsletter" }), + description: z + .string() + .meta({ description: "(nullable) Public description of the newsletter" }) + .nullish(), + status: z.union([z.literal("active"), z.literal("archived")]).meta({ description: "active or archived - denotes if the newsletter is active or archived", }), }), diff --git a/packages/ts-ghost-admin-api/src/schemas/newsletters.ts b/packages/ts-ghost-admin-api/src/schemas/newsletters.ts index 1e889a65..9dcbdac4 100644 --- a/packages/ts-ghost-admin-api/src/schemas/newsletters.ts +++ b/packages/ts-ghost-admin-api/src/schemas/newsletters.ts @@ -1,10 +1,10 @@ -import { z } from "zod/v3"; +import { z } from "zod"; export const adminNewsletterCreateSchema = z.object({ name: z.string(), description: z.string().max(3000).optional(), sender_name: z.string(), - sender_email: z.string().email().nullish(), + sender_email: z.email().nullish(), sender_reply_to: z.string().optional(), status: z.union([z.literal("active"), z.literal("archived")]).optional(), subscribe_on_signup: z.boolean().optional(), diff --git a/packages/ts-ghost-admin-api/src/schemas/offers.ts b/packages/ts-ghost-admin-api/src/schemas/offers.ts index cc55a7aa..e7603491 100644 --- a/packages/ts-ghost-admin-api/src/schemas/offers.ts +++ b/packages/ts-ghost-admin-api/src/schemas/offers.ts @@ -1,5 +1,5 @@ import isSlug from "validator/lib/isSlug"; -import { z } from "zod/v3"; +import { z } from "zod"; const baseOffersCreateSchema = z.object({ name: z.string(), @@ -9,12 +9,13 @@ const baseOffersCreateSchema = z.object({ display_title: z.string().optional(), display_description: z.string().optional(), cadence: z.union([z.literal("year"), z.literal("month")]), - amount: z.number({ + amount: z.number().meta({ description: "Amount of the percent or fixed amount in the smallest unit of the currency", }), duration: z.union([z.literal("once"), z.literal("forever"), z.literal("repeating")]), duration_in_months: z - .number({ + .number() + .meta({ description: "Number of months offer should be repeated when duration is repeating", }) .nullish(), diff --git a/packages/ts-ghost-admin-api/src/schemas/pages.ts b/packages/ts-ghost-admin-api/src/schemas/pages.ts index d5d939fa..6273f596 100644 --- a/packages/ts-ghost-admin-api/src/schemas/pages.ts +++ b/packages/ts-ghost-admin-api/src/schemas/pages.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { basePagesSchema } from "@ts-ghost/core-api"; import { adminAuthorsSchema } from "./authors"; @@ -82,55 +82,55 @@ export const adminPagesCreateSchema = z.object({ .array( z.union([ z.object({ - id: z.string({ description: "The ID of the tags" }), + id: z.string().meta({ description: "The ID of the tags" }), }), z.object({ - name: z.string({ description: "The name of the tags" }), + name: z.string().meta({ description: "The name of the tags" }), }), z.object({ - slug: z.string({ description: "The slug of the tags" }), + slug: z.string().meta({ description: "The slug of the tags" }), }), ]), - { - description: `The tags associated with the post array of either slug, id or name`, - }, ) + .meta({ + description: `The tags associated with the post array of either slug, id or name`, + }) .optional(), tiers: z .array( z.union([ z.object({ - id: z.string({ description: "The ID of the tiers" }), + id: z.string().meta({ description: "The ID of the tiers" }), }), z.object({ - name: z.string({ description: "The name of the tiers" }), + name: z.string().meta({ description: "The name of the tiers" }), }), z.object({ - slug: z.string({ description: "The slug of the tiers" }), + slug: z.string().meta({ description: "The slug of the tiers" }), }), ]), - { - description: `The tiers associated with the post array of either slug, id or name`, - }, ) + .meta({ + description: `The tiers associated with the post array of either slug, id or name`, + }) .optional(), authors: z .array( z.union([ z.object({ - id: z.string({ description: "The ID of the author" }), + id: z.string().meta({ description: "The ID of the author" }), }), z.object({ - name: z.string({ description: "The name of the author" }), + name: z.string().meta({ description: "The name of the author" }), }), z.object({ - email: z.string({ description: "The email of the author" }), + email: z.string().meta({ description: "The email of the author" }), }), ]), - { - description: `Specifing author via id, name or slug.`, - }, ) + .meta({ + description: `Specifying author via id, name or slug.`, + }) .optional(), }); diff --git a/packages/ts-ghost-admin-api/src/schemas/posts.ts b/packages/ts-ghost-admin-api/src/schemas/posts.ts index 49474e3e..a77b7de7 100644 --- a/packages/ts-ghost-admin-api/src/schemas/posts.ts +++ b/packages/ts-ghost-admin-api/src/schemas/posts.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { baseEmailSchema, baseNewsletterSchema, basePostsSchema } from "@ts-ghost/core-api"; import { adminAuthorsSchema } from "./authors"; @@ -88,63 +88,63 @@ const basePostsCreateSchema = z.object({ .array( z.union([ z.object({ - id: z.string({ description: "The ID of the tags" }), + id: z.string().meta({ description: "The ID of the tags" }), }), z.object({ - name: z.string({ description: "The name of the tags" }), + name: z.string().meta({ description: "The name of the tags" }), }), z.object({ - slug: z.string({ description: "The slug of the tags" }), + slug: z.string().meta({ description: "The slug of the tags" }), }), ]), - { - description: `The tags associated with the post array of either slug, id or name`, - }, ) + .meta({ + description: `The tags associated with the post array of either slug, id or name`, + }) .optional(), tiers: z .array( z.union([ z.object({ - id: z.string({ description: "The ID of the tiers" }), + id: z.string().meta({ description: "The ID of the tiers" }), }), z.object({ - name: z.string({ description: "The name of the tiers" }), + name: z.string().meta({ description: "The name of the tiers" }), }), z.object({ - slug: z.string({ description: "The slug of the tiers" }), + slug: z.string().meta({ description: "The slug of the tiers" }), }), ]), - { - description: `The tiers associated with the post array of either slug, id or name`, - }, ) + .meta({ + description: `The tiers associated with the post array of either slug, id or name`, + }) .optional(), authors: z .array( z.union([ z.object({ - id: z.string({ description: "The ID of the author" }), + id: z.string().meta({ description: "The ID of the author" }), }), z.object({ - name: z.string({ description: "The name of the author" }), + name: z.string().meta({ description: "The name of the author" }), }), z.object({ - email: z.string({ description: "The email of the author" }), + email: z.string().meta({ description: "The email of the author" }), }), ]), - { - description: `Specifing author via id, name or slug.`, - }, ) + .meta({ + description: `Specifying author via id, name or slug.`, + }) .optional(), newsletter: z .union([ z.object({ - id: z.string({ description: "The ID of the newsletter" }), + id: z.string().meta({ description: "The ID of the newsletter" }), }), z.object({ - slug: z.string({ description: "The slug of the newsletter" }), + slug: z.string().meta({ description: "The slug of the newsletter" }), }), ]) .optional(), diff --git a/packages/ts-ghost-admin-api/src/schemas/tags.ts b/packages/ts-ghost-admin-api/src/schemas/tags.ts index 47cdb686..831d545e 100644 --- a/packages/ts-ghost-admin-api/src/schemas/tags.ts +++ b/packages/ts-ghost-admin-api/src/schemas/tags.ts @@ -1,10 +1,10 @@ -import { z } from "zod/v3"; +import { z } from "zod"; export const adminTagsCreateSchema = z.object({ name: z.string().min(1).max(191), slug: z.string().max(191).optional(), description: z.string().max(500).optional(), - feature_image: z.string().url().optional(), + feature_image: z.url().optional(), visibility: z.union([z.literal("public"), z.literal("internal")]).optional(), meta_title: z.string().max(300).optional(), meta_description: z.string().max(500).optional(), diff --git a/packages/ts-ghost-admin-api/src/schemas/tiers.ts b/packages/ts-ghost-admin-api/src/schemas/tiers.ts index 1af9643d..65de369a 100644 --- a/packages/ts-ghost-admin-api/src/schemas/tiers.ts +++ b/packages/ts-ghost-admin-api/src/schemas/tiers.ts @@ -1,24 +1,24 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { baseTiersSchema } from "@ts-ghost/core-api"; export const adminTiersSchema = baseTiersSchema.merge( z.object({ - monthly_price_id: z.string({ description: "Monthly Stripe price id" }).nullish(), - yearly_price_id: z.string({ description: "Yearly Stripe price id" }).nullish(), + monthly_price_id: z.string().meta({ description: "Monthly Stripe price id" }).nullish(), + yearly_price_id: z.string().meta({ description: "Yearly Stripe price id" }).nullish(), }), ); export type Tiers = z.infer; export const adminTiersCreateSchema = z.object({ - name: z.string({ description: "Name of the tier" }).min(1).max(2000), - description: z.string({ description: "Description of the tier" }).optional(), - welcome_page_url: z.string({ description: "Welcome page URL of the tier" }).optional(), + name: z.string().meta({ description: "Name of the tier" }).min(1).max(2000), + description: z.string().meta({ description: "Description of the tier" }).optional(), + welcome_page_url: z.string().meta({ description: "Welcome page URL of the tier" }).optional(), visibility: z.union([z.literal("public"), z.literal("none")]).optional(), - monthly_price: z.number({ description: "Monthly price of the tier" }).optional(), - yearly_price: z.number({ description: "Yearly price of the tier" }).optional(), - trial_days: z.number({ description: "Trial days of the tier" }).optional(), - benefits: z.array(z.string({ description: "Benefits of the tier" })).optional(), - currency: z.string({ description: "Currency of the tier" }).optional(), - active: z.boolean({ description: "Active of the tier" }).optional(), + monthly_price: z.number().meta({ description: "Monthly price of the tier" }).optional(), + yearly_price: z.number().meta({ description: "Yearly price of the tier" }).optional(), + trial_days: z.number().meta({ description: "Trial days of the tier" }).optional(), + benefits: z.array(z.string().meta({ description: "Benefits of the tier" })).optional(), + currency: z.string().meta({ description: "Currency of the tier" }).optional(), + active: z.boolean().meta({ description: "Active of the tier" }).optional(), }); diff --git a/packages/ts-ghost-admin-api/src/schemas/users.ts b/packages/ts-ghost-admin-api/src/schemas/users.ts index 2828b2c5..07b24604 100644 --- a/packages/ts-ghost-admin-api/src/schemas/users.ts +++ b/packages/ts-ghost-admin-api/src/schemas/users.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { baseAuthorsSchema } from "@ts-ghost/core-api"; export const adminUsersSchema = baseAuthorsSchema.merge( @@ -24,10 +24,10 @@ export const adminUsersSchema = baseAuthorsSchema.merge( description: z.string(), created_at: z.string().nullish(), updated_at: z.string().nullish(), - }), + }) ) .optional(), - }), + }) ); export type User = z.infer; diff --git a/packages/ts-ghost-admin-api/src/schemas/webhooks.ts b/packages/ts-ghost-admin-api/src/schemas/webhooks.ts index 7e09d5a5..8053d104 100644 --- a/packages/ts-ghost-admin-api/src/schemas/webhooks.ts +++ b/packages/ts-ghost-admin-api/src/schemas/webhooks.ts @@ -1,89 +1,99 @@ -import { z } from "zod/v3"; +import { z } from "zod"; export const ghostEventTypes = z.union([ - z.literal("site.changed", { + z.literal("site.changed").meta({ description: "Triggered whenever any content changes in your site data or settings", }), - z.literal("post.added", { description: "Triggered whenever a post is added to Ghost" }), - z.literal("post.deleted", { description: "Triggered whenever a post is deleted from Ghost" }), - z.literal("post.edited", { description: "Triggered whenever a post is edited in Ghost" }), - z.literal("post.published", { description: "Triggered whenever a post is published to Ghost" }), - z.literal("post.published.edited", { + z.literal("post.added").meta({ description: "Triggered whenever a post is added to Ghost" }), + z.literal("post.deleted").meta({ description: "Triggered whenever a post is deleted from Ghost" }), + z.literal("post.edited").meta({ description: "Triggered whenever a post is edited in Ghost" }), + z.literal("post.published").meta({ description: "Triggered whenever a post is published to Ghost" }), + z.literal("post.published.edited").meta({ description: "Triggered whenever a published post is edited in Ghost", }), - z.literal("post.unpublished", { description: "Triggered whenever a post is unpublished from Ghost" }), - z.literal("post.scheduled", { + z.literal("post.unpublished").meta({ description: "Triggered whenever a post is unpublished from Ghost" }), + z.literal("post.scheduled").meta({ description: "Triggered whenever a post is scheduled to be published in Ghost", }), - z.literal("post.unscheduled", { + z.literal("post.unscheduled").meta({ description: "Triggered whenever a post is unscheduled from publishing in Ghost", }), - z.literal("post.rescheduled", { + z.literal("post.rescheduled").meta({ description: "Triggered whenever a post is rescheduled to publish in Ghost", }), - z.literal("page.added", { description: "Triggered whenever a page is added to Ghost" }), - z.literal("page.deleted", { description: "Triggered whenever a page is deleted from Ghost" }), - z.literal("page.edited", { description: "Triggered whenever a page is edited in Ghost" }), - z.literal("page.published", { description: "Triggered whenever a page is published to Ghost" }), - z.literal("page.published.edited", { + z.literal("page.added").meta({ description: "Triggered whenever a page is added to Ghost" }), + z.literal("page.deleted").meta({ description: "Triggered whenever a page is deleted from Ghost" }), + z.literal("page.edited").meta({ description: "Triggered whenever a page is edited in Ghost" }), + z.literal("page.published").meta({ description: "Triggered whenever a page is published to Ghost" }), + z.literal("page.published.edited").meta({ description: "Triggered whenever a published page is edited in Ghost", }), - z.literal("page.unpublished", { description: "Triggered whenever a page is unpublished from Ghost" }), - z.literal("page.scheduled", { + z.literal("page.unpublished").meta({ description: "Triggered whenever a page is unpublished from Ghost" }), + z.literal("page.scheduled").meta({ description: "Triggered whenever a page is scheduled to be published in Ghost", }), - z.literal("page.unscheduled", { + z.literal("page.unscheduled").meta({ description: "Triggered whenever a page is unscheduled from publishing in Ghost", }), - z.literal("page.rescheduled", { + z.literal("page.rescheduled").meta({ description: "Triggered whenever a page is rescheduled to publish in Ghost", }), - z.literal("tag.added", { description: "Triggered whenever a tag is added to Ghost" }), - z.literal("tag.edited", { description: "Triggered whenever a tag is edited in Ghost" }), - z.literal("tag.deleted", { description: "Triggered whenever a tag is deleted from Ghost" }), - z.literal("post.tag.attached", { description: "Triggered whenever a tag is attached to a post in Ghost" }), - z.literal("post.tag.detached", { + z.literal("tag.added").meta({ description: "Triggered whenever a tag is added to Ghost" }), + z.literal("tag.edited").meta({ description: "Triggered whenever a tag is edited in Ghost" }), + z.literal("tag.deleted").meta({ description: "Triggered whenever a tag is deleted from Ghost" }), + z + .literal("post.tag.attached") + .meta({ description: "Triggered whenever a tag is attached to a post in Ghost" }), + z.literal("post.tag.detached").meta({ description: "Triggered whenever a tag is detached from a post in Ghost", }), - z.literal("page.tag.attached", { description: "Triggered whenever a tag is attached to a page in Ghost" }), - z.literal("page.tag.detached", { + z + .literal("page.tag.attached") + .meta({ description: "Triggered whenever a tag is attached to a page in Ghost" }), + z.literal("page.tag.detached").meta({ description: "Triggered whenever a tag is detached from a page in Ghost", }), - z.literal("member.added", { description: "Triggered whenever a member is added to Ghost" }), - z.literal("member.edited", { description: "Triggered whenever a member is edited in Ghost" }), - z.literal("member.deleted", { description: "Triggered whenever a member is deleted from Ghost" }), + z.literal("member.added").meta({ description: "Triggered whenever a member is added to Ghost" }), + z.literal("member.edited").meta({ description: "Triggered whenever a member is edited in Ghost" }), + z.literal("member.deleted").meta({ description: "Triggered whenever a member is deleted from Ghost" }), ]); export type GhostWebhookEventTypes = z.infer; export const adminWebhookSchema = z.object({ - id: z.string({ description: "The ID of the webhook" }), + id: z.string().meta({ description: "The ID of the webhook" }), event: ghostEventTypes, - target_url: z.string({ description: "The URL of the webhook" }).url(), - name: z.string({ description: "The name of the webhook" }).nullish(), - secret: z.string({ description: "The secret of the webhook" }).nullish(), - api_version: z.string({ description: "The API version of the webhook" }).nullish(), - integration_id: z.string({ description: "The ID of the integration" }).nullish(), - status: z.string({ description: "The status of the webhook" }).nullish(), - last_triggered_at: z.string({ description: "The date and time of the last webhook trigger" }).nullish(), - last_triggered_status: z.string({ description: "The status of the last webhook trigger" }).nullish(), - last_triggered_error: z.string({ description: "The error of the last webhook trigger" }).nullish(), - created_at: z.string({ description: "The date and time of the webhook creation" }), - updated_at: z.string({ description: "The date and time of the webhook update" }).nullish(), + target_url: z.url().meta({ description: "The URL of the webhook" }), + name: z.string().meta({ description: "The name of the webhook" }).nullish(), + secret: z.string().meta({ description: "The secret of the webhook" }).nullish(), + api_version: z.string().meta({ description: "The API version of the webhook" }).nullish(), + integration_id: z.string().meta({ description: "The ID of the integration" }).nullish(), + status: z.string().meta({ description: "The status of the webhook" }).nullish(), + last_triggered_at: z + .string() + .meta({ description: "The date and time of the last webhook trigger" }) + .nullish(), + last_triggered_status: z + .string() + .meta({ description: "The status of the last webhook trigger" }) + .nullish(), + last_triggered_error: z.string().meta({ description: "The error of the last webhook trigger" }).nullish(), + created_at: z.string().meta({ description: "The date and time of the webhook creation" }), + updated_at: z.string().meta({ description: "The date and time of the webhook update" }).nullish(), }); export const adminWebhookCreateSchema = z.object({ event: ghostEventTypes, - target_url: z.string({ description: "The URL of the webhook" }).url(), - name: z.string({ description: "The name of the webhook" }).optional(), - secret: z.string({ description: "The secret of the webhook" }).nullish(), - api_version: z.string({ description: "The API version of the webhook" }).nullish(), - integration_id: z.string({ description: "The ID of the integration" }).nullish(), + target_url: z.url().meta({ description: "The URL of the webhook" }), + name: z.string().meta({ description: "The name of the webhook" }).optional(), + secret: z.string().meta({ description: "The secret of the webhook" }).nullish(), + api_version: z.string().meta({ description: "The API version of the webhook" }).nullish(), + integration_id: z.string().meta({ description: "The ID of the integration" }).nullish(), }); export const adminWebhookUpdateSchema = z.object({ event: ghostEventTypes.optional(), - target_url: z.string({ description: "The URL of the webhook" }).url().optional(), - name: z.string({ description: "The name of the webhook" }).optional(), - api_version: z.string({ description: "The API version of the webhook" }).nullish(), + target_url: z.url().meta({ description: "The URL of the webhook" }).optional(), + name: z.string().meta({ description: "The name of the webhook" }).optional(), + api_version: z.string().meta({ description: "The API version of the webhook" }).nullish(), }); diff --git a/packages/ts-ghost-content-api/src/authors/authors.test.ts b/packages/ts-ghost-content-api/src/authors/authors.test.ts index 1d915f80..6c4c8d77 100644 --- a/packages/ts-ghost-content-api/src/authors/authors.test.ts +++ b/packages/ts-ghost-content-api/src/authors/authors.test.ts @@ -62,7 +62,7 @@ describe("authors api .browse() Args Type-safety", () => { }); test(".browse 'fields' argument should ony accept valid fields", () => { - expect( + expect(() => api.authors .browse() .fields({ @@ -70,7 +70,7 @@ describe("authors api .browse() Args Type-safety", () => { foo: true, }) .getOutputFields(), - ).toEqual([]); + ).toThrow(); expect(api.authors.browse().fields({ location: true }).getOutputFields()).toEqual(["location"]); expect(api.authors.browse().fields({ name: true, website: true }).getOutputFields()).toEqual([ diff --git a/packages/ts-ghost-content-api/src/authors/schemas.ts b/packages/ts-ghost-content-api/src/authors/schemas.ts index e67a7a47..a51a851b 100644 --- a/packages/ts-ghost-content-api/src/authors/schemas.ts +++ b/packages/ts-ghost-content-api/src/authors/schemas.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { ghostIdentitySchema, ghostMetadataSchema, ghostMetaSchema } from "@ts-ghost/core-api"; export const authorsSchema = z.object({ diff --git a/packages/ts-ghost-content-api/src/content-api.ts b/packages/ts-ghost-content-api/src/content-api.ts index dc08f5cd..d5f34b74 100644 --- a/packages/ts-ghost-content-api/src/content-api.ts +++ b/packages/ts-ghost-content-api/src/content-api.ts @@ -5,6 +5,7 @@ import { HTTPClientFactory, slugOrIdSchema, } from "@ts-ghost/core-api"; +import { DebugOption } from "@ts-ghost/core-api/helpers/debug"; import { authorsIncludeSchema, authorsSchema } from "./authors/schemas"; import { pagesIncludeSchema, pagesSchema } from "./pages/schemas"; @@ -31,6 +32,7 @@ export class TSGhostContentAPI { test("pages.browse() with mix of incude and fields... this is mostly broken on Ghost side", async () => { const result = await api.pages .browse() - .fields({ slug: true, title: true, primary_author: true }) + .fields({ slug: true, title: true, primary_author: true, authors: true }) .include({ authors: true }) .fetch(); expect(result).not.toBeUndefined(); @@ -173,7 +173,6 @@ describe("pages integration tests browse", () => { expect(page.primary_author?.slug).toBe("phildl"); // @ts-expect-error expect(page.id).toBeUndefined(); - // @ts-expect-error expect(page.authors).toBeUndefined(); } }); diff --git a/packages/ts-ghost-content-api/src/pages/schemas.ts b/packages/ts-ghost-content-api/src/pages/schemas.ts index 34ab096d..de05f19f 100644 --- a/packages/ts-ghost-content-api/src/pages/schemas.ts +++ b/packages/ts-ghost-content-api/src/pages/schemas.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { ghostCodeInjectionSchema, ghostIdentitySchema, diff --git a/packages/ts-ghost-content-api/src/posts/posts.integration.test.ts b/packages/ts-ghost-content-api/src/posts/posts.integration.test.ts index e9a8a376..bd6fa1ef 100644 --- a/packages/ts-ghost-content-api/src/posts/posts.integration.test.ts +++ b/packages/ts-ghost-content-api/src/posts/posts.integration.test.ts @@ -152,7 +152,7 @@ describe("posts integration tests browse", () => { test("posts.browse() with mix of incude and fields... this is mostly broken on Ghost side", async () => { const result = await api.posts .browse() - .fields({ slug: true, title: true, primary_author: true }) + .fields({ slug: true, title: true, primary_author: true, authors: true }) .include({ authors: true }) .fetch(); expect(result).not.toBeUndefined(); @@ -170,7 +170,6 @@ describe("posts integration tests browse", () => { expect(post.primary_author?.slug).toBe("phildl"); // @ts-expect-error expect(post.id).toBeUndefined(); - // @ts-expect-error expect(post.authors).toBeUndefined(); } }); diff --git a/packages/ts-ghost-content-api/src/posts/posts.test.ts b/packages/ts-ghost-content-api/src/posts/posts.test.ts index c815be70..519a9c03 100644 --- a/packages/ts-ghost-content-api/src/posts/posts.test.ts +++ b/packages/ts-ghost-content-api/src/posts/posts.test.ts @@ -20,11 +20,13 @@ describe("posts api .browse() Args Type-safety", () => { foo: true, } satisfies { [k in keyof Post]?: true | undefined }; - let test = api.posts - .browse() - // @ts-expect-error - shouldnt accept invalid params - .fields(outputFields); - expect(test.getOutputFields()).toEqual(["slug", "title"]); + expect(() => + api.posts + .browse() + // @ts-expect-error - shouldnt accept invalid params + .fields(outputFields), + ).toThrow(); + // expect(test.getOutputFields()).toEqual(["slug", "title"]); const fields = ["slug", "title", "foo"] as const; const unknownOriginFields = fields.reduce( @@ -34,8 +36,8 @@ describe("posts api .browse() Args Type-safety", () => { }, {} as { [k in keyof Post]?: true | undefined }, ); - const result = api.posts.browse().fields(unknownOriginFields); - expect(result.getOutputFields()).toEqual(["slug", "title"]); + expect(() => api.posts.browse().fields(unknownOriginFields)).toThrow(); + // expect(result.getOutputFields()).toEqual(["slug", "title"]); }); test(".browse() params, output fields declare const", () => { const outputFields = { diff --git a/packages/ts-ghost-content-api/src/posts/schemas.ts b/packages/ts-ghost-content-api/src/posts/schemas.ts index 03268056..226f793e 100644 --- a/packages/ts-ghost-content-api/src/posts/schemas.ts +++ b/packages/ts-ghost-content-api/src/posts/schemas.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { ghostCodeInjectionSchema, ghostIdentitySchema, diff --git a/packages/ts-ghost-content-api/src/settings/schemas.ts b/packages/ts-ghost-content-api/src/settings/schemas.ts index c7b2bf7b..6fb4ab9c 100644 --- a/packages/ts-ghost-content-api/src/settings/schemas.ts +++ b/packages/ts-ghost-content-api/src/settings/schemas.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; export const settingsSchema = z.object({ title: z.string(), @@ -17,13 +17,13 @@ export const settingsSchema = z.object({ z.object({ label: z.string(), url: z.string(), - }), + }) ), secondary_navigation: z.array( z.object({ label: z.string(), url: z.string(), - }), + }) ), meta_title: z.string().nullable(), meta_description: z.string().nullable(), diff --git a/packages/ts-ghost-content-api/src/tags/schemas.ts b/packages/ts-ghost-content-api/src/tags/schemas.ts index 08d2a49a..62abbb20 100644 --- a/packages/ts-ghost-content-api/src/tags/schemas.ts +++ b/packages/ts-ghost-content-api/src/tags/schemas.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { ghostCodeInjectionSchema, ghostIdentitySchema, diff --git a/packages/ts-ghost-content-api/src/tiers/schemas.ts b/packages/ts-ghost-content-api/src/tiers/schemas.ts index fba65641..81caf128 100644 --- a/packages/ts-ghost-content-api/src/tiers/schemas.ts +++ b/packages/ts-ghost-content-api/src/tiers/schemas.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { ghostIdentitySchema, ghostVisibilitySchema } from "@ts-ghost/core-api"; export const tiersSchema = z.object({ diff --git a/packages/ts-ghost-core-api/CHANGELOG.md b/packages/ts-ghost-core-api/CHANGELOG.md index b7c4cc3e..e9d54601 100644 --- a/packages/ts-ghost-core-api/CHANGELOG.md +++ b/packages/ts-ghost-core-api/CHANGELOG.md @@ -472,7 +472,7 @@ - `output` option was removed because it is no longer necessary at the QueryBuilder level (kept on Fetchers) ```ts - import { z } from "zod/v3"; + import { z } from "zod"; import { QueryBuilder, type ContentAPICredentials } from "@ts-ghost/core-api"; const api: ContentAPICredentials = { @@ -489,10 +489,7 @@ }); // the "identity" schema is used to validate the inputs of the `read`method of the QueryBuilder - const identitySchema = z.union([ - z.object({ slug: z.string() }), - z.object({ id: z.string() }), - ]); + const identitySchema = z.union([z.object({ slug: z.string() }), z.object({ id: z.string() })]); const simplifiedIncludeSchema = z.object({ count: z.literal(true).optional(), @@ -514,7 +511,7 @@ Example: ```ts - import { z } from "zod/v3"; + import { z } from "zod"; import { adminMembersSchema, QueryBuilder } from "@ts-ghost/core-api"; const membersIncludeSchema = z.object({}); diff --git a/packages/ts-ghost-core-api/README.md b/packages/ts-ghost-core-api/README.md index 742911e3..5f2b4001 100644 --- a/packages/ts-ghost-core-api/README.md +++ b/packages/ts-ghost-core-api/README.md @@ -55,7 +55,7 @@ All these methods like `read` and `browse` gives you back the appropriate `Fetch ### Instantiation ```ts -import { z } from "zod/v3"; +import { z } from "zod"; import { APIComposer, type ContentAPICredentials } from "@ts-ghost/core-api"; const api: ContentAPICredentials = { @@ -115,7 +115,7 @@ After instantiation you can use the `APIComposer` to build your queries with 2 a The `browse` and `read` methods accept a config object with 2 properties: `input` and an `output`. These params mimic the way Ghost API Content is built but with the power of Zod and TypeScript they are type-safe here. ```ts -import { z } from "zod/v3"; +import { z } from "zod"; import { APIComposer, type ContentAPICredentials } from "@ts-ghost/core-api"; const api: ContentAPICredentials = { diff --git a/packages/ts-ghost-core-api/src/api-composer.test.ts b/packages/ts-ghost-core-api/src/api-composer.test.ts index 546c5c2e..97e70d21 100644 --- a/packages/ts-ghost-core-api/src/api-composer.test.ts +++ b/packages/ts-ghost-core-api/src/api-composer.test.ts @@ -1,6 +1,6 @@ import createFetchMock from "vitest-fetch-mock"; import { describe, expect, test } from "vitest"; -import { z } from "zod/v3"; +import { z } from "zod"; import { APIComposer } from "./api-composer"; import { BrowseFetcher, ReadFetcher } from "./fetchers"; diff --git a/packages/ts-ghost-core-api/src/api-composer.ts b/packages/ts-ghost-core-api/src/api-composer.ts index eccdc3ea..e8120f79 100644 --- a/packages/ts-ghost-core-api/src/api-composer.ts +++ b/packages/ts-ghost-core-api/src/api-composer.ts @@ -1,4 +1,5 @@ -import { z, ZodRawShape, ZodTypeAny } from "zod/v3"; +import { z, ZodObject as ZodObjectV4, ZodRawShape } from "zod"; +import * as z4 from "zod/v4/core"; import { DeleteFetcher } from "./fetchers"; import { BrowseFetcher } from "./fetchers/browse-fetcher"; @@ -9,8 +10,8 @@ import type { HTTPClientFactory } from "./helpers/http-client"; import type { APIResource } from "./schemas"; import type { IsAny } from "./utils"; -function isZodObject(schema: z.ZodObject | z.ZodTypeAny): schema is z.ZodObject { - return (schema as z.ZodObject).partial !== undefined; +function isZodObject(schema: z4.$ZodType): schema is ZodObjectV4 { + return (schema as ZodObjectV4).partial !== undefined; } /** @@ -19,12 +20,12 @@ function isZodObject(schema: z.ZodObject | z.ZodTypeAny): schema is z.ZodOb export class APIComposer< const Resource extends APIResource = any, Shape extends ZodRawShape = any, - IdentityShape extends z.ZodTypeAny = any, + IdentityShape extends z4.$ZodType<{ slug?: string; id?: string; email?: string }> = any, IncludeShape extends ZodRawShape = any, - CreateShape extends ZodTypeAny = any, - CreateOptions extends ZodTypeAny = any, - UpdateShape extends ZodTypeAny = any, - UpdateOptions extends ZodTypeAny = any, + CreateSchema extends z4.$ZodType = any, + CreateOptions extends z4.$ZodType = any, + UpdateSchema extends z4.$ZodObject = any, + UpdateOptions extends z4.$ZodObject = any, > { constructor( protected resource: Resource, @@ -32,9 +33,9 @@ export class APIComposer< schema: z.ZodObject; identitySchema: IdentityShape; include: z.ZodObject; - createSchema?: CreateShape; + createSchema?: CreateSchema; createOptionsSchema?: CreateOptions; - updateSchema?: UpdateShape; + updateSchema?: UpdateSchema; updateOptionsSchema?: UpdateOptions; }, protected httpClientFactory: HTTPClientFactory, @@ -73,7 +74,7 @@ export class APIComposer< * Read function that accepts Identify fields like id, slug or email. Will return an instance * of ReadFetcher class. */ - public read(options: z.infer) { + public read(options: z4.infer) { return new ReadFetcher( this.resource, { @@ -82,20 +83,20 @@ export class APIComposer< include: this.config.include, }, { - identity: this.config.identitySchema.parse(options), + identity: z.parse(this.config.identitySchema, options), }, this.httpClientFactory.create(), ); } - public async add(data: z.input, options?: z.infer) { + public async add(data: z4.input, options?: z4.infer) { if (!this.config.createSchema) { throw new Error("No createSchema defined"); } - const parsedData = this.config.createSchema.parse(data); + const parsedData = z.parse(this.config.createSchema, data); const parsedOptions = this.config.createOptionsSchema && options - ? this.config.createOptionsSchema.parse(options) + ? z.parse(this.config.createOptionsSchema, options) : undefined; const fetcher = new MutationFetcher( this.resource, @@ -103,8 +104,8 @@ export class APIComposer< output: this.config.schema, paramsShape: this.config.createOptionsSchema, }, - parsedOptions, - { method: "POST", body: parsedData }, + parsedOptions as ({ id?: string } & z4.output) | undefined, + { method: "POST", body: parsedData as Record }, this.httpClientFactory.create(), ); return fetcher.submit(); @@ -112,20 +113,20 @@ export class APIComposer< public async edit( id: string, - data: IsAny extends true ? Partial> : z.input, - options?: z.infer, + data: IsAny extends true ? Partial> : z4.input, + options?: z4.infer, ) { - let updateSchema: z.ZodTypeAny | z.ZodObject | undefined = this.config.updateSchema; + let updateSchema: z4.$ZodObject | undefined = this.config.updateSchema; if (!this.config.updateSchema && this.config.createSchema && isZodObject(this.config.createSchema)) { updateSchema = this.config.createSchema.partial(); } if (!updateSchema) { throw new Error("No updateSchema defined"); } - const cleanId = z.string().nonempty().parse(id); - const parsedData = updateSchema.parse(data); + const cleanId = z.string().min(1).parse(id); + const parsedData = z.parse(updateSchema, data); const parsedOptions = - this.config.updateOptionsSchema && options ? this.config.updateOptionsSchema.parse(options) : {}; + this.config.updateOptionsSchema && options ? z.parse(this.config.updateOptionsSchema, options) : {}; if (Object.keys(parsedData).length === 0) { throw new Error("No data to edit"); @@ -136,7 +137,7 @@ export class APIComposer< output: this.config.schema, paramsShape: this.config.updateOptionsSchema, }, - { id: cleanId, ...parsedOptions }, + { id: cleanId, ...parsedOptions } as ({ id?: string } & z4.output) | undefined, { method: "PUT", body: parsedData }, this.httpClientFactory.create(), ); @@ -144,7 +145,7 @@ export class APIComposer< } public async delete(id: string) { - const cleanId = z.string().nonempty().parse(id); + const cleanId = z.string().min(1).parse(id); const fetcher = new DeleteFetcher(this.resource, { id: cleanId }, this.httpClientFactory.create()); return fetcher.submit(); } diff --git a/packages/ts-ghost-core-api/src/fetchers/basic-fetcher.test.ts b/packages/ts-ghost-core-api/src/fetchers/basic-fetcher.test.ts index 4dbd81bb..8bc74210 100644 --- a/packages/ts-ghost-core-api/src/fetchers/basic-fetcher.test.ts +++ b/packages/ts-ghost-core-api/src/fetchers/basic-fetcher.test.ts @@ -1,6 +1,6 @@ import createFetchMock from "vitest-fetch-mock"; import { afterEach, assert, describe, expect, test, vi } from "vitest"; -import { z } from "zod/v3"; +import { z } from "zod"; import { HTTPClient, HTTPClientOptions } from "../helpers/http-client"; import { BasicFetcher } from "./basic-fetcher"; diff --git a/packages/ts-ghost-core-api/src/fetchers/basic-fetcher.ts b/packages/ts-ghost-core-api/src/fetchers/basic-fetcher.ts index 1c62b9b7..85aba001 100644 --- a/packages/ts-ghost-core-api/src/fetchers/basic-fetcher.ts +++ b/packages/ts-ghost-core-api/src/fetchers/basic-fetcher.ts @@ -1,5 +1,6 @@ -import { z, ZodTypeAny } from "zod/v3"; +import { z, ZodTypeAny } from "zod"; +import { DebugOption } from "../helpers/debug"; import type { HTTPClient } from "../helpers/http-client"; import type { APIResource } from "../schemas/shared"; @@ -16,7 +17,7 @@ export class BasicFetcher { + const credentials: HTTPClientOptions = { + url: "https://ghost.org", + key: "1234", + version: "v6.0", + endpoint: "content", + }; + let httpClient: HTTPClient; + + const simplifiedSchema = z.object({ + title: z.string(), + slug: z.string(), + published: z.boolean().optional(), + includeMe: z.number().optional(), + html: z.string().optional(), + }); + + const simplifiedIncludeSchema = z.object({ + includeMe: z.literal(true).optional(), + }); + + beforeEach(() => { + httpClient = new HTTPClient(credentials); + fetchMocker.enableMocks(); + }); + afterEach(() => { + fetchMocker.resetMocks(); + vi.restoreAllMocks(); + }); + + test("that BrowseFetcher returns the correct types", async () => { + const browseFetcher = new BrowseFetcher( + "posts", + { + schema: simplifiedSchema, + output: simplifiedSchema, + include: simplifiedIncludeSchema, + }, + { + browseParams: {}, + formats: ["html"], + }, + httpClient, + ); + fetchMocker.doMock(fixture); + expectTypeOf(await browseFetcher.fetch()).toEqualTypeOf< + | { + success: true; + meta: { + pagination: { + pages: number; + page: number; + limit: number | "all"; + total: number; + prev: number | null; + next: number | null; + }; + }; + data: { + title: string; + slug: string; + published?: boolean | undefined; + includeMe?: number | undefined; + html?: string | undefined; + }[]; + } + | { + success: false; + errors: { + type: string; + message: string; + }[]; + } + >(); + expectTypeOf(await browseFetcher.fields({ title: true, html: true }).fetch()).toEqualTypeOf< + | { + success: true; + meta: { + pagination: { + pages: number; + page: number; + limit: number | "all"; + total: number; + prev: number | null; + next: number | null; + }; + }; + data: { + title: string; + html?: string | undefined; + }[]; + } + | { + success: false; + errors: { + type: string; + message: string; + }[]; + } + >(); + + expectTypeOf( + await browseFetcher.fields({ title: true, html: true }).formats({ html: true }).fetch(), + ).toEqualTypeOf< + | { + success: true; + meta: { + pagination: { + pages: number; + page: number; + limit: number | "all"; + total: number; + prev: number | null; + next: number | null; + }; + }; + data: { + title: string; + html: string; + }[]; + } + | { + success: false; + errors: { + type: string; + message: string; + }[]; + } + >(); + }); +}); diff --git a/packages/ts-ghost-core-api/src/fetchers/browse-fetcher.test.ts b/packages/ts-ghost-core-api/src/fetchers/browse-fetcher.test.ts index b0074955..3b481449 100644 --- a/packages/ts-ghost-core-api/src/fetchers/browse-fetcher.test.ts +++ b/packages/ts-ghost-core-api/src/fetchers/browse-fetcher.test.ts @@ -1,6 +1,6 @@ import createFetchMock from "vitest-fetch-mock"; import { assert, describe, expect, test } from "vitest"; -import { z } from "zod/v3"; +import { z } from "zod"; import { HTTPClient, type HTTPClientOptions } from "../helpers"; import { BrowseFetcher } from "./browse-fetcher"; @@ -247,7 +247,7 @@ describe("BrowseFetcher", () => { ); const result = await browseFetcher.fetch(); expect(fetchMocker).toHaveBeenCalledWith( - "https://ghost.org/ghost/api/content/posts/?order=title+DESC&limit=10&fields=title%2Cslug%2Ccount&include=count&key=1234", + "https://ghost.org/ghost/api/content/posts/?order=title+DESC&limit=10&fields=title%2Cslug&include=count&key=1234", { headers: { "Content-Type": "application/json", @@ -436,7 +436,7 @@ describe("BrowseFetcher", () => { fetchMocker.doMockOnce(postsStub); await browseFetcher.fetch(); expect(fetchMocker).toHaveBeenCalledWith( - "https://ghost.org/ghost/api/admin/posts/?order=title+DESC&limit=10&fields=title%2Cslug%2Ccount&include=count", + "https://ghost.org/ghost/api/admin/posts/?order=title+DESC&limit=10&fields=title%2Cslug&include=count", { headers: { "Content-Type": "application/json", @@ -756,7 +756,7 @@ describe("BrowseFetcher output tests suite", () => { ); const res = fetcher .formats({ html: true }) - .include({ count: true, "nested.key": true }) + .include({ "nested.key": true, count: true }) .fields({ html: true, published: true, count: true }); expect(res.getIncludes()).toStrictEqual(["count", "nested.key"]); expect(res.getOutputFields()).toStrictEqual(["html", "published", "count"]); @@ -765,7 +765,7 @@ describe("BrowseFetcher output tests suite", () => { fetchMocker.doMockOnce(fixture); await res.fetch(); expect(fetchMocker).toHaveBeenCalledWith( - "https://ghost.org/ghost/api/content/posts/?fields=html%2Cpublished%2Ccount&include=count%2Cnested.key&formats=html&key=1234", + "https://ghost.org/ghost/api/content/posts/?fields=html%2Cpublished&include=count%2Cnested.key&formats=html&key=1234", { headers: { "Content-Type": "application/json", @@ -785,26 +785,28 @@ describe("BrowseFetcher output tests suite", () => { {}, httpClient, ); - const res = fetcher - // @ts-expect-error - foobar is not defined - .formats({ html: true, foobar: true }) - // @ts-expect-error - foo is not in the include schema - .include({ count: true, foo: true }) - // @ts-expect-error - barbaz is not in the output schema schema - .fields({ html: true, published: true, count: true, barbaz: true }); - expect(res.getIncludes()).toStrictEqual(["count"]); - expect(res.getOutputFields()).toStrictEqual(["html", "published", "count"]); - expect(res.getFormats()).toStrictEqual(["html"]); - fetchMocker.doMockOnce(fixture); - await res.fetch(); - expect(fetchMocker).toHaveBeenCalledWith( - "https://ghost.org/ghost/api/content/posts/?fields=html%2Cpublished%2Ccount&include=count&formats=html&key=1234", - { - headers: { - "Content-Type": "application/json", - "Accept-Version": "v6.0", - }, - }, - ); + expect(() => + fetcher + // @ts-expect-error - foobar is not defined + .formats({ html: true, foobar: true }) + // @ts-expect-error - foo is not in the include schema + .include({ count: true, foo: true }) + // @ts-expect-error - barbaz is not in the output schema schema + .fields({ html: true, published: true, count: true, barbaz: true }), + ).toThrow(); + // expect(res.getIncludes()).toStrictEqual(["count"]); + // expect(res.getOutputFields()).toStrictEqual(["html", "published", "count"]); + // expect(res.getFormats()).toStrictEqual(["html"]); + // fetchMocker.doMockOnce(fixture); + // await res.fetch(); + // expect(fetchMocker).toHaveBeenCalledWith( + // "https://ghost.org/ghost/api/content/posts/?fields=html%2Cpublished&include=count&formats=html&key=1234", + // { + // headers: { + // "Content-Type": "application/json", + // "Accept-Version": "v6.0", + // }, + // }, + // ); }); }); diff --git a/packages/ts-ghost-core-api/src/fetchers/browse-fetcher.ts b/packages/ts-ghost-core-api/src/fetchers/browse-fetcher.ts index da132086..731d96f2 100644 --- a/packages/ts-ghost-core-api/src/fetchers/browse-fetcher.ts +++ b/packages/ts-ghost-core-api/src/fetchers/browse-fetcher.ts @@ -1,9 +1,10 @@ -import { z, ZodRawShape } from "zod/v3"; +import { z, ZodRawShape } from "zod"; import { BrowseParamsSchema } from "../helpers/browse-params"; +import { DebugOption } from "../helpers/debug"; import type { HTTPClient } from "../helpers/http-client"; import { ghostMetaSchema, type APIResource } from "../schemas/shared"; -import type { Exactly, Mask } from "../utils"; +import type { Exactly, Mask, NoUnrecognizedKeys } from "../utils"; import { contentFormats, type ContentFormats } from "./formats"; export class BrowseFetcher< @@ -30,7 +31,7 @@ export class BrowseFetcher< include?: (keyof IncludeShape)[]; fields?: Fields; formats?: string[]; - } = { browseParams: {} as Params, include: [], fields: {} as z.noUnrecognized }, + } = { browseParams: {} as Params, include: [], fields: {} as NoUnrecognizedKeys }, protected httpClient: HTTPClient, ) { this._buildUrlParams(); @@ -45,7 +46,7 @@ export class BrowseFetcher< * @returns A new Fetcher with the fixed output shape and the formats specified */ public formats>>( - formats: z.noUnrecognized, + formats: NoUnrecognizedKeys, ) { const params = { ...this._params, @@ -71,16 +72,23 @@ export class BrowseFetcher< * @param include Include specific keys from the include shape * @returns A new Fetcher with the fixed output shape and the formats specified */ - public include>(include: z.noUnrecognized) { + public include>(include: NoUnrecognizedKeys) { const params = { ...this._params, include: Object.keys(this.config.include.parse(include)), }; + // remove dot-notation from the include object key + const requiredIncludeKeys = Object.fromEntries( + Object.keys(include) + .filter((key) => !key.includes(".")) + .map((key) => [key, include[key]]), + ); + return new BrowseFetcher( this.resource, { schema: this.config.schema, - output: this.config.output.required(include as Exactly), + output: this.config.output.required(requiredIncludeKeys as Exactly), include: this.config.include, }, params, @@ -95,7 +103,7 @@ export class BrowseFetcher< * @param fields Any keys from the resource Schema * @returns A new Fetcher with the fixed output shape having only the selected Fields */ - public fields>(fields: z.noUnrecognized) { + public fields>(fields: NoUnrecognizedKeys) { const newOutput = this.config.output.pick(fields as Exactly); return new BrowseFetcher( this.resource, @@ -141,7 +149,7 @@ export class BrowseFetcher< }; if (inputKeys.length !== outputKeys.length && outputKeys.length > 0) { - this._urlParams.fields = outputKeys.join(","); + this._urlParams.fields = outputKeys.filter((key) => key !== "count").join(","); } if (this._params.include && this._params.include.length > 0) { this._urlParams.include = this._params.include.join(","); @@ -190,7 +198,7 @@ export class BrowseFetcher< ]); } - public async fetch(options?: RequestInit) { + public async fetch(options?: RequestInit & DebugOption) { const resultSchema = this._getResultSchema(); const result = await this.httpClient.fetch({ resource: this.resource, @@ -220,7 +228,7 @@ export class BrowseFetcher< return resultSchema.parse(data); } - public async paginate(options?: RequestInit) { + public async paginate(options?: RequestInit & DebugOption) { if (!this._params.browseParams?.page) { this._params.browseParams = { ...this._params.browseParams, diff --git a/packages/ts-ghost-core-api/src/fetchers/delete-fetcher.ts b/packages/ts-ghost-core-api/src/fetchers/delete-fetcher.ts index c7f194af..9555bd36 100644 --- a/packages/ts-ghost-core-api/src/fetchers/delete-fetcher.ts +++ b/packages/ts-ghost-core-api/src/fetchers/delete-fetcher.ts @@ -1,5 +1,6 @@ -import { z } from "zod/v3"; +import { z } from "zod"; +import { DebugOption } from "../helpers/debug"; import type { HTTPClient } from "../helpers/http-client"; import type { APIResource } from "../schemas/shared"; @@ -29,7 +30,7 @@ export class DeleteFetcher { this._pathnameIdentity = this._params.id; } - public async submit() { + public async submit(options?: RequestInit & DebugOption) { const schema = z.discriminatedUnion("success", [ z.object({ success: z.literal(true), @@ -51,6 +52,7 @@ export class DeleteFetcher { resource: this.resource, pathnameIdentity: this._pathnameIdentity, options: { + ...options, method: "DELETE", }, }); diff --git a/packages/ts-ghost-core-api/src/fetchers/mutation-fetcher.test.ts b/packages/ts-ghost-core-api/src/fetchers/mutation-fetcher.test.ts index 91580f4e..e7e33403 100644 --- a/packages/ts-ghost-core-api/src/fetchers/mutation-fetcher.test.ts +++ b/packages/ts-ghost-core-api/src/fetchers/mutation-fetcher.test.ts @@ -1,6 +1,6 @@ import createFetchMock from "vitest-fetch-mock"; import { describe, expect, test } from "vitest"; -import { z } from "zod/v3"; +import { z } from "zod"; import { HTTPClient, type HTTPClientOptions } from "../helpers/http-client"; import { MutationFetcher } from "./mutation-fetcher"; diff --git a/packages/ts-ghost-core-api/src/fetchers/mutation-fetcher.ts b/packages/ts-ghost-core-api/src/fetchers/mutation-fetcher.ts index bc04eae6..852888e4 100644 --- a/packages/ts-ghost-core-api/src/fetchers/mutation-fetcher.ts +++ b/packages/ts-ghost-core-api/src/fetchers/mutation-fetcher.ts @@ -1,12 +1,14 @@ -import { z, ZodRawShape, ZodTypeAny } from "zod/v3"; +import { z } from "zod"; +import * as z4 from "zod/v4/core"; +import { DebugOption } from "../helpers/debug"; import { HTTPClient } from "../helpers/http-client"; import type { APIResource } from "../schemas/shared"; export class MutationFetcher< const Resource extends APIResource = any, - OutputShape extends ZodRawShape = any, - ParamsShape extends ZodTypeAny = any, + OutputShape extends z4.$ZodType = any, + ParamsShape extends z4.$ZodType = any, const HTTPVerb extends "POST" | "PUT" = "POST", > { protected _urlParams: Record = {}; @@ -16,10 +18,10 @@ export class MutationFetcher< constructor( protected resource: Resource, protected config: { - output: z.ZodObject; + output: OutputShape; paramsShape?: ParamsShape; }, - private _params: ({ id?: string } & ParamsShape["_output"]) | undefined, + private _params: ({ id?: string } & z4.output) | undefined, protected _options: { method: HTTPVerb; body: Record; @@ -55,7 +57,7 @@ export class MutationFetcher< } } - public async submit() { + public async submit(options?: RequestInit & DebugOption) { const schema = z.discriminatedUnion("success", [ z.object({ success: z.literal(true), @@ -84,10 +86,7 @@ export class MutationFetcher< resource: this.resource, searchParams: this._urlSearchParams, pathnameIdentity: this._pathnameIdentity, - options: { - method: this._options.method, - body: JSON.stringify(createData), - }, + options: { ...options, method: this._options.method, body: JSON.stringify(createData) }, }); let result: any = {}; if (response.errors) { diff --git a/packages/ts-ghost-core-api/src/fetchers/read-fetcher.test-d.ts b/packages/ts-ghost-core-api/src/fetchers/read-fetcher.test-d.ts new file mode 100644 index 00000000..fe17c71e --- /dev/null +++ b/packages/ts-ghost-core-api/src/fetchers/read-fetcher.test-d.ts @@ -0,0 +1,121 @@ +import createFetchMock from "vitest-fetch-mock"; +import { describe, expectTypeOf, test } from "vitest"; +import { z } from "zod"; + +import { HTTPClient, HTTPClientOptions } from "../helpers/http-client"; +import { ReadFetcher } from "./read-fetcher"; + +const fetchMocker = createFetchMock(vi); + +const fixture = JSON.stringify({ + posts: [ + { + title: "title", + slug: "this-is-a-slug-test", + includeMe: 1, + html: "html", + }, + ], +}); + +describe("ReadFetcher", () => { + const credentials: HTTPClientOptions = { + url: "https://ghost.org", + key: "1234", + version: "v6.0", + endpoint: "content", + }; + let httpClient: HTTPClient; + + const simplifiedSchema = z.object({ + title: z.string(), + slug: z.string(), + published: z.boolean().optional(), + includeMe: z.number().optional(), + html: z.string().optional(), + }); + + const simplifiedIncludeSchema = z.object({ + includeMe: z.literal(true).optional(), + }); + + beforeEach(() => { + httpClient = new HTTPClient(credentials); + fetchMocker.enableMocks(); + }); + afterEach(() => { + fetchMocker.resetMocks(); + vi.restoreAllMocks(); + }); + + test("that ReadFetcher returns the correct types", async () => { + const readFetcher = new ReadFetcher( + "posts", + { + schema: simplifiedSchema, + output: simplifiedSchema, + include: simplifiedIncludeSchema, + }, + { + identity: { id: "eh873jdLsnaUDj7149DSASJhdqsdj" }, + formats: ["html"], + }, + httpClient, + ); + fetchMocker.doMock(fixture); + expectTypeOf(await readFetcher.fetch()).toEqualTypeOf< + | { + success: true; + data: { + title: string; + slug: string; + published?: boolean | undefined; + includeMe?: number | undefined; + html?: string | undefined; + }; + } + | { + success: false; + errors: { + type: string; + message: string; + }[]; + } + >(); + expectTypeOf(await readFetcher.fields({ title: true, html: true }).fetch()).toEqualTypeOf< + | { + success: true; + data: { + title: string; + html?: string | undefined; + }; + } + | { + success: false; + errors: { + type: string; + message: string; + }[]; + } + >(); + + expectTypeOf( + await readFetcher.fields({ title: true, html: true }).formats({ html: true }).fetch(), + ).toEqualTypeOf< + | { + success: true; + data: { + title: string; + html: string; + }; + } + | { + success: false; + errors: { + type: string; + message: string; + }[]; + } + >(); + }); +}); diff --git a/packages/ts-ghost-core-api/src/fetchers/read-fetcher.test.ts b/packages/ts-ghost-core-api/src/fetchers/read-fetcher.test.ts index c36c7816..a3ced0a7 100644 --- a/packages/ts-ghost-core-api/src/fetchers/read-fetcher.test.ts +++ b/packages/ts-ghost-core-api/src/fetchers/read-fetcher.test.ts @@ -1,6 +1,6 @@ import createFetchMock from "vitest-fetch-mock"; import { assert, describe, expect, test } from "vitest"; -import { z } from "zod/v3"; +import { z } from "zod"; import { HTTPClient, HTTPClientOptions } from "../helpers/http-client"; import { ReadFetcher } from "./read-fetcher"; @@ -491,27 +491,29 @@ describe("ReadFetcherFetcher outputs test suite", () => { { identity: { slug: "this-is-a-slug" } }, httpClient, ); - const res = fetcher - // @ts-expect-error - foobar is not defined - .formats({ html: true, foobar: true }) - // @ts-expect-error - foo is not in the include schema - .include({ count: true, foo: true }) - // @ts-expect-error - barbaz is not in the output schema schema - .fields({ html: true, published: true, count: true, barbaz: true }); - expect(res.getIncludes()).toStrictEqual(["count"]); - expect(res.getOutputFields()).toStrictEqual(["html", "published", "count"]); - expect(res.getFormats()).toStrictEqual(["html"]); - fetchMocker.doMockOnce(fixture); - await res.fetch(); - expect(fetchMocker).toHaveBeenCalledTimes(1); - expect(fetchMocker).toHaveBeenCalledWith( - "https://ghost.org/ghost/api/content/posts/slug/this-is-a-slug/?fields=html%2Cpublished%2Ccount&include=count&formats=html&key=1234", - { - headers: { - "Content-Type": "application/json", - "Accept-Version": "v6.0", - }, - }, - ); + expect(() => + fetcher + // @ts-expect-error - foobar is not defined + .formats({ html: true, foobar: true }) + // @ts-expect-error - foo is not in the include schema + .include({ count: true, foo: true }) + // @ts-expect-error - barbaz is not in the output schema schema + .fields({ html: true, published: true, count: true, barbaz: true }), + ).toThrow(); + // expect(res.getIncludes()).toStrictEqual(["count"]); + // expect(res.getOutputFields()).toStrictEqual(["html", "published", "count"]); + // expect(res.getFormats()).toStrictEqual(["html"]); + // fetchMocker.doMockOnce(fixture); + // await res.fetch(); + // expect(fetchMocker).toHaveBeenCalledTimes(1); + // expect(fetchMocker).toHaveBeenCalledWith( + // "https://ghost.org/ghost/api/content/posts/slug/this-is-a-slug/?fields=html%2Cpublished%2Ccount&include=count&formats=html&key=1234", + // { + // headers: { + // "Content-Type": "application/json", + // "Accept-Version": "v6.0", + // }, + // }, + // ); }); }); diff --git a/packages/ts-ghost-core-api/src/fetchers/read-fetcher.ts b/packages/ts-ghost-core-api/src/fetchers/read-fetcher.ts index 3c6be857..c3ef9543 100644 --- a/packages/ts-ghost-core-api/src/fetchers/read-fetcher.ts +++ b/packages/ts-ghost-core-api/src/fetchers/read-fetcher.ts @@ -1,8 +1,9 @@ -import { z, ZodRawShape } from "zod/v3"; +import { z, ZodRawShape } from "zod"; +import { DebugOption } from "../helpers/debug"; import type { HTTPClient } from "../helpers/http-client"; import { type APIResource, type GhostIdentityInput } from "../schemas/shared"; -import type { Exactly, Mask } from "../utils"; +import type { Exactly, Mask, NoUnrecognizedKeys } from "../utils"; import { contentFormats, type ContentFormats } from "./formats"; export class ReadFetcher< @@ -44,8 +45,9 @@ export class ReadFetcher< * @returns A new Fetcher with the fixed output shape and the formats specified */ public formats>>( - formats: z.noUnrecognized, + formats: NoUnrecognizedKeys, ) { + const newOutput = this.config.output.required(formats as Exactly); const params = { ...this._params, formats: Object.keys(formats).filter((key) => contentFormats.includes(key)), @@ -54,7 +56,7 @@ export class ReadFetcher< this.resource, { schema: this.config.schema, - output: this.config.output.required(formats as Exactly), + output: newOutput, include: this.config.include, }, params, @@ -70,16 +72,22 @@ export class ReadFetcher< * @param include Include specific keys from the include shape * @returns A new Fetcher with the fixed output shape and the formats specified */ - public include>(include: z.noUnrecognized) { + public include>(include: NoUnrecognizedKeys) { const params = { ...this._params, include: Object.keys(this.config.include.parse(include)), }; + // remove dot-notation from the include object key + const requiredIncludeKeys = Object.fromEntries( + Object.keys(include) + .filter((key) => !key.includes(".")) + .map((key) => [key, include[key]]), + ); return new ReadFetcher( this.resource, { schema: this.config.schema, - output: this.config.output.required(include as Exactly), + output: this.config.output.required(requiredIncludeKeys as Exactly), include: this.config.include, }, params, @@ -94,7 +102,7 @@ export class ReadFetcher< * @param fields Any keys from the resource Schema * @returns A new Fetcher with the fixed output shape having only the selected Fields */ - public fields>(fields: z.noUnrecognized) { + public fields>(fields: NoUnrecognizedKeys) { const newOutput = this.config.output.pick(fields as Exactly); return new ReadFetcher( this.resource, @@ -157,7 +165,7 @@ export class ReadFetcher< } } - public async fetch(options?: RequestInit) { + public async fetch(options?: RequestInit & DebugOption) { const res = z.discriminatedUnion("success", [ z.object({ success: z.literal(true), diff --git a/packages/ts-ghost-core-api/src/helpers/browse-params.test.ts b/packages/ts-ghost-core-api/src/helpers/browse-params.test.ts index 8831008c..dac8af55 100644 --- a/packages/ts-ghost-core-api/src/helpers/browse-params.test.ts +++ b/packages/ts-ghost-core-api/src/helpers/browse-params.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { z } from "zod/v3"; +import { z } from "zod"; import { parseBrowseParams } from "./browse-params"; diff --git a/packages/ts-ghost-core-api/src/helpers/browse-params.ts b/packages/ts-ghost-core-api/src/helpers/browse-params.ts index 40e941d4..0e747150 100644 --- a/packages/ts-ghost-core-api/src/helpers/browse-params.ts +++ b/packages/ts-ghost-core-api/src/helpers/browse-params.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; type Split = Str extends `${infer Start}${Separator}${infer Rest}` ? [Start, ...Split] diff --git a/packages/ts-ghost-core-api/src/helpers/debug.ts b/packages/ts-ghost-core-api/src/helpers/debug.ts new file mode 100644 index 00000000..8251ff59 --- /dev/null +++ b/packages/ts-ghost-core-api/src/helpers/debug.ts @@ -0,0 +1,11 @@ +export type DebugOption = { + debug?: boolean; + logger?: (message?: any, ...optionalParams: any[]) => void; +}; + +export const resolveDebugLogger = (options?: DebugOption) => { + if (options?.debug) { + return options.logger ? options.logger : console.log; + } + return () => {}; +}; diff --git a/packages/ts-ghost-core-api/src/helpers/fields.test.ts b/packages/ts-ghost-core-api/src/helpers/fields.test.ts index 7628241f..83313f7f 100644 --- a/packages/ts-ghost-core-api/src/helpers/fields.test.ts +++ b/packages/ts-ghost-core-api/src/helpers/fields.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { z } from "zod/v3"; +import { z } from "zod"; import { schemaWithPickedFields } from "./fields"; diff --git a/packages/ts-ghost-core-api/src/helpers/fields.ts b/packages/ts-ghost-core-api/src/helpers/fields.ts index 7c070f19..2661f6ff 100644 --- a/packages/ts-ghost-core-api/src/helpers/fields.ts +++ b/packages/ts-ghost-core-api/src/helpers/fields.ts @@ -1,6 +1,6 @@ -import { z } from "zod/v3"; +import { z } from "zod"; -import type { Exactly, Mask } from "../utils"; +import type { Exactly, Mask, NoUnrecognizedKeys } from "../utils"; /** * Parse a Fields object and generate a new Output Schema @@ -13,5 +13,5 @@ export const schemaWithPickedFields = , fields?: Fields, ) => { - return schema.pick((fields as Exactly) || ({} as z.noUnrecognized)); + return schema.pick((fields as Exactly) || ({} as NoUnrecognizedKeys)); }; diff --git a/packages/ts-ghost-core-api/src/helpers/http-client.ts b/packages/ts-ghost-core-api/src/helpers/http-client.ts index 60292436..a1c34191 100644 --- a/packages/ts-ghost-core-api/src/helpers/http-client.ts +++ b/packages/ts-ghost-core-api/src/helpers/http-client.ts @@ -1,13 +1,14 @@ import { SignJWT } from "jose"; import type { APICredentials, APIResource } from "../schemas"; +import { DebugOption, resolveDebugLogger } from "./debug"; export type HTTPClientOptions = { key: string; version: APICredentials["version"]; url: APICredentials["url"]; endpoint: "content" | "admin"; -}; +} & DebugOption; export interface IHTTPClient { get baseURL(): URL | undefined; @@ -22,7 +23,7 @@ export interface IHTTPClient { }: { resource: APIResource; searchParams?: URLSearchParams; - options?: RequestInit; + options?: RequestInit & DebugOption; pathnameIdentity?: string; }): Promise; fetchRawResponse({ @@ -33,7 +34,7 @@ export interface IHTTPClient { }: { resource: APIResource; searchParams?: URLSearchParams; - options?: RequestInit; + options?: RequestInit & DebugOption; pathnameIdentity?: string; }): Promise; } @@ -107,9 +108,10 @@ export class HTTPClient implement }: { resource: APIResource; searchParams?: URLSearchParams; - options?: RequestInit; + options?: RequestInit & DebugOption; pathnameIdentity?: string; }) { + const debug = resolveDebugLogger({ ...this.config, ...options }); if (this._baseURL === undefined) throw new Error("URL is undefined"); let path = `${resource}/`; if (pathnameIdentity !== undefined) { @@ -126,6 +128,7 @@ export class HTTPClient implement } let result = undefined; const headers = await this.genHeaders(); + debug("url", url.toString(), "headers", headers, "options", options); try { result = await ( await fetch(url.toString(), { @@ -133,7 +136,9 @@ export class HTTPClient implement headers, }) ).json(); + debug("result", result, "status", result.status); } catch (e) { + debug("error", e); return { status: "error", errors: [ diff --git a/packages/ts-ghost-core-api/src/helpers/index.ts b/packages/ts-ghost-core-api/src/helpers/index.ts index 91321b20..7f1c45bd 100644 --- a/packages/ts-ghost-core-api/src/helpers/index.ts +++ b/packages/ts-ghost-core-api/src/helpers/index.ts @@ -1,3 +1,4 @@ export * from "./browse-params"; export * from "./fields"; export * from "./http-client"; +export * from "./debug"; diff --git a/packages/ts-ghost-core-api/src/schemas/authors.ts b/packages/ts-ghost-core-api/src/schemas/authors.ts index 80a1fbf1..a05fcda3 100644 --- a/packages/ts-ghost-core-api/src/schemas/authors.ts +++ b/packages/ts-ghost-core-api/src/schemas/authors.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { ghostIdentitySchema, ghostMetadataSchema } from "./shared"; diff --git a/packages/ts-ghost-core-api/src/schemas/email.ts b/packages/ts-ghost-core-api/src/schemas/email.ts index a0b5a73b..a2a879d1 100644 --- a/packages/ts-ghost-core-api/src/schemas/email.ts +++ b/packages/ts-ghost-core-api/src/schemas/email.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; export const baseEmailSchema = z.object({ id: z.string(), diff --git a/packages/ts-ghost-core-api/src/schemas/members.ts b/packages/ts-ghost-core-api/src/schemas/members.ts index a8199022..d6a224d0 100644 --- a/packages/ts-ghost-core-api/src/schemas/members.ts +++ b/packages/ts-ghost-core-api/src/schemas/members.ts @@ -1,38 +1,44 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { baseNewsletterSchema } from "./newsletter"; import { baseSubscriptionsSchema } from "./subscriptions"; export const baseMembersSchema = z.object({ id: z.string(), - email: z.string({ description: "The email address of the member" }), - name: z.string({ description: "The name of the member" }).nullable(), - note: z.string({ description: "(nullable) A note about the member" }).nullish(), - geolocation: z.string({ description: "(nullable) The geolocation of the member" }).nullish(), - created_at: z.string({ description: "The date and time the member was created" }), + email: z.string().meta({ description: "The email address of the member" }), + name: z.string().meta({ description: "The name of the member" }).nullable(), + note: z.string().meta({ description: "(nullable) A note about the member" }).nullish(), + geolocation: z.string().meta({ description: "(nullable) The geolocation of the member" }).nullish(), + created_at: z.string().meta({ description: "The date and time the member was created" }), updated_at: z - .string({ description: "(nullable) The date and time the member was last updated" }) + .string() + .meta({ description: "(nullable) The date and time the member was last updated" }) .nullish(), labels: z.array( - z.object({ - id: z.string({ description: "The ID of the label" }), - name: z.string({ description: "The name of the label" }), - slug: z.string({ description: "The slug of the label" }), - created_at: z.string({ description: "The date and time the label was created" }), - updated_at: z - .string({ description: "(nullable) The date and time the label was last updated" }) - .nullish(), - }), - { description: "The labels associated with the member" }, + z + .object({ + id: z.string().meta({ description: "The ID of the label" }), + name: z.string().meta({ description: "The name of the label" }), + slug: z.string().meta({ description: "The slug of the label" }), + created_at: z.string().meta({ description: "The date and time the label was created" }), + updated_at: z + .string() + .meta({ description: "(nullable) The date and time the label was last updated" }) + .nullish(), + }) + .meta({ description: "The labels associated with the member" }), ), - subscriptions: z.array(baseSubscriptionsSchema, { - description: "The subscriptions associated with the member", - }), - avatar_image: z.string({ description: "The URL of the member's avatar image" }), - email_count: z.number({ description: "The number of emails sent to the member" }), - email_opened_count: z.number({ description: "The number of emails opened by the member" }), - email_open_rate: z.number({ description: "(nullable) The open rate of the member" }).nullish(), - status: z.string({ description: "The status of the member" }), - last_seen_at: z.string({ description: "(nullable) The date and time the member was last seen" }).nullish(), + subscriptions: z + .array(baseSubscriptionsSchema) + .meta({ description: "The subscriptions associated with the member" }), + avatar_image: z.string().meta({ description: "The URL of the member's avatar image" }), + email_count: z.number().meta({ description: "The number of emails sent to the member" }), + email_opened_count: z.number().meta({ description: "The number of emails opened by the member" }), + email_open_rate: z.number().meta({ description: "(nullable) The open rate of the member" }).nullish(), + status: z.string().meta({ description: "The status of the member" }), + last_seen_at: z + .string() + .meta({ description: "(nullable) The date and time the member was last seen" }) + .nullish(), newsletters: z.array(baseNewsletterSchema), }); diff --git a/packages/ts-ghost-core-api/src/schemas/newsletter.ts b/packages/ts-ghost-core-api/src/schemas/newsletter.ts index 9625989a..dc2d4412 100644 --- a/packages/ts-ghost-core-api/src/schemas/newsletter.ts +++ b/packages/ts-ghost-core-api/src/schemas/newsletter.ts @@ -1,54 +1,59 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { ghostIdentitySchema } from "./shared"; export const baseNewsletterSchema = z.object({ ...ghostIdentitySchema.shape, - name: z.string({ description: "Public name for the newsletter" }), - description: z.string({ description: "(nullable) Public description of the newsletter" }).nullish(), - sender_name: z.string({ description: "(nullable) The sender name of the emails" }).nullish(), + name: z.string().meta({ description: "Public name for the newsletter" }), + description: z.string().meta({ description: "(nullable) Public description of the newsletter" }).nullish(), + sender_name: z.string().meta({ description: "(nullable) The sender name of the emails" }).nullish(), sender_email: z - .string({ description: "(nullable) The email from which to send emails. Requires validation." }) + .string() + .meta({ description: "(nullable) The email from which to send emails. Requires validation." }) .nullish(), - sender_reply_to: z.string({ + sender_reply_to: z.string().meta({ description: "The reply-to email address for sent emails. Can be either newsletter (= use sender_email) or support (use support email from Portal settings).", }), - status: z.union([z.literal("active"), z.literal("archived")], { + status: z.union([z.literal("active"), z.literal("archived")]).meta({ description: "active or archived - denotes if the newsletter is active or archived", }), visibility: z.union([z.literal("public"), z.literal("members")]), - subscribe_on_signup: z.boolean({ + subscribe_on_signup: z.boolean().meta({ description: "true/false. Whether members should automatically subscribe to this newsletter on signup", }), - sort_order: z.number({ description: "The order in which newsletters are displayed in the Portal" }), + sort_order: z.number().meta({ description: "The order in which newsletters are displayed in the Portal" }), header_image: z - .string({ + .string() + .meta({ description: "(nullable) Path to an image to show at the top of emails. Recommended size 1200x600", }) .nullish(), - show_header_icon: z.boolean({ description: "true/false. Show the site icon in emails" }), - show_header_title: z.boolean({ description: "true/false. Show the site name in emails" }), - title_font_category: z.union([z.literal("serif"), z.literal("sans_serif")], { + show_header_icon: z.boolean().meta({ description: "true/false. Show the site icon in emails" }), + show_header_title: z.boolean().meta({ description: "true/false. Show the site name in emails" }), + title_font_category: z.union([z.literal("serif"), z.literal("sans_serif")]).meta({ description: "Title font style. Either serif or sans_serif", }), title_alignment: z.string().nullish(), - show_feature_image: z.boolean({ description: "true/false. Show the post's feature image in emails" }), - body_font_category: z.union([z.literal("serif"), z.literal("sans_serif")], { + show_feature_image: z + .boolean() + .meta({ description: "true/false. Show the post's feature image in emails" }), + body_font_category: z.union([z.literal("serif"), z.literal("sans_serif")]).meta({ description: "Body font style. Either serif or sans_serif", }), footer_content: z - .string({ + .string() + .meta({ description: "(nullable) Extra information or legal text to show in the footer of emails. Should contain valid HTML.", }) .nullish(), - show_badge: z.boolean({ + show_badge: z.boolean().meta({ description: "true/false. Show you’re a part of the indie publishing movement by adding a small Ghost badge in the footer", }), created_at: z.string(), updated_at: z.string().nullish(), - show_header_name: z.boolean({ description: "true/false. Show the newsletter name in emails" }), + show_header_name: z.boolean().meta({ description: "true/false. Show the newsletter name in emails" }), uuid: z.string(), }); diff --git a/packages/ts-ghost-core-api/src/schemas/offers.ts b/packages/ts-ghost-core-api/src/schemas/offers.ts index 3c3e93f9..d6110d6e 100644 --- a/packages/ts-ghost-core-api/src/schemas/offers.ts +++ b/packages/ts-ghost-core-api/src/schemas/offers.ts @@ -1,44 +1,53 @@ -import { z } from "zod/v3"; +import { z } from "zod"; export const baseOffersSchema = z.object({ id: z.string(), - name: z.string({ description: "Internal name for an offer, must be unique" }).default(""), - code: z.string({ description: "Shortcode for the offer, for example: https://yoursite.com/black-friday" }), - display_title: z.string({ description: "Name displayed in the offer window" }).nullish(), - display_description: z.string({ description: "Text displayed in the offer window" }).nullish(), + name: z.string().meta({ description: "Internal name for an offer, must be unique" }).default(""), + code: z + .string() + .meta({ description: "Shortcode for the offer, for example: https://yoursite.com/black-friday" }), + display_title: z.string().meta({ description: "Name displayed in the offer window" }).nullish(), + display_description: z.string().meta({ description: "Text displayed in the offer window" }).nullish(), type: z.union([z.literal("percent"), z.literal("fixed"), z.literal("trial")]), cadence: z.union([z.literal("month"), z.literal("year")]), - amount: z.number({ + amount: z.number().meta({ description: `Offer discount amount, as a percentage or fixed value as set in type. Amount is always denoted by the smallest currency unit (e.g., 100 cents instead of $1.00 in USD)`, }), - duration: z.union([z.literal("once"), z.literal("forever"), z.literal("repeating"), z.literal("trial")], { - description: "once/forever/repeating. repeating duration is only available when cadence is month", - }), + duration: z + .union([z.literal("once"), z.literal("forever"), z.literal("repeating"), z.literal("trial")]) + .meta({ + description: "once/forever/repeating. repeating duration is only available when cadence is month", + }), duration_in_months: z - .number({ description: "Number of months offer should be repeated when duration is repeating" }) + .number() + .meta({ description: "Number of months offer should be repeated when duration is repeating" }) .nullish(), currency_restriction: z - .boolean({ + .boolean() + .meta({ description: "Denotes whether the offer `currency` is restricted. If so, changing the currency invalidates the offer", }) .nullish(), currency: z - .string({ + .string() + .meta({ description: "fixed type offers only - specifies tier's currency as three letter ISO currency code", }) .nullish(), - status: z.union([z.literal("active"), z.literal("archived")], { + status: z.union([z.literal("active"), z.literal("archived")]).meta({ description: "active or archived - denotes if the offer is active or archived", }), - redemption_count: z.number({ description: "Number of times the offer has been redeemed" }).nullish(), - tier: z.object( - { + redemption_count: z + .number() + .meta({ description: "Number of times the offer has been redeemed" }) + .nullish(), + tier: z + .object({ id: z.string(), name: z.string().nullish(), - }, - { description: "Tier on which offer is applied" }, - ), + }) + .meta({ description: "Tier on which offer is applied" }), }); diff --git a/packages/ts-ghost-core-api/src/schemas/pages.ts b/packages/ts-ghost-core-api/src/schemas/pages.ts index 46275f23..ff3f127e 100644 --- a/packages/ts-ghost-core-api/src/schemas/pages.ts +++ b/packages/ts-ghost-core-api/src/schemas/pages.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { baseAuthorsSchema } from "./authors"; import { diff --git a/packages/ts-ghost-core-api/src/schemas/posts.ts b/packages/ts-ghost-core-api/src/schemas/posts.ts index dafceed3..e3e52914 100644 --- a/packages/ts-ghost-core-api/src/schemas/posts.ts +++ b/packages/ts-ghost-core-api/src/schemas/posts.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { baseAuthorsSchema } from "./authors"; import { diff --git a/packages/ts-ghost-core-api/src/schemas/settings.ts b/packages/ts-ghost-core-api/src/schemas/settings.ts index 1267a3b0..5000d37b 100644 --- a/packages/ts-ghost-core-api/src/schemas/settings.ts +++ b/packages/ts-ghost-core-api/src/schemas/settings.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; export const baseSettingsSchema = z.object({ title: z.string(), @@ -17,13 +17,13 @@ export const baseSettingsSchema = z.object({ z.object({ label: z.string(), url: z.string(), - }), + }) ), secondary_navigation: z.array( z.object({ label: z.string(), url: z.string(), - }), + }) ), meta_title: z.string().nullable(), meta_description: z.string().nullable(), diff --git a/packages/ts-ghost-core-api/src/schemas/shared.ts b/packages/ts-ghost-core-api/src/schemas/shared.ts index 5bb61a61..8c70aba2 100644 --- a/packages/ts-ghost-core-api/src/schemas/shared.ts +++ b/packages/ts-ghost-core-api/src/schemas/shared.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import type { HTTPClient } from "../helpers/http-client"; @@ -10,10 +10,10 @@ export const ghostIdentitySchema = z.object({ export const ghostIdentityInputSchema = z.object({ slug: z.string().optional(), id: z.string().optional(), - email: z.string().email().optional(), + email: z.email().optional(), }); -export type GhostIdentityInput = z.infer; +export type GhostIdentityInput = z.output; export type GhostIdentity = z.infer; diff --git a/packages/ts-ghost-core-api/src/schemas/site.ts b/packages/ts-ghost-core-api/src/schemas/site.ts index 197ceea1..8ff8cdfc 100644 --- a/packages/ts-ghost-core-api/src/schemas/site.ts +++ b/packages/ts-ghost-core-api/src/schemas/site.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; export const baseSiteSchema = z.object({ title: z.string(), diff --git a/packages/ts-ghost-core-api/src/schemas/subscriptions.ts b/packages/ts-ghost-core-api/src/schemas/subscriptions.ts index f4508956..28a0a8e0 100644 --- a/packages/ts-ghost-core-api/src/schemas/subscriptions.ts +++ b/packages/ts-ghost-core-api/src/schemas/subscriptions.ts @@ -1,34 +1,33 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { baseOffersSchema } from "./offers"; import { baseTiersSchema } from "./tiers"; export const baseSubscriptionsSchema = z.object({ - id: z.string({ description: "Stripe subscription ID sub_XXXX" }), - customer: z.object( - { + id: z.string().meta({ description: "Stripe subscription ID sub_XXXX" }), + customer: z + .object({ id: z.string(), name: z.string().nullable(), email: z.string(), - }, - { description: "Stripe customer attached to the subscription" }, - ), - status: z.string({ description: "Subscription status" }), - start_date: z.string({ description: "Subscription start date" }), - default_payment_card_last4: z.string({ description: "Last 4 digits of the card" }).nullable(), - cancel_at_period_end: z.boolean({ + }) + .meta({ description: "Stripe customer attached to the subscription" }), + status: z.string().meta({ description: "Subscription status" }), + start_date: z.string().meta({ description: "Subscription start date" }), + default_payment_card_last4: z.string().meta({ description: "Last 4 digits of the card" }).nullable(), + cancel_at_period_end: z.boolean().meta({ description: "If the subscription should be canceled or renewed at period end", }), - cancellation_reason: z.string({ description: "Reason for subscription cancellation" }).nullable(), - current_period_end: z.string({ description: "Subscription end date" }), + cancellation_reason: z.string().meta({ description: "Reason for subscription cancellation" }).nullable(), + current_period_end: z.string().meta({ description: "Subscription end date" }), price: z.object({ - id: z.string({ description: "Stripe price ID" }), - price_id: z.string({ description: "Ghost price ID" }), - nickname: z.string({ description: "Price nickname" }), - amount: z.number({ description: "Price amount" }), - interval: z.string({ description: "Price interval" }), - type: z.string({ description: "Price type" }), - currency: z.string({ description: "Price currency" }), + id: z.string().meta({ description: "Stripe price ID" }), + price_id: z.string().meta({ description: "Ghost price ID" }), + nickname: z.string().meta({ description: "Price nickname" }), + amount: z.number().meta({ description: "Price amount" }), + interval: z.string().meta({ description: "Price interval" }), + type: z.string().meta({ description: "Price type" }), + currency: z.string().meta({ description: "Price currency" }), }), tier: baseTiersSchema.nullish(), offer: baseOffersSchema.nullish(), diff --git a/packages/ts-ghost-core-api/src/schemas/tags.ts b/packages/ts-ghost-core-api/src/schemas/tags.ts index e9303a2b..6d1732cf 100644 --- a/packages/ts-ghost-core-api/src/schemas/tags.ts +++ b/packages/ts-ghost-core-api/src/schemas/tags.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { ghostCodeInjectionSchema, diff --git a/packages/ts-ghost-core-api/src/schemas/tiers.ts b/packages/ts-ghost-core-api/src/schemas/tiers.ts index f5e7a7d6..e91b4c67 100644 --- a/packages/ts-ghost-core-api/src/schemas/tiers.ts +++ b/packages/ts-ghost-core-api/src/schemas/tiers.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v3"; +import { z } from "zod"; import { ghostIdentitySchema, ghostVisibilitySchema } from "./shared"; diff --git a/packages/ts-ghost-core-api/src/utils.d.ts b/packages/ts-ghost-core-api/src/utils.d.ts index 7564aa10..1d9e8c8f 100644 --- a/packages/ts-ghost-core-api/src/utils.d.ts +++ b/packages/ts-ghost-core-api/src/utils.d.ts @@ -11,3 +11,7 @@ export declare type InferFetcherDataShape Promise >; export type IsAny = 0 extends 1 & T ? true : false; + +export type NoUnrecognizedKeys = { + [k in keyof Obj]: k extends keyof Shape ? Obj[k] : never; +};