fimo 0.4.0-experimental.4 → 0.4.0-staging.1
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 +66 -27
- package/assets/content-templates/types-template.eta +34 -15
- package/assets/skills/fimo/references/content.md +52 -35
- package/assets/skills/fimo-cli/references/content.md +12 -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 +121 -23
- 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 +5 -0
- package/dist/runtime/primitives/lib/primitives.d.ts.map +1 -1
- package/dist/runtime/primitives/lib/query.d.ts +12 -12
- package/dist/runtime/primitives/lib/query.d.ts.map +1 -1
- package/dist/runtime/primitives/public-surface.test.js +4 -4
- package/dist/scripts/generated-types.test.d.ts +1 -0
- package/dist/scripts/generated-types.test.js +172 -0
- package/dist/scripts/sync-schemas.js +25 -6
- package/dist/scripts/sync-schemas.test.js +10 -10
- package/dist/scripts/validate-schemas.js +48 -13
- package/dist/scripts/validate-schemas.test.js +13 -13
- package/package.json +1 -1
- package/scripts/lib/release-core.mjs +24 -0
- package/scripts/lib/release-core.test.ts +37 -2
- package/scripts/publish-npm.mjs +4 -9
- package/templates/react-router/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<%
|
|
2
2
|
/**
|
|
3
3
|
* INPUT ▸ model = {
|
|
4
|
-
* uid: "
|
|
5
|
-
* pascal: "
|
|
6
|
-
* fields: {key: {type, required?, multiple?}}
|
|
4
|
+
* uid: "Post", // matches src/schemas/<uid>.json (letters, numbers, underscores)
|
|
5
|
+
* pascal: "Post", // pascalCase(uid), used for generated type names
|
|
6
|
+
* fields: {key: {type, required?, multiple?, many?, target?, targetPascal?}},
|
|
7
|
+
* relationImports: [{uid, pascal}] // relation targets other than this schema
|
|
7
8
|
* }
|
|
8
9
|
*/
|
|
9
10
|
%>
|
|
@@ -14,15 +15,15 @@ import { FimoString, FimoMedia, FimoBoolean, FimoRichText, useLocale } from "fim
|
|
|
14
15
|
import fimoConfig from "virtual:fimo-config";
|
|
15
16
|
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
17
|
<%
|
|
17
|
-
if (model.
|
|
18
|
-
model.
|
|
18
|
+
if (model.referenceImports && model.referenceImports.length > 0) {
|
|
19
|
+
model.referenceImports.forEach((referenceImport) => {
|
|
19
20
|
-%>
|
|
20
|
-
import { wrapWithSource as wrap<%=
|
|
21
|
+
import { wrapWithSource as wrap<%= referenceImport.pascal %>WithSource } from "./<%= referenceImport.pascal %>";
|
|
21
22
|
<%
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
25
|
-%>
|
|
25
|
-
export type { <%= model.pascal %>, <%= model.pascal %>Fields, <%= model.pascal %>FimoMeta, <%= model.pascal %>Input, <%= model.pascal %>Populate, <%= model.pascal %>Query, <%= model.pascal %>
|
|
26
|
+
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
27
|
|
|
27
28
|
/*───────────────────────────────────────────────────────────────*/
|
|
28
29
|
/* ──────────── 🎁 Source wrapping ──────────── */
|
|
@@ -79,7 +80,21 @@ if (model.fields) {
|
|
|
79
80
|
<%
|
|
80
81
|
}
|
|
81
82
|
break;
|
|
82
|
-
case '
|
|
83
|
+
case 'reference': {
|
|
84
|
+
if (Array.isArray(field.targetRefs) && field.targetRefs.length > 0) {
|
|
85
|
+
const wrapperFor = (ref) => ref.uid === model.uid ? 'wrapWithSource' : `wrap${ref.pascal}WithSource`;
|
|
86
|
+
const wrappers = `{ ${field.targetRefs.map((ref) => `"${ref.uid}": ${wrapperFor(ref)}`).join(', ')} }`;
|
|
87
|
+
if (field.many) {
|
|
88
|
+
-%>
|
|
89
|
+
<%= key %>: Array.isArray(entry.<%= key %>) ? entry.<%= key %>.map((value: any) => wrapPolymorphic(value, <%= wrappers %>)) : entry.<%= key %>,
|
|
90
|
+
<%
|
|
91
|
+
} else {
|
|
92
|
+
-%>
|
|
93
|
+
<%= key %>: wrapPolymorphic(entry.<%= key %>, <%= wrappers %>),
|
|
94
|
+
<%
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
83
98
|
const wrapper = field.target === model.uid ? 'wrapWithSource' : `wrap${field.targetPascal}WithSource`;
|
|
84
99
|
if (field.many) {
|
|
85
100
|
-%>
|
|
@@ -103,6 +118,29 @@ if (model.fields) {
|
|
|
103
118
|
} as <%= model.pascal %>;
|
|
104
119
|
}
|
|
105
120
|
|
|
121
|
+
<%
|
|
122
|
+
const hasPolymorphicReferences = model.fields && Object.values(model.fields).some((field) => field.type === 'reference' && Array.isArray(field.targetRefs) && field.targetRefs.length > 0);
|
|
123
|
+
if (hasPolymorphicReferences) {
|
|
124
|
+
-%>
|
|
125
|
+
/**
|
|
126
|
+
* Wraps one polymorphic reference value. Populated entries carry their type
|
|
127
|
+
* marker; the wrapper picks the right per-type wrapping and stamps a
|
|
128
|
+
* top-level $type so the generated union narrows in a switch. Raw
|
|
129
|
+
* (unpopulated) values pass through untouched.
|
|
130
|
+
*/
|
|
131
|
+
function wrapPolymorphic(value: any, wrappers: Record<string, (entry: Record<string, any>) => any>): any {
|
|
132
|
+
if (value == null || typeof value !== "object" || typeof value.id !== "string") return value;
|
|
133
|
+
const contentType = value.__fimo && typeof value.__fimo.contentType === "string" ? value.__fimo.contentType : null;
|
|
134
|
+
const wrapper = contentType ? wrappers[contentType] : undefined;
|
|
135
|
+
if (!wrapper) return value;
|
|
136
|
+
const wrapped = wrapper(value) as Record<string, unknown>;
|
|
137
|
+
wrapped.$type = contentType;
|
|
138
|
+
return wrapped;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
<%
|
|
142
|
+
}
|
|
143
|
+
-%>
|
|
106
144
|
/*───────────────────────────────────────────────────────────────*/
|
|
107
145
|
/* ──────────── 🔌 Low-level fetch helpers ──────────── */
|
|
108
146
|
/*───────────────────────────────────────────────────────────────*/
|
|
@@ -119,7 +157,7 @@ function useQueryLocale(locale?: string): string {
|
|
|
119
157
|
}
|
|
120
158
|
|
|
121
159
|
const LOCALIZED_CONTENT = <%= model.localized ? 'true' : 'false' %>;
|
|
122
|
-
const
|
|
160
|
+
const LOCALIZED_REFERENCE_FIELDS = new Set<string>(<%= JSON.stringify(model.localizedReferenceFields) %>);
|
|
123
161
|
|
|
124
162
|
function populateFields(populate: <%= model.pascal %>Populate | undefined): string[] {
|
|
125
163
|
if (populate == null) return [];
|
|
@@ -131,7 +169,7 @@ function readLocale(
|
|
|
131
169
|
ownerUsesLocale: boolean,
|
|
132
170
|
fallbackLocale = defaultLocale()
|
|
133
171
|
): string | undefined {
|
|
134
|
-
const populatesLocalizedTarget = populateFields(options.populate).some((field) =>
|
|
172
|
+
const populatesLocalizedTarget = populateFields(options.populate).some((field) => LOCALIZED_REFERENCE_FIELDS.has(field));
|
|
135
173
|
return ownerUsesLocale || populatesLocalizedTarget ? options.locale ?? fallbackLocale : undefined;
|
|
136
174
|
}
|
|
137
175
|
|
|
@@ -145,7 +183,7 @@ function fimoHeaders(extra?: Record<string, string>): Record<string, string> {
|
|
|
145
183
|
|
|
146
184
|
function withReadOptions<TPopulate extends <%= model.pascal %>Populate | undefined>(
|
|
147
185
|
url: string,
|
|
148
|
-
options: { locale?: string; populate?: TPopulate } = {},
|
|
186
|
+
options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {},
|
|
149
187
|
ownerUsesLocale = LOCALIZED_CONTENT
|
|
150
188
|
): string {
|
|
151
189
|
const u = new URL(url, "http://fimo.local");
|
|
@@ -154,6 +192,7 @@ function withReadOptions<TPopulate extends <%= model.pascal %>Populate | undefin
|
|
|
154
192
|
if (options.populate != null) {
|
|
155
193
|
u.searchParams.set("populate", typeof options.populate === "string" ? options.populate : options.populate.join(","));
|
|
156
194
|
}
|
|
195
|
+
if (options.localeFallbacks) u.searchParams.set("localeFallbacks", "true");
|
|
157
196
|
return `${u.pathname}${u.search}`;
|
|
158
197
|
}
|
|
159
198
|
|
|
@@ -178,7 +217,7 @@ function splitWritePayload(payload: Partial<<%= model.pascal %>Input>): Record<s
|
|
|
178
217
|
<% if (!model.isSingleton) { -%>
|
|
179
218
|
export async function getById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(
|
|
180
219
|
id: string,
|
|
181
|
-
options: { locale?: string; populate?: TPopulate } = {}
|
|
220
|
+
options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}
|
|
182
221
|
): Promise<<%= model.pascal %>Result<undefined, TPopulate>> {
|
|
183
222
|
const res = await fetch(`${base}${withReadOptions(`/entries/<%= model.uid %>/${id}`, options, false)}`, { headers: fimoHeaders() });
|
|
184
223
|
if (!res.ok) throw new Error(await res.text());
|
|
@@ -188,7 +227,7 @@ export async function getById<TPopulate extends <%= model.pascal %>Populate | un
|
|
|
188
227
|
|
|
189
228
|
export async function getBySlug<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(
|
|
190
229
|
slug: string,
|
|
191
|
-
options: { locale?: string; populate?: TPopulate } = {}
|
|
230
|
+
options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}
|
|
192
231
|
): Promise<<%= model.pascal %>Result<undefined, TPopulate> | null> {
|
|
193
232
|
const res = await fetch(`${base}${withReadOptions(`/entries/<%= model.uid %>/slug/${encodeURIComponent(slug)}`, options)}`, { headers: fimoHeaders() });
|
|
194
233
|
if (res.status === 404) return null;
|
|
@@ -200,7 +239,7 @@ export async function getBySlug<TPopulate extends <%= model.pascal %>Populate |
|
|
|
200
239
|
export async function getByField<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(
|
|
201
240
|
fieldName: string,
|
|
202
241
|
value: string,
|
|
203
|
-
options: { locale?: string; populate?: TPopulate } = {}
|
|
242
|
+
options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}
|
|
204
243
|
): Promise<<%= model.pascal %>Result<undefined, TPopulate> | null> {
|
|
205
244
|
const res = await fetch(`${base}${withReadOptions(`/entries/<%= model.uid %>/field/${encodeURIComponent(fieldName)}/${encodeURIComponent(value)}`, options)}`, { headers: fimoHeaders() });
|
|
206
245
|
if (res.status === 404) return null;
|
|
@@ -282,7 +321,7 @@ export async function remove(id: string): Promise<void> {
|
|
|
282
321
|
}
|
|
283
322
|
<% } else { -%>
|
|
284
323
|
export async function get<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(
|
|
285
|
-
options: { locale?: string; populate?: TPopulate } = {}
|
|
324
|
+
options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}
|
|
286
325
|
): Promise<<%= model.pascal %>Result<undefined, TPopulate>> {
|
|
287
326
|
const res = await fetch(`${base}${withReadOptions(`/documents/<%= model.uid %>`, options)}`, { headers: fimoHeaders() });
|
|
288
327
|
if (!res.ok) throw new Error(await res.text());
|
|
@@ -311,25 +350,25 @@ export async function update(payload: Partial<<%= model.pascal %>Input>): Promis
|
|
|
311
350
|
<% if (!model.isSingleton) { -%>
|
|
312
351
|
const qk = {
|
|
313
352
|
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,
|
|
353
|
+
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,
|
|
354
|
+
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,
|
|
355
|
+
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
356
|
list : (params: <%= model.pascal %>Query) => ["<%= model.pascal %>", params] as const
|
|
318
357
|
};
|
|
319
358
|
|
|
320
|
-
export function useGetById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate } = {}) {
|
|
359
|
+
export function useGetById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
321
360
|
const locale = readLocale(options, false, useQueryLocale(options.locale));
|
|
322
361
|
const localizedOptions = { ...options, locale };
|
|
323
362
|
return useQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.byId(id, localizedOptions), queryFn: () => getById(id, localizedOptions) });
|
|
324
363
|
}
|
|
325
364
|
|
|
326
|
-
export function useGetBySlug<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(slug: string, options: { locale?: string; populate?: TPopulate } = {}) {
|
|
365
|
+
export function useGetBySlug<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(slug: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
327
366
|
const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
|
|
328
367
|
const localizedOptions = { ...options, locale };
|
|
329
368
|
return useQuery<<%= model.pascal %>Result<undefined, TPopulate> | null, Error>({ queryKey: qk.bySlug(slug, localizedOptions), queryFn: () => getBySlug(slug, localizedOptions), enabled: !!slug });
|
|
330
369
|
}
|
|
331
370
|
|
|
332
|
-
export function useGetByField<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(fieldName: string, value: string, options: { locale?: string; populate?: TPopulate } = {}) {
|
|
371
|
+
export function useGetByField<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(fieldName: string, value: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
333
372
|
const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
|
|
334
373
|
const localizedOptions = { ...options, locale };
|
|
335
374
|
return useQuery<<%= model.pascal %>Result<undefined, TPopulate> | null, Error>({ queryKey: qk.byField(fieldName, value, localizedOptions), queryFn: () => getByField(fieldName, value, localizedOptions), enabled: !!value });
|
|
@@ -344,19 +383,19 @@ export function useGet<
|
|
|
344
383
|
return useQuery<<%= model.pascal %>Result<TFields, TPopulate>[], Error>({ queryKey: qk.list(localizedParams), queryFn: () => get(localizedParams) });
|
|
345
384
|
}
|
|
346
385
|
|
|
347
|
-
export function useSuspenseGetById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate } = {}) {
|
|
386
|
+
export function useSuspenseGetById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
348
387
|
const locale = readLocale(options, false, useQueryLocale(options.locale));
|
|
349
388
|
const localizedOptions = { ...options, locale };
|
|
350
389
|
return useSuspenseQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.byId(id, localizedOptions), queryFn: () => getById(id, localizedOptions) });
|
|
351
390
|
}
|
|
352
391
|
|
|
353
|
-
export function useSuspenseGetBySlug<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(slug: string, options: { locale?: string; populate?: TPopulate } = {}) {
|
|
392
|
+
export function useSuspenseGetBySlug<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(slug: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
354
393
|
const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
|
|
355
394
|
const localizedOptions = { ...options, locale };
|
|
356
395
|
return useSuspenseQuery<<%= model.pascal %>Result<undefined, TPopulate> | null, Error>({ queryKey: qk.bySlug(slug, localizedOptions), queryFn: () => getBySlug(slug, localizedOptions) });
|
|
357
396
|
}
|
|
358
397
|
|
|
359
|
-
export function useSuspenseGetByField<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(fieldName: string, value: string, options: { locale?: string; populate?: TPopulate } = {}) {
|
|
398
|
+
export function useSuspenseGetByField<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(fieldName: string, value: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
360
399
|
const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
|
|
361
400
|
const localizedOptions = { ...options, locale };
|
|
362
401
|
return useSuspenseQuery<<%= model.pascal %>Result<undefined, TPopulate> | null, Error>({ queryKey: qk.byField(fieldName, value, localizedOptions), queryFn: () => getByField(fieldName, value, localizedOptions) });
|
|
@@ -403,16 +442,16 @@ export function useRemove() {
|
|
|
403
442
|
<% } else { -%>
|
|
404
443
|
const qk = {
|
|
405
444
|
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
|
|
445
|
+
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
446
|
};
|
|
408
447
|
|
|
409
|
-
export function useGet<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(options: { locale?: string; populate?: TPopulate } = {}) {
|
|
448
|
+
export function useGet<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
410
449
|
const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
|
|
411
450
|
const localizedOptions = { ...options, locale };
|
|
412
451
|
return useQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.document(localizedOptions), queryFn: () => get(localizedOptions) });
|
|
413
452
|
}
|
|
414
453
|
|
|
415
|
-
export function useSuspenseGet<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(options: { locale?: string; populate?: TPopulate } = {}) {
|
|
454
|
+
export function useSuspenseGet<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
416
455
|
const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
|
|
417
456
|
const localizedOptions = { ...options, locale };
|
|
418
457
|
return useSuspenseQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.document(localizedOptions), queryFn: () => get(localizedOptions) });
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
<%
|
|
2
2
|
/**
|
|
3
3
|
* INPUT ▸ model = {
|
|
4
|
-
* uid: "
|
|
5
|
-
* pascal: "
|
|
6
|
-
* fields: {key: {type, required?, multiple?}}
|
|
4
|
+
* uid: "Post", // matches src/schemas/<uid>.json (letters, numbers, underscores)
|
|
5
|
+
* pascal: "Post", // pascalCase(uid), used for generated type names
|
|
6
|
+
* fields: {key: {type, required?, multiple?, many?, target?, targetPascal?}},
|
|
7
|
+
* relationImports: [{uid, pascal}] // relation targets other than this schema
|
|
7
8
|
* }
|
|
8
9
|
*/
|
|
9
10
|
%>
|
|
10
11
|
// 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
|
|
12
|
+
import { type Fields, type FimoBoolean, type FimoMedia, type FimoPolymorphicReference, 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
13
|
<%
|
|
13
|
-
if (model.
|
|
14
|
-
model.
|
|
14
|
+
if (model.referenceImports && model.referenceImports.length > 0) {
|
|
15
|
+
model.referenceImports.forEach((referenceImport) => {
|
|
15
16
|
-%>
|
|
16
|
-
import type { <%=
|
|
17
|
+
import type { <%= referenceImport.pascal %> } from "./<%= referenceImport.pascal %>.types";
|
|
17
18
|
<%
|
|
18
19
|
});
|
|
19
20
|
}
|
|
@@ -30,7 +31,7 @@ export interface <%= model.pascal %>FimoMeta {
|
|
|
30
31
|
contentType: string;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
export interface <%= model.pascal %> extends
|
|
34
|
+
export interface <%= model.pascal %> extends WithReferences<<%= model.pascal %>References> {
|
|
34
35
|
id: string;
|
|
35
36
|
documentId: string;
|
|
36
37
|
slug: FimoString;
|
|
@@ -51,6 +52,7 @@ if (model.fields) {
|
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
-%>
|
|
55
|
+
}
|
|
54
56
|
|
|
55
57
|
export interface <%= model.pascal %>Input {
|
|
56
58
|
slug?: string;
|
|
@@ -93,8 +95,10 @@ function getTypeForField(field) {
|
|
|
93
95
|
return 'Record<string, any>';
|
|
94
96
|
case 'media':
|
|
95
97
|
return field.multiple ? 'FimoMedia[]' : 'FimoMedia';
|
|
96
|
-
case '
|
|
97
|
-
|
|
98
|
+
case 'reference': {
|
|
99
|
+
const raw = Array.isArray(field.targets) && field.targets.length > 0 ? 'FimoPolymorphicReference' : 'FimoReference';
|
|
100
|
+
return field.many ? `${raw}[]` : `${raw} | null`;
|
|
101
|
+
}
|
|
98
102
|
default:
|
|
99
103
|
return 'any';
|
|
100
104
|
}
|
|
@@ -118,20 +122,35 @@ function getInputTypeForField(field) {
|
|
|
118
122
|
const media = 'Pick<FimoMedia, "url" | "alt" | "name" | "mime" | "width" | "height">';
|
|
119
123
|
return field.multiple ? `${media}[]` : media;
|
|
120
124
|
}
|
|
121
|
-
case '
|
|
122
|
-
|
|
125
|
+
case 'reference': {
|
|
126
|
+
const raw = Array.isArray(field.targets) && field.targets.length > 0 ? 'FimoPolymorphicReference' : 'FimoReference';
|
|
127
|
+
return field.many ? `${raw}[]` : `${raw} | null`;
|
|
128
|
+
}
|
|
123
129
|
default:
|
|
124
130
|
return 'unknown';
|
|
125
131
|
}
|
|
126
132
|
}
|
|
127
133
|
-%>
|
|
128
|
-
}
|
|
129
134
|
|
|
130
|
-
export interface <%= model.pascal %>
|
|
135
|
+
export interface <%= model.pascal %>References {
|
|
131
136
|
<%
|
|
132
137
|
if (model.fields) {
|
|
133
138
|
Object.entries(model.fields).forEach(([key, field]) => {
|
|
134
|
-
if (field.type !== '
|
|
139
|
+
if (field.type !== 'reference') {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Polymorphic populated values form a discriminated union on $type —
|
|
143
|
+
// the generated wrapper stamps it, so `switch (item.$type)` narrows.
|
|
144
|
+
if (Array.isArray(field.targetRefs) && field.targetRefs.length > 0) {
|
|
145
|
+
const memberFor = (ref) => `(${ref.uid === model.uid ? model.pascal : ref.pascal} & { $type: "${ref.uid}" })`;
|
|
146
|
+
const union = field.targetRefs.map(memberFor).join(' | ');
|
|
147
|
+
const type = field.many ? `(${union})[]` : `${union} | null`;
|
|
148
|
+
-%>
|
|
149
|
+
<%= key %>: <%= type %>;
|
|
150
|
+
<%
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (typeof field.targetPascal !== 'string') {
|
|
135
154
|
return;
|
|
136
155
|
}
|
|
137
156
|
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,26 @@ 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.
|
|
228
|
+
|
|
229
|
+
A populated reference whose target has no variant in the requested locale resolves to null. Reads accept an opt-in `localeFallbacks: true` that resolves those to the best available variant instead (project default first), marked by its `locale` field — never implement fallbacks by hand; public reads stay strict unless the option is passed.
|
|
230
|
+
|
|
231
|
+
**Polymorphic references:** a field may declare `targets: ["Post", "Video"]` instead of a single `target` (never both) when one slot genuinely holds different types — mixed feeds, hero blocks, curated collections. Prefer a fixed `target` whenever every value is the same type: fixed fields keep `where` filtering and simpler generated types. Polymorphic values store `{ contentType, documentId }`; populated values in generated hooks carry a top-level `$type` that narrows the union:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
for (const item of page.featured) {
|
|
235
|
+
switch (item.$type) {
|
|
236
|
+
case 'Post':
|
|
237
|
+
return item.title; // typed as Post here
|
|
238
|
+
case 'Video':
|
|
239
|
+
return item.clips; // typed as Video here
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Filtering on polymorphic fields is not supported yet.
|
|
228
245
|
|
|
229
246
|
**Generated API:**
|
|
230
247
|
|
|
@@ -290,8 +307,8 @@ BlogPost.useGet({
|
|
|
290
307
|
```
|
|
291
308
|
|
|
292
309
|
Operators: scalar equality, `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$contains`, `$startsWith`, `$null`.
|
|
293
|
-
|
|
294
|
-
For to-many
|
|
310
|
+
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".
|
|
311
|
+
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
312
|
|
|
296
313
|
Use `fields` to keep list payloads small. The API always keeps `id`, and returns only the requested fields beyond that:
|
|
297
314
|
|
|
@@ -304,7 +321,7 @@ const { data: summaries = [] } = BlogPost.useGet({
|
|
|
304
321
|
// summaries: Array<Pick<BlogPost, 'id' | 'slug' | 'title' | 'excerpt' | 'coverImage'>>
|
|
305
322
|
```
|
|
306
323
|
|
|
307
|
-
Use `populate` to embed
|
|
324
|
+
Use `populate` to embed reference targets in read results:
|
|
308
325
|
|
|
309
326
|
```tsx
|
|
310
327
|
const { data: posts = [] } = BlogPost.useGet({
|
|
@@ -327,11 +344,11 @@ type PostCardItem = BlogPostResult<
|
|
|
327
344
|
>;
|
|
328
345
|
```
|
|
329
346
|
|
|
330
|
-
Do not type populated hook data as `BlogPost[]`: plain `BlogPost` keeps
|
|
347
|
+
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
348
|
|
|
332
|
-
`BlogPostPopulate` is the generated input type for allowed populate values. It comes from the schema's
|
|
349
|
+
`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
350
|
|
|
334
|
-
Populate is depth-one. It only works for
|
|
351
|
+
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
352
|
|
|
336
353
|
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
354
|
|
|
@@ -348,7 +365,7 @@ The entry objects a hook returns are **not** plain JS values. The generated clie
|
|
|
348
365
|
| `number` | `number` | plain — not wrapped |
|
|
349
366
|
| `json` | `Record<string, any>` | plain — not wrapped |
|
|
350
367
|
| `date` | `any` (raw ISO date string) | not wrapped (`FimoDate` is not emitted — see below) |
|
|
351
|
-
| `
|
|
368
|
+
| `reference` | `FimoReference` / `FimoReference[]` | reference object(s) with target `documentId`; use `populate` when embedded target rows are needed |
|
|
352
369
|
|
|
353
370
|
Rules of thumb:
|
|
354
371
|
|
|
@@ -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,24 @@ 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
|
|
63
|
+
`--where` is a JSON object keyed by schema field name. Simple values mean equality and stay typed inside the JSON (`{"featured":true}`, `{"category":"react"}`, `{"rank":{"$gte":3}}`); operator objects support `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$contains`, `$startsWith`, and `$null`, subject to the field type. Public Fimo-owned metadata lives under `__fimo`; currently `__fimo.contentType` is filterable.
|
|
64
|
+
|
|
65
|
+
Reference filters take the target `documentId` string even though stored values are objects (`{"author":"750e..."}`). On a to-many field, `$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` / `--sort -field` on scalar fields.
|
|
64
66
|
`--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
67
|
|
|
66
|
-
`--populate` is a comma-separated list of
|
|
68
|
+
`--populate` is a comma-separated list of reference fields to embed. Without it, 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 (scalar fields 400), and populate never selects which target fields come back. A populated target missing in the requested locale resolves to null unless you pass `localeFallbacks=true`, which falls back to the best available locale variant, marked by its `locale` field. Limits: 200 references per to-many write, 500 distinct populated documents per field per request (400 past either).
|
|
69
|
+
|
|
70
|
+
Polymorphic references declare `targets: ["Post", "Product"]` instead of `target` (never both) and their values carry both parts: `{ "contentType": "Post", "documentId": "..." }`. Populate returns mixed full entries, and generated hooks expose `$type` on each populated value to branch on. `--where` on polymorphic fields is not supported yet and fails with a clear error.
|
|
67
71
|
|
|
68
72
|
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
73
|
|
|
70
|
-
For
|
|
74
|
+
For reference schema JSON and entry value shape, load `fimo/references/content.md` § Reference fields. The short version:
|
|
71
75
|
|
|
72
76
|
```json
|
|
73
77
|
{
|
|
74
78
|
"fields": {
|
|
75
|
-
"author": { "type": "
|
|
76
|
-
"contributors": { "type": "
|
|
79
|
+
"author": { "type": "reference", "target": "Author" },
|
|
80
|
+
"contributors": { "type": "reference", "target": "Author", "many": true }
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
83
|
```
|
|
@@ -84,7 +88,7 @@ Entry bodies store reference objects with target `documentId` values:
|
|
|
84
88
|
fimo entries create BlogPost --body '{"data":{"slug":"hello","title":"Hello","author":{"documentId":"750e8400-e29b-41d4-a716-446655440000"},"contributors":[]}}'
|
|
85
89
|
```
|
|
86
90
|
|
|
87
|
-
Do not add `localized` or field-level `i18n` to
|
|
91
|
+
Do not add `localized` or field-level `i18n` to reference fields. The reference value belongs to the entry variant that stores it.
|
|
88
92
|
|
|
89
93
|
## Localized and shared content
|
|
90
94
|
|
|
@@ -92,7 +96,7 @@ Schemas are localized by default. Omit `--locale` for default-locale entries. Pa
|
|
|
92
96
|
|
|
93
97
|
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
98
|
|
|
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
|
|
99
|
+
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
100
|
|
|
97
101
|
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
102
|
|
|
@@ -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:
|