fimo 0.5.0 → 0.6.0-experimental.2
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 +83 -38
- package/assets/content-templates/types-template.eta +6 -2
- package/assets/skills/fimo/SKILL.md +1 -1
- package/assets/skills/fimo/references/content.md +13 -14
- package/assets/skills/fimo/references/locales.md +8 -0
- package/assets/skills/fimo/references/setup-react-router.md +2 -2
- package/assets/skills/fimo/references/ui.md +1 -1
- package/assets/skills/fimo-cli/references/content.md +9 -6
- package/assets/skills/fimo-cli/references/locales.md +6 -0
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +112 -55
- package/dist/runtime/app/prefetch.js +1 -1
- package/dist/runtime/content/utils/retrieve-data-sources.d.ts +1 -0
- package/dist/runtime/content/utils/retrieve-data-sources.d.ts.map +1 -1
- package/dist/runtime/content/utils/retrieve-data-sources.js +27 -4
- package/dist/runtime/paths/get-fimo-paths.d.ts.map +1 -1
- package/dist/runtime/paths/get-fimo-paths.js +16 -6
- package/dist/runtime/paths/get-fimo-paths.test.d.ts +2 -0
- package/dist/runtime/paths/get-fimo-paths.test.d.ts.map +1 -0
- package/dist/runtime/paths/get-fimo-paths.test.js +69 -0
- package/dist/runtime/primitives/lib/query.d.ts +11 -9
- package/dist/runtime/primitives/lib/query.d.ts.map +1 -1
- package/dist/runtime/primitives/public-surface.test.js +3 -3
- package/dist/runtime/seo/buildSeoForPath.d.ts +1 -3
- package/dist/runtime/seo/buildSeoForPath.d.ts.map +1 -1
- package/dist/runtime/seo/buildSeoForPath.js +4 -1
- package/dist/scripts/resolve-routes.js +1 -1
- package/dist/scripts/sync-schemas.test.js +14 -3
- package/package.json +2 -2
- package/release.json +2 -2
- package/templates/react-router/package.json +10 -7
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// AUTO-GENERATED ⚠️ – do not edit by hand
|
|
12
12
|
import { <% if (!model.isSingleton) { %>useInfiniteQuery, <% } %>useSuspenseQuery, useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
13
13
|
<% if (!model.isSingleton) { -%>
|
|
14
|
-
import type { InfiniteData, UseInfiniteQueryResult, UseQueryResult } from "@tanstack/react-query";
|
|
14
|
+
import type { InfiniteData, UseInfiniteQueryResult, UseQueryResult, UseSuspenseQueryResult } from "@tanstack/react-query";
|
|
15
15
|
<% } -%>
|
|
16
16
|
import { FimoString, FimoMedia, FimoBoolean, FimoRichText, useLocale<% if (!model.isSingleton) { %>, parseCollectionResponse<% } %> } from "fimo/ui";
|
|
17
17
|
// @ts-ignore - virtual module provided by Fimo's Vite plugin
|
|
@@ -36,16 +36,35 @@ export type { <%= model.pascal %>, <%= model.pascal %>Fields, <%= model.pascal %
|
|
|
36
36
|
* Wraps raw API data with Fimo types for source tracking
|
|
37
37
|
*/
|
|
38
38
|
export function wrapWithSource(entry: Record<string, any>): <%= model.pascal %> {
|
|
39
|
+
<% if (!model.isSingleton) { -%>
|
|
40
|
+
const documentId = entry.documentId;
|
|
41
|
+
const locale = entry.locale;
|
|
42
|
+
const sourcePrefix = `<%= model.pascal %>.v2:${documentId}:${encodeURIComponent(locale)}`;
|
|
43
|
+
<% } else { -%>
|
|
39
44
|
const id = entry.id;
|
|
40
|
-
const
|
|
45
|
+
const documentId = entry.documentId;
|
|
46
|
+
const locale = entry.locale ?? defaultLocale();
|
|
47
|
+
const usesV2Identity = typeof id !== "string";
|
|
48
|
+
const sourcePrefix = usesV2Identity
|
|
49
|
+
? `<%= model.pascal %>.v2:${documentId}:${encodeURIComponent(locale)}`
|
|
50
|
+
: `<%= model.pascal %>.${id}`;
|
|
51
|
+
<% } -%>
|
|
41
52
|
const entryFimo = entry.__fimo && typeof entry.__fimo === "object" ? entry.__fimo : {};
|
|
42
53
|
|
|
43
54
|
return {
|
|
44
|
-
|
|
45
|
-
documentId
|
|
55
|
+
<% if (!model.isSingleton) { -%>
|
|
56
|
+
documentId,
|
|
57
|
+
<% } else { -%>
|
|
58
|
+
...(typeof id === "string" ? { id } : {}),
|
|
59
|
+
documentId,
|
|
60
|
+
<% } -%>
|
|
46
61
|
slug: entry.slug != null ? new FimoString(entry.slug, `${sourcePrefix}.slug`) : entry.slug,
|
|
47
|
-
<% if (model.
|
|
48
|
-
locale
|
|
62
|
+
<% if (!model.isSingleton) { -%>
|
|
63
|
+
locale,
|
|
64
|
+
<% } else if (model.localized) { -%>
|
|
65
|
+
locale,
|
|
66
|
+
<% } else { -%>
|
|
67
|
+
...(usesV2Identity ? { locale } : {}),
|
|
49
68
|
<% } -%>
|
|
50
69
|
createdAt: entry.createdAt,
|
|
51
70
|
updatedAt: entry.updatedAt,
|
|
@@ -101,11 +120,11 @@ if (model.fields) {
|
|
|
101
120
|
const wrapper = field.target === model.uid ? 'wrapWithSource' : `wrap${field.targetPascal}WithSource`;
|
|
102
121
|
if (field.many) {
|
|
103
122
|
-%>
|
|
104
|
-
<%= key %>: Array.isArray(entry.<%= key %>) ? entry.<%= key %>.map((value: any) => value
|
|
123
|
+
<%= key %>: Array.isArray(entry.<%= key %>) ? entry.<%= key %>.map((value: any) => isPopulatedEntry(value) ? <%= wrapper %>(value) : value) : entry.<%= key %>,
|
|
105
124
|
<%
|
|
106
125
|
} else {
|
|
107
126
|
-%>
|
|
108
|
-
<%= key %>: entry.<%= key %>
|
|
127
|
+
<%= key %>: isPopulatedEntry(entry.<%= key %>) ? <%= wrapper %>(entry.<%= key %>) : entry.<%= key %>,
|
|
109
128
|
<%
|
|
110
129
|
}
|
|
111
130
|
break;
|
|
@@ -121,6 +140,23 @@ if (model.fields) {
|
|
|
121
140
|
} as <%= model.pascal %>;
|
|
122
141
|
}
|
|
123
142
|
|
|
143
|
+
interface PopulatedEntryRecord extends Record<string, unknown> {
|
|
144
|
+
documentId: string;
|
|
145
|
+
__fimo: { contentType: string };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isPopulatedEntry(value: unknown): value is PopulatedEntryRecord {
|
|
149
|
+
if (value == null || typeof value !== "object") {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
const entry = value as Record<string, unknown>;
|
|
153
|
+
const fimo = entry.__fimo;
|
|
154
|
+
return typeof entry.documentId === "string"
|
|
155
|
+
&& fimo != null
|
|
156
|
+
&& typeof fimo === "object"
|
|
157
|
+
&& typeof (fimo as Record<string, unknown>).contentType === "string";
|
|
158
|
+
}
|
|
159
|
+
|
|
124
160
|
<%
|
|
125
161
|
const hasPolymorphicReferences = model.fields && Object.values(model.fields).some((field) => field.type === 'reference' && Array.isArray(field.targetRefs) && field.targetRefs.length > 0);
|
|
126
162
|
if (hasPolymorphicReferences) {
|
|
@@ -132,7 +168,9 @@ if (hasPolymorphicReferences) {
|
|
|
132
168
|
* (unpopulated) values pass through untouched.
|
|
133
169
|
*/
|
|
134
170
|
function wrapPolymorphic(value: any, wrappers: Record<string, (entry: Record<string, any>) => any>): any {
|
|
135
|
-
if (value
|
|
171
|
+
if (!isPopulatedEntry(value)) {
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
136
174
|
const contentType = value.__fimo && typeof value.__fimo.contentType === "string" ? value.__fimo.contentType : null;
|
|
137
175
|
const wrapper = contentType ? wrappers[contentType] : undefined;
|
|
138
176
|
if (!wrapper) return value;
|
|
@@ -172,8 +210,12 @@ function readLocale(
|
|
|
172
210
|
ownerUsesLocale: boolean,
|
|
173
211
|
fallbackLocale = defaultLocale()
|
|
174
212
|
): string | undefined {
|
|
213
|
+
<% if (!model.isSingleton) { -%>
|
|
214
|
+
return options.locale ?? fallbackLocale;
|
|
215
|
+
<% } else { -%>
|
|
175
216
|
const populatesLocalizedTarget = populateFields(options.populate).some((field) => LOCALIZED_REFERENCE_FIELDS.has(field));
|
|
176
217
|
return ownerUsesLocale || populatesLocalizedTarget ? options.locale ?? fallbackLocale : undefined;
|
|
218
|
+
<% } -%>
|
|
177
219
|
}
|
|
178
220
|
|
|
179
221
|
function fimoHeaders(extra?: Record<string, string>): Record<string, string> {
|
|
@@ -249,11 +291,11 @@ export type InfiniteListResult<TItem> = UseInfiniteQueryResult<
|
|
|
249
291
|
readonly isLoadingMore: boolean;
|
|
250
292
|
};
|
|
251
293
|
|
|
252
|
-
export async function
|
|
253
|
-
|
|
294
|
+
export async function getByDocumentId<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(
|
|
295
|
+
documentId: string,
|
|
254
296
|
options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}
|
|
255
297
|
): Promise<<%= model.pascal %>Result<undefined, TPopulate>> {
|
|
256
|
-
const res = await fetch(`${base}${withReadOptions(`/entries/<%= model.uid %>/${
|
|
298
|
+
const res = await fetch(`${base}${withReadOptions(`/v2/entries/<%= model.uid %>/${documentId}`, options)}`, { headers: fimoHeaders() });
|
|
257
299
|
if (!res.ok) throw new Error(await res.text());
|
|
258
300
|
const data = await res.json();
|
|
259
301
|
return wrapWithSource(data.data) as <%= model.pascal %>Result<undefined, TPopulate>;
|
|
@@ -263,7 +305,7 @@ export async function getBySlug<TPopulate extends <%= model.pascal %>Populate |
|
|
|
263
305
|
slug: string,
|
|
264
306
|
options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}
|
|
265
307
|
): Promise<<%= model.pascal %>Result<undefined, TPopulate> | null> {
|
|
266
|
-
const res = await fetch(`${base}${withReadOptions(`/entries/<%= model.uid %>/slug/${encodeURIComponent(slug)}`, options)}`, { headers: fimoHeaders() });
|
|
308
|
+
const res = await fetch(`${base}${withReadOptions(`/v2/entries/<%= model.uid %>/slug/${encodeURIComponent(slug)}`, options)}`, { headers: fimoHeaders() });
|
|
267
309
|
if (res.status === 404) return null;
|
|
268
310
|
if (!res.ok) throw new Error(await res.text());
|
|
269
311
|
const data = await res.json();
|
|
@@ -275,7 +317,7 @@ export async function getByField<TPopulate extends <%= model.pascal %>Populate |
|
|
|
275
317
|
value: string,
|
|
276
318
|
options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}
|
|
277
319
|
): Promise<<%= model.pascal %>Result<undefined, TPopulate> | null> {
|
|
278
|
-
const res = await fetch(`${base}${withReadOptions(`/entries/<%= model.uid %>/field/${encodeURIComponent(fieldName)}/${encodeURIComponent(value)}`, options)}`, { headers: fimoHeaders() });
|
|
320
|
+
const res = await fetch(`${base}${withReadOptions(`/v2/entries/<%= model.uid %>/field/${encodeURIComponent(fieldName)}/${encodeURIComponent(value)}`, options)}`, { headers: fimoHeaders() });
|
|
279
321
|
if (res.status === 404) return null;
|
|
280
322
|
if (!res.ok) throw new Error(await res.text());
|
|
281
323
|
const data = await res.json();
|
|
@@ -321,7 +363,7 @@ export async function list<
|
|
|
321
363
|
TPopulate extends <%= model.pascal %>Populate | undefined = undefined,
|
|
322
364
|
>(params: <%= model.pascal %>Query<TFields, TPopulate> = {} as <%= model.pascal %>Query<TFields, TPopulate>): Promise<CollectionPage<<%= model.pascal %>Result<TFields, TPopulate>>> {
|
|
323
365
|
const qs = toQueryString(params);
|
|
324
|
-
const res = await fetch(`${base}/entries/<%= model.uid %>${qs ? "?" + qs : ""}`, { headers: fimoHeaders() });
|
|
366
|
+
const res = await fetch(`${base}/v2/entries/<%= model.uid %>${qs ? "?" + qs : ""}`, { headers: fimoHeaders() });
|
|
325
367
|
if (!res.ok) throw new Error(await res.text());
|
|
326
368
|
const data = parseCollectionResponse(await res.json());
|
|
327
369
|
const items = data.data.map(wrapWithSource) as <%= model.pascal %>Result<TFields, TPopulate>[];
|
|
@@ -343,7 +385,7 @@ export async function get<
|
|
|
343
385
|
}
|
|
344
386
|
|
|
345
387
|
export async function create(payload: <%= model.pascal %>Input): Promise<<%= model.pascal %>> {
|
|
346
|
-
const res = await fetch(`${base}/entries/<%= model.uid %>`, {
|
|
388
|
+
const res = await fetch(`${base}/v2/entries/<%= model.uid %>`, {
|
|
347
389
|
method: "POST",
|
|
348
390
|
headers: fimoHeaders({ "Content-Type": "application/json" }),
|
|
349
391
|
body: JSON.stringify(splitWritePayload(payload))
|
|
@@ -353,19 +395,22 @@ export async function create(payload: <%= model.pascal %>Input): Promise<<%= mod
|
|
|
353
395
|
return wrapWithSource(data.data);
|
|
354
396
|
}
|
|
355
397
|
|
|
356
|
-
export async function update(
|
|
357
|
-
const
|
|
398
|
+
export async function update(documentId: string, payload: Partial<<%= model.pascal %>Input>): Promise<<%= model.pascal %>> {
|
|
399
|
+
const write = splitWritePayload(payload) as { data: Record<string, unknown>; locale?: string };
|
|
400
|
+
const url = withReadOptions(`/v2/entries/<%= model.uid %>/${documentId}`, { locale: write.locale });
|
|
401
|
+
const res = await fetch(`${base}${url}`, {
|
|
358
402
|
method: "PUT",
|
|
359
403
|
headers: fimoHeaders({ "Content-Type": "application/json" }),
|
|
360
|
-
body: JSON.stringify(
|
|
404
|
+
body: JSON.stringify({ data: write.data })
|
|
361
405
|
});
|
|
362
406
|
if (!res.ok) throw new Error(await res.text());
|
|
363
407
|
const data = await res.json();
|
|
364
408
|
return wrapWithSource(data.data);
|
|
365
409
|
}
|
|
366
410
|
|
|
367
|
-
export async function remove(
|
|
368
|
-
const
|
|
411
|
+
export async function remove(documentId: string, options: { locale?: string } = {}): Promise<void> {
|
|
412
|
+
const url = withReadOptions(`/v2/entries/<%= model.uid %>/${documentId}`, options);
|
|
413
|
+
const res = await fetch(`${base}${url}`, { method: "DELETE", headers: fimoHeaders() });
|
|
369
414
|
if (!res.ok) throw new Error(await res.text());
|
|
370
415
|
}
|
|
371
416
|
<% } else { -%>
|
|
@@ -399,7 +444,7 @@ export async function update(payload: Partial<<%= model.pascal %>Input>): Promis
|
|
|
399
444
|
<% if (!model.isSingleton) { -%>
|
|
400
445
|
const qk = {
|
|
401
446
|
root : ["<%= model.pascal %>"] as const,
|
|
402
|
-
|
|
447
|
+
byDocumentId: <TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(documentId: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) => ["<%= model.pascal %>", "document", documentId, options.locale, options.populate] as const,
|
|
403
448
|
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,
|
|
404
449
|
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,
|
|
405
450
|
list : (params: <%= model.pascal %>Query) => ["<%= model.pascal %>", params] as const,
|
|
@@ -409,10 +454,10 @@ const qk = {
|
|
|
409
454
|
>(params: InfiniteListQuery<TFields, TPopulate>) => ["<%= model.pascal %>", "infinite", params] as const
|
|
410
455
|
};
|
|
411
456
|
|
|
412
|
-
export function
|
|
413
|
-
const locale =
|
|
457
|
+
export function useGetByDocumentId<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(documentId: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}): UseQueryResult<<%= model.pascal %>Result<undefined, TPopulate>, Error> {
|
|
458
|
+
const locale = useQueryLocale(options.locale);
|
|
414
459
|
const localizedOptions = { ...options, locale };
|
|
415
|
-
return useQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.
|
|
460
|
+
return useQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.byDocumentId(documentId, localizedOptions), queryFn: () => getByDocumentId(documentId, localizedOptions) });
|
|
416
461
|
}
|
|
417
462
|
|
|
418
463
|
export function useGetBySlug<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(slug: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
@@ -475,10 +520,10 @@ export function useInfiniteList<
|
|
|
475
520
|
};
|
|
476
521
|
}
|
|
477
522
|
|
|
478
|
-
export function
|
|
479
|
-
const locale =
|
|
523
|
+
export function useSuspenseGetByDocumentId<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(documentId: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}): UseSuspenseQueryResult<<%= model.pascal %>Result<undefined, TPopulate>, Error> {
|
|
524
|
+
const locale = useQueryLocale(options.locale);
|
|
480
525
|
const localizedOptions = { ...options, locale };
|
|
481
|
-
return useSuspenseQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.
|
|
526
|
+
return useSuspenseQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.byDocumentId(documentId, localizedOptions), queryFn: () => getByDocumentId(documentId, localizedOptions) });
|
|
482
527
|
}
|
|
483
528
|
|
|
484
529
|
export function useSuspenseGetBySlug<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(slug: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
|
|
@@ -516,22 +561,22 @@ export function useCreate() {
|
|
|
516
561
|
|
|
517
562
|
export function useUpdate() {
|
|
518
563
|
const qc = useQueryClient();
|
|
519
|
-
return useMutation<<%= model.pascal %>, Error, {
|
|
520
|
-
mutationFn : ({
|
|
564
|
+
return useMutation<<%= model.pascal %>, Error, { documentId: string; locale?: string } & Partial<<%= model.pascal %>Input>>({
|
|
565
|
+
mutationFn : ({ documentId, ...patch }) => update(documentId, patch),
|
|
521
566
|
onSuccess : (_data, vars) => {
|
|
522
567
|
qc.invalidateQueries({ queryKey: qk.root });
|
|
523
|
-
qc.invalidateQueries({ queryKey: qk.
|
|
568
|
+
qc.invalidateQueries({ queryKey: qk.byDocumentId(vars.documentId, { locale: vars.locale ?? defaultLocale() }) });
|
|
524
569
|
}
|
|
525
570
|
});
|
|
526
571
|
}
|
|
527
572
|
|
|
528
573
|
export function useRemove() {
|
|
529
574
|
const qc = useQueryClient();
|
|
530
|
-
return useMutation<void, Error, string>({
|
|
531
|
-
mutationFn : remove,
|
|
532
|
-
onSuccess : (_data,
|
|
575
|
+
return useMutation<void, Error, { documentId: string; locale?: string }>({
|
|
576
|
+
mutationFn : ({ documentId, locale }) => remove(documentId, { locale }),
|
|
577
|
+
onSuccess : (_data, { documentId, locale }) => {
|
|
533
578
|
qc.invalidateQueries({ queryKey: qk.root });
|
|
534
|
-
qc.removeQueries({ queryKey:
|
|
579
|
+
qc.removeQueries({ queryKey: qk.byDocumentId(documentId, { locale: locale ?? defaultLocale() }), exact: false });
|
|
535
580
|
}
|
|
536
581
|
});
|
|
537
582
|
}
|
|
@@ -569,7 +614,7 @@ export function useUpdate() {
|
|
|
569
614
|
export default {
|
|
570
615
|
<% if (!model.isSingleton) { -%>
|
|
571
616
|
/* async helpers */
|
|
572
|
-
|
|
617
|
+
getByDocumentId,
|
|
573
618
|
getBySlug,
|
|
574
619
|
getByField,
|
|
575
620
|
list,
|
|
@@ -579,13 +624,13 @@ export default {
|
|
|
579
624
|
remove,
|
|
580
625
|
|
|
581
626
|
/* hooks */
|
|
582
|
-
|
|
627
|
+
useGetByDocumentId,
|
|
583
628
|
useGetBySlug,
|
|
584
629
|
useGetByField,
|
|
585
630
|
useList,
|
|
586
631
|
useInfiniteList,
|
|
587
632
|
useGet,
|
|
588
|
-
|
|
633
|
+
useSuspenseGetByDocumentId,
|
|
589
634
|
useSuspenseGet,
|
|
590
635
|
useSuspenseGetByField,
|
|
591
636
|
useSuspenseGetBySlug,
|
|
@@ -32,11 +32,15 @@ export interface <%= model.pascal %>FimoMeta {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export interface <%= model.pascal %> extends WithReferences<<%= model.pascal %>References> {
|
|
35
|
-
|
|
35
|
+
<% if (model.isSingleton) { -%>
|
|
36
|
+
id?: string;
|
|
37
|
+
<% } -%>
|
|
36
38
|
documentId: string;
|
|
37
39
|
slug: FimoString;
|
|
38
|
-
<% if (model.localized) { -%>
|
|
40
|
+
<% if (!model.isSingleton || model.localized) { -%>
|
|
39
41
|
locale: string;
|
|
42
|
+
<% } else { -%>
|
|
43
|
+
locale?: string;
|
|
40
44
|
<% } -%>
|
|
41
45
|
createdAt: string;
|
|
42
46
|
updatedAt: string;
|
|
@@ -43,7 +43,7 @@ Workflow / CLI-side concerns (pushing schemas, generating images, managing label
|
|
|
43
43
|
## Decision Tree (specific tasks)
|
|
44
44
|
|
|
45
45
|
- **Setting up Fimo in a new project** — pick the framework:
|
|
46
|
-
- React Router
|
|
46
|
+
- React Router framework mode (new projects use React Router 8) → `references/setup-react-router.md`
|
|
47
47
|
- Plain Vite + React (or upgrading a legacy v3 project) → `references/setup-plain-vite.md`
|
|
48
48
|
- Writing schema JSON / picking field types / Tiptap richtext → `references/content.md`
|
|
49
49
|
- Writing form JSON / generated form client / form code → `references/forms.md`
|
|
@@ -191,10 +191,9 @@ Import from the public schema module, e.g. `import BlogPost from '@/schemas/Blog
|
|
|
191
191
|
|
|
192
192
|
```ts
|
|
193
193
|
interface BlogPost {
|
|
194
|
-
id: string;
|
|
195
194
|
documentId: string;
|
|
196
195
|
slug: FimoString;
|
|
197
|
-
locale: string;
|
|
196
|
+
locale: string;
|
|
198
197
|
createdAt: string;
|
|
199
198
|
updatedAt: string;
|
|
200
199
|
__fimo: {
|
|
@@ -219,7 +218,7 @@ When a project has multiple configured locales, create and update each localized
|
|
|
219
218
|
|
|
220
219
|
For shared content, set `i18n: { "enabled": false }` on the schema and create each entry once. Shared entries:
|
|
221
220
|
|
|
222
|
-
-
|
|
221
|
+
- expose the effective request locale in collection v2 responses;
|
|
223
222
|
- are returned in every locale context without duplication;
|
|
224
223
|
- ignore locale on owner reads and writes;
|
|
225
224
|
- still use the active/requested locale when a populated reference targets a localized schema.
|
|
@@ -247,17 +246,17 @@ Filtering on polymorphic fields is not supported yet.
|
|
|
247
246
|
|
|
248
247
|
```ts
|
|
249
248
|
// low-level fetch helpers
|
|
250
|
-
|
|
249
|
+
getByDocumentId<TPopulate extends BlogPostPopulate | undefined>(documentId: string, options?: { locale?: string; populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate>>
|
|
251
250
|
getBySlug<TPopulate extends BlogPostPopulate | undefined>(slug: string, options?: { populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate> | null>
|
|
252
251
|
getByField<TPopulate extends BlogPostPopulate | undefined>(fieldName: string, value: string, options?: { populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate> | null>
|
|
253
252
|
list<TFields extends BlogPostFields | undefined, TPopulate extends BlogPostPopulate | undefined>(params?: BlogPostQuery<TFields, TPopulate>): Promise<CollectionPage<BlogPostResult<TFields, TPopulate>>>
|
|
254
253
|
get<TFields extends BlogPostFields | undefined, TPopulate extends BlogPostPopulate | undefined>(params?: BlogPostQuery<TFields, TPopulate>): Promise<BlogPostResult<TFields, TPopulate>[]>
|
|
255
254
|
create(payload: BlogPostInput): Promise<BlogPost>
|
|
256
|
-
update(
|
|
257
|
-
remove(
|
|
255
|
+
update(documentId: string, payload: Partial<BlogPostInput>): Promise<BlogPost>
|
|
256
|
+
remove(documentId: string, options?: { locale?: string }): Promise<void>
|
|
258
257
|
|
|
259
258
|
// react-query hooks
|
|
260
|
-
|
|
259
|
+
useGetByDocumentId(documentId, { locale?, populate? }) // → { data: BlogPostResult<undefined, TPopulate> | undefined, isLoading, ... }
|
|
261
260
|
useGetBySlug(slug, { populate? }) // → { data: BlogPostResult<undefined, TPopulate> | null | undefined, ... } — ideal for /:slug detail routes
|
|
262
261
|
useGetByField(fieldName, value, { populate? })
|
|
263
262
|
useList<TFields, TPopulate>(params?) // → { data: CollectionPage<BlogPostResult<TFields, TPopulate>> | undefined, ... }
|
|
@@ -266,7 +265,7 @@ useGet<TFields, TPopulate>(params?) // → legacy array-shaped { data:
|
|
|
266
265
|
useCreate() / useUpdate() / useRemove()
|
|
267
266
|
|
|
268
267
|
// Suspense variants (same signatures, throw to a <Suspense> boundary instead of returning isLoading)
|
|
269
|
-
|
|
268
|
+
useSuspenseGetByDocumentId(documentId) / useSuspenseGetBySlug(slug) / useSuspenseGetByField(fieldName, value) / useSuspenseGet(params?)
|
|
270
269
|
```
|
|
271
270
|
|
|
272
271
|
Write inputs contain plain JSON values. Read models contain Fimo wrapper values for source tracking, so use `BlogPostInput` rather than sending a `BlogPost` back to `create` or `update`.
|
|
@@ -279,7 +278,7 @@ update(payload: Partial<SettingsInput>): Promise<Settings>
|
|
|
279
278
|
useGet(options?) / useSuspenseGet(options?) / useUpdate()
|
|
280
279
|
```
|
|
281
280
|
|
|
282
|
-
Singleton clients do not expose `create`, `remove`, `
|
|
281
|
+
Singleton clients do not expose `create`, `remove`, `getByDocumentId`, or list queries. Localized singletons have one variant per locale; non-localized singletons have one locale-free row.
|
|
283
282
|
|
|
284
283
|
Choose the collection interface from the UI behavior. Use `useList` when the UI controls a page, and read pagination state from `pageInfo`:
|
|
285
284
|
|
|
@@ -338,7 +337,7 @@ Operators: scalar equality, `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `
|
|
|
338
337
|
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".
|
|
339
338
|
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.
|
|
340
339
|
|
|
341
|
-
Use `fields` to keep list payloads small. The API always keeps `
|
|
340
|
+
Use `fields` to keep list payloads small. The v2 API always keeps `documentId` and `locale`, and returns only the requested fields beyond that:
|
|
342
341
|
|
|
343
342
|
```tsx
|
|
344
343
|
const { data: summaries = [] } = BlogPost.useGet({
|
|
@@ -346,7 +345,7 @@ const { data: summaries = [] } = BlogPost.useGet({
|
|
|
346
345
|
sort: '-publishedAt',
|
|
347
346
|
limit: 12,
|
|
348
347
|
});
|
|
349
|
-
// summaries: Array<Pick<BlogPost, '
|
|
348
|
+
// summaries: Array<Pick<BlogPost, 'documentId' | 'locale' | 'slug' | 'title' | 'excerpt' | 'coverImage'>>
|
|
350
349
|
```
|
|
351
350
|
|
|
352
351
|
Use `populate` to embed reference targets in read results:
|
|
@@ -434,7 +433,7 @@ export function BlogList() {
|
|
|
434
433
|
return (
|
|
435
434
|
<ul>
|
|
436
435
|
{page.items.map((post) => (
|
|
437
|
-
<li key={post.
|
|
436
|
+
<li key={`${post.documentId}:${post.locale}`}>
|
|
438
437
|
<Text value={post.title} as="h2" />
|
|
439
438
|
<Image value={post.coverImage} width={800} height={400} fit="cover" />
|
|
440
439
|
<RichText value={post.body} className="prose" />
|
|
@@ -451,9 +450,9 @@ For the `<Text>` / `<RichText>` / `<Image>` / `<Date>` components and the rules
|
|
|
451
450
|
## Read path (published app, no auth)
|
|
452
451
|
|
|
453
452
|
```ts
|
|
454
|
-
const res = await fetch(`${import.meta.env.VITE_API_URL}/entries/<typeName>?fields=slug,title&limit=12`);
|
|
453
|
+
const res = await fetch(`${import.meta.env.VITE_API_URL}/v2/entries/<typeName>?locale=en&fields=slug,title&limit=12`);
|
|
455
454
|
const { data, meta } = await res.json();
|
|
456
|
-
// data: Entry[] — fields flattened at top level (
|
|
455
|
+
// data: Entry[] — fields flattened at top level (documentId, locale, slug, title, ...)
|
|
457
456
|
// meta: { limit, offset, total, hasMore }
|
|
458
457
|
```
|
|
459
458
|
|
|
@@ -67,6 +67,14 @@ This helper exists so the Fimo UI route selector, preview navigation, labels, an
|
|
|
67
67
|
|
|
68
68
|
Copyable examples live in `assets/react-router-locales/routes.ts` and `assets/react-router-locales/navigation.tsx`.
|
|
69
69
|
|
|
70
|
+
## Existing Custom Locale Systems
|
|
71
|
+
|
|
72
|
+
Before changing a multilingual app, inspect its locale provider, route table, language switcher, redirects, and current public URLs. Preserve that behavior while migrating.
|
|
73
|
+
|
|
74
|
+
For React Router projects, prefer converging on one locale contract owned by `fimo-config.json`, `fimoRoutes()`, and `FimoLink`. Move existing providers and language-switcher UI into the framework root instead of replacing them or adding a second locale context. Keep route metadata canonical and reuse page modules across locales.
|
|
75
|
+
|
|
76
|
+
If adopting the standard helper would change established public URLs, do not silently rewrite them. Keep the custom router temporarily, pass its resolved locale to `<FimoProviders locale={locale}>`, and ask the user to choose the canonical URLs and redirects before normalizing the route table.
|
|
77
|
+
|
|
70
78
|
## Links
|
|
71
79
|
|
|
72
80
|
Use `FimoLink` for internal links that should preserve or switch locale:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Setup Fimo on React Router
|
|
1
|
+
# Setup Fimo on React Router 8
|
|
2
2
|
|
|
3
3
|
This recipe describes the canonical Fimo + React Router framework-mode setup.
|
|
4
4
|
It mirrors what `fimo init` scaffolds today; you can also apply it to an
|
|
@@ -6,7 +6,7 @@ existing RR project.
|
|
|
6
6
|
|
|
7
7
|
## Prerequisites
|
|
8
8
|
|
|
9
|
-
- Node
|
|
9
|
+
- Node 22.22+
|
|
10
10
|
- A Fimo project created via `fimo init` (gives you `projectId`,
|
|
11
11
|
`organizationId`, and an `apiUrl`).
|
|
12
12
|
|
|
@@ -42,7 +42,7 @@ The template uses **Vite + React 19 + TypeScript + Tailwind v4 + shadcn/ui + rea
|
|
|
42
42
|
|
|
43
43
|
### Routing
|
|
44
44
|
|
|
45
|
-
- This template uses React Router
|
|
45
|
+
- This template uses React Router 8 framework mode (file-based route config in `src/routes.ts`). Use `<FimoLink>` from `fimo/react-router` for locale-aware internal links, especially in projects with more than one configured locale. Use React Router `<Link>` / `<NavLink>` only when you intentionally do not want Fimo's locale behavior. Never raw `<a href="/...">` for internal navigation because it causes full page reloads.
|
|
46
46
|
- Reserve `<a>` for external links, with `target="_blank" rel="noreferrer"`.
|
|
47
47
|
|
|
48
48
|
### Loading states
|
|
@@ -20,6 +20,9 @@ Steps 1, 3, and 5 reference the project skill (`fimo`) for shape and React API.
|
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
fimo schemas push [TypeName] # push JSON to API (create or update). Omit TypeName to push all.
|
|
23
|
+
# Pushing all sends every schema as ONE atomic batch, so types that
|
|
24
|
+
# reference each other (even cycles) can be added in a single push.
|
|
25
|
+
# Files not present are never deleted — use `schemas delete` for that.
|
|
23
26
|
fimo schemas list # list all registered schemas
|
|
24
27
|
fimo schemas get <TypeName> # get a single schema
|
|
25
28
|
fimo schemas delete <TypeName> # delete a schema
|
|
@@ -40,9 +43,9 @@ fimo entries create <TypeName> --body '<json>'
|
|
|
40
43
|
fimo entries create <TypeName> --locale es --document-id <documentId> --body '<json>'
|
|
41
44
|
fimo entries list <TypeName> [--limit N] [--offset N] [--search Q] [--where JSON] [--sort F] [--fields FIELDS] [--populate FIELDS]
|
|
42
45
|
fimo entries list <TypeName> --locale es
|
|
43
|
-
fimo entries get <TypeName> <
|
|
44
|
-
fimo entries update <TypeName> <
|
|
45
|
-
fimo entries delete <TypeName> <
|
|
46
|
+
fimo entries get <TypeName> <documentId> --locale es
|
|
47
|
+
fimo entries update <TypeName> <documentId> --locale es --body '<json>'
|
|
48
|
+
fimo entries delete <TypeName> <documentId> --locale es
|
|
46
49
|
```
|
|
47
50
|
|
|
48
51
|
`--body` is a **required** option on `create` and `update`. Its JSON shape (where `slug` goes, how `media` fields look, Tiptap richtext format) lives in `fimo/references/content.md`. **Always read that first before constructing an entry payload — slug must be inside `data`, media has a specific shape, richtext must be Tiptap JSONContent.**
|
|
@@ -63,7 +66,7 @@ Use the canonical read query flags: `--search`, `--sort`, `--where`, `--limit`,
|
|
|
63
66
|
`--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
67
|
|
|
65
68
|
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.
|
|
66
|
-
`--fields` is a comma-separated projection for list payloads. The API always keeps `
|
|
69
|
+
`--fields` is a comma-separated projection for list payloads. The v2 API always keeps `documentId` and `locale`; pass only the other fields your view needs, such as `slug,title,excerpt,coverImage`.
|
|
67
70
|
|
|
68
71
|
`--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
72
|
|
|
@@ -92,11 +95,11 @@ Do not add `localized` or field-level `i18n` to reference fields. The reference
|
|
|
92
95
|
|
|
93
96
|
## Localized and shared content
|
|
94
97
|
|
|
95
|
-
Schemas are localized by default. Omit `--locale` for default-locale
|
|
98
|
+
Schemas are localized by default. Omit `--locale` for default-locale creates and lists. `entries get`, `entries update`, and `entries delete` require `--locale` because collection entry identity is always `documentId + locale`.
|
|
96
99
|
|
|
97
100
|
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.
|
|
98
101
|
|
|
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.
|
|
102
|
+
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. V2 shared entry responses expose the effective request locale even though storage still has one locale-free row. No other `i18n` options and no field-level localization settings are supported.
|
|
100
103
|
|
|
101
104
|
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.
|
|
102
105
|
|
|
@@ -67,6 +67,12 @@ import { fimoRoutes } from 'fimo/react-router/routes';
|
|
|
67
67
|
|
|
68
68
|
For custom frameworks, the project may implement its own URL scheme, but it still needs to pass the active locale into Fimo and keep canonical route metadata if it wants Fimo Studio route selection to work.
|
|
69
69
|
|
|
70
|
+
## Migrate An Existing Locale System
|
|
71
|
+
|
|
72
|
+
Inspect the existing locale provider, route table, switcher, redirects, and public URLs before editing configuration. Preserve working URL behavior during the migration.
|
|
73
|
+
|
|
74
|
+
For React Router projects, prefer one contract based on `fimo-config.json`, `fimoRoutes()`, and `FimoLink`; migrate existing wrappers into the framework root instead of layering a second locale context. If that would change established URLs, keep the custom router temporarily, pass its active locale to `FimoProviders`, and ask the user to choose canonical URLs and redirects before rewriting routes.
|
|
75
|
+
|
|
70
76
|
## Labels And Content
|
|
71
77
|
|
|
72
78
|
Labels and content are separate translated surfaces:
|