diff --git a/public/examples/blog.yml b/public/examples/blog.yml index 5fe6d8a..480f4e7 100644 --- a/public/examples/blog.yml +++ b/public/examples/blog.yml @@ -84,3 +84,42 @@ collections: options: - { label: "Lead", value: "lead" } - { label: "Body", value: "body" } + + # Relation fields + - label: "Author" + name: "author" + widget: "relation" + collection: "authors" + multiple: false + required: true + search_fields: ["name"] + value_field: "name" + display_fields: ["name"] + + - label: "Tags" + name: "tags" + widget: "relation" + collection: "tags" + multiple: true + required: false + search_fields: ["title"] + value_field: "title" + display_fields: ["title"] + + - label: "Categories" + name: "categories" + widget: "relation" + collection: "categories" + multiple: true + required: true + search_fields: ["name"] + value_field: "name" + display_fields: ["name"] + + - label: "Post Type" + name: "post_type" + widget: "relation" + collection: "types" + multiple: false + required: false + search_fields: ["type"] \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index ab97d94..0221be6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -56,9 +56,16 @@ export async function loadAndTransformCollections( const keys = { name: collection.name }; const name = naming.replace(/%%(\w+)%%/, (_, k: keyof typeof keys) => keys[k] ?? _); const path = resolve(to, name); + const relational = collection.fields?.some(field => field.widget === 'relation'); + const astroImports = ['z']; + + if (relational) { + // Collections containing relational fields require `reference` + astroImports.push('reference'); + } // build content and prettify if possible - const raw = `import { z } from 'astro:content';\n\nexport const schema = ${cptime};\n`; + const raw = `import { ${astroImports.sort().join(', ')} } from 'astro:content';\n\nexport const schema = ${cptime};\n`; const pretty = await tryOrFail(() => formatCode(raw, 'typescript'), ERROR.FORMATTING_FAILED); // prepare folder if non-existent, remove existing and write file diff --git a/src/transformers/field-relation.transform.spec.ts b/src/transformers/field-relation.transform.spec.ts new file mode 100644 index 0000000..fe2bb22 --- /dev/null +++ b/src/transformers/field-relation.transform.spec.ts @@ -0,0 +1,66 @@ +import type { CmsFieldBase, CmsFieldRelation } from 'decap-cms-core'; +import z from 'zod'; + +import { transformRelationField } from './field-relation.transform.js'; + +describe('field-relation.transform', () => { + // Constructs a base relation field configuration with overrides + function buildField( + partial: Partial, + ): CmsFieldBase & CmsFieldRelation { + return { + name: 'test_field', + widget: 'relation', + collection: 'authors', + multiple: false, + required: true, + search_fields: ['name'], + value_field: 'name', + display_fields: ['name'], + ...partial, + }; + } + + // Normalises quotes in the cptime output for consistent comparison + function normaliseCptime(cptime: string): string { + return cptime.replace(/'/g, '"'); + } + + it('transforms a single required relation field into a string schema', () => { + const field = buildField({ multiple: false, required: true, collection: 'authors' }); + const { runtime, cptime } = transformRelationField(field, z); + + expect(runtime._def.typeName).toBe(z.ZodFirstPartyTypeKind.ZodString); + expect(normaliseCptime(cptime)).toBe('reference("authors")'); + expect(() => runtime.parse('some-author-id')).not.toThrow(); + expect(() => runtime.parse(['not allowed'])).toThrow(); + }); + + it('transforms a single non-required relation field into the same string schema', () => { + const field = buildField({ multiple: false, required: false, collection: 'authors' }); + const { runtime, cptime } = transformRelationField(field, z); + + expect(runtime._def.typeName).toBe(z.ZodFirstPartyTypeKind.ZodString); + expect(normaliseCptime(cptime)).toBe('reference("authors")'); + expect(() => runtime.parse('some-author-id')).not.toThrow(); + }); + + it('transforms a multiple required relation field into an array of strings schema', () => { + const field = buildField({ multiple: true, required: true, collection: 'tags' }); + const { runtime, cptime } = transformRelationField(field, z); + + expect(runtime._def.typeName).toBe(z.ZodFirstPartyTypeKind.ZodArray); + expect(normaliseCptime(cptime)).toBe('z.array(reference("tags"))'); + expect(() => runtime.parse(['tag1', 'tag2'])).not.toThrow(); + expect(() => runtime.parse('invalid')).toThrow(); + }); + + it('transforms a multiple non-required relation field into the same array schema', () => { + const field = buildField({ multiple: true, required: false, collection: 'categories' }); + const { runtime, cptime } = transformRelationField(field, z); + + expect(runtime._def.typeName).toBe(z.ZodFirstPartyTypeKind.ZodArray); + expect(normaliseCptime(cptime)).toBe('z.array(reference("categories"))'); + expect(() => runtime.parse(['cat1', 'cat2'])).not.toThrow(); + }); +}); diff --git a/src/transformers/field-relation.transform.ts b/src/transformers/field-relation.transform.ts index 8db2fec..ed73f4c 100644 --- a/src/transformers/field-relation.transform.ts +++ b/src/transformers/field-relation.transform.ts @@ -1,14 +1,15 @@ import type { CmsFieldBase, CmsFieldRelation } from 'decap-cms-core'; -import type { ZodString } from 'zod'; +import type { ZodTypeAny } from 'zod'; import type { Transformer } from '../utils/transform.utils.js'; -// TODO implement transform relations -// https://decapcms.org/docs/widgets/#relation -export const transformRelationField: Transformer = ( - _, +export const transformRelationField: Transformer = ( + { collection, multiple = false }, z, -) => ({ - runtime: z.string(), - cptime: 'z.string()', -}); +) => { + const runtime = multiple ? z.array(z.string()) : z.string(); + + const cptime = multiple ? `z.array(reference('${collection}'))` : `reference('${collection}')`; + + return { runtime, cptime }; +}; diff --git a/src/transformers/field.transform.spec.ts b/src/transformers/field.transform.spec.ts index ffbd6ec..44a0998 100644 --- a/src/transformers/field.transform.spec.ts +++ b/src/transformers/field.transform.spec.ts @@ -85,7 +85,10 @@ describe('field.transform', () => { it(`interchangeable results ${desc}`, () => { const { cptime, runtime } = transform(field, z); - const compiled = new Function('z', `return ${transpileFrom(cptime)};`)(z); + const compiled = new Function('z', 'reference', `return ${transpileFrom(cptime)};`)( + z, + (_collection: string) => z.string(), + ); expect(serializeShape(compiled)).toEqual(serializeShape(runtime)); });