From 92de02b421b1d6d246d3aa48bbbbf562bb2fa0b0 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Thu, 5 Jan 2023 11:51:44 -0500 Subject: [PATCH 1/7] feat: require `schema: z.object({...})` --- .../src/content/types.generated.d.ts | 81 ++++++++++--------- .../src/content/template/types.generated.d.ts | 4 +- packages/astro/src/content/utils.ts | 13 ++- 3 files changed, 54 insertions(+), 44 deletions(-) diff --git a/examples/with-content/src/content/types.generated.d.ts b/examples/with-content/src/content/types.generated.d.ts index 906aabb752f7..6780d96c55b3 100644 --- a/examples/with-content/src/content/types.generated.d.ts +++ b/examples/with-content/src/content/types.generated.d.ts @@ -10,7 +10,7 @@ declare module 'astro:content' { defaultSlug: string; collection: string; body: string; - data: import('astro/zod').infer>; + data: import('astro/zod').infer; }) => string | Promise; }; export function defineCollection( @@ -30,7 +30,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 +42,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..b3991df65207 100644 --- a/packages/astro/src/content/template/types.generated.d.ts +++ b/packages/astro/src/content/template/types.generated.d.ts @@ -10,7 +10,7 @@ declare module 'astro:content' { defaultSlug: string; collection: string; body: string; - data: import('astro/zod').infer>; + data: import('astro/zod').infer; }) => string | Promise; }; export function defineCollection( @@ -30,7 +30,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..04d5a81b51f4 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -4,7 +4,7 @@ import type fsMod from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createServer, ErrorPayload as ViteErrorPayload, normalizePath, ViteDevServer } from 'vite'; -import { z } from 'zod'; +import { z, ZodType } from 'zod'; import { AstroSettings } from '../@types/astro.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { astroContentVirtualModPlugin } from './vite-plugin-content-virtual-mod.js'; @@ -68,8 +68,17 @@ 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 (!(collectionConfig.schema instanceof ZodType)) { + 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 { From e828e0504920b7a7a5ecf973aa59c82689025f6d Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Thu, 5 Jan 2023 11:55:39 -0500 Subject: [PATCH 2/7] fix: update zod object type check --- packages/astro/src/content/utils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index 04d5a81b51f4..9b3b556cc000 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -4,7 +4,7 @@ import type fsMod from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createServer, ErrorPayload as ViteErrorPayload, normalizePath, ViteDevServer } from 'vite'; -import { z, ZodType } from 'zod'; +import { z } from 'zod'; import { AstroSettings } from '../@types/astro.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { astroContentVirtualModPlugin } from './vite-plugin-content-virtual-mod.js'; @@ -69,7 +69,10 @@ export async function getEntryData(entry: Entry, collectionConfig: CollectionCon let data = entry.data; if (collectionConfig.schema) { // TODO: remove for 2.0 stable release - if (!(collectionConfig.schema instanceof ZodType)) { + 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: {...}\`.`, From 2fe90cfc8523e6894e0aec3de532c5a668c2c60e Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Thu, 5 Jan 2023 12:01:22 -0500 Subject: [PATCH 3/7] fix: update types template --- packages/astro/src/content/template/types.generated.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/content/template/types.generated.d.ts b/packages/astro/src/content/template/types.generated.d.ts index b3991df65207..2b24d2384bbe 100644 --- a/packages/astro/src/content/template/types.generated.d.ts +++ b/packages/astro/src/content/template/types.generated.d.ts @@ -3,7 +3,7 @@ declare module 'astro:content' { export type CollectionEntry = typeof entryMap[C][keyof typeof entryMap[C]] & Render; - type BaseCollectionConfig = { + type BaseCollectionConfig = { schema?: S; slug?: (entry: { id: CollectionEntry['id']; @@ -13,7 +13,7 @@ declare module 'astro:content' { data: import('astro/zod').infer; }) => string | Promise; }; - export function defineCollection( + export function defineCollection( input: BaseCollectionConfig ): BaseCollectionConfig; From 00567ab4010916f37773aadbad1e46cf486f86f8 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Thu, 5 Jan 2023 12:01:31 -0500 Subject: [PATCH 4/7] chore: update with-content config --- examples/with-content/src/content/config.ts | 4 ++-- examples/with-content/src/content/types.generated.d.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 6780d96c55b3..e2a4379684e3 100644 --- a/examples/with-content/src/content/types.generated.d.ts +++ b/examples/with-content/src/content/types.generated.d.ts @@ -3,7 +3,7 @@ declare module 'astro:content' { export type CollectionEntry = typeof entryMap[C][keyof typeof entryMap[C]] & Render; - type BaseCollectionConfig = { + type BaseCollectionConfig = { schema?: S; slug?: (entry: { id: CollectionEntry['id']; @@ -13,7 +13,7 @@ declare module 'astro:content' { data: import('astro/zod').infer; }) => string | Promise; }; - export function defineCollection( + export function defineCollection( input: BaseCollectionConfig ): BaseCollectionConfig; From 88776a86fc6e66e34c454bfa7b72cc69ee070403 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Thu, 5 Jan 2023 12:02:57 -0500 Subject: [PATCH 5/7] chore: update test fixture configs --- .../fixtures/content-collections/src/content/config.ts | 8 ++++---- .../content-ssr-integration/src/content/config.ts | 4 ++-- .../src/content/config.ts | 4 ++-- .../astro/test/fixtures/content/src/content/config.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) 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..140a729a54d1 100644 --- a/packages/astro/test/fixtures/content-collections/src/content/config.ts +++ b/packages/astro/test/fixtures/content-collections/src/content/config.ts @@ -4,18 +4,18 @@ const withSlugConfig = defineCollection({ slug({ 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)), - } + }) }); export const collections = { 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 }; From 7721e5f2de6a8105b43d4712fca43c139d9c016e Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Thu, 5 Jan 2023 12:23:58 -0500 Subject: [PATCH 6/7] test: zod union type --- .../astro/test/content-collections.test.js | 29 +++++++++++++++++++ .../content-collections/src/content/config.ts | 18 +++++++++++- .../content/with-union-schema/newsletter.md | 6 ++++ .../src/content/with-union-schema/post.md | 7 +++++ .../src/pages/collections.json.js | 4 ++- .../src/pages/entries.json.js | 4 ++- 6 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 packages/astro/test/fixtures/content-collections/src/content/with-union-schema/newsletter.md create mode 100644 packages/astro/test/fixtures/content-collections/src/content/with-union-schema/post.md 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 140a729a54d1..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,11 +2,12 @@ import { z, defineCollection } from 'astro:content'; const withSlugConfig = defineCollection({ slug({ id, data }) { + console.log({id, data}) return `${data.prefix}-${id}`; }, schema: z.object({ prefix: z.string(), - }) + }), }); const withSchemaConfig = defineCollection({ @@ -18,7 +19,22 @@ const withSchemaConfig = defineCollection({ }) }); +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}), } } From 8c4ef106bcbe7b1c35b0bb0582e8c4c357cda49f Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Thu, 5 Jan 2023 13:03:03 -0500 Subject: [PATCH 7/7] refactor: enumerate valid schema types --- .../src/content/types.generated.d.ts | 17 +++++++++++++++-- .../src/content/template/types.generated.d.ts | 17 +++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/examples/with-content/src/content/types.generated.d.ts b/examples/with-content/src/content/types.generated.d.ts index e2a4379684e3..834d542bd44a 100644 --- a/examples/with-content/src/content/types.generated.d.ts +++ b/examples/with-content/src/content/types.generated.d.ts @@ -3,7 +3,20 @@ 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']; @@ -13,7 +26,7 @@ declare module 'astro:content' { data: import('astro/zod').infer; }) => string | Promise; }; - export function defineCollection( + export function defineCollection( input: BaseCollectionConfig ): BaseCollectionConfig; diff --git a/packages/astro/src/content/template/types.generated.d.ts b/packages/astro/src/content/template/types.generated.d.ts index 2b24d2384bbe..f91a9ef3d15a 100644 --- a/packages/astro/src/content/template/types.generated.d.ts +++ b/packages/astro/src/content/template/types.generated.d.ts @@ -3,7 +3,20 @@ 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']; @@ -13,7 +26,7 @@ declare module 'astro:content' { data: import('astro/zod').infer; }) => string | Promise; }; - export function defineCollection( + export function defineCollection( input: BaseCollectionConfig ): BaseCollectionConfig;