diff --git a/examples/with-content/src/content/config.ts b/examples/with-content/src/content/config.ts index 9d436060ab9a..30cbbf293e86 100644 --- a/examples/with-content/src/content/config.ts +++ b/examples/with-content/src/content/config.ts @@ -2,7 +2,7 @@ import { defineCollection, z } from 'astro:content'; const blog = defineCollection({ // Type-check frontmatter using a schema - schema: { + schema: z.object({ title: z.string(), description: z.string(), // Transform string to Date object @@ -12,7 +12,7 @@ const blog = defineCollection({ .optional() .transform((str) => (str ? new Date(str) : undefined)), heroImage: z.string().optional(), - }, + }), }); export const collections = { blog }; diff --git a/examples/with-content/src/content/types.generated.d.ts b/examples/with-content/src/content/types.generated.d.ts index 906aabb752f7..834d542bd44a 100644 --- a/examples/with-content/src/content/types.generated.d.ts +++ b/examples/with-content/src/content/types.generated.d.ts @@ -3,17 +3,30 @@ declare module 'astro:content' { export type CollectionEntry = typeof entryMap[C][keyof typeof entryMap[C]] & Render; - type BaseCollectionConfig = { + type BaseSchemaWithoutEffects = + | import('astro/zod').AnyZodObject + | import('astro/zod').ZodUnion + | import('astro/zod').ZodDiscriminatedUnion + | import('astro/zod').ZodIntersection< + import('astro/zod').AnyZodObject, + import('astro/zod').AnyZodObject + >; + + type BaseSchema = + | BaseSchemaWithoutEffects + | import('astro/zod').ZodEffects; + + type BaseCollectionConfig = { schema?: S; slug?: (entry: { id: CollectionEntry['id']; defaultSlug: string; collection: string; body: string; - data: import('astro/zod').infer>; + data: import('astro/zod').infer; }) => string | Promise; }; - export function defineCollection( + export function defineCollection( input: BaseCollectionConfig ): BaseCollectionConfig; @@ -30,7 +43,7 @@ declare module 'astro:content' { ): Promise<(typeof entryMap[C][E] & Render)[]>; type InferEntrySchema = import('astro/zod').infer< - import('astro/zod').ZodObject['schema']> + Required['schema'] >; type Render = { @@ -42,44 +55,45 @@ declare module 'astro:content' { }; const entryMap: { - blog: { - 'first-post.md': { - id: 'first-post.md'; - slug: 'first-post'; - body: string; - collection: 'blog'; - data: InferEntrySchema<'blog'>; - }; - 'markdown-style-guide.md': { - id: 'markdown-style-guide.md'; - slug: 'markdown-style-guide'; - body: string; - collection: 'blog'; - data: InferEntrySchema<'blog'>; - }; - 'second-post.md': { - id: 'second-post.md'; - slug: 'second-post'; - body: string; - collection: 'blog'; - data: InferEntrySchema<'blog'>; - }; - 'third-post.md': { - id: 'third-post.md'; - slug: 'third-post'; - body: string; - collection: 'blog'; - data: InferEntrySchema<'blog'>; - }; - 'using-mdx.mdx': { - id: 'using-mdx.mdx'; - slug: 'using-mdx'; - body: string; - collection: 'blog'; - data: InferEntrySchema<'blog'>; - }; - }; + "blog": { +"first-post.md": { + id: "first-post.md", + slug: "first-post", + body: string, + collection: "blog", + data: InferEntrySchema<"blog"> +}, +"markdown-style-guide.md": { + id: "markdown-style-guide.md", + slug: "markdown-style-guide", + body: string, + collection: "blog", + data: InferEntrySchema<"blog"> +}, +"second-post.md": { + id: "second-post.md", + slug: "second-post", + body: string, + collection: "blog", + data: InferEntrySchema<"blog"> +}, +"third-post.md": { + id: "third-post.md", + slug: "third-post", + body: string, + collection: "blog", + data: InferEntrySchema<"blog"> +}, +"using-mdx.mdx": { + id: "using-mdx.mdx", + slug: "using-mdx", + body: string, + collection: "blog", + data: InferEntrySchema<"blog"> +}, +}, + }; - type ContentConfig = typeof import('./config'); + type ContentConfig = typeof import("./config"); } diff --git a/packages/astro/src/content/template/types.generated.d.ts b/packages/astro/src/content/template/types.generated.d.ts index 0edf3dcd2307..f91a9ef3d15a 100644 --- a/packages/astro/src/content/template/types.generated.d.ts +++ b/packages/astro/src/content/template/types.generated.d.ts @@ -3,17 +3,30 @@ declare module 'astro:content' { export type CollectionEntry = typeof entryMap[C][keyof typeof entryMap[C]] & Render; - type BaseCollectionConfig = { + type BaseSchemaWithoutEffects = + | import('astro/zod').AnyZodObject + | import('astro/zod').ZodUnion + | import('astro/zod').ZodDiscriminatedUnion + | import('astro/zod').ZodIntersection< + import('astro/zod').AnyZodObject, + import('astro/zod').AnyZodObject + >; + + type BaseSchema = + | BaseSchemaWithoutEffects + | import('astro/zod').ZodEffects; + + type BaseCollectionConfig = { schema?: S; slug?: (entry: { id: CollectionEntry['id']; defaultSlug: string; collection: string; body: string; - data: import('astro/zod').infer>; + data: import('astro/zod').infer; }) => string | Promise; }; - export function defineCollection( + export function defineCollection( input: BaseCollectionConfig ): BaseCollectionConfig; @@ -30,7 +43,7 @@ declare module 'astro:content' { ): Promise<(typeof entryMap[C][E] & Render)[]>; type InferEntrySchema = import('astro/zod').infer< - import('astro/zod').ZodObject['schema']> + Required['schema'] >; type Render = { diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index 07f19faf0497..9b3b556cc000 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -68,8 +68,20 @@ export async function getEntrySlug(entry: Entry, collectionConfig: CollectionCon export async function getEntryData(entry: Entry, collectionConfig: CollectionConfig) { let data = entry.data; if (collectionConfig.schema) { + // TODO: remove for 2.0 stable release + if ( + typeof collectionConfig.schema === 'object' && + !('safeParseAsync' in collectionConfig.schema) + ) { + throw new AstroError({ + title: 'Invalid content collection config', + message: `New: Content collection schemas must be Zod objects. Update your collection config to use \`schema: z.object({...})\` instead of \`schema: {...}\`.`, + hint: 'See https://docs.astro.build/en/reference/api-reference/#definecollection for an example.', + code: 99999, + }); + } // Use `safeParseAsync` to allow async transforms - const parsed = await z.object(collectionConfig.schema).safeParseAsync(entry.data, { errorMap }); + const parsed = await collectionConfig.schema.safeParseAsync(entry.data, { errorMap }); if (parsed.success) { data = parsed.data; } else { diff --git a/packages/astro/test/content-collections.test.js b/packages/astro/test/content-collections.test.js index 934f50017b7b..66b4fdf36629 100644 --- a/packages/astro/test/content-collections.test.js +++ b/packages/astro/test/content-collections.test.js @@ -103,6 +103,25 @@ describe('Content Collections', () => { const slugs = json.withSlugConfig.map((item) => item.slug); expect(slugs).to.deep.equal(['fancy-one.md', 'excellent-three.md', 'interesting-two.md']); }); + + it('Returns `with union schema` collection', async () => { + expect(json).to.haveOwnProperty('withUnionSchema'); + expect(Array.isArray(json.withUnionSchema)).to.equal(true); + + const post = json.withUnionSchema.find((item) => item.id === 'post.md'); + expect(post).to.not.be.undefined; + expect(post.data).to.deep.equal({ + type: 'post', + title: 'My Post', + description: 'This is my post', + }); + const newsletter = json.withUnionSchema.find((item) => item.id === 'newsletter.md'); + expect(newsletter).to.not.be.undefined; + expect(newsletter.data).to.deep.equal({ + type: 'newsletter', + subject: 'My Newsletter', + }); + }); }); describe('Entry', () => { @@ -130,6 +149,16 @@ describe('Content Collections', () => { expect(json).to.haveOwnProperty('twoWithSlugConfig'); expect(json.twoWithSlugConfig.slug).to.equal('interesting-two.md'); }); + + it('Returns `with union schema` collection entry', async () => { + expect(json).to.haveOwnProperty('postWithUnionSchema'); + expect(json.postWithUnionSchema.id).to.equal('post.md'); + expect(json.postWithUnionSchema.data).to.deep.equal({ + type: 'post', + title: 'My Post', + description: 'This is my post', + }); + }); }); }); diff --git a/packages/astro/test/fixtures/content-collections/src/content/config.ts b/packages/astro/test/fixtures/content-collections/src/content/config.ts index eadc52a7471e..f3b4e921af0d 100644 --- a/packages/astro/test/fixtures/content-collections/src/content/config.ts +++ b/packages/astro/test/fixtures/content-collections/src/content/config.ts @@ -2,23 +2,39 @@ import { z, defineCollection } from 'astro:content'; const withSlugConfig = defineCollection({ slug({ id, data }) { + console.log({id, data}) return `${data.prefix}-${id}`; }, - schema: { + schema: z.object({ prefix: z.string(), - } + }), }); const withSchemaConfig = defineCollection({ - schema: { + schema: z.object({ title: z.string(), isDraft: z.boolean().default(false), lang: z.enum(['en', 'fr', 'es']).default('en'), publishedAt: z.date().transform((val) => new Date(val)), - } + }) +}); + +const withUnionSchema = defineCollection({ + schema: z.discriminatedUnion('type', [ + z.object({ + type: z.literal('post'), + title: z.string(), + description: z.string(), + }), + z.object({ + type: z.literal('newsletter'), + subject: z.string(), + }), + ]), }); export const collections = { 'with-slug-config': withSlugConfig, 'with-schema-config': withSchemaConfig, + 'with-union-schema': withUnionSchema, } diff --git a/packages/astro/test/fixtures/content-collections/src/content/with-union-schema/newsletter.md b/packages/astro/test/fixtures/content-collections/src/content/with-union-schema/newsletter.md new file mode 100644 index 000000000000..6e8703a1b646 --- /dev/null +++ b/packages/astro/test/fixtures/content-collections/src/content/with-union-schema/newsletter.md @@ -0,0 +1,6 @@ +--- +type: newsletter +subject: My Newsletter +--- + +# It's a newsletter! diff --git a/packages/astro/test/fixtures/content-collections/src/content/with-union-schema/post.md b/packages/astro/test/fixtures/content-collections/src/content/with-union-schema/post.md new file mode 100644 index 000000000000..fb260d6645f4 --- /dev/null +++ b/packages/astro/test/fixtures/content-collections/src/content/with-union-schema/post.md @@ -0,0 +1,7 @@ +--- +type: post +title: My Post +description: This is my post +--- + +# It's a post! diff --git a/packages/astro/test/fixtures/content-collections/src/pages/collections.json.js b/packages/astro/test/fixtures/content-collections/src/pages/collections.json.js index 007f8a383722..897f2ebdd227 100644 --- a/packages/astro/test/fixtures/content-collections/src/pages/collections.json.js +++ b/packages/astro/test/fixtures/content-collections/src/pages/collections.json.js @@ -6,7 +6,9 @@ export async function get() { const withoutConfig = stripAllRenderFn(await getCollection('without-config')); const withSchemaConfig = stripAllRenderFn(await getCollection('with-schema-config')); const withSlugConfig = stripAllRenderFn(await getCollection('with-slug-config')); + const withUnionSchema = stripAllRenderFn(await getCollection('with-union-schema')); + return { - body: devalue.stringify({withoutConfig, withSchemaConfig, withSlugConfig}), + body: devalue.stringify({withoutConfig, withSchemaConfig, withSlugConfig, withUnionSchema}), } } diff --git a/packages/astro/test/fixtures/content-collections/src/pages/entries.json.js b/packages/astro/test/fixtures/content-collections/src/pages/entries.json.js index 141a7b7d193f..7c9d8f1d4e47 100644 --- a/packages/astro/test/fixtures/content-collections/src/pages/entries.json.js +++ b/packages/astro/test/fixtures/content-collections/src/pages/entries.json.js @@ -6,7 +6,9 @@ export async function get() { const columbiaWithoutConfig = stripRenderFn(await getEntry('without-config', 'columbia.md')); const oneWithSchemaConfig = stripRenderFn(await getEntry('with-schema-config', 'one.md')); const twoWithSlugConfig = stripRenderFn(await getEntry('with-slug-config', 'two.md')); + const postWithUnionSchema = stripRenderFn(await getEntry('with-union-schema', 'post.md')); + return { - body: devalue.stringify({columbiaWithoutConfig, oneWithSchemaConfig, twoWithSlugConfig}), + body: devalue.stringify({columbiaWithoutConfig, oneWithSchemaConfig, twoWithSlugConfig, postWithUnionSchema}), } } diff --git a/packages/astro/test/fixtures/content-ssr-integration/src/content/config.ts b/packages/astro/test/fixtures/content-ssr-integration/src/content/config.ts index f32eba6de67c..d22a45648565 100644 --- a/packages/astro/test/fixtures/content-ssr-integration/src/content/config.ts +++ b/packages/astro/test/fixtures/content-ssr-integration/src/content/config.ts @@ -1,7 +1,7 @@ import { defineCollection, z } from 'astro:content'; const blog = defineCollection({ - schema: { + schema: z.object({ title: z.string(), description: z.string(), pubDate: z.string().transform((str) => new Date(str)), @@ -10,7 +10,7 @@ const blog = defineCollection({ .optional() .transform((str) => (str ? new Date(str) : undefined)), heroImage: z.string().optional(), - }, + }), }); export const collections = { blog }; diff --git a/packages/astro/test/fixtures/content-static-paths-integration/src/content/config.ts b/packages/astro/test/fixtures/content-static-paths-integration/src/content/config.ts index f32eba6de67c..d22a45648565 100644 --- a/packages/astro/test/fixtures/content-static-paths-integration/src/content/config.ts +++ b/packages/astro/test/fixtures/content-static-paths-integration/src/content/config.ts @@ -1,7 +1,7 @@ import { defineCollection, z } from 'astro:content'; const blog = defineCollection({ - schema: { + schema: z.object({ title: z.string(), description: z.string(), pubDate: z.string().transform((str) => new Date(str)), @@ -10,7 +10,7 @@ const blog = defineCollection({ .optional() .transform((str) => (str ? new Date(str) : undefined)), heroImage: z.string().optional(), - }, + }), }); export const collections = { blog }; diff --git a/packages/astro/test/fixtures/content/src/content/config.ts b/packages/astro/test/fixtures/content/src/content/config.ts index 5c37f2755620..27c9d91b5483 100644 --- a/packages/astro/test/fixtures/content/src/content/config.ts +++ b/packages/astro/test/fixtures/content/src/content/config.ts @@ -1,10 +1,10 @@ import { z, defineCollection } from 'astro:content'; const blog = defineCollection({ - schema: { + schema: z.object({ title: z.string(), description: z.string().max(60, 'For SEO purposes, keep descriptions short!'), - }, + }), }); export const collections = { blog };