fimo 0.4.0-experimental.4 → 0.4.0-experimental.5
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 +25 -24
- package/assets/content-templates/types-template.eta +9 -9
- package/assets/skills/fimo/references/content.md +35 -35
- package/assets/skills/fimo-cli/references/content.md +8 -8
- package/assets/skills/fimo-migration/SKILL.md +1 -1
- package/assets/skills/fimo-migration/references/content.md +4 -4
- package/assets/skills/fimo-migration/references/import-contract.md +1 -1
- package/assets/skills/fimo-migration/references/manifest.md +1 -1
- package/assets/skills/fimo-migration/references/platforms.md +1 -1
- package/assets/skills/fimo-migration/references/workflow.md +1 -1
- package/assets/skills/fimo-migration/scripts/build-canonical-manifest.mjs +2 -2
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +51 -20
- package/dist/runtime/primitives/index.d.ts +1 -1
- package/dist/runtime/primitives/index.d.ts.map +1 -1
- package/dist/runtime/primitives/lib/index.d.ts +1 -1
- package/dist/runtime/primitives/lib/index.d.ts.map +1 -1
- package/dist/runtime/primitives/lib/query.d.ts +11 -11
- package/dist/runtime/primitives/lib/query.d.ts.map +1 -1
- package/dist/runtime/primitives/public-surface.test.js +4 -4
- package/dist/scripts/sync-schemas.js +5 -5
- package/dist/scripts/sync-schemas.test.js +10 -10
- package/dist/scripts/validate-schemas.js +11 -11
- package/dist/scripts/validate-schemas.test.js +13 -13
- package/package.json +1 -1
- package/templates/react-router/package.json +1 -1
|
@@ -14,15 +14,15 @@ import { FimoString, FimoMedia, FimoBoolean, FimoRichText, useLocale } from "fim
|
|
|
14
14
|
import fimoConfig from "virtual:fimo-config";
|
|
15
15
|
import type { <%= model.pascal %>, <%= model.pascal %>Input, <%= model.pascal %>Populate, <%= model.pascal %>Result<% if (!model.isSingleton) { %>, <%= model.pascal %>Fields, <%= model.pascal %>Query<% } %> } from "./<%= model.pascal %>.types";
|
|
16
16
|
<%
|
|
17
|
-
if (model.
|
|
18
|
-
model.
|
|
17
|
+
if (model.referenceImports && model.referenceImports.length > 0) {
|
|
18
|
+
model.referenceImports.forEach((referenceImport) => {
|
|
19
19
|
-%>
|
|
20
|
-
import { wrapWithSource as wrap<%=
|
|
20
|
+
import { wrapWithSource as wrap<%= referenceImport.pascal %>WithSource } from "./<%= referenceImport.pascal %>";
|
|
21
21
|
<%
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
-%>
|
|
25
|
-
export type { <%= model.pascal %>, <%= model.pascal %>Fields, <%= model.pascal %>FimoMeta, <%= model.pascal %>Input, <%= model.pascal %>Populate, <%= model.pascal %>Query, <%= model.pascal %>
|
|
25
|
+
export type { <%= model.pascal %>, <%= model.pascal %>Fields, <%= model.pascal %>FimoMeta, <%= model.pascal %>Input, <%= model.pascal %>Populate, <%= model.pascal %>Query, <%= model.pascal %>References, <%= model.pascal %>Result, <%= model.pascal %>Sort, <%= model.pascal %>Where } from "./<%= model.pascal %>.types";
|
|
26
26
|
|
|
27
27
|
/*───────────────────────────────────────────────────────────────*/
|
|
28
28
|
/* ──────────── 🎁 Source wrapping ──────────── */
|
|
@@ -79,7 +79,7 @@ if (model.fields) {
|
|
|
79
79
|
<%
|
|
80
80
|
}
|
|
81
81
|
break;
|
|
82
|
-
case '
|
|
82
|
+
case 'reference': {
|
|
83
83
|
const wrapper = field.target === model.uid ? 'wrapWithSource' : `wrap${field.targetPascal}WithSource`;
|
|
84
84
|
if (field.many) {
|
|
85
85
|
-%>
|
|
@@ -119,7 +119,7 @@ function useQueryLocale(locale?: string): string {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
const LOCALIZED_CONTENT = <%= model.localized ? 'true' : 'false' %>;
|
|
122
|
-
const
|
|
122
|
+
const LOCALIZED_REFERENCE_FIELDS = new Set<string>(<%= JSON.stringify(model.localizedReferenceFields) %>);
|
|
123
123
|
|
|
124
124
|
function populateFields(populate: <%= model.pascal %>Populate | undefined): string[] {
|
|
125
125
|
if (populate == null) return [];
|
|
@@ -131,7 +131,7 @@ function readLocale(
|
|
|
131
131
|
ownerUsesLocale: boolean,
|
|
132
132
|
fallbackLocale = defaultLocale()
|
|
133
133
|
): string | undefined {
|
|
134
|
-
const populatesLocalizedTarget = populateFields(options.populate).some((field) =>
|
|
134
|
+
const populatesLocalizedTarget = populateFields(options.populate).some((field) => LOCALIZED_REFERENCE_FIELDS.has(field));
|
|
135
135
|
return ownerUsesLocale || populatesLocalizedTarget ? options.locale ?? fallbackLocale : undefined;
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -145,7 +145,7 @@ function fimoHeaders(extra?: Record<string, string>): Record<string, string> {
|
|
|
145
145
|
|
|
146
146
|
function withReadOptions<TPopulate extends <%= model.pascal %>Populate | undefined>(
|
|
147
147
|
url: string,
|
|
148
|
-
options: { locale?: string; populate?: TPopulate } = {},
|
|
148
|
+
options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {},
|
|
149
149
|
ownerUsesLocale = LOCALIZED_CONTENT
|
|
150
150
|
): string {
|
|
151
151
|
const u = new URL(url, "http://fimo.local");
|
|
@@ -154,6 +154,7 @@ function withReadOptions<TPopulate extends <%= model.pascal %>Populate | undefin
|
|
|
154
154
|
if (options.populate != null) {
|
|
155
155
|
u.searchParams.set("populate", typeof options.populate === "string" ? options.populate : options.populate.join(","));
|
|
156
156
|
}
|
|
157
|
+
if (options.localeFallbacks) u.searchParams.set("localeFallbacks", "true");
|
|
157
158
|
return `${u.pathname}${u.search}`;
|
|
158
159
|
}
|
|
159
160
|
|
|
@@ -178,7 +179,7 @@ function splitWritePayload(payload: Partial<<%= model.pascal %>Input>): Record<s
|
|
|
178
179
|
<% if (!model.isSingleton) { -%>
|
|
179
180
|
export async function getById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(
|
|
180
181
|
id: string,
|
|
181
|
-
options: { locale?: string; populate?: TPopulate } = {}
|
|
182
|
+
options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}
|
|
182
183
|
): Promise<<%= model.pascal %>Result<undefined, TPopulate>> {
|
|
183
184
|
const res = await fetch(`${base}${withReadOptions(`/entries/<%= model.uid %>/${id}`, options, false)}`, { headers: fimoHeaders() });
|
|
184
185
|
if (!res.ok) throw new Error(await res.text());
|
|
@@ -188,7 +189,7 @@ export async function getById<TPopulate extends <%= model.pascal %>Populate | un
|
|
|
188
189
|
|
|
189
190
|
export async function getBySlug<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(
|
|
190
191
|
slug: string,
|
|
191
|
-
options: { locale?: string; populate?: TPopulate } = {}
|
|
192
|
+
options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}
|
|
192
193
|
): Promise<<%= model.pascal %>Result<undefined, TPopulate> | null> {
|
|
193
194
|
const res = await fetch(`${base}${withReadOptions(`/entries/<%= model.uid %>/slug/${encodeURIComponent(slug)}`, options)}`, { headers: fimoHeaders() });
|
|
194
195
|
if (res.status === 404) return null;
|
|
@@ -200,7 +201,7 @@ export async function getBySlug<TPopulate extends <%= model.pascal %>Populate |
|
|
|
200
201
|
export async function getByField<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(
|
|
201
202
|
fieldName: string,
|
|
202
203
|
value: string,
|
|
203
|
-
options: { locale?: string; populate?: TPopulate } = {}
|
|
204
|
+
options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}
|
|
204
205
|
): Promise<<%= model.pascal %>Result<undefined, TPopulate> | null> {
|
|
205
206
|
const res = await fetch(`${base}${withReadOptions(`/entries/<%= model.uid %>/field/${encodeURIComponent(fieldName)}/${encodeURIComponent(value)}`, options)}`, { headers: fimoHeaders() });
|
|
206
207
|
if (res.status === 404) return null;
|
|
@@ -282,7 +283,7 @@ export async function remove(id: string): Promise<void> {
|
|
|
282
283
|
}
|
|
283
284
|
<% } else { -%>
|
|
284
285
|
export async function get<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(
|
|
285
|
-
options: { locale?: string; populate?: TPopulate } = {}
|
|
286
|
+
options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}
|
|
286
287
|
): Promise<<%= model.pascal %>Result<undefined, TPopulate>> {
|
|
287
288
|
const res = await fetch(`${base}${withReadOptions(`/documents/<%= model.uid %>`, options)}`, { headers: fimoHeaders() });
|
|
288
289
|
if (!res.ok) throw new Error(await res.text());
|
|
@@ -311,25 +312,25 @@ export async function update(payload: Partial<<%= model.pascal %>Input>): Promis
|
|
|
311
312
|
<% if (!model.isSingleton) { -%>
|
|
312
313
|
const qk = {
|
|
313
314
|
root : ["<%= model.pascal %>"] as const,
|
|
314
|
-
byId : <TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate } = {}) => ["<%= model.pascal %>", id, options.locale, options.populate] as const,
|
|
315
|
-
bySlug : <TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(slug: string, options: { locale?: string; populate?: TPopulate } = {}) => ["<%= model.pascal %>", "slug", slug, options.locale, options.populate] as const,
|
|
316
|
-
byField : <TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(field: string, value: string, options: { locale?: string; populate?: TPopulate } = {}) => ["<%= model.pascal %>", field, value, options.locale, options.populate] as const,
|
|
315
|
+
byId : <TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) => ["<%= model.pascal %>", id, options.locale, options.populate] as const,
|
|
316
|
+
bySlug : <TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(slug: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) => ["<%= model.pascal %>", "slug", slug, options.locale, options.populate] as const,
|
|
317
|
+
byField : <TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(field: string, value: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) => ["<%= model.pascal %>", field, value, options.locale, options.populate] as const,
|
|
317
318
|
list : (params: <%= model.pascal %>Query) => ["<%= model.pascal %>", params] as const
|
|
318
319
|
};
|
|
319
320
|
|
|
320
|
-
export function useGetById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate } = {}) {
|
|
321
|
+
export function useGetById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
321
322
|
const locale = readLocale(options, false, useQueryLocale(options.locale));
|
|
322
323
|
const localizedOptions = { ...options, locale };
|
|
323
324
|
return useQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.byId(id, localizedOptions), queryFn: () => getById(id, localizedOptions) });
|
|
324
325
|
}
|
|
325
326
|
|
|
326
|
-
export function useGetBySlug<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(slug: string, options: { locale?: string; populate?: TPopulate } = {}) {
|
|
327
|
+
export function useGetBySlug<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(slug: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
327
328
|
const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
|
|
328
329
|
const localizedOptions = { ...options, locale };
|
|
329
330
|
return useQuery<<%= model.pascal %>Result<undefined, TPopulate> | null, Error>({ queryKey: qk.bySlug(slug, localizedOptions), queryFn: () => getBySlug(slug, localizedOptions), enabled: !!slug });
|
|
330
331
|
}
|
|
331
332
|
|
|
332
|
-
export function useGetByField<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(fieldName: string, value: string, options: { locale?: string; populate?: TPopulate } = {}) {
|
|
333
|
+
export function useGetByField<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(fieldName: string, value: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
333
334
|
const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
|
|
334
335
|
const localizedOptions = { ...options, locale };
|
|
335
336
|
return useQuery<<%= model.pascal %>Result<undefined, TPopulate> | null, Error>({ queryKey: qk.byField(fieldName, value, localizedOptions), queryFn: () => getByField(fieldName, value, localizedOptions), enabled: !!value });
|
|
@@ -344,19 +345,19 @@ export function useGet<
|
|
|
344
345
|
return useQuery<<%= model.pascal %>Result<TFields, TPopulate>[], Error>({ queryKey: qk.list(localizedParams), queryFn: () => get(localizedParams) });
|
|
345
346
|
}
|
|
346
347
|
|
|
347
|
-
export function useSuspenseGetById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate } = {}) {
|
|
348
|
+
export function useSuspenseGetById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
348
349
|
const locale = readLocale(options, false, useQueryLocale(options.locale));
|
|
349
350
|
const localizedOptions = { ...options, locale };
|
|
350
351
|
return useSuspenseQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.byId(id, localizedOptions), queryFn: () => getById(id, localizedOptions) });
|
|
351
352
|
}
|
|
352
353
|
|
|
353
|
-
export function useSuspenseGetBySlug<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(slug: string, options: { locale?: string; populate?: TPopulate } = {}) {
|
|
354
|
+
export function useSuspenseGetBySlug<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(slug: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
354
355
|
const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
|
|
355
356
|
const localizedOptions = { ...options, locale };
|
|
356
357
|
return useSuspenseQuery<<%= model.pascal %>Result<undefined, TPopulate> | null, Error>({ queryKey: qk.bySlug(slug, localizedOptions), queryFn: () => getBySlug(slug, localizedOptions) });
|
|
357
358
|
}
|
|
358
359
|
|
|
359
|
-
export function useSuspenseGetByField<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(fieldName: string, value: string, options: { locale?: string; populate?: TPopulate } = {}) {
|
|
360
|
+
export function useSuspenseGetByField<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(fieldName: string, value: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
360
361
|
const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
|
|
361
362
|
const localizedOptions = { ...options, locale };
|
|
362
363
|
return useSuspenseQuery<<%= model.pascal %>Result<undefined, TPopulate> | null, Error>({ queryKey: qk.byField(fieldName, value, localizedOptions), queryFn: () => getByField(fieldName, value, localizedOptions) });
|
|
@@ -403,16 +404,16 @@ export function useRemove() {
|
|
|
403
404
|
<% } else { -%>
|
|
404
405
|
const qk = {
|
|
405
406
|
root: ["<%= model.pascal %>"] as const,
|
|
406
|
-
document: <TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(options: { locale?: string; populate?: TPopulate } = {}) => ["<%= model.pascal %>", "document", options.locale, options.populate] as const
|
|
407
|
+
document: <TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) => ["<%= model.pascal %>", "document", options.locale, options.populate] as const
|
|
407
408
|
};
|
|
408
409
|
|
|
409
|
-
export function useGet<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(options: { locale?: string; populate?: TPopulate } = {}) {
|
|
410
|
+
export function useGet<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
410
411
|
const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
|
|
411
412
|
const localizedOptions = { ...options, locale };
|
|
412
413
|
return useQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.document(localizedOptions), queryFn: () => get(localizedOptions) });
|
|
413
414
|
}
|
|
414
415
|
|
|
415
|
-
export function useSuspenseGet<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(options: { locale?: string; populate?: TPopulate } = {}) {
|
|
416
|
+
export function useSuspenseGet<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
416
417
|
const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
|
|
417
418
|
const localizedOptions = { ...options, locale };
|
|
418
419
|
return useSuspenseQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.document(localizedOptions), queryFn: () => get(localizedOptions) });
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
%>
|
|
10
10
|
// AUTO-GENERATED ⚠️ – do not edit by hand
|
|
11
|
-
import { type Fields, type FimoBoolean, type FimoMedia, type FimoReference, type FimoRichText, type FimoRichTextContent, type FimoString, type Populate, type PopulatedResult, type ProjectedWithPopulate, type Query, type Sort, type Where, type
|
|
11
|
+
import { type Fields, type FimoBoolean, type FimoMedia, type FimoReference, type FimoRichText, type FimoRichTextContent, type FimoString, type Populate, type PopulatedResult, type ProjectedWithPopulate, type Query, type Sort, type Where, type WithReferences } from "fimo/ui";
|
|
12
12
|
<%
|
|
13
|
-
if (model.
|
|
14
|
-
model.
|
|
13
|
+
if (model.referenceImports && model.referenceImports.length > 0) {
|
|
14
|
+
model.referenceImports.forEach((referenceImport) => {
|
|
15
15
|
-%>
|
|
16
|
-
import type { <%=
|
|
16
|
+
import type { <%= referenceImport.pascal %> } from "./<%= referenceImport.pascal %>.types";
|
|
17
17
|
<%
|
|
18
18
|
});
|
|
19
19
|
}
|
|
@@ -30,7 +30,7 @@ export interface <%= model.pascal %>FimoMeta {
|
|
|
30
30
|
contentType: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export interface <%= model.pascal %> extends
|
|
33
|
+
export interface <%= model.pascal %> extends WithReferences<<%= model.pascal %>References> {
|
|
34
34
|
id: string;
|
|
35
35
|
documentId: string;
|
|
36
36
|
slug: FimoString;
|
|
@@ -93,7 +93,7 @@ function getTypeForField(field) {
|
|
|
93
93
|
return 'Record<string, any>';
|
|
94
94
|
case 'media':
|
|
95
95
|
return field.multiple ? 'FimoMedia[]' : 'FimoMedia';
|
|
96
|
-
case '
|
|
96
|
+
case 'reference':
|
|
97
97
|
return field.many ? 'FimoReference[]' : 'FimoReference | null';
|
|
98
98
|
default:
|
|
99
99
|
return 'any';
|
|
@@ -118,7 +118,7 @@ function getInputTypeForField(field) {
|
|
|
118
118
|
const media = 'Pick<FimoMedia, "url" | "alt" | "name" | "mime" | "width" | "height">';
|
|
119
119
|
return field.multiple ? `${media}[]` : media;
|
|
120
120
|
}
|
|
121
|
-
case '
|
|
121
|
+
case 'reference':
|
|
122
122
|
return field.many ? 'FimoReference[]' : 'FimoReference | null';
|
|
123
123
|
default:
|
|
124
124
|
return 'unknown';
|
|
@@ -127,11 +127,11 @@ function getInputTypeForField(field) {
|
|
|
127
127
|
-%>
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
export interface <%= model.pascal %>
|
|
130
|
+
export interface <%= model.pascal %>References {
|
|
131
131
|
<%
|
|
132
132
|
if (model.fields) {
|
|
133
133
|
Object.entries(model.fields).forEach(([key, field]) => {
|
|
134
|
-
if (field.type !== '
|
|
134
|
+
if (field.type !== 'reference' || typeof field.targetPascal !== 'string') {
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
137
|
const type = field.many ? `${field.targetPascal}[]` : `${field.targetPascal} | null`;
|
|
@@ -18,8 +18,8 @@ This file covers the **package surface**: the JSON shape you write, the generate
|
|
|
18
18
|
"featured": { "type": "boolean" },
|
|
19
19
|
"coverImage": { "type": "media" },
|
|
20
20
|
"metadata": { "type": "json" },
|
|
21
|
-
"author": { "type": "
|
|
22
|
-
"contributors": { "type": "
|
|
21
|
+
"author": { "type": "reference", "target": "Author" },
|
|
22
|
+
"contributors": { "type": "reference", "target": "Author", "many": true }
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
```
|
|
@@ -45,22 +45,22 @@ something else (e.g. `handle`, `permalink`).
|
|
|
45
45
|
|
|
46
46
|
## Field types
|
|
47
47
|
|
|
48
|
-
| Type
|
|
49
|
-
|
|
|
50
|
-
| `string`
|
|
51
|
-
| `richtext`
|
|
52
|
-
| `number`
|
|
53
|
-
| `boolean`
|
|
54
|
-
| `date`
|
|
55
|
-
| `media`
|
|
56
|
-
| `json`
|
|
57
|
-
| `
|
|
48
|
+
| Type | `data` JSON shape | Use for |
|
|
49
|
+
| ----------- | ------------------------------------------------------------------------------------- | --------------------------------- |
|
|
50
|
+
| `string` | `"..."` | Titles, short text |
|
|
51
|
+
| `richtext` | Tiptap JSONContent (see below) | Blog body, descriptions, articles |
|
|
52
|
+
| `number` | `42` | Counts, quantities |
|
|
53
|
+
| `boolean` | `true` / `false` | Flags |
|
|
54
|
+
| `date` | ISO date string, e.g. `"2026-06-03"` | Publish dates, timestamps |
|
|
55
|
+
| `media` | `{ "source": "local"\|"external", "url": "https://...", "alt": "..." }` | Images, video |
|
|
56
|
+
| `json` | any JSON | Structured metadata |
|
|
57
|
+
| `reference` | `{ "documentId": "..." }`, `null`, or an array of reference objects when `many: true` | References to another schema |
|
|
58
58
|
|
|
59
59
|
A `date` field currently generates as `any` in the TS client, so for typed access (and the `<Date>` component) prefer a `string` field holding an ISO date — which is what the `publishDate` examples below do.
|
|
60
60
|
|
|
61
|
-
##
|
|
61
|
+
## Reference fields
|
|
62
62
|
|
|
63
|
-
Use `
|
|
63
|
+
Use `reference` when one content entry references another content entry. Do not store entry IDs in a `json` field as a workaround.
|
|
64
64
|
|
|
65
65
|
```json
|
|
66
66
|
{
|
|
@@ -68,8 +68,8 @@ Use `relation` when one content entry references another content entry. Do not s
|
|
|
68
68
|
"name": "Blog Posts",
|
|
69
69
|
"fields": {
|
|
70
70
|
"title": { "type": "string" },
|
|
71
|
-
"author": { "type": "
|
|
72
|
-
"contributors": { "type": "
|
|
71
|
+
"author": { "type": "reference", "target": "Author" },
|
|
72
|
+
"contributors": { "type": "reference", "target": "Author", "many": true }
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
```
|
|
@@ -77,21 +77,21 @@ Use `relation` when one content entry references another content entry. Do not s
|
|
|
77
77
|
Rules:
|
|
78
78
|
|
|
79
79
|
- `target` is required and must be the PascalCase schema `uid` of the referenced content type.
|
|
80
|
-
- Omit `many` for a to-one
|
|
81
|
-
- Self-references are allowed, e.g. a `Category` schema can have `parent: { "type": "
|
|
80
|
+
- Omit `many` for a to-one reference. Use `"many": true` for a to-many reference.
|
|
81
|
+
- Self-references are allowed, e.g. a `Category` schema can have `parent: { "type": "reference", "target": "Category" }`.
|
|
82
82
|
- Entry values store reference objects with the target `documentId`, not row `id` values: `{ "documentId": "..." }`.
|
|
83
|
-
- Do not include `contentType` for normal fixed-target
|
|
84
|
-
- Do not add `localized` or field-level `i18n` to a
|
|
85
|
-
- Use `null` for an empty to-one
|
|
86
|
-
- The API validates that referenced target documents exist and rejects duplicate values in to-many
|
|
83
|
+
- Do not include `contentType` for normal fixed-target references; the schema's `target` already defines it.
|
|
84
|
+
- Do not add `localized` or field-level `i18n` to a reference. The reference value belongs to the entry variant that stores it.
|
|
85
|
+
- Use `null` for an empty to-one reference and `[]` for an empty to-many reference.
|
|
86
|
+
- The API validates that referenced target documents exist and rejects duplicate values in to-many reference arrays.
|
|
87
87
|
|
|
88
88
|
Entry payload example:
|
|
89
89
|
|
|
90
90
|
```json
|
|
91
91
|
{
|
|
92
92
|
"data": {
|
|
93
|
-
"slug": "
|
|
94
|
-
"title": "
|
|
93
|
+
"slug": "reference-example",
|
|
94
|
+
"title": "Reference example",
|
|
95
95
|
"author": { "documentId": "750e8400-e29b-41d4-a716-446655440000" },
|
|
96
96
|
"contributors": [{ "documentId": "750e8400-e29b-41d4-a716-446655440000" }]
|
|
97
97
|
}
|
|
@@ -104,8 +104,8 @@ After editing schema JSON, run `fimo schemas push {Uid}` and then `fimo validate
|
|
|
104
104
|
|
|
105
105
|
- Adding fields and changing labels preserve entries.
|
|
106
106
|
- Removing a field removes only that field's values.
|
|
107
|
-
- A one-to-many
|
|
108
|
-
- A many-to-one change,
|
|
107
|
+
- A one-to-many reference change wraps each existing reference in an array.
|
|
108
|
+
- A many-to-one change, reference target change, scalar type change, or scalar/reference change clears only the changed field.
|
|
109
109
|
- Collection-to-singleton removes that type's entries and cleans references to those documents.
|
|
110
110
|
- Singleton-to-collection preserves the existing row.
|
|
111
111
|
|
|
@@ -222,9 +222,9 @@ For shared content, set `i18n: { "enabled": false }` on the schema and create ea
|
|
|
222
222
|
- have no public `locale` property;
|
|
223
223
|
- are returned in every locale context without duplication;
|
|
224
224
|
- ignore locale on owner reads and writes;
|
|
225
|
-
- still use the active/requested locale when a populated
|
|
225
|
+
- still use the active/requested locale when a populated reference targets a localized schema.
|
|
226
226
|
|
|
227
|
-
Do not duplicate shared entries across locales
|
|
227
|
+
Do not duplicate shared entries across locales or add field-level localization settings. Do not implement locale fallbacks by hand: reads accept an opt-in `localeFallbacks: true` option that resolves populated references missing in the requested locale to the best available variant (project default first), marked by its `locale` field. Public reads stay strict unless the option is passed.
|
|
228
228
|
|
|
229
229
|
**Generated API:**
|
|
230
230
|
|
|
@@ -290,8 +290,8 @@ BlogPost.useGet({
|
|
|
290
290
|
```
|
|
291
291
|
|
|
292
292
|
Operators: scalar equality, `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$contains`, `$startsWith`, `$null`.
|
|
293
|
-
|
|
294
|
-
For to-many
|
|
293
|
+
Reference field values are reference objects, but reference filters use the target `documentId` string, for example `where: { author: "750e..." }`; for to-many references that means "contains this target document".
|
|
294
|
+
For to-many references, `$eq` and `$contains` mean membership, `$in` means any listed document is present, `$ne` and `$nin` mean none are present, and `$null: true` means empty or missing. Reference fields cannot be sorted.
|
|
295
295
|
|
|
296
296
|
Use `fields` to keep list payloads small. The API always keeps `id`, and returns only the requested fields beyond that:
|
|
297
297
|
|
|
@@ -304,7 +304,7 @@ const { data: summaries = [] } = BlogPost.useGet({
|
|
|
304
304
|
// summaries: Array<Pick<BlogPost, 'id' | 'slug' | 'title' | 'excerpt' | 'coverImage'>>
|
|
305
305
|
```
|
|
306
306
|
|
|
307
|
-
Use `populate` to embed
|
|
307
|
+
Use `populate` to embed reference targets in read results:
|
|
308
308
|
|
|
309
309
|
```tsx
|
|
310
310
|
const { data: posts = [] } = BlogPost.useGet({
|
|
@@ -327,11 +327,11 @@ type PostCardItem = BlogPostResult<
|
|
|
327
327
|
>;
|
|
328
328
|
```
|
|
329
329
|
|
|
330
|
-
Do not type populated hook data as `BlogPost[]`: plain `BlogPost` keeps
|
|
330
|
+
Do not type populated hook data as `BlogPost[]`: plain `BlogPost` keeps reference fields as `FimoReference` objects, while `BlogPostResult<TFields, TPopulate>` models the projected/populated read result.
|
|
331
331
|
|
|
332
|
-
`BlogPostPopulate` is the generated input type for allowed populate values. It comes from the schema's
|
|
332
|
+
`BlogPostPopulate` is the generated input type for allowed populate values. It comes from the schema's reference fields, so a post schema with `author` and `contributors` only accepts those names in `populate`. Most app code never imports it directly; it is mainly used by `BlogPostQuery` and hook generics.
|
|
333
333
|
|
|
334
|
-
Populate is depth-one. It only works for
|
|
334
|
+
Populate is depth-one. It only works for reference fields; trying to populate a scalar field returns 400. To-many reference order is preserved.
|
|
335
335
|
|
|
336
336
|
The generated hooks do **not** surface pagination meta (`total`, `hasMore`, `limit`, `offset`). If you need it, read it from a raw fetch to `/entries/<typeName>` (see the **Read path** section below), which returns the full `{ data, meta }` envelope.
|
|
337
337
|
|
|
@@ -348,7 +348,7 @@ The entry objects a hook returns are **not** plain JS values. The generated clie
|
|
|
348
348
|
| `number` | `number` | plain — not wrapped |
|
|
349
349
|
| `json` | `Record<string, any>` | plain — not wrapped |
|
|
350
350
|
| `date` | `any` (raw ISO date string) | not wrapped (`FimoDate` is not emitted — see below) |
|
|
351
|
-
| `
|
|
351
|
+
| `reference` | `FimoReference` / `FimoReference[]` | reference object(s) with target `documentId`; use `populate` when embedded target rows are needed |
|
|
352
352
|
|
|
353
353
|
Rules of thumb:
|
|
354
354
|
|
|
@@ -29,7 +29,7 @@ The command is `fimo schemas` (plural). There is no singular `fimo schema` alias
|
|
|
29
29
|
|
|
30
30
|
All commands accept `-C, --cwd <dir>` to point at a different project directory.
|
|
31
31
|
|
|
32
|
-
Schema transitions are automatic. Adding fields and changing labels preserve content. Removing a field removes only that field's stored values. One-to-many
|
|
32
|
+
Schema transitions are automatic. Adding fields and changing labels preserve content. Removing a field removes only that field's stored values. One-to-many reference changes wrap existing references in arrays. Field type changes, scalar/reference changes, reference target changes, and many-to-one changes clear only the changed field. Collection-to-singleton removes that type's entries and cleans inbound references; singleton-to-collection preserves its row. There are no reset or data-loss flags: changing the schema is the migration request. Run `fimo validate` after pushing to verify the regenerated client and whole project.
|
|
33
33
|
|
|
34
34
|
## Entries CLI commands
|
|
35
35
|
|
|
@@ -60,20 +60,20 @@ fimo entries list BlogPost --fields slug,title,author --populate author
|
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
Use the canonical read query flags: `--search`, `--sort`, `--where`, `--limit`, `--offset`, `--fields`, and `--populate`.
|
|
63
|
-
`--where` is a JSON object keyed by schema field name. Simple values mean equality, so booleans, strings, and numbers should stay typed inside the JSON (`{"featured":true}`, `{"category":"react"}`, `{"rank":{"$gte":3}}`).
|
|
63
|
+
`--where` is a JSON object keyed by schema field name. Simple values mean equality, so booleans, strings, and numbers should stay typed inside the JSON (`{"featured":true}`, `{"category":"react"}`, `{"rank":{"$gte":3}}`). Reference field values are reference objects, but reference filters use the target `documentId` string, for example `{"author":"750e..."}`; for to-many references that means "contains this target document". Public Fimo-owned metadata lives under `__fimo`; currently `__fimo.contentType` is filterable. Operator objects support `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$contains`, `$startsWith`, and `$null`, subject to the field type. For to-many references, `$eq` and `$contains` mean membership, `$in` means any listed document is present, `$ne` and `$nin` mean none are present, and `$null: true` means empty or missing. Reference fields cannot be sorted. Use `--sort field` for ascending and `--sort -field` for descending on scalar fields.
|
|
64
64
|
`--fields` is a comma-separated projection for list payloads. The API always keeps `id`; pass only the fields your view needs, such as `slug,title,excerpt,coverImage`.
|
|
65
65
|
|
|
66
|
-
`--populate` is a comma-separated list of
|
|
66
|
+
`--populate` is a comma-separated list of reference fields to embed. Without `--populate`, reference fields return reference objects like `{ "documentId": "..." }`. With `--populate author,contributors`, those fields return the target entry objects for the requested locale. Only reference fields can be populated; asking to populate a scalar field returns 400. Populated targets missing in the requested locale resolve to null by default; pass `localeFallbacks=true` (query param, or the `localeFallbacks` option in generated hooks) to fall back to the best available locale variant, marked by its `locale` field. Limits: a to-many field accepts at most 200 references per write; one populate request resolves at most 500 distinct target documents per field and returns 400 past that.
|
|
67
67
|
|
|
68
68
|
In generated React code, the equivalent is `populate: ['author', 'contributors'] as const` in the schema hook/query params. Let hooks infer the result when possible; if a named TypeScript shape is needed, use the generated `BlogPostResult<TFields, TPopulate>` type documented in `fimo/references/content.md`. Writes use the separate generated `BlogPostInput` type.
|
|
69
69
|
|
|
70
|
-
For
|
|
70
|
+
For reference schema JSON and entry value shape, load `fimo/references/content.md` § Reference fields. The short version:
|
|
71
71
|
|
|
72
72
|
```json
|
|
73
73
|
{
|
|
74
74
|
"fields": {
|
|
75
|
-
"author": { "type": "
|
|
76
|
-
"contributors": { "type": "
|
|
75
|
+
"author": { "type": "reference", "target": "Author" },
|
|
76
|
+
"contributors": { "type": "reference", "target": "Author", "many": true }
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
```
|
|
@@ -84,7 +84,7 @@ Entry bodies store reference objects with target `documentId` values:
|
|
|
84
84
|
fimo entries create BlogPost --body '{"data":{"slug":"hello","title":"Hello","author":{"documentId":"750e8400-e29b-41d4-a716-446655440000"},"contributors":[]}}'
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
Do not add `localized` or field-level `i18n` to
|
|
87
|
+
Do not add `localized` or field-level `i18n` to reference fields. The reference value belongs to the entry variant that stores it.
|
|
88
88
|
|
|
89
89
|
## Localized and shared content
|
|
90
90
|
|
|
@@ -92,7 +92,7 @@ Schemas are localized by default. Omit `--locale` for default-locale entries. Pa
|
|
|
92
92
|
|
|
93
93
|
When creating a localized variant of an existing entry, pass the source entry's `documentId` with `--document-id`. Keep projects single-locale unless the user asks for multilingual or localized output; when multiple locales are configured, create and update every locale explicitly.
|
|
94
94
|
|
|
95
|
-
For content shared across locales, define `"i18n": { "enabled": false }` at the schema root and create each entry once. Do not pass `--document-id` to manufacture locale variants. Shared entry responses omit `locale`; a read locale is only used to resolve populated
|
|
95
|
+
For content shared across locales, define `"i18n": { "enabled": false }` at the schema root and create each entry once. Do not pass `--document-id` to manufacture locale variants. Shared entry responses omit `locale`; a read locale is only used to resolve populated references that target localized schemas. No other `i18n` options and no field-level localization settings are supported.
|
|
96
96
|
|
|
97
97
|
Use `"isSingleton": true` for a type with exactly one logical document. Generated singleton clients expose `get`/`update` and their React Query hooks, not collection create/remove APIs.
|
|
98
98
|
|
|
@@ -97,7 +97,7 @@ node <this-skill>/scripts/write-migration-report.mjs <project-root>
|
|
|
97
97
|
|
|
98
98
|
- `references/workflow.md` - full decision tree and phase order.
|
|
99
99
|
- `references/manifest.md` - canonical migration ledger schema.
|
|
100
|
-
- `references/content.md` - CMS, entries,
|
|
100
|
+
- `references/content.md` - CMS, entries, references, and source value rules.
|
|
101
101
|
- `references/media.md` - media inventory, upload, dedupe, and replacement rules.
|
|
102
102
|
- `references/forms.md` - form classification and Fimo replacement rules.
|
|
103
103
|
- `references/import-contract.md` - exact import obligations for media, entries,
|
|
@@ -73,20 +73,20 @@ For every content type:
|
|
|
73
73
|
Do not wire a page to remote Fimo content until the matching schema exists and
|
|
74
74
|
the page has a seed/empty state for local development.
|
|
75
75
|
|
|
76
|
-
##
|
|
76
|
+
## References
|
|
77
77
|
|
|
78
78
|
Create references only when:
|
|
79
79
|
|
|
80
|
-
- exports contain stable
|
|
80
|
+
- exports contain stable reference IDs
|
|
81
81
|
- repeated values clearly match another exported collection
|
|
82
82
|
- the user confirms the relationship
|
|
83
83
|
|
|
84
84
|
Use a two-pass import for references:
|
|
85
85
|
|
|
86
86
|
1. import all referenced entries with source IDs and slugs
|
|
87
|
-
2. resolve
|
|
87
|
+
2. resolve reference fields to target IDs/slugs
|
|
88
88
|
|
|
89
|
-
If
|
|
89
|
+
If reference confidence is low, keep a string/tag field and record an unresolved
|
|
90
90
|
item.
|
|
91
91
|
|
|
92
92
|
## Skips
|
|
@@ -50,7 +50,7 @@ Import requirements:
|
|
|
50
50
|
- entries are created or updated in Fimo, not only written as seed JSON
|
|
51
51
|
- source count, generated count, imported count, skipped count, and unresolved
|
|
52
52
|
count are recorded per content type
|
|
53
|
-
-
|
|
53
|
+
- references are resolved in a second pass only when stable IDs or confirmed
|
|
54
54
|
mappings exist
|
|
55
55
|
|
|
56
56
|
Acceptance:
|
|
@@ -37,7 +37,7 @@ Use these notes to decide what is source of truth and what must be rebuilt.
|
|
|
37
37
|
- Wix/Velo app logic, dynamic pages, search, forms, ecommerce, auth, and member
|
|
38
38
|
areas need explicit replacement plans.
|
|
39
39
|
- Large collections may be split across several files. Preserve stable IDs if
|
|
40
|
-
present so
|
|
40
|
+
present so references can be reconstructed later.
|
|
41
41
|
|
|
42
42
|
## Carrd and static exports
|
|
43
43
|
|
|
@@ -85,7 +85,7 @@ Before writing page code:
|
|
|
85
85
|
2. Map each CMS export or inferred collection to a Fimo schema or unresolved item.
|
|
86
86
|
3. Preserve stable source IDs, slugs, dates, SEO, authors, categories, tags,
|
|
87
87
|
locales, and publish status.
|
|
88
|
-
4. Decide which
|
|
88
|
+
4. Decide which references are real. Do not invent references from vague text.
|
|
89
89
|
5. Build seed/import entries with source IDs and media references preserved.
|
|
90
90
|
6. Record source count and planned import count for each content type.
|
|
91
91
|
|
|
@@ -222,7 +222,7 @@ function normalizeSourceContentTypes(contentTypes) {
|
|
|
222
222
|
plannedCount: numberOrNull(type.plannedCount),
|
|
223
223
|
importedCount: numberOrNull(type.importedCount),
|
|
224
224
|
fields: type.fields ?? {},
|
|
225
|
-
|
|
225
|
+
references: type.references ?? [],
|
|
226
226
|
}));
|
|
227
227
|
}
|
|
228
228
|
|
|
@@ -240,7 +240,7 @@ function normalizeUrlCollections(collections) {
|
|
|
240
240
|
plannedCount: asArray(collection.sourceRoutes).length,
|
|
241
241
|
importedCount: 0,
|
|
242
242
|
fields: Object.fromEntries(asArray(collection.fields).map((field) => [field, { source: field }])),
|
|
243
|
-
|
|
243
|
+
references: [],
|
|
244
244
|
notes: ['Inferred from crawled route patterns. Review before import.'],
|
|
245
245
|
}));
|
|
246
246
|
}
|
package/dist/cli/bundle.json
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -42008,7 +42008,7 @@ var init_common = __esm({
|
|
|
42008
42008
|
"boolean",
|
|
42009
42009
|
"date",
|
|
42010
42010
|
"json",
|
|
42011
|
-
"
|
|
42011
|
+
"reference"
|
|
42012
42012
|
]);
|
|
42013
42013
|
RESERVED_CONTENT_FIELD_NAMES = [
|
|
42014
42014
|
"id",
|
|
@@ -42152,6 +42152,36 @@ var init_realtime = __esm({
|
|
|
42152
42152
|
}
|
|
42153
42153
|
});
|
|
42154
42154
|
|
|
42155
|
+
// ../../packages/api-contracts/dist/esm/types/references.js
|
|
42156
|
+
function referenceDocumentId(value) {
|
|
42157
|
+
if (typeof value === "string") {
|
|
42158
|
+
return value;
|
|
42159
|
+
}
|
|
42160
|
+
if (value && typeof value === "object" && "documentId" in value) {
|
|
42161
|
+
const documentId = value.documentId;
|
|
42162
|
+
return typeof documentId === "string" ? documentId : null;
|
|
42163
|
+
}
|
|
42164
|
+
return null;
|
|
42165
|
+
}
|
|
42166
|
+
function toReferenceValue(value) {
|
|
42167
|
+
const documentId = referenceDocumentId(value);
|
|
42168
|
+
return documentId ? {
|
|
42169
|
+
documentId
|
|
42170
|
+
} : null;
|
|
42171
|
+
}
|
|
42172
|
+
var ReferenceValueSchema;
|
|
42173
|
+
var init_references = __esm({
|
|
42174
|
+
"../../packages/api-contracts/dist/esm/types/references.js"() {
|
|
42175
|
+
"use strict";
|
|
42176
|
+
init_zod();
|
|
42177
|
+
__name(referenceDocumentId, "referenceDocumentId");
|
|
42178
|
+
__name(toReferenceValue, "toReferenceValue");
|
|
42179
|
+
ReferenceValueSchema = external_exports.preprocess((value) => toReferenceValue(value) ?? value, external_exports.object({
|
|
42180
|
+
documentId: external_exports.uuid()
|
|
42181
|
+
}));
|
|
42182
|
+
}
|
|
42183
|
+
});
|
|
42184
|
+
|
|
42155
42185
|
// ../../packages/api-contracts/dist/esm/types/audit-log.js
|
|
42156
42186
|
var AUDIT_ACTIONS, AuditActionSchema, AUDIT_OUTCOMES, AuditOutcomeSchema, AUDIT_ACTOR_KINDS, AuditActorKindSchema;
|
|
42157
42187
|
var init_audit_log = __esm({
|
|
@@ -45513,6 +45543,7 @@ var init_esm = __esm({
|
|
|
45513
45543
|
init_attachments();
|
|
45514
45544
|
init_env2();
|
|
45515
45545
|
init_realtime();
|
|
45546
|
+
init_references();
|
|
45516
45547
|
init_audit_log();
|
|
45517
45548
|
init_project_dto();
|
|
45518
45549
|
init_project_env_dto();
|
|
@@ -47120,10 +47151,10 @@ function normalizeFields(fields) {
|
|
|
47120
47151
|
})
|
|
47121
47152
|
);
|
|
47122
47153
|
}
|
|
47123
|
-
function
|
|
47154
|
+
function referenceImportsFor(schema, fields) {
|
|
47124
47155
|
const imports = /* @__PURE__ */ new Map();
|
|
47125
47156
|
for (const field of Object.values(fields)) {
|
|
47126
|
-
if (field.type !== "
|
|
47157
|
+
if (field.type !== "reference" || !field.target || field.target === schema.uid) {
|
|
47127
47158
|
continue;
|
|
47128
47159
|
}
|
|
47129
47160
|
imports.set(field.target, { uid: field.target, pascal: pascalCase(field.target) });
|
|
@@ -47137,9 +47168,9 @@ function schemaToModel(schema, localizedByUid) {
|
|
|
47137
47168
|
pascal: pascalCase(schema.uid),
|
|
47138
47169
|
isSingleton: schema.isSingleton ?? false,
|
|
47139
47170
|
localized: schema.i18n?.enabled !== false,
|
|
47140
|
-
|
|
47171
|
+
localizedReferenceFields: Object.entries(fields).filter(([, field]) => field.type === "reference" && field.target && localizedByUid.get(field.target) !== false).map(([fieldName]) => fieldName),
|
|
47141
47172
|
fields,
|
|
47142
|
-
|
|
47173
|
+
referenceImports: referenceImportsFor(schema, fields)
|
|
47143
47174
|
};
|
|
47144
47175
|
}
|
|
47145
47176
|
function syncSchemas(options = {}) {
|
|
@@ -47246,7 +47277,7 @@ var init_sync_schemas = __esm({
|
|
|
47246
47277
|
__name(getSchemaFiles, "getSchemaFiles");
|
|
47247
47278
|
__name(readSchema, "readSchema");
|
|
47248
47279
|
__name(normalizeFields, "normalizeFields");
|
|
47249
|
-
__name(
|
|
47280
|
+
__name(referenceImportsFor, "referenceImportsFor");
|
|
47250
47281
|
__name(schemaToModel, "schemaToModel");
|
|
47251
47282
|
__name(syncSchemas, "syncSchemas");
|
|
47252
47283
|
}
|
|
@@ -103088,34 +103119,34 @@ function validateSchemaFile(absolutePath, file2, schemaUids = /* @__PURE__ */ ne
|
|
|
103088
103119
|
file: file2
|
|
103089
103120
|
});
|
|
103090
103121
|
}
|
|
103091
|
-
if (type === "
|
|
103122
|
+
if (type === "reference") {
|
|
103092
103123
|
const { target, many } = definition;
|
|
103093
103124
|
if (typeof target !== "string" || !CONTENT_TYPE_NAME_PATTERN.test(target)) {
|
|
103094
103125
|
errors2.push({
|
|
103095
|
-
rule: "schemas/
|
|
103096
|
-
message: `
|
|
103126
|
+
rule: "schemas/reference-target",
|
|
103127
|
+
message: `Reference field "${fieldName}" must declare a valid "target" content type.`,
|
|
103097
103128
|
file: file2
|
|
103098
103129
|
});
|
|
103099
103130
|
} else if (schemaUids.size > 0 && !schemaUids.has(target)) {
|
|
103100
103131
|
errors2.push({
|
|
103101
|
-
rule: "schemas/
|
|
103102
|
-
message: `
|
|
103132
|
+
rule: "schemas/reference-target",
|
|
103133
|
+
message: `Reference field "${fieldName}" targets unknown schema "${target}".`,
|
|
103103
103134
|
file: file2
|
|
103104
103135
|
});
|
|
103105
103136
|
}
|
|
103106
103137
|
if (many !== void 0 && typeof many !== "boolean") {
|
|
103107
103138
|
errors2.push({
|
|
103108
|
-
rule: "schemas/
|
|
103109
|
-
message: `
|
|
103139
|
+
rule: "schemas/reference-many",
|
|
103140
|
+
message: `Reference field "${fieldName}" must use a boolean "many" value.`,
|
|
103110
103141
|
file: file2
|
|
103111
103142
|
});
|
|
103112
103143
|
}
|
|
103113
103144
|
} else {
|
|
103114
|
-
for (const
|
|
103115
|
-
if (
|
|
103145
|
+
for (const referenceOnlyKey of ["target", "many"]) {
|
|
103146
|
+
if (referenceOnlyKey in definition) {
|
|
103116
103147
|
errors2.push({
|
|
103117
|
-
rule: "schemas/
|
|
103118
|
-
message: `Field "${fieldName}": "${
|
|
103148
|
+
rule: "schemas/reference-shape",
|
|
103149
|
+
message: `Field "${fieldName}": "${referenceOnlyKey}" is only supported on reference fields.`,
|
|
103119
103150
|
file: file2
|
|
103120
103151
|
});
|
|
103121
103152
|
}
|
|
@@ -104893,8 +104924,8 @@ function fieldSignature(field) {
|
|
|
104893
104924
|
const type = typeof f.type === "string" ? f.type : "unknown";
|
|
104894
104925
|
const required2 = f.required === true;
|
|
104895
104926
|
const parts = [];
|
|
104896
|
-
if (type === "
|
|
104897
|
-
parts.push(`
|
|
104927
|
+
if (type === "reference" && typeof f.target === "string") {
|
|
104928
|
+
parts.push(`reference(${f.target})`);
|
|
104898
104929
|
} else if (type === "enum" && Array.isArray(f.options)) {
|
|
104899
104930
|
parts.push(`enum(${f.options.map(String).join("|")})`);
|
|
104900
104931
|
} else if (type === "number") {
|
|
@@ -110196,7 +110227,7 @@ schemas2.command("get").description("Get a schema by type name").argument("<type
|
|
|
110196
110227
|
schemas2.command("push").description("Push schema JSON files to the API (create or update)").argument("[typeName]", "Schema name to push (omit to push all)").option("-C, --cwd <dir>", "Project directory").action(schemaPushCommand);
|
|
110197
110228
|
schemas2.command("delete").description("Delete a schema by type name").argument("<typeName>", "Content type name").option("-C, --cwd <dir>", "Project directory").action(schemaDeleteCommand);
|
|
110198
110229
|
var entries = program.command("entries").description("Manage content entries");
|
|
110199
|
-
entries.command("list").description("List entries of a content type").argument("<typeName>", "Content type name").option("-C, --cwd <dir>", "Project directory").option("--limit <n>", "Max entries to return").option("--offset <n>", "Offset for pagination").option("--search <query>", "Search query").option("--where <json>", `JSON filter object, e.g. '{"featured":true}'`).option("--sort <field>", "Sort field, prefix with - for descending").option("--fields <fields>", "Comma-separated fields to return, e.g. slug,title,excerpt").option("--populate <fields>", "Comma-separated
|
|
110230
|
+
entries.command("list").description("List entries of a content type").argument("<typeName>", "Content type name").option("-C, --cwd <dir>", "Project directory").option("--limit <n>", "Max entries to return").option("--offset <n>", "Offset for pagination").option("--search <query>", "Search query").option("--where <json>", `JSON filter object, e.g. '{"featured":true}'`).option("--sort <field>", "Sort field, prefix with - for descending").option("--fields <fields>", "Comma-separated fields to return, e.g. slug,title,excerpt").option("--populate <fields>", "Comma-separated reference fields to populate, e.g. author,contributors").option("--locale <locale>", "Locale to list (default en)").option("--sort-by <field>", "Sort field").option("--sort-direction <dir>", "Sort direction (asc|desc)").action(entriesListCommand);
|
|
110200
110231
|
entries.command("get").description("Get a single entry").argument("<typeName>", "Content type name").argument("<id>", "Entry ID").option("-C, --cwd <dir>", "Project directory").option("--locale <locale>", "Locale to read (default en)").action(entriesGetCommand);
|
|
110201
110232
|
entries.command("create").description("Create a new entry").argument("<typeName>", "Content type name").option("-C, --cwd <dir>", "Project directory").requiredOption("--body <json>", "Entry JSON body (slug + data)").option("--locale <locale>", "Entry locale (default en)").option("--document-id <id>", "Document ID shared by localized variants").action(entriesCreateCommand);
|
|
110202
110233
|
entries.command("update").description("Update an existing entry").argument("<typeName>", "Content type name").argument("<id>", "Entry ID").option("-C, --cwd <dir>", "Project directory").requiredOption("--body <json>", "Updated entry JSON body").option("--locale <locale>", "Entry locale").option("--document-id <id>", "Document ID shared by localized variants").action(entriesUpdateCommand);
|
|
@@ -7,5 +7,5 @@ export { Boolean } from './components/Boolean.js';
|
|
|
7
7
|
export { RichText } from './components/RichText.js';
|
|
8
8
|
export { useLabels, useTranslations } from './translations.js';
|
|
9
9
|
export { FimoString, FimoDate, FimoMedia, FimoBoolean, FimoRichText, type FimoReference, type FimoSource, type FimoPart, type FimoRichTextContent, type FimoRichTextNodeProps, type FimoRichTextMarkProps, type FimoRichTextComponents, FIMO_SOURCE, FIMO_PARTS, isFimoString, isFimoDate, hasFimoSource, getFimoSource, getFimoParts, fimo, useFimoLiveValue, } from './lib/index.js';
|
|
10
|
-
export type { BoolOps, DateOps, Field, Fields, FilterFor, NullOnlyOps, NumberOps, Populate, Populated, PopulatedResult, Projected, ProjectedWithPopulate, Query,
|
|
10
|
+
export type { BoolOps, DateOps, Field, Fields, FilterFor, NullOnlyOps, NumberOps, Populate, Populated, PopulatedResult, Projected, ProjectedWithPopulate, Query, ReferenceField, ReferenceMap, ScalarOps, Sort, SortField, StringOps, WithReferences, Where, } from './lib/index.js';
|
|
11
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/runtime/primitives/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAGpD,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAG/D,OAAO,EACL,UAAU,EACV,QAAQ,EACR,SAAS,EACT,WAAW,EACX,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,WAAW,EACX,UAAU,EACV,YAAY,EACZ,UAAU,EACV,aAAa,EACb,aAAa,EACb,YAAY,EACZ,IAAI,EACJ,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAExB,YAAY,EACV,OAAO,EACP,OAAO,EACP,KAAK,EACL,MAAM,EACN,SAAS,EACT,WAAW,EACX,SAAS,EACT,QAAQ,EACR,SAAS,EACT,eAAe,EACf,SAAS,EACT,qBAAqB,EACrB,KAAK,EACL,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/runtime/primitives/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAGpD,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAG/D,OAAO,EACL,UAAU,EACV,QAAQ,EACR,SAAS,EACT,WAAW,EACX,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,WAAW,EACX,UAAU,EACV,YAAY,EACZ,UAAU,EACV,aAAa,EACb,aAAa,EACb,YAAY,EACZ,IAAI,EACJ,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAExB,YAAY,EACV,OAAO,EACP,OAAO,EACP,KAAK,EACL,MAAM,EACN,SAAS,EACT,WAAW,EACX,SAAS,EACT,QAAQ,EACR,SAAS,EACT,eAAe,EACf,SAAS,EACT,qBAAqB,EACrB,KAAK,EACL,cAAc,EACd,YAAY,EACZ,SAAS,EACT,IAAI,EACJ,SAAS,EACT,SAAS,EACT,cAAc,EACd,KAAK,GACN,MAAM,gBAAgB,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { FimoString, FimoDate, FimoMedia, FimoBoolean, FimoRichText, type FimoReference, type FimoSource, type FimoPart, type FimoRichTextContent, type FimoRichTextNodeProps, type FimoRichTextMarkProps, type FimoRichTextComponents, FIMO_SOURCE, FIMO_PARTS, isFimoString, isFimoDate, hasFimoSource, getFimoSource, getFimoParts, } from './primitives.js';
|
|
2
2
|
export { fimo } from './template.js';
|
|
3
3
|
export { useFimoLiveValue } from './useFimoLiveValue.js';
|
|
4
|
-
export type { BoolOps, DateOps, Field, Fields, FilterFor, NullOnlyOps, NumberOps, Populate, Populated, PopulatedResult, ProjectedWithPopulate, Projected, Query,
|
|
4
|
+
export type { BoolOps, DateOps, Field, Fields, FilterFor, NullOnlyOps, NumberOps, Populate, Populated, PopulatedResult, ProjectedWithPopulate, Projected, Query, ReferenceField, ReferenceMap, ScalarOps, Sort, SortField, StringOps, Where, WithReferences, } from './query.js';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/runtime/primitives/lib/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,UAAU,EACV,QAAQ,EACR,SAAS,EACT,WAAW,EACX,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,WAAW,EACX,UAAU,EACV,YAAY,EACZ,UAAU,EACV,aAAa,EACb,aAAa,EACb,YAAY,GACb,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAErC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,YAAY,EACV,OAAO,EACP,OAAO,EACP,KAAK,EACL,MAAM,EACN,SAAS,EACT,WAAW,EACX,SAAS,EACT,QAAQ,EACR,SAAS,EACT,eAAe,EACf,qBAAqB,EACrB,SAAS,EACT,KAAK,EACL,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/runtime/primitives/lib/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,UAAU,EACV,QAAQ,EACR,SAAS,EACT,WAAW,EACX,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,WAAW,EACX,UAAU,EACV,YAAY,EACZ,UAAU,EACV,aAAa,EACb,aAAa,EACb,YAAY,GACb,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAErC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,YAAY,EACV,OAAO,EACP,OAAO,EACP,KAAK,EACL,MAAM,EACN,SAAS,EACT,WAAW,EACX,SAAS,EACT,QAAQ,EACR,SAAS,EACT,eAAe,EACf,qBAAqB,EACrB,SAAS,EACT,KAAK,EACL,cAAc,EACd,YAAY,EACZ,SAAS,EACT,IAAI,EACJ,SAAS,EACT,SAAS,EACT,KAAK,EACL,cAAc,GACf,MAAM,YAAY,CAAC"}
|
|
@@ -25,14 +25,14 @@ export type BoolOps = ScalarOps<boolean>;
|
|
|
25
25
|
export type NullOnlyOps = {
|
|
26
26
|
$null?: boolean;
|
|
27
27
|
};
|
|
28
|
-
export type
|
|
28
|
+
export type ReferenceOps = {
|
|
29
29
|
$eq?: string | null;
|
|
30
30
|
$ne?: string | null;
|
|
31
31
|
$in?: string[];
|
|
32
32
|
$nin?: string[];
|
|
33
33
|
$null?: boolean;
|
|
34
34
|
};
|
|
35
|
-
export type
|
|
35
|
+
export type ManyReferenceOps = {
|
|
36
36
|
$eq?: string;
|
|
37
37
|
$ne?: string;
|
|
38
38
|
$in?: string[];
|
|
@@ -40,7 +40,7 @@ export type ManyRelationOps = {
|
|
|
40
40
|
$contains?: string;
|
|
41
41
|
$null?: boolean;
|
|
42
42
|
};
|
|
43
|
-
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 |
|
|
43
|
+
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 | ReferenceOps : [T] extends [FimoReference[]] ? string | ManyReferenceOps : [T] extends [FimoMedia | FimoMedia[]] ? NullOnlyOps : [T] extends [FimoRichText] ? string | StringOps : NullOnlyOps;
|
|
44
44
|
type BaseTopLevelWhere = {
|
|
45
45
|
id?: string | StringOps;
|
|
46
46
|
documentId?: string | StringOps;
|
|
@@ -69,18 +69,18 @@ type ListItem<TValue> = TValue extends readonly (infer K)[] ? K : TValue;
|
|
|
69
69
|
export type Projected<T extends {
|
|
70
70
|
id: string;
|
|
71
71
|
}, TFields extends Fields<T> | undefined> = [TFields] extends [undefined] ? T : Pick<T, Extract<'id' | SelectedFields<TFields>, keyof T>>;
|
|
72
|
-
export declare const
|
|
73
|
-
export interface
|
|
74
|
-
readonly [
|
|
72
|
+
export declare const FIMO_REFERENCES: unique symbol;
|
|
73
|
+
export interface WithReferences<TReferences> {
|
|
74
|
+
readonly [FIMO_REFERENCES]?: TReferences;
|
|
75
75
|
}
|
|
76
|
-
export type
|
|
77
|
-
export type
|
|
78
|
-
export type Populate<TEntry> =
|
|
79
|
-
type PopulatedKeys<TEntry, TPopulate> = Extract<ListItem<TPopulate>, keyof
|
|
76
|
+
export type ReferenceMap<TEntry> = TEntry extends WithReferences<infer TReferences> ? TReferences : never;
|
|
77
|
+
export type ReferenceField<TEntry> = Extract<keyof ReferenceMap<TEntry>, string>;
|
|
78
|
+
export type Populate<TEntry> = ReferenceField<TEntry> | readonly ReferenceField<TEntry>[];
|
|
79
|
+
type PopulatedKeys<TEntry, TPopulate> = Extract<ListItem<TPopulate>, keyof ReferenceMap<TEntry> & keyof TEntry>;
|
|
80
80
|
export type Populated<TEntry extends {
|
|
81
81
|
id: string;
|
|
82
82
|
}, TPopulate extends Populate<TEntry>> = Pick<TEntry, 'id'> & Omit<TEntry, PopulatedKeys<TEntry, TPopulate>> & {
|
|
83
|
-
[K in PopulatedKeys<TEntry, TPopulate>]:
|
|
83
|
+
[K in PopulatedKeys<TEntry, TPopulate>]: ReferenceMap<TEntry>[K];
|
|
84
84
|
};
|
|
85
85
|
export type PopulatedResult<TEntry extends {
|
|
86
86
|
id: string;
|
|
@@ -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,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEjH,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IACzB,GAAG,CAAC,EAAE,CAAC,CAAC;IACR,GAAG,CAAC,EAAE,CAAC,CAAC;IACR,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;IACV,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;IACX,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,CAAC,GAAG;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,CAAC,GAAG;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG,SAAS,CAAC;AAEhC,MAAM,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;AAEzC,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../../../src/runtime/primitives/lib/query.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,7 +16,7 @@ describe('fimo/ui public type surface', () => {
|
|
|
16
16
|
const sharedWhere = { locale: 'en' };
|
|
17
17
|
expectTypeOf(sharedWhere).toEqualTypeOf();
|
|
18
18
|
});
|
|
19
|
-
it('supports the
|
|
19
|
+
it('supports the reference populate pattern emitted into generated schemas', () => {
|
|
20
20
|
const query = {
|
|
21
21
|
where: {
|
|
22
22
|
sponsor: { $in: ['750e8400-e29b-41d4-a716-446655440000'] },
|
|
@@ -32,13 +32,13 @@ describe('fimo/ui public type surface', () => {
|
|
|
32
32
|
expectTypeOf().toEqualTypeOf();
|
|
33
33
|
expectTypeOf().toEqualTypeOf();
|
|
34
34
|
expectTypeOf().toEqualTypeOf();
|
|
35
|
-
// @ts-expect-error generated
|
|
35
|
+
// @ts-expect-error generated reference-aware queries only allow reference fields in populate.
|
|
36
36
|
const badQuery = { populate: 'name' };
|
|
37
37
|
expectTypeOf(badQuery).toEqualTypeOf();
|
|
38
|
-
// @ts-expect-error
|
|
38
|
+
// @ts-expect-error reference filters use target documentId strings, not raw reference objects.
|
|
39
39
|
const badWhere = { where: { sponsor: { documentId: '750e8400-e29b-41d4-a716-446655440000' } } };
|
|
40
40
|
expectTypeOf(badWhere).toEqualTypeOf();
|
|
41
|
-
// @ts-expect-error contains is only valid for to-many
|
|
41
|
+
// @ts-expect-error contains is only valid for to-many reference fields.
|
|
42
42
|
const badToOneContains = { where: { sponsor: { $contains: '750e8400-e29b-41d4-a716-446655440000' } } };
|
|
43
43
|
expectTypeOf(badToOneContains).toEqualTypeOf();
|
|
44
44
|
});
|
|
@@ -48,10 +48,10 @@ function normalizeFields(fields) {
|
|
|
48
48
|
];
|
|
49
49
|
}));
|
|
50
50
|
}
|
|
51
|
-
function
|
|
51
|
+
function referenceImportsFor(schema, fields) {
|
|
52
52
|
const imports = new Map();
|
|
53
53
|
for (const field of Object.values(fields)) {
|
|
54
|
-
if (field.type !== '
|
|
54
|
+
if (field.type !== 'reference' || !field.target || field.target === schema.uid) {
|
|
55
55
|
continue;
|
|
56
56
|
}
|
|
57
57
|
imports.set(field.target, { uid: field.target, pascal: pascalCase(field.target) });
|
|
@@ -65,11 +65,11 @@ function schemaToModel(schema, localizedByUid) {
|
|
|
65
65
|
pascal: pascalCase(schema.uid),
|
|
66
66
|
isSingleton: schema.isSingleton ?? false,
|
|
67
67
|
localized: schema.i18n?.enabled !== false,
|
|
68
|
-
|
|
69
|
-
.filter(([, field]) => field.type === '
|
|
68
|
+
localizedReferenceFields: Object.entries(fields)
|
|
69
|
+
.filter(([, field]) => field.type === 'reference' && field.target && localizedByUid.get(field.target) !== false)
|
|
70
70
|
.map(([fieldName]) => fieldName),
|
|
71
71
|
fields,
|
|
72
|
-
|
|
72
|
+
referenceImports: referenceImportsFor(schema, fields),
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
export function syncSchemas(options = {}) {
|
|
@@ -15,7 +15,7 @@ function writeSchema(name, schema) {
|
|
|
15
15
|
writeFileSync(join(scratch, 'src/schemas', `${name}.json`), JSON.stringify(schema, null, 2));
|
|
16
16
|
}
|
|
17
17
|
describe('syncSchemas', () => {
|
|
18
|
-
it('generates raw
|
|
18
|
+
it('generates raw references plus typed populated helpers', () => {
|
|
19
19
|
writeSchema('Author', {
|
|
20
20
|
uid: 'Author',
|
|
21
21
|
name: 'Authors',
|
|
@@ -28,9 +28,9 @@ describe('syncSchemas', () => {
|
|
|
28
28
|
name: 'Posts',
|
|
29
29
|
fields: {
|
|
30
30
|
title: { type: 'string' },
|
|
31
|
-
author: { type: '
|
|
32
|
-
contributors: { type: '
|
|
33
|
-
relatedPosts: { type: '
|
|
31
|
+
author: { type: 'reference', target: 'Author' },
|
|
32
|
+
contributors: { type: 'reference', target: 'Author', many: true },
|
|
33
|
+
relatedPosts: { type: 'reference', target: 'Post', many: true },
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
36
|
expect(syncSchemas({ cwd: scratch, verbose: false })).toEqual({ created: 4, updated: 0, deleted: 0 });
|
|
@@ -40,7 +40,7 @@ describe('syncSchemas', () => {
|
|
|
40
40
|
expect(post).toContain('import type { Post, PostInput, PostPopulate, PostResult, PostFields, PostQuery } from "./Post.types";');
|
|
41
41
|
expect(post).toContain('from "./Post.types";');
|
|
42
42
|
expect(post).not.toContain('import type { Author } from "./Author";');
|
|
43
|
-
expect(post).not.toContain('export interface Post extends
|
|
43
|
+
expect(post).not.toContain('export interface Post extends WithReferences<PostReferences>');
|
|
44
44
|
expect(post).not.toContain('type FimoReference');
|
|
45
45
|
expect(post).toContain('Promise<PostResult<undefined, TPopulate>>');
|
|
46
46
|
expect(post).toContain('TPopulate extends PostPopulate | undefined');
|
|
@@ -49,12 +49,12 @@ describe('syncSchemas', () => {
|
|
|
49
49
|
expect(post).toContain('export async function create(payload: PostInput): Promise<Post>');
|
|
50
50
|
expect(post).toContain('return wrapWithSource(data.data);');
|
|
51
51
|
expect(postTypes).toContain('import type { Author } from "./Author.types";');
|
|
52
|
-
expect(postTypes).toContain('export interface Post extends
|
|
52
|
+
expect(postTypes).toContain('export interface Post extends WithReferences<PostReferences>');
|
|
53
53
|
expect(postTypes).toContain('type FimoReference');
|
|
54
54
|
expect(postTypes).toContain('author: FimoReference | null;');
|
|
55
55
|
expect(postTypes).toContain('contributors: FimoReference[];');
|
|
56
56
|
expect(postTypes).toContain('relatedPosts: FimoReference[];');
|
|
57
|
-
expect(postTypes).toContain('export interface
|
|
57
|
+
expect(postTypes).toContain('export interface PostReferences');
|
|
58
58
|
expect(postTypes).toContain('export interface PostInput');
|
|
59
59
|
expect(postTypes).toContain('author?: FimoReference | null;');
|
|
60
60
|
expect(postTypes).toContain('author: Author | null;');
|
|
@@ -80,8 +80,8 @@ describe('syncSchemas', () => {
|
|
|
80
80
|
name: 'Settings',
|
|
81
81
|
i18n: { enabled: false },
|
|
82
82
|
fields: {
|
|
83
|
-
homepage: { type: '
|
|
84
|
-
category: { type: '
|
|
83
|
+
homepage: { type: 'reference', target: 'Homepage' },
|
|
84
|
+
category: { type: 'reference', target: 'Category' },
|
|
85
85
|
},
|
|
86
86
|
});
|
|
87
87
|
syncSchemas({ cwd: scratch, verbose: false });
|
|
@@ -92,7 +92,7 @@ describe('syncSchemas', () => {
|
|
|
92
92
|
expect(settingsTypes).not.toContain('locale?: string;');
|
|
93
93
|
expect(homepageTypes).toContain('locale: string;');
|
|
94
94
|
expect(settings).toContain('const LOCALIZED_CONTENT = false;');
|
|
95
|
-
expect(settings).toContain('const
|
|
95
|
+
expect(settings).toContain('const LOCALIZED_REFERENCE_FIELDS = new Set<string>(["homepage"]);');
|
|
96
96
|
expect(settings).not.toContain(' locale: entry.locale,');
|
|
97
97
|
expect(settings).toContain('readLocale(params, LOCALIZED_CONTENT, useQueryLocale(params.locale))');
|
|
98
98
|
expect(settings.match(/readLocale\(params, LOCALIZED_CONTENT, useQueryLocale\(params\.locale\)\)/g)?.length).toBe(2);
|
|
@@ -169,36 +169,36 @@ export function validateSchemaFile(absolutePath, file, schemaUids = new Set()) {
|
|
|
169
169
|
file,
|
|
170
170
|
});
|
|
171
171
|
}
|
|
172
|
-
if (type === '
|
|
172
|
+
if (type === 'reference') {
|
|
173
173
|
const { target, many } = definition;
|
|
174
174
|
if (typeof target !== 'string' || !CONTENT_TYPE_NAME_PATTERN.test(target)) {
|
|
175
175
|
errors.push({
|
|
176
|
-
rule: 'schemas/
|
|
177
|
-
message: `
|
|
176
|
+
rule: 'schemas/reference-target',
|
|
177
|
+
message: `Reference field "${fieldName}" must declare a valid "target" content type.`,
|
|
178
178
|
file,
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
181
|
else if (schemaUids.size > 0 && !schemaUids.has(target)) {
|
|
182
182
|
errors.push({
|
|
183
|
-
rule: 'schemas/
|
|
184
|
-
message: `
|
|
183
|
+
rule: 'schemas/reference-target',
|
|
184
|
+
message: `Reference field "${fieldName}" targets unknown schema "${target}".`,
|
|
185
185
|
file,
|
|
186
186
|
});
|
|
187
187
|
}
|
|
188
188
|
if (many !== undefined && typeof many !== 'boolean') {
|
|
189
189
|
errors.push({
|
|
190
|
-
rule: 'schemas/
|
|
191
|
-
message: `
|
|
190
|
+
rule: 'schemas/reference-many',
|
|
191
|
+
message: `Reference field "${fieldName}" must use a boolean "many" value.`,
|
|
192
192
|
file,
|
|
193
193
|
});
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
else {
|
|
197
|
-
for (const
|
|
198
|
-
if (
|
|
197
|
+
for (const referenceOnlyKey of ['target', 'many']) {
|
|
198
|
+
if (referenceOnlyKey in definition) {
|
|
199
199
|
errors.push({
|
|
200
|
-
rule: 'schemas/
|
|
201
|
-
message: `Field "${fieldName}": "${
|
|
200
|
+
rule: 'schemas/reference-shape',
|
|
201
|
+
message: `Field "${fieldName}": "${referenceOnlyKey}" is only supported on reference fields.`,
|
|
202
202
|
file,
|
|
203
203
|
});
|
|
204
204
|
}
|
|
@@ -36,20 +36,20 @@ describe('validateSchemas', () => {
|
|
|
36
36
|
body: { type: 'richtext' },
|
|
37
37
|
coverImage: { type: 'media' },
|
|
38
38
|
featured: { type: 'boolean' },
|
|
39
|
-
author: { type: '
|
|
39
|
+
author: { type: 'reference', target: 'Author' },
|
|
40
40
|
},
|
|
41
41
|
},
|
|
42
42
|
});
|
|
43
43
|
expect(validateSchemas({ cwd })).toEqual({ errors: [], schemasChecked: 2 });
|
|
44
44
|
});
|
|
45
|
-
it('accepts self-referential
|
|
45
|
+
it('accepts self-referential reference fields', () => {
|
|
46
46
|
const cwd = writeProject({
|
|
47
47
|
'Item.json': {
|
|
48
48
|
uid: 'Item',
|
|
49
49
|
name: 'Items',
|
|
50
50
|
fields: {
|
|
51
51
|
title: { type: 'string' },
|
|
52
|
-
parent: { type: '
|
|
52
|
+
parent: { type: 'reference', target: 'Item' },
|
|
53
53
|
},
|
|
54
54
|
},
|
|
55
55
|
});
|
|
@@ -141,32 +141,32 @@ describe('validateSchemas', () => {
|
|
|
141
141
|
expect(errors[0].message).toContain('"decimal"');
|
|
142
142
|
expect(errors[0].message).toContain('string, richtext, media');
|
|
143
143
|
});
|
|
144
|
-
it('rejects
|
|
144
|
+
it('rejects reference fields without a valid target', () => {
|
|
145
145
|
const cwd = writeProject({
|
|
146
146
|
'Post.json': {
|
|
147
147
|
uid: 'Post',
|
|
148
148
|
name: 'Posts',
|
|
149
|
-
fields: { author: { type: '
|
|
149
|
+
fields: { author: { type: 'reference' } },
|
|
150
150
|
},
|
|
151
151
|
});
|
|
152
152
|
const { errors } = validateSchemas({ cwd });
|
|
153
153
|
expect(errors).toHaveLength(1);
|
|
154
|
-
expect(errors[0]).toMatchObject({ rule: 'schemas/
|
|
154
|
+
expect(errors[0]).toMatchObject({ rule: 'schemas/reference-target' });
|
|
155
155
|
});
|
|
156
|
-
it('rejects
|
|
156
|
+
it('rejects reference fields targeting a missing local schema', () => {
|
|
157
157
|
const cwd = writeProject({
|
|
158
158
|
'Post.json': {
|
|
159
159
|
uid: 'Post',
|
|
160
160
|
name: 'Posts',
|
|
161
|
-
fields: { author: { type: '
|
|
161
|
+
fields: { author: { type: 'reference', target: 'Author' } },
|
|
162
162
|
},
|
|
163
163
|
});
|
|
164
164
|
const { errors } = validateSchemas({ cwd });
|
|
165
165
|
expect(errors).toHaveLength(1);
|
|
166
|
-
expect(errors[0]).toMatchObject({ rule: 'schemas/
|
|
166
|
+
expect(errors[0]).toMatchObject({ rule: 'schemas/reference-target' });
|
|
167
167
|
expect(errors[0].message).toContain('unknown schema "Author"');
|
|
168
168
|
});
|
|
169
|
-
it('rejects
|
|
169
|
+
it('rejects reference-only properties on scalar fields', () => {
|
|
170
170
|
const cwd = writeProject({
|
|
171
171
|
'Post.json': {
|
|
172
172
|
uid: 'Post',
|
|
@@ -176,7 +176,7 @@ describe('validateSchemas', () => {
|
|
|
176
176
|
});
|
|
177
177
|
const { errors } = validateSchemas({ cwd });
|
|
178
178
|
expect(errors).toHaveLength(1);
|
|
179
|
-
expect(errors[0]).toMatchObject({ rule: 'schemas/
|
|
179
|
+
expect(errors[0]).toMatchObject({ rule: 'schemas/reference-shape' });
|
|
180
180
|
});
|
|
181
181
|
it('rejects schema uid names that cannot be used as content type identifiers', () => {
|
|
182
182
|
const cwd = writeProject({
|
|
@@ -214,12 +214,12 @@ describe('validateSchemas', () => {
|
|
|
214
214
|
expect(errors).toHaveLength(1);
|
|
215
215
|
expect(errors[0]).toMatchObject({ rule: 'schemas/field-i18n' });
|
|
216
216
|
});
|
|
217
|
-
it('rejects
|
|
217
|
+
it('rejects reference-level locale settings', () => {
|
|
218
218
|
const cwd = writeProject({
|
|
219
219
|
'Page.json': {
|
|
220
220
|
uid: 'Page',
|
|
221
221
|
name: 'Pages',
|
|
222
|
-
fields: { parent: { type: '
|
|
222
|
+
fields: { parent: { type: 'reference', target: 'Page', localized: true } },
|
|
223
223
|
},
|
|
224
224
|
});
|
|
225
225
|
const { errors } = validateSchemas({ cwd });
|
package/package.json
CHANGED