Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2 Relations and References #3

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions public/examples/blog.yml
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a relation example as well, wouldn't it make more sense to extend that instead?

Original file line number Diff line number Diff line change
Expand Up @@ -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"]
9 changes: 8 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Copy link
Owner

@davidenke davidenke Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would fail on nested fields. This should be a recursive lookup. Or what might be better, is some additional property in the result payload of the transformers, so the imports would be build after transformations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't think about or test with nested relations, my bad

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem. I changed the transform result to include a dependencies property, this should do the trick.

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
Expand Down
66 changes: 66 additions & 0 deletions src/transformers/field-relation.transform.spec.ts
Original file line number Diff line number Diff line change
@@ -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>,
): 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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the test data is explicitly defined and the transformers are deterministic, this should not be necessary. Instead, we must trust the resulting output to have a specific kind of quote and expect them to have always the same shape.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was only added because of a battle between eslint and prettier, so it's there to satisfy the IDE only.

I couldn't get eslint and prettier rules to agree on quotes.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. But badly configured tooling shouldn't force us to do such things. I'll align the configuration.

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();
});
});
19 changes: 10 additions & 9 deletions src/transformers/field-relation.transform.ts
Original file line number Diff line number Diff line change
@@ -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<ZodString, CmsFieldBase & CmsFieldRelation> = (
_,
export const transformRelationField: Transformer<ZodTypeAny, CmsFieldBase & CmsFieldRelation> = (
{ collection, multiple = false },
z,
) => ({
runtime: z.string(),
cptime: 'z.string()',
});
) => {
const runtime = multiple ? z.array(z.string()) : z.string();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the reason I haven't thrown it together yet... 😅
Even though I like the idea using the generated reference function, this is no option for the runtime version.

Tbh, I'm not sure if supporting the runtime types anyway, as I personally just generate the types in watch mode as static files. So it maybe makes sense to drop them entirely...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also not really using / interested in the runtime side of things. I include adc as part of my build process and watch the content dir to trigger a new build.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll drop the runtime support, that reduces complexity for several aspects.
Maybe that can be implemented one day using the approach now available for tests using eval (or something less evil)...


const cptime = multiple ? `z.array(reference('${collection}'))` : `reference('${collection}')`;

return { runtime, cptime };
};
5 changes: 4 additions & 1 deletion src/transformers/field.transform.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});

Expand Down