fimo 0.4.0-experimental.2 → 0.4.0-experimental.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/content-templates/hooks-template.eta +193 -121
- package/assets/content-templates/types-template.eta +162 -0
- package/assets/skills/fimo/references/content.md +92 -39
- package/assets/skills/fimo/references/locales.md +1 -1
- package/assets/skills/fimo-cli/references/content.md +17 -9
- package/assets/skills/fimo-cli/references/locales.md +2 -2
- package/dist/cli/bundle.json +2 -2
- package/dist/cli/index.js +188 -64
- package/dist/runtime/primitives/index.d.ts +2 -2
- package/dist/runtime/primitives/index.d.ts.map +1 -1
- package/dist/runtime/primitives/lib/index.d.ts +2 -2
- package/dist/runtime/primitives/lib/index.d.ts.map +1 -1
- package/dist/runtime/primitives/lib/primitives.d.ts +3 -0
- package/dist/runtime/primitives/lib/primitives.d.ts.map +1 -1
- package/dist/runtime/primitives/lib/query.d.ts +46 -8
- package/dist/runtime/primitives/lib/query.d.ts.map +1 -1
- package/dist/runtime/primitives/public-surface.test.js +27 -1
- package/dist/scripts/sync-schemas.js +91 -27
- package/dist/scripts/sync-schemas.test.d.ts +1 -0
- package/dist/scripts/sync-schemas.test.js +121 -0
- package/dist/scripts/validate-schemas.js +31 -4
- package/dist/scripts/validate-schemas.test.js +49 -0
- package/package.json +1 -1
- package/templates/react-router/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FimoBoolean, FimoDate, FimoMedia, FimoRichText, FimoString } from './primitives.js';
|
|
1
|
+
import type { FimoBoolean, FimoDate, FimoMedia, FimoReference, FimoRichText, FimoString } from './primitives.js';
|
|
2
2
|
export type ScalarOps<T> = {
|
|
3
3
|
$eq?: T;
|
|
4
4
|
$ne?: T;
|
|
@@ -25,20 +25,36 @@ export type BoolOps = ScalarOps<boolean>;
|
|
|
25
25
|
export type NullOnlyOps = {
|
|
26
26
|
$null?: boolean;
|
|
27
27
|
};
|
|
28
|
-
export type
|
|
29
|
-
|
|
28
|
+
export type RelationOps = {
|
|
29
|
+
$eq?: string | null;
|
|
30
|
+
$ne?: string | null;
|
|
31
|
+
$in?: string[];
|
|
32
|
+
$nin?: string[];
|
|
33
|
+
$contains?: string;
|
|
34
|
+
$null?: boolean;
|
|
35
|
+
};
|
|
36
|
+
export type ManyRelationOps = {
|
|
37
|
+
$eq?: string;
|
|
38
|
+
$ne?: string;
|
|
39
|
+
$contains?: string;
|
|
40
|
+
$null?: boolean;
|
|
41
|
+
};
|
|
42
|
+
export type FilterFor<T> = [T] extends [FimoString | string] ? string | StringOps : [T] extends [FimoBoolean | boolean] ? boolean | BoolOps : [T] extends [FimoDate | Date] ? string | DateOps : [T] extends [number] ? number | NumberOps : [T] extends [FimoReference | null] ? string | null | RelationOps : [T] extends [FimoReference[]] ? string | ManyRelationOps : [T] extends [FimoMedia | FimoMedia[]] ? NullOnlyOps : [T] extends [FimoRichText] ? string | StringOps : NullOnlyOps;
|
|
43
|
+
type BaseTopLevelWhere = {
|
|
30
44
|
id?: string | StringOps;
|
|
31
45
|
documentId?: string | StringOps;
|
|
32
46
|
slug?: string | StringOps;
|
|
33
|
-
locale?: string | StringOps;
|
|
34
47
|
createdAt?: string | DateOps;
|
|
35
48
|
updatedAt?: string | DateOps;
|
|
36
49
|
__fimo?: {
|
|
37
50
|
contentType?: string | StringOps;
|
|
38
51
|
};
|
|
39
52
|
};
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
type TopLevelWhere<T> = BaseTopLevelWhere & ('locale' extends keyof T ? {
|
|
54
|
+
locale?: string | StringOps;
|
|
55
|
+
} : {});
|
|
56
|
+
export type Where<T> = TopLevelWhere<T> & {
|
|
57
|
+
[K in Exclude<keyof T, keyof BaseTopLevelWhere | 'locale'>]?: FilterFor<T[K]>;
|
|
42
58
|
};
|
|
43
59
|
type FimoMetadataField = 'contentType';
|
|
44
60
|
type FimoField = '__fimo' | `__fimo.${FimoMetadataField}`;
|
|
@@ -48,12 +64,34 @@ export type Field<T> = Extract<keyof T, string> | 'createdAt' | 'updatedAt' | Fi
|
|
|
48
64
|
export type Fields<T> = Field<T> | readonly Field<T>[];
|
|
49
65
|
type SelectedField<TField> = TField extends `__fimo.${string}` ? '__fimo' : TField;
|
|
50
66
|
type SelectedFields<TFields> = TFields extends readonly (infer K)[] ? SelectedField<K> : SelectedField<TFields>;
|
|
67
|
+
type ListItem<TValue> = TValue extends readonly (infer K)[] ? K : TValue;
|
|
51
68
|
export type Projected<T extends {
|
|
52
69
|
id: string;
|
|
53
70
|
}, TFields extends Fields<T> | undefined> = [TFields] extends [undefined] ? T : Pick<T, Extract<'id' | SelectedFields<TFields>, keyof T>>;
|
|
71
|
+
export declare const FIMO_RELATIONS: unique symbol;
|
|
72
|
+
export interface WithRelations<TRelations> {
|
|
73
|
+
readonly [FIMO_RELATIONS]?: TRelations;
|
|
74
|
+
}
|
|
75
|
+
export type RelationMap<TEntry> = TEntry extends WithRelations<infer TRelations> ? TRelations : never;
|
|
76
|
+
export type RelationField<TEntry> = Extract<keyof RelationMap<TEntry>, string>;
|
|
77
|
+
export type Populate<TEntry> = RelationField<TEntry> | readonly RelationField<TEntry>[];
|
|
78
|
+
type PopulatedKeys<TEntry, TPopulate> = Extract<ListItem<TPopulate>, keyof RelationMap<TEntry> & keyof TEntry>;
|
|
79
|
+
export type Populated<TEntry extends {
|
|
80
|
+
id: string;
|
|
81
|
+
}, TPopulate extends Populate<TEntry>> = Pick<TEntry, 'id'> & Omit<TEntry, PopulatedKeys<TEntry, TPopulate>> & {
|
|
82
|
+
[K in PopulatedKeys<TEntry, TPopulate>]: RelationMap<TEntry>[K];
|
|
83
|
+
};
|
|
84
|
+
export type PopulatedResult<TEntry extends {
|
|
85
|
+
id: string;
|
|
86
|
+
}, TPopulate extends Populate<TEntry> | undefined> = [
|
|
87
|
+
TPopulate
|
|
88
|
+
] extends [undefined] ? TEntry : Populated<TEntry, NonNullable<TPopulate>>;
|
|
89
|
+
export type ProjectedWithPopulate<T extends {
|
|
90
|
+
id: string;
|
|
91
|
+
}, TFields extends string | readonly string[] | undefined, TPopulate extends string | readonly string[] | undefined> = [TFields] extends [undefined] ? T : Pick<T, Extract<'id' | SelectedFields<TFields> | ListItem<NonNullable<TPopulate>>, keyof T>>;
|
|
54
92
|
export type Query<T extends {
|
|
55
93
|
id: string;
|
|
56
|
-
}, TFields extends Fields<T> | undefined = Fields<T> | undefined> = {
|
|
94
|
+
}, TFields extends Fields<T> | undefined = Fields<T> | undefined, TPopulate extends string | readonly string[] | undefined = string | readonly string[] | undefined> = {
|
|
57
95
|
where?: Where<T>;
|
|
58
96
|
locale?: string;
|
|
59
97
|
search?: string;
|
|
@@ -61,7 +99,7 @@ export type Query<T extends {
|
|
|
61
99
|
limit?: number;
|
|
62
100
|
offset?: number;
|
|
63
101
|
fields?: TFields;
|
|
64
|
-
populate?:
|
|
102
|
+
populate?: TPopulate;
|
|
65
103
|
};
|
|
66
104
|
export {};
|
|
67
105
|
//# sourceMappingURL=query.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../../../src/runtime/primitives/lib/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../../../src/runtime/primitives/lib/query.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,11 +6,37 @@ describe('fimo/ui public type surface', () => {
|
|
|
6
6
|
const query = {
|
|
7
7
|
where: { name: 'BASIC' },
|
|
8
8
|
sort: '-price',
|
|
9
|
-
populate: 'name',
|
|
10
9
|
limit: 10,
|
|
11
10
|
};
|
|
12
11
|
expectTypeOf(query.fields).toEqualTypeOf();
|
|
12
|
+
expectTypeOf().toEqualTypeOf();
|
|
13
|
+
const localizedWhere = { locale: 'en' };
|
|
14
|
+
expectTypeOf(localizedWhere).toEqualTypeOf();
|
|
15
|
+
// @ts-expect-error locale is not a filter on non-localized generated entry types.
|
|
16
|
+
const sharedWhere = { locale: 'en' };
|
|
17
|
+
expectTypeOf(sharedWhere).toEqualTypeOf();
|
|
18
|
+
});
|
|
19
|
+
it('supports the relation populate pattern emitted into generated schemas', () => {
|
|
20
|
+
const query = {
|
|
21
|
+
where: {
|
|
22
|
+
sponsor: { $eq: '750e8400-e29b-41d4-a716-446655440000' },
|
|
23
|
+
relatedPackages: { $contains: '850e8400-e29b-41d4-a716-446655440000' },
|
|
24
|
+
},
|
|
25
|
+
fields: ['name'],
|
|
26
|
+
populate: ['sponsor'],
|
|
27
|
+
};
|
|
28
|
+
expectTypeOf(query.fields).toEqualTypeOf();
|
|
13
29
|
expectTypeOf(query.populate).toEqualTypeOf();
|
|
14
30
|
expectTypeOf().toEqualTypeOf();
|
|
31
|
+
expectTypeOf().toEqualTypeOf();
|
|
32
|
+
expectTypeOf().toEqualTypeOf();
|
|
33
|
+
expectTypeOf().toEqualTypeOf();
|
|
34
|
+
expectTypeOf().toEqualTypeOf();
|
|
35
|
+
// @ts-expect-error generated relation-aware queries only allow relation fields in populate.
|
|
36
|
+
const badQuery = { populate: 'name' };
|
|
37
|
+
expectTypeOf(badQuery).toEqualTypeOf();
|
|
38
|
+
// @ts-expect-error relation filters use target documentId strings, not raw reference objects.
|
|
39
|
+
const badWhere = { where: { sponsor: { documentId: '750e8400-e29b-41d4-a716-446655440000' } } };
|
|
40
|
+
expectTypeOf(badWhere).toEqualTypeOf();
|
|
15
41
|
});
|
|
16
42
|
});
|
|
@@ -5,8 +5,8 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
5
5
|
* hook files generated from the hooks template.
|
|
6
6
|
*
|
|
7
7
|
* - Scans `<cwd>/src/schemas/*.json`.
|
|
8
|
-
* - Renders `assets/content-templates/hooks-template.eta`.
|
|
9
|
-
* - Creates/updates `<cwd>/src/schemas/<Pascal>.ts`; deletes orphans.
|
|
8
|
+
* - Renders `assets/content-templates/types-template.eta` and `hooks-template.eta`.
|
|
9
|
+
* - Creates/updates `<cwd>/src/schemas/<Pascal>.types.ts` and `<Pascal>.ts`; deletes orphans.
|
|
10
10
|
*/
|
|
11
11
|
import { pascalCase } from 'change-case';
|
|
12
12
|
import { Eta } from 'eta';
|
|
@@ -28,6 +28,50 @@ function readSchema(path) {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
function normalizeFields(fields) {
|
|
32
|
+
return Object.fromEntries(Object.entries(fields).map(([key, value]) => {
|
|
33
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
34
|
+
return [key, { type: undefined }];
|
|
35
|
+
}
|
|
36
|
+
const field = value;
|
|
37
|
+
const target = typeof field.target === 'string' ? field.target : undefined;
|
|
38
|
+
return [
|
|
39
|
+
key,
|
|
40
|
+
{
|
|
41
|
+
...field,
|
|
42
|
+
type: typeof field.type === 'string' ? field.type : undefined,
|
|
43
|
+
target,
|
|
44
|
+
targetPascal: target ? pascalCase(target) : undefined,
|
|
45
|
+
many: typeof field.many === 'boolean' ? field.many : undefined,
|
|
46
|
+
multiple: typeof field.multiple === 'boolean' ? field.multiple : undefined,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
function relationImportsFor(schema, fields) {
|
|
52
|
+
const imports = new Map();
|
|
53
|
+
for (const field of Object.values(fields)) {
|
|
54
|
+
if (field.type !== 'relation' || !field.target || field.target === schema.uid) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
imports.set(field.target, { uid: field.target, pascal: pascalCase(field.target) });
|
|
58
|
+
}
|
|
59
|
+
return [...imports.values()].sort((a, b) => a.pascal.localeCompare(b.pascal));
|
|
60
|
+
}
|
|
61
|
+
function schemaToModel(schema, localizedByUid) {
|
|
62
|
+
const fields = normalizeFields(schema.fields ?? {});
|
|
63
|
+
return {
|
|
64
|
+
uid: schema.uid,
|
|
65
|
+
pascal: pascalCase(schema.uid),
|
|
66
|
+
isSingleton: schema.isSingleton ?? false,
|
|
67
|
+
localized: schema.i18n?.enabled !== false,
|
|
68
|
+
localizedRelationFields: Object.entries(fields)
|
|
69
|
+
.filter(([, field]) => field.type === 'relation' && field.target && localizedByUid.get(field.target) !== false)
|
|
70
|
+
.map(([fieldName]) => fieldName),
|
|
71
|
+
fields,
|
|
72
|
+
relationImports: relationImportsFor(schema, fields),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
31
75
|
export function syncSchemas(options = {}) {
|
|
32
76
|
const cwd = options.cwd ?? process.cwd();
|
|
33
77
|
const log = (msg) => {
|
|
@@ -36,10 +80,26 @@ export function syncSchemas(options = {}) {
|
|
|
36
80
|
}
|
|
37
81
|
};
|
|
38
82
|
const schemasDir = resolve(cwd, SCHEMAS_REL);
|
|
39
|
-
const
|
|
40
|
-
|
|
83
|
+
const templates = [
|
|
84
|
+
{
|
|
85
|
+
suffix: '.types.ts',
|
|
86
|
+
path: assetPath('content-templates', 'types-template.eta'),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
suffix: '.ts',
|
|
90
|
+
path: assetPath('content-templates', 'hooks-template.eta'),
|
|
91
|
+
},
|
|
92
|
+
].map((template) => ({
|
|
93
|
+
...template,
|
|
94
|
+
source: readFileSync(template.path, 'utf-8'),
|
|
95
|
+
}));
|
|
41
96
|
const eta = new Eta({ views: undefined, useWith: true, varName: 'it', autoEscape: false });
|
|
42
97
|
const schemaFiles = getSchemaFiles(schemasDir);
|
|
98
|
+
const schemasByPath = new Map(schemaFiles.flatMap((schemaPath) => {
|
|
99
|
+
const schema = readSchema(schemaPath);
|
|
100
|
+
return schema ? [[schemaPath, schema]] : [];
|
|
101
|
+
}));
|
|
102
|
+
const localizedByUid = new Map([...schemasByPath.values()].map((schema) => [schema.uid, schema.i18n?.enabled !== false]));
|
|
43
103
|
const expectedTsFiles = new Set();
|
|
44
104
|
let created = 0;
|
|
45
105
|
let updated = 0;
|
|
@@ -49,9 +109,8 @@ export function syncSchemas(options = {}) {
|
|
|
49
109
|
.split('/')
|
|
50
110
|
.pop()
|
|
51
111
|
.replace(/\.json$/, '');
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
const schema = readSchema(jsonPath);
|
|
112
|
+
const pascal = pascalCase(uid);
|
|
113
|
+
const schema = schemasByPath.get(jsonPath);
|
|
55
114
|
if (!schema) {
|
|
56
115
|
console.error(`Failed to parse schema: ${jsonPath}`);
|
|
57
116
|
continue;
|
|
@@ -59,29 +118,34 @@ export function syncSchemas(options = {}) {
|
|
|
59
118
|
if (!schema.uid) {
|
|
60
119
|
schema.uid = uid;
|
|
61
120
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
121
|
+
const model = schemaToModel(schema, localizedByUid);
|
|
122
|
+
for (const template of templates) {
|
|
123
|
+
const tsPath = join(schemasDir, `${pascal}${template.suffix}`);
|
|
124
|
+
expectedTsFiles.add(tsPath);
|
|
125
|
+
try {
|
|
126
|
+
const content = eta.renderString(template.source, {
|
|
127
|
+
model,
|
|
128
|
+
});
|
|
129
|
+
const dir = dirname(tsPath);
|
|
130
|
+
if (!existsSync(dir)) {
|
|
131
|
+
mkdirSync(dir, { recursive: true });
|
|
132
|
+
}
|
|
133
|
+
if (!existsSync(tsPath)) {
|
|
134
|
+
writeFileSync(tsPath, content, 'utf-8');
|
|
135
|
+
created++;
|
|
136
|
+
log(`Created: ${tsPath}`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (readFileSync(tsPath, 'utf-8') !== content) {
|
|
140
|
+
writeFileSync(tsPath, content, 'utf-8');
|
|
141
|
+
updated++;
|
|
142
|
+
log(`Updated: ${tsPath}`);
|
|
143
|
+
}
|
|
75
144
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
updated++;
|
|
79
|
-
log(`Updated: ${tsPath}`);
|
|
145
|
+
catch (error) {
|
|
146
|
+
console.error(`Failed to generate ${tsPath}:`, error);
|
|
80
147
|
}
|
|
81
148
|
}
|
|
82
|
-
catch (error) {
|
|
83
|
-
console.error(`Failed to generate ${tsPath}:`, error);
|
|
84
|
-
}
|
|
85
149
|
}
|
|
86
150
|
if (existsSync(schemasDir)) {
|
|
87
151
|
const allTsFiles = readdirSync(schemasDir)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { syncSchemas } from './sync-schemas.js';
|
|
6
|
+
let scratch;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
scratch = mkdtempSync(join(tmpdir(), 'fimo-sync-schemas-'));
|
|
9
|
+
mkdirSync(join(scratch, 'src/schemas'), { recursive: true });
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
function writeSchema(name, schema) {
|
|
15
|
+
writeFileSync(join(scratch, 'src/schemas', `${name}.json`), JSON.stringify(schema, null, 2));
|
|
16
|
+
}
|
|
17
|
+
describe('syncSchemas', () => {
|
|
18
|
+
it('generates raw relation references plus typed populated relation helpers', () => {
|
|
19
|
+
writeSchema('Author', {
|
|
20
|
+
uid: 'Author',
|
|
21
|
+
name: 'Authors',
|
|
22
|
+
fields: {
|
|
23
|
+
name: { type: 'string' },
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
writeSchema('Post', {
|
|
27
|
+
uid: 'Post',
|
|
28
|
+
name: 'Posts',
|
|
29
|
+
fields: {
|
|
30
|
+
title: { type: 'string' },
|
|
31
|
+
author: { type: 'relation', target: 'Author' },
|
|
32
|
+
contributors: { type: 'relation', target: 'Author', many: true },
|
|
33
|
+
relatedPosts: { type: 'relation', target: 'Post', many: true },
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
expect(syncSchemas({ cwd: scratch, verbose: false })).toEqual({ created: 4, updated: 0, deleted: 0 });
|
|
37
|
+
const post = readFileSync(join(scratch, 'src/schemas/Post.ts'), 'utf-8');
|
|
38
|
+
const postTypes = readFileSync(join(scratch, 'src/schemas/Post.types.ts'), 'utf-8');
|
|
39
|
+
expect(post).toContain('import { wrapWithSource as wrapAuthorWithSource } from "./Author";');
|
|
40
|
+
expect(post).toContain('import type { Post, PostInput, PostPopulate, PostResult, PostFields, PostQuery } from "./Post.types";');
|
|
41
|
+
expect(post).toContain('from "./Post.types";');
|
|
42
|
+
expect(post).not.toContain('import type { Author } from "./Author";');
|
|
43
|
+
expect(post).not.toContain('export interface Post extends WithRelations<PostRelations>');
|
|
44
|
+
expect(post).not.toContain('type FimoReference');
|
|
45
|
+
expect(post).toContain('Promise<PostResult<undefined, TPopulate>>');
|
|
46
|
+
expect(post).toContain('TPopulate extends PostPopulate | undefined');
|
|
47
|
+
expect(post).toContain('wrapAuthorWithSource(value)');
|
|
48
|
+
expect(post).toContain('wrapWithSource(value)');
|
|
49
|
+
expect(post).toContain('export async function create(payload: PostInput): Promise<Post>');
|
|
50
|
+
expect(post).toContain('return wrapWithSource(data.data);');
|
|
51
|
+
expect(postTypes).toContain('import type { Author } from "./Author.types";');
|
|
52
|
+
expect(postTypes).toContain('export interface Post extends WithRelations<PostRelations>');
|
|
53
|
+
expect(postTypes).toContain('type FimoReference');
|
|
54
|
+
expect(postTypes).toContain('author: FimoReference | null;');
|
|
55
|
+
expect(postTypes).toContain('contributors: FimoReference[];');
|
|
56
|
+
expect(postTypes).toContain('relatedPosts: FimoReference[];');
|
|
57
|
+
expect(postTypes).toContain('export interface PostRelations');
|
|
58
|
+
expect(postTypes).toContain('export interface PostInput');
|
|
59
|
+
expect(postTypes).toContain('author?: FimoReference | null;');
|
|
60
|
+
expect(postTypes).toContain('author: Author | null;');
|
|
61
|
+
expect(postTypes).toContain('contributors: Author[];');
|
|
62
|
+
expect(postTypes).toContain('relatedPosts: Post[];');
|
|
63
|
+
expect(postTypes).toContain('export type PostResult<');
|
|
64
|
+
expect(postTypes).not.toContain('PostPopulated');
|
|
65
|
+
});
|
|
66
|
+
it('generates locale-free shared types and locale-sensitive populated target keys', () => {
|
|
67
|
+
writeSchema('Homepage', {
|
|
68
|
+
uid: 'Homepage',
|
|
69
|
+
name: 'Homepages',
|
|
70
|
+
fields: { title: { type: 'string' } },
|
|
71
|
+
});
|
|
72
|
+
writeSchema('Category', {
|
|
73
|
+
uid: 'Category',
|
|
74
|
+
name: 'Categories',
|
|
75
|
+
i18n: { enabled: false },
|
|
76
|
+
fields: { name: { type: 'string' } },
|
|
77
|
+
});
|
|
78
|
+
writeSchema('Settings', {
|
|
79
|
+
uid: 'Settings',
|
|
80
|
+
name: 'Settings',
|
|
81
|
+
i18n: { enabled: false },
|
|
82
|
+
fields: {
|
|
83
|
+
homepage: { type: 'relation', target: 'Homepage' },
|
|
84
|
+
category: { type: 'relation', target: 'Category' },
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
syncSchemas({ cwd: scratch, verbose: false });
|
|
88
|
+
const settings = readFileSync(join(scratch, 'src/schemas/Settings.ts'), 'utf-8');
|
|
89
|
+
const settingsTypes = readFileSync(join(scratch, 'src/schemas/Settings.types.ts'), 'utf-8');
|
|
90
|
+
const homepageTypes = readFileSync(join(scratch, 'src/schemas/Homepage.types.ts'), 'utf-8');
|
|
91
|
+
expect(settingsTypes).not.toContain('locale: string;');
|
|
92
|
+
expect(settingsTypes).not.toContain('locale?: string;');
|
|
93
|
+
expect(homepageTypes).toContain('locale: string;');
|
|
94
|
+
expect(settings).toContain('const LOCALIZED_CONTENT = false;');
|
|
95
|
+
expect(settings).toContain('const LOCALIZED_RELATION_FIELDS = new Set<string>(["homepage"]);');
|
|
96
|
+
expect(settings).not.toContain(' locale: entry.locale,');
|
|
97
|
+
expect(settings).toContain('readLocale(params, LOCALIZED_CONTENT, useQueryLocale(params.locale))');
|
|
98
|
+
expect(settings.match(/readLocale\(params, LOCALIZED_CONTENT, useQueryLocale\(params\.locale\)\)/g)?.length).toBe(2);
|
|
99
|
+
expect(settings.match(/readLocale\(options, false, useQueryLocale\(options\.locale\)\)/g)?.length).toBe(2);
|
|
100
|
+
});
|
|
101
|
+
it('generates singleton document APIs without collection mutations', () => {
|
|
102
|
+
writeSchema('Settings', {
|
|
103
|
+
uid: 'Settings',
|
|
104
|
+
name: 'Settings',
|
|
105
|
+
isSingleton: true,
|
|
106
|
+
i18n: { enabled: false },
|
|
107
|
+
fields: { siteName: { type: 'string', required: true } },
|
|
108
|
+
});
|
|
109
|
+
syncSchemas({ cwd: scratch, verbose: false });
|
|
110
|
+
const settings = readFileSync(join(scratch, 'src/schemas/Settings.ts'), 'utf-8');
|
|
111
|
+
const settingsTypes = readFileSync(join(scratch, 'src/schemas/Settings.types.ts'), 'utf-8');
|
|
112
|
+
expect(settings).toContain('fetch(`${base}${withReadOptions(`/documents/Settings`, options)}`');
|
|
113
|
+
expect(settings).toContain('export async function update(payload: Partial<SettingsInput>)');
|
|
114
|
+
expect(settings).toContain('export function useSuspenseGet');
|
|
115
|
+
expect(settings).not.toContain('export async function create(');
|
|
116
|
+
expect(settings).not.toContain('export async function remove(');
|
|
117
|
+
expect(settings).not.toContain('export function useCreate(');
|
|
118
|
+
expect(settingsTypes).toContain('siteName: string;');
|
|
119
|
+
expect(settingsTypes).not.toContain('documentId?: string;');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -63,7 +63,7 @@ export function validateSchemaFile(absolutePath, file, schemaUids = new Set()) {
|
|
|
63
63
|
return [{ rule: 'schemas/shape', message: 'Schema must be a JSON object.', file }];
|
|
64
64
|
}
|
|
65
65
|
const errors = [];
|
|
66
|
-
const { uid, name, fields } = schema;
|
|
66
|
+
const { uid, name, fields, i18n, isSingleton } = schema;
|
|
67
67
|
const expectedUid = basename(file, '.json');
|
|
68
68
|
if (typeof uid !== 'string' || uid.length === 0) {
|
|
69
69
|
errors.push({
|
|
@@ -93,6 +93,32 @@ export function validateSchemaFile(absolutePath, file, schemaUids = new Set()) {
|
|
|
93
93
|
file,
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
|
+
if (isSingleton !== undefined && typeof isSingleton !== 'boolean') {
|
|
97
|
+
errors.push({
|
|
98
|
+
rule: 'schemas/singleton',
|
|
99
|
+
message: '"isSingleton" must be a boolean when provided.',
|
|
100
|
+
file,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (i18n !== undefined) {
|
|
104
|
+
if (typeof i18n !== 'object' || i18n === null || Array.isArray(i18n)) {
|
|
105
|
+
errors.push({
|
|
106
|
+
rule: 'schemas/i18n',
|
|
107
|
+
message: '"i18n" must be an object like { "enabled": false }.',
|
|
108
|
+
file,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const keys = Object.keys(i18n);
|
|
113
|
+
if (keys.length !== 1 || keys[0] !== 'enabled' || typeof i18n.enabled !== 'boolean') {
|
|
114
|
+
errors.push({
|
|
115
|
+
rule: 'schemas/i18n',
|
|
116
|
+
message: '"i18n" only supports one boolean option: { "enabled": true | false }.',
|
|
117
|
+
file,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
96
122
|
if (typeof fields !== 'object' || fields === null || Array.isArray(fields)) {
|
|
97
123
|
errors.push({
|
|
98
124
|
rule: 'schemas/fields',
|
|
@@ -135,10 +161,11 @@ export function validateSchemaFile(absolutePath, file, schemaUids = new Set()) {
|
|
|
135
161
|
file,
|
|
136
162
|
});
|
|
137
163
|
}
|
|
138
|
-
if ('i18n' in definition) {
|
|
164
|
+
if ('i18n' in definition || 'localized' in definition) {
|
|
165
|
+
const key = 'i18n' in definition ? 'i18n' : 'localized';
|
|
139
166
|
errors.push({
|
|
140
167
|
rule: 'schemas/field-i18n',
|
|
141
|
-
message: `Field "${fieldName}": field-level locale settings ("
|
|
168
|
+
message: `Field "${fieldName}": field-level locale settings ("${key}") are not supported.`,
|
|
142
169
|
file,
|
|
143
170
|
});
|
|
144
171
|
}
|
|
@@ -167,7 +194,7 @@ export function validateSchemaFile(absolutePath, file, schemaUids = new Set()) {
|
|
|
167
194
|
}
|
|
168
195
|
}
|
|
169
196
|
else {
|
|
170
|
-
for (const relationOnlyKey of ['target', 'many'
|
|
197
|
+
for (const relationOnlyKey of ['target', 'many']) {
|
|
171
198
|
if (relationOnlyKey in definition) {
|
|
172
199
|
errors.push({
|
|
173
200
|
rule: 'schemas/relation-shape',
|
|
@@ -55,6 +55,42 @@ describe('validateSchemas', () => {
|
|
|
55
55
|
});
|
|
56
56
|
expect(validateSchemas({ cwd })).toEqual({ errors: [], schemasChecked: 1 });
|
|
57
57
|
});
|
|
58
|
+
it('accepts a non-localized schema policy', () => {
|
|
59
|
+
const cwd = writeProject({
|
|
60
|
+
'Author.json': {
|
|
61
|
+
uid: 'Author',
|
|
62
|
+
name: 'Authors',
|
|
63
|
+
i18n: { enabled: false },
|
|
64
|
+
fields: { name: { type: 'string' } },
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
expect(validateSchemas({ cwd })).toEqual({ errors: [], schemasChecked: 1 });
|
|
68
|
+
});
|
|
69
|
+
it('rejects a non-boolean singleton policy', () => {
|
|
70
|
+
const cwd = writeProject({
|
|
71
|
+
'Settings.json': {
|
|
72
|
+
uid: 'Settings',
|
|
73
|
+
name: 'Settings',
|
|
74
|
+
isSingleton: 'yes',
|
|
75
|
+
fields: {},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const { errors } = validateSchemas({ cwd });
|
|
79
|
+
expect(errors).toEqual([expect.objectContaining({ rule: 'schemas/singleton' })]);
|
|
80
|
+
});
|
|
81
|
+
it.each([true, { enabled: 'no' }, { enabled: false, fallback: 'en' }, { fallback: 'en' }])('rejects an invalid schema-level i18n policy %#', (i18n) => {
|
|
82
|
+
const cwd = writeProject({
|
|
83
|
+
'Author.json': {
|
|
84
|
+
uid: 'Author',
|
|
85
|
+
name: 'Authors',
|
|
86
|
+
i18n,
|
|
87
|
+
fields: { name: { type: 'string' } },
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
const { errors } = validateSchemas({ cwd });
|
|
91
|
+
expect(errors).toHaveLength(1);
|
|
92
|
+
expect(errors[0]).toMatchObject({ rule: 'schemas/i18n' });
|
|
93
|
+
});
|
|
58
94
|
it('returns no errors when the schemas directory does not exist', () => {
|
|
59
95
|
scratch = mkdtempSync(join(tmpdir(), 'validate-schemas-'));
|
|
60
96
|
expect(validateSchemas({ cwd: scratch })).toEqual({ errors: [], schemasChecked: 0 });
|
|
@@ -178,6 +214,19 @@ describe('validateSchemas', () => {
|
|
|
178
214
|
expect(errors).toHaveLength(1);
|
|
179
215
|
expect(errors[0]).toMatchObject({ rule: 'schemas/field-i18n' });
|
|
180
216
|
});
|
|
217
|
+
it('rejects relation-level locale settings', () => {
|
|
218
|
+
const cwd = writeProject({
|
|
219
|
+
'Page.json': {
|
|
220
|
+
uid: 'Page',
|
|
221
|
+
name: 'Pages',
|
|
222
|
+
fields: { parent: { type: 'relation', target: 'Page', localized: true } },
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
const { errors } = validateSchemas({ cwd });
|
|
226
|
+
expect(errors).toHaveLength(1);
|
|
227
|
+
expect(errors[0]).toMatchObject({ rule: 'schemas/field-i18n' });
|
|
228
|
+
expect(errors[0].message).toContain('"localized"');
|
|
229
|
+
});
|
|
181
230
|
it('rejects a uid that does not match the file name', () => {
|
|
182
231
|
const cwd = writeProject({
|
|
183
232
|
'TeamMember.json': {
|
package/package.json
CHANGED