fimo 0.4.0-experimental.1 → 0.4.0-experimental.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,80 +8,21 @@
8
8
  */
9
9
  %>
10
10
  // AUTO-GENERATED ⚠️ – do not edit by hand
11
- import { useSuspenseQuery, useQuery, useMutation, useQueryClient, QueryKey } from "@tanstack/react-query";
12
- import { FimoString, FimoDate, FimoMedia, FimoBoolean, FimoRichText, useLocale, type Fields, type Projected, type Query, type Sort, type Where } from "fimo/ui";
11
+ import { useSuspenseQuery, useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
12
+ import { FimoString, FimoMedia, FimoBoolean, FimoRichText, useLocale } from "fimo/ui";
13
13
  // @ts-ignore - virtual module provided by Fimo's Vite plugin
14
14
  import fimoConfig from "virtual:fimo-config";
15
-
16
- /*───────────────────────────────────────────────────────────────*/
17
- /* ──────────── 📄 Type definitions ──────────── */
18
- /*───────────────────────────────────────────────────────────────*/
19
-
20
- /**
21
- * <%= model.pascal %> entity interface
22
- */
23
- export interface <%= model.pascal %>FimoMeta {
24
- contentType: string;
25
- translationProgress: { state: "queued" | "translating"; fields?: string[] } | null;
26
- }
27
-
28
- export interface <%= model.pascal %> {
29
- id: string;
30
- documentId: string;
31
- slug: FimoString;
32
- locale: string;
33
- createdAt: string;
34
- updatedAt: string;
35
- __fimo: <%= model.pascal %>FimoMeta;
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";
36
16
  <%
37
- // Generate interface properties based on fields
38
- if (model.fields) {
39
- Object.entries(model.fields).forEach(([key, field]) => {
40
- const type = getTypeForField(field);
17
+ if (model.relationImports && model.relationImports.length > 0) {
18
+ model.relationImports.forEach((relationImport) => {
41
19
  -%>
42
- <%= key %>: <%= type %>;
20
+ import { wrapWithSource as wrap<%= relationImport.pascal %>WithSource } from "./<%= relationImport.pascal %>";
43
21
  <%
44
22
  });
45
23
  }
46
-
47
- /**
48
- * Helper function to convert schema field types to TypeScript types
49
- * Uses Fimo wrapper types for source tracking
50
- */
51
- function getTypeForField(field) {
52
- switch(field.type) {
53
- case 'string':
54
- return 'FimoString';
55
- case 'richtext':
56
- return 'FimoRichText';
57
- case 'number':
58
- return 'number';
59
- case 'boolean':
60
- return 'FimoBoolean';
61
- case 'date':
62
- return 'string';
63
- case 'json':
64
- return 'Record<string, any>';
65
- case 'media':
66
- return field.multiple ? 'FimoMedia[]' : 'FimoMedia';
67
- case 'relation':
68
- return field.many ? 'Array<string | Record<string, any>>' : 'string | Record<string, any> | null';
69
- default:
70
- return 'any';
71
- }
72
- }
73
24
  -%>
74
- }
75
-
76
- /*───────────────────────────────────────────────────────────────*/
77
- /* ──────────── 🔎 Query types ──────────── */
78
- /*───────────────────────────────────────────────────────────────*/
79
-
80
- export type <%= model.pascal %>Where = Where<<%= model.pascal %>>;
81
- export type <%= model.pascal %>Sort = Sort<<%= model.pascal %>>;
82
- export type <%= model.pascal %>Fields = Fields<<%= model.pascal %>>;
83
- export type <%= model.pascal %>Query<TFields extends <%= model.pascal %>Fields | undefined = <%= model.pascal %>Fields | undefined> = Query<<%= model.pascal %>, TFields>;
84
- export type <%= model.pascal %>Result<TFields extends <%= model.pascal %>Fields | undefined = undefined> = Projected<<%= model.pascal %>, TFields>;
25
+ export type { <%= model.pascal %>, <%= model.pascal %>Fields, <%= model.pascal %>FimoMeta, <%= model.pascal %>Input, <%= model.pascal %>Populate, <%= model.pascal %>Query, <%= model.pascal %>Relations, <%= model.pascal %>Result, <%= model.pascal %>Sort, <%= model.pascal %>Where } from "./<%= model.pascal %>.types";
85
26
 
86
27
  /*───────────────────────────────────────────────────────────────*/
87
28
  /* ──────────── 🎁 Source wrapping ──────────── */
@@ -90,7 +31,7 @@ export type <%= model.pascal %>Result<TFields extends <%= model.pascal %>Fields
90
31
  /**
91
32
  * Wraps raw API data with Fimo types for source tracking
92
33
  */
93
- function wrapWithSource(entry: Record<string, any>): <%= model.pascal %> {
34
+ export function wrapWithSource(entry: Record<string, any>): <%= model.pascal %> {
94
35
  const id = entry.id;
95
36
  const sourcePrefix = `<%= model.pascal %>.${id}`;
96
37
  const entryFimo = entry.__fimo && typeof entry.__fimo === "object" ? entry.__fimo : {};
@@ -99,12 +40,13 @@ function wrapWithSource(entry: Record<string, any>): <%= model.pascal %> {
99
40
  id,
100
41
  documentId: entry.documentId,
101
42
  slug: entry.slug != null ? new FimoString(entry.slug, `${sourcePrefix}.slug`) : entry.slug,
43
+ <% if (model.localized) { -%>
102
44
  locale: entry.locale,
45
+ <% } -%>
103
46
  createdAt: entry.createdAt,
104
47
  updatedAt: entry.updatedAt,
105
48
  __fimo: {
106
49
  contentType: typeof entryFimo.contentType === "string" ? entryFimo.contentType : "<%= model.uid %>",
107
- translationProgress: entryFimo.translationProgress && typeof entryFimo.translationProgress === "object" ? entryFimo.translationProgress as <%= model.pascal %>FimoMeta["translationProgress"] : null,
108
50
  },
109
51
  <%
110
52
  if (model.fields) {
@@ -137,6 +79,19 @@ if (model.fields) {
137
79
  <%
138
80
  }
139
81
  break;
82
+ case 'relation': {
83
+ const wrapper = field.target === model.uid ? 'wrapWithSource' : `wrap${field.targetPascal}WithSource`;
84
+ if (field.many) {
85
+ -%>
86
+ <%= key %>: Array.isArray(entry.<%= key %>) ? entry.<%= key %>.map((value: any) => value != null && typeof value === "object" && typeof value.id === "string" ? <%= wrapper %>(value) : value) : entry.<%= key %>,
87
+ <%
88
+ } else {
89
+ -%>
90
+ <%= key %>: entry.<%= key %> != null && typeof entry.<%= key %> === "object" && typeof entry.<%= key %>.id === "string" ? <%= wrapper %>(entry.<%= key %>) : entry.<%= key %>,
91
+ <%
92
+ }
93
+ break;
94
+ }
140
95
  default:
141
96
  -%>
142
97
  <%= key %>: entry.<%= key %>,
@@ -159,7 +114,25 @@ function defaultLocale(): string {
159
114
  }
160
115
 
161
116
  function useQueryLocale(locale?: string): string {
162
- return locale ?? useLocale().locale ?? defaultLocale();
117
+ const activeLocale = useLocale().locale;
118
+ return locale ?? activeLocale ?? defaultLocale();
119
+ }
120
+
121
+ const LOCALIZED_CONTENT = <%= model.localized ? 'true' : 'false' %>;
122
+ const LOCALIZED_RELATION_FIELDS = new Set<string>(<%= JSON.stringify(model.localizedRelationFields) %>);
123
+
124
+ function populateFields(populate: <%= model.pascal %>Populate | undefined): string[] {
125
+ if (populate == null) return [];
126
+ return typeof populate === "string" ? [populate] : [...populate];
127
+ }
128
+
129
+ function readLocale(
130
+ options: { locale?: string; populate?: <%= model.pascal %>Populate },
131
+ ownerUsesLocale: boolean,
132
+ fallbackLocale = defaultLocale()
133
+ ): string | undefined {
134
+ const populatesLocalizedTarget = populateFields(options.populate).some((field) => LOCALIZED_RELATION_FIELDS.has(field));
135
+ return ownerUsesLocale || populatesLocalizedTarget ? options.locale ?? fallbackLocale : undefined;
163
136
  }
164
137
 
165
138
  function fimoHeaders(extra?: Record<string, string>): Record<string, string> {
@@ -170,13 +143,21 @@ function fimoHeaders(extra?: Record<string, string>): Record<string, string> {
170
143
  return h;
171
144
  }
172
145
 
173
- function withLocale(url: string, locale?: string): string {
146
+ function withReadOptions<TPopulate extends <%= model.pascal %>Populate | undefined>(
147
+ url: string,
148
+ options: { locale?: string; populate?: TPopulate } = {},
149
+ ownerUsesLocale = LOCALIZED_CONTENT
150
+ ): string {
174
151
  const u = new URL(url, "http://fimo.local");
175
- u.searchParams.set("locale", locale ?? defaultLocale());
152
+ const locale = readLocale(options, ownerUsesLocale);
153
+ if (locale) u.searchParams.set("locale", locale);
154
+ if (options.populate != null) {
155
+ u.searchParams.set("populate", typeof options.populate === "string" ? options.populate : options.populate.join(","));
156
+ }
176
157
  return `${u.pathname}${u.search}`;
177
158
  }
178
159
 
179
- function splitWritePayload(payload: Partial<<%= model.pascal %>>): Record<string, unknown> {
160
+ function splitWritePayload(payload: Partial<<%= model.pascal %>Input>): Record<string, unknown> {
180
161
  const {
181
162
  id,
182
163
  documentId,
@@ -194,31 +175,45 @@ function splitWritePayload(payload: Partial<<%= model.pascal %>>): Record<string
194
175
  };
195
176
  }
196
177
 
197
- export async function getById(id: string, options: { locale?: string } = {}): Promise<<%= model.pascal %>> {
198
- const res = await fetch(`${base}${withLocale(`/entries/<%= model.uid %>/${id}`, options.locale)}`, { headers: fimoHeaders() });
178
+ <% if (!model.isSingleton) { -%>
179
+ export async function getById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(
180
+ id: string,
181
+ options: { locale?: string; populate?: TPopulate } = {}
182
+ ): Promise<<%= model.pascal %>Result<undefined, TPopulate>> {
183
+ const res = await fetch(`${base}${withReadOptions(`/entries/<%= model.uid %>/${id}`, options, false)}`, { headers: fimoHeaders() });
199
184
  if (!res.ok) throw new Error(await res.text());
200
185
  const data = await res.json();
201
- return wrapWithSource(data.data);
186
+ return wrapWithSource(data.data) as <%= model.pascal %>Result<undefined, TPopulate>;
202
187
  }
203
188
 
204
- export async function getBySlug(slug: string, options: { locale?: string } = {}): Promise<<%= model.pascal %> | null> {
205
- const res = await fetch(`${base}${withLocale(`/entries/<%= model.uid %>/slug/${encodeURIComponent(slug)}`, options.locale)}`, { headers: fimoHeaders() });
189
+ export async function getBySlug<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(
190
+ slug: string,
191
+ options: { locale?: string; populate?: TPopulate } = {}
192
+ ): Promise<<%= model.pascal %>Result<undefined, TPopulate> | null> {
193
+ const res = await fetch(`${base}${withReadOptions(`/entries/<%= model.uid %>/slug/${encodeURIComponent(slug)}`, options)}`, { headers: fimoHeaders() });
206
194
  if (res.status === 404) return null;
207
195
  if (!res.ok) throw new Error(await res.text());
208
196
  const data = await res.json();
209
- return wrapWithSource(data.data);
197
+ return wrapWithSource(data.data) as <%= model.pascal %>Result<undefined, TPopulate>;
210
198
  }
211
199
 
212
- export async function getByField(fieldName: string, value: string, options: { locale?: string } = {}): Promise<<%= model.pascal %> | null> {
213
- const res = await fetch(`${base}${withLocale(`/entries/<%= model.uid %>/field/${encodeURIComponent(fieldName)}/${encodeURIComponent(value)}`, options.locale)}`, { headers: fimoHeaders() });
200
+ export async function getByField<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(
201
+ fieldName: string,
202
+ value: string,
203
+ options: { locale?: string; populate?: TPopulate } = {}
204
+ ): Promise<<%= model.pascal %>Result<undefined, TPopulate> | null> {
205
+ const res = await fetch(`${base}${withReadOptions(`/entries/<%= model.uid %>/field/${encodeURIComponent(fieldName)}/${encodeURIComponent(value)}`, options)}`, { headers: fimoHeaders() });
214
206
  if (res.status === 404) return null;
215
207
  if (!res.ok) throw new Error(await res.text());
216
208
  const data = await res.json();
217
- return wrapWithSource(data.data);
209
+ return wrapWithSource(data.data) as <%= model.pascal %>Result<undefined, TPopulate>;
218
210
  }
219
211
 
220
- function toQueryString<TFields extends <%= model.pascal %>Fields | undefined = undefined>(params: <%= model.pascal %>Query<TFields> = {} as <%= model.pascal %>Query<TFields>): string {
221
- const { where, sort, fields, populate, ...flat } = params;
212
+ function toQueryString<
213
+ TFields extends <%= model.pascal %>Fields | undefined = undefined,
214
+ TPopulate extends <%= model.pascal %>Populate | undefined = undefined,
215
+ >(params: <%= model.pascal %>Query<TFields, TPopulate> = {} as <%= model.pascal %>Query<TFields, TPopulate>): string {
216
+ const { where, sort, fields, populate, locale: requestedLocale, ...flat } = params;
222
217
  const query = new URLSearchParams();
223
218
 
224
219
  for (const [key, value] of Object.entries(flat)) {
@@ -242,22 +237,24 @@ function toQueryString<TFields extends <%= model.pascal %>Fields | undefined = u
242
237
  query.set("where", JSON.stringify(where));
243
238
  }
244
239
 
245
- if (!query.has("locale")) {
246
- query.set("locale", defaultLocale());
247
- }
240
+ const locale = readLocale({ locale: requestedLocale, populate }, LOCALIZED_CONTENT);
241
+ if (locale) query.set("locale", locale);
248
242
 
249
243
  return query.toString();
250
244
  }
251
245
 
252
- export async function get<TFields extends <%= model.pascal %>Fields | undefined = undefined>(params: <%= model.pascal %>Query<TFields> = {} as <%= model.pascal %>Query<TFields>): Promise<<%= model.pascal %>Result<TFields>[]> {
246
+ export async function get<
247
+ TFields extends <%= model.pascal %>Fields | undefined = undefined,
248
+ TPopulate extends <%= model.pascal %>Populate | undefined = undefined,
249
+ >(params: <%= model.pascal %>Query<TFields, TPopulate> = {} as <%= model.pascal %>Query<TFields, TPopulate>): Promise<<%= model.pascal %>Result<TFields, TPopulate>[]> {
253
250
  const qs = toQueryString(params);
254
251
  const res = await fetch(`${base}/entries/<%= model.uid %>${qs ? "?" + qs : ""}`, { headers: fimoHeaders() });
255
252
  if (!res.ok) throw new Error(await res.text());
256
253
  const data = await res.json();
257
- return data.data.map(wrapWithSource) as <%= model.pascal %>Result<TFields>[];
254
+ return data.data.map(wrapWithSource) as <%= model.pascal %>Result<TFields, TPopulate>[];
258
255
  }
259
256
 
260
- export async function create(payload: Partial<<%= model.pascal %>>): Promise<<%= model.pascal %>> {
257
+ export async function create(payload: <%= model.pascal %>Input): Promise<<%= model.pascal %>> {
261
258
  const res = await fetch(`${base}/entries/<%= model.uid %>`, {
262
259
  method: "POST",
263
260
  headers: fimoHeaders({ "Content-Type": "application/json" }),
@@ -265,10 +262,10 @@ export async function create(payload: Partial<<%= model.pascal %>>): Promise<<%=
265
262
  });
266
263
  if (!res.ok) throw new Error(await res.text());
267
264
  const data = await res.json();
268
- return data.data;
265
+ return wrapWithSource(data.data);
269
266
  }
270
267
 
271
- export async function update(id: string, payload: Partial<<%= model.pascal %>>): Promise<<%= model.pascal %>> {
268
+ export async function update(id: string, payload: Partial<<%= model.pascal %>Input>): Promise<<%= model.pascal %>> {
272
269
  const res = await fetch(`${base}/entries/<%= model.uid %>/${id}`, {
273
270
  method: "PUT",
274
271
  headers: fimoHeaders({ "Content-Type": "application/json" }),
@@ -276,71 +273,107 @@ export async function update(id: string, payload: Partial<<%= model.pascal %>>):
276
273
  });
277
274
  if (!res.ok) throw new Error(await res.text());
278
275
  const data = await res.json();
279
- return data.data;
276
+ return wrapWithSource(data.data);
280
277
  }
281
278
 
282
279
  export async function remove(id: string): Promise<void> {
283
280
  const res = await fetch(`${base}/entries/<%= model.uid %>/${id}`, { method: "DELETE", headers: fimoHeaders() });
284
281
  if (!res.ok) throw new Error(await res.text());
285
282
  }
283
+ <% } else { -%>
284
+ export async function get<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(
285
+ options: { locale?: string; populate?: TPopulate } = {}
286
+ ): Promise<<%= model.pascal %>Result<undefined, TPopulate>> {
287
+ const res = await fetch(`${base}${withReadOptions(`/documents/<%= model.uid %>`, options)}`, { headers: fimoHeaders() });
288
+ if (!res.ok) throw new Error(await res.text());
289
+ const data = await res.json();
290
+ return wrapWithSource(data.data) as <%= model.pascal %>Result<undefined, TPopulate>;
291
+ }
292
+
293
+ export async function update(payload: Partial<<%= model.pascal %>Input>): Promise<<%= model.pascal %>> {
294
+ const write = splitWritePayload(payload) as { data: Record<string, unknown>; locale?: string };
295
+ const url = withReadOptions(`/documents/<%= model.uid %>`, { locale: write.locale });
296
+ const res = await fetch(`${base}${url}`, {
297
+ method: "PUT",
298
+ headers: fimoHeaders({ "Content-Type": "application/json" }),
299
+ body: JSON.stringify({ data: write.data })
300
+ });
301
+ if (!res.ok) throw new Error(await res.text());
302
+ const data = await res.json();
303
+ return wrapWithSource(data.data);
304
+ }
305
+ <% } -%>
286
306
 
287
307
  /*───────────────────────────────────────────────────────────────*/
288
308
  /* ──────────── 🍯 React-Query hooks ──────────── */
289
309
  /*───────────────────────────────────────────────────────────────*/
290
310
 
311
+ <% if (!model.isSingleton) { -%>
291
312
  const qk = {
292
313
  root : ["<%= model.pascal %>"] as const,
293
- byId : (id: string, locale?: string) => ["<%= model.pascal %>", id, locale ?? defaultLocale()] as const,
294
- bySlug : (slug: string, locale?: string) => ["<%= model.pascal %>", "slug", slug, locale ?? defaultLocale()] as const,
295
- byField : (field: string, value: string, locale?: string) => ["<%= model.pascal %>", field, value, locale ?? defaultLocale()] 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,
296
317
  list : (params: <%= model.pascal %>Query) => ["<%= model.pascal %>", params] as const
297
318
  };
298
319
 
299
- export function useGetById(id: string, options: { locale?: string } = {}) {
300
- const locale = useQueryLocale(options.locale);
301
- return useQuery<<%= model.pascal %>, Error>({ queryKey: qk.byId(id, locale), queryFn: () => getById(id, { ...options, locale }) });
320
+ export function useGetById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate } = {}) {
321
+ const locale = readLocale(options, false, useQueryLocale(options.locale));
322
+ const localizedOptions = { ...options, locale };
323
+ return useQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.byId(id, localizedOptions), queryFn: () => getById(id, localizedOptions) });
302
324
  }
303
325
 
304
- export function useGetBySlug(slug: string, options: { locale?: string } = {}) {
305
- const locale = useQueryLocale(options.locale);
306
- return useQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.bySlug(slug, locale), queryFn: () => getBySlug(slug, { ...options, locale }), enabled: !!slug });
326
+ export function useGetBySlug<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(slug: string, options: { locale?: string; populate?: TPopulate } = {}) {
327
+ const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
328
+ const localizedOptions = { ...options, locale };
329
+ return useQuery<<%= model.pascal %>Result<undefined, TPopulate> | null, Error>({ queryKey: qk.bySlug(slug, localizedOptions), queryFn: () => getBySlug(slug, localizedOptions), enabled: !!slug });
307
330
  }
308
331
 
309
- export function useGetByField(fieldName: string, value: string, options: { locale?: string } = {}) {
310
- const locale = useQueryLocale(options.locale);
311
- return useQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.byField(fieldName, value, locale), queryFn: () => getByField(fieldName, value, { ...options, locale }), enabled: !!value });
332
+ export function useGetByField<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(fieldName: string, value: string, options: { locale?: string; populate?: TPopulate } = {}) {
333
+ const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
334
+ const localizedOptions = { ...options, locale };
335
+ return useQuery<<%= model.pascal %>Result<undefined, TPopulate> | null, Error>({ queryKey: qk.byField(fieldName, value, localizedOptions), queryFn: () => getByField(fieldName, value, localizedOptions), enabled: !!value });
312
336
  }
313
337
 
314
- export function useGet<TFields extends <%= model.pascal %>Fields | undefined = undefined>(params: <%= model.pascal %>Query<TFields> = {} as <%= model.pascal %>Query<TFields>) {
315
- const locale = useQueryLocale(params.locale);
338
+ export function useGet<
339
+ TFields extends <%= model.pascal %>Fields | undefined = undefined,
340
+ TPopulate extends <%= model.pascal %>Populate | undefined = undefined,
341
+ >(params: <%= model.pascal %>Query<TFields, TPopulate> = {} as <%= model.pascal %>Query<TFields, TPopulate>) {
342
+ const locale = readLocale(params, LOCALIZED_CONTENT, useQueryLocale(params.locale));
316
343
  const localizedParams = { ...params, locale };
317
- return useQuery<<%= model.pascal %>Result<TFields>[], Error>({ queryKey: qk.list(localizedParams), queryFn: () => get(localizedParams) });
344
+ return useQuery<<%= model.pascal %>Result<TFields, TPopulate>[], Error>({ queryKey: qk.list(localizedParams), queryFn: () => get(localizedParams) });
318
345
  }
319
346
 
320
- export function useSuspenseGetById(id: string, options: { locale?: string } = {}) {
321
- const locale = useQueryLocale(options.locale);
322
- return useSuspenseQuery<<%= model.pascal %>, Error>({ queryKey: qk.byId(id, locale), queryFn: () => getById(id, { ...options, locale }) });
347
+ export function useSuspenseGetById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate } = {}) {
348
+ const locale = readLocale(options, false, useQueryLocale(options.locale));
349
+ const localizedOptions = { ...options, locale };
350
+ return useSuspenseQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.byId(id, localizedOptions), queryFn: () => getById(id, localizedOptions) });
323
351
  }
324
352
 
325
- export function useSuspenseGetBySlug(slug: string, options: { locale?: string } = {}) {
326
- const locale = useQueryLocale(options.locale);
327
- return useSuspenseQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.bySlug(slug, locale), queryFn: () => getBySlug(slug, { ...options, locale }) });
353
+ export function useSuspenseGetBySlug<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(slug: string, options: { locale?: string; populate?: TPopulate } = {}) {
354
+ const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
355
+ const localizedOptions = { ...options, locale };
356
+ return useSuspenseQuery<<%= model.pascal %>Result<undefined, TPopulate> | null, Error>({ queryKey: qk.bySlug(slug, localizedOptions), queryFn: () => getBySlug(slug, localizedOptions) });
328
357
  }
329
358
 
330
- export function useSuspenseGetByField(fieldName: string, value: string, options: { locale?: string } = {}) {
331
- const locale = useQueryLocale(options.locale);
332
- return useSuspenseQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.byField(fieldName, value, locale), queryFn: () => getByField(fieldName, value, { ...options, locale }) });
359
+ export function useSuspenseGetByField<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(fieldName: string, value: string, options: { locale?: string; populate?: TPopulate } = {}) {
360
+ const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
361
+ const localizedOptions = { ...options, locale };
362
+ return useSuspenseQuery<<%= model.pascal %>Result<undefined, TPopulate> | null, Error>({ queryKey: qk.byField(fieldName, value, localizedOptions), queryFn: () => getByField(fieldName, value, localizedOptions) });
333
363
  }
334
364
 
335
- export function useSuspenseGet<TFields extends <%= model.pascal %>Fields | undefined = undefined>(params: <%= model.pascal %>Query<TFields> = {} as <%= model.pascal %>Query<TFields>) {
336
- const locale = useQueryLocale(params.locale);
365
+ export function useSuspenseGet<
366
+ TFields extends <%= model.pascal %>Fields | undefined = undefined,
367
+ TPopulate extends <%= model.pascal %>Populate | undefined = undefined,
368
+ >(params: <%= model.pascal %>Query<TFields, TPopulate> = {} as <%= model.pascal %>Query<TFields, TPopulate>) {
369
+ const locale = readLocale(params, LOCALIZED_CONTENT, useQueryLocale(params.locale));
337
370
  const localizedParams = { ...params, locale };
338
- return useSuspenseQuery<<%= model.pascal %>Result<TFields>[], Error>({ queryKey: qk.list(localizedParams), queryFn: () => get(localizedParams) });
371
+ return useSuspenseQuery<<%= model.pascal %>Result<TFields, TPopulate>[], Error>({ queryKey: qk.list(localizedParams), queryFn: () => get(localizedParams) });
339
372
  }
340
373
 
341
374
  export function useCreate() {
342
375
  const qc = useQueryClient();
343
- return useMutation<<%= model.pascal %>, Error, Partial<<%= model.pascal %>>>({
376
+ return useMutation<<%= model.pascal %>, Error, <%= model.pascal %>Input>({
344
377
  mutationFn : create,
345
378
  onSuccess : () => qc.invalidateQueries({ queryKey: qk.root })
346
379
  });
@@ -348,9 +381,12 @@ export function useCreate() {
348
381
 
349
382
  export function useUpdate() {
350
383
  const qc = useQueryClient();
351
- return useMutation<<%= model.pascal %>, Error, { id: string } & Partial<<%= model.pascal %>>>({
384
+ return useMutation<<%= model.pascal %>, Error, { id: string } & Partial<<%= model.pascal %>Input>>({
352
385
  mutationFn : ({ id, ...patch }) => update(id, patch),
353
- onSuccess : (_data, vars) => qc.invalidateQueries({ queryKey: qk.byId(vars.id) })
386
+ onSuccess : (_data, vars) => {
387
+ qc.invalidateQueries({ queryKey: qk.root });
388
+ qc.invalidateQueries({ queryKey: qk.byId(vars.id) });
389
+ }
354
390
  });
355
391
  }
356
392
 
@@ -360,16 +396,43 @@ export function useRemove() {
360
396
  mutationFn : remove,
361
397
  onSuccess : (_data, id) => {
362
398
  qc.invalidateQueries({ queryKey: qk.root });
363
- qc.removeQueries({ queryKey: qk.byId(id) as unknown as QueryKey });
399
+ qc.removeQueries({ queryKey: ["<%= model.pascal %>", id], exact: false });
364
400
  }
365
401
  });
366
402
  }
403
+ <% } else { -%>
404
+ const qk = {
405
+ 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
+ };
408
+
409
+ export function useGet<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(options: { locale?: string; populate?: TPopulate } = {}) {
410
+ const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
411
+ const localizedOptions = { ...options, locale };
412
+ return useQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.document(localizedOptions), queryFn: () => get(localizedOptions) });
413
+ }
414
+
415
+ export function useSuspenseGet<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(options: { locale?: string; populate?: TPopulate } = {}) {
416
+ const locale = readLocale(options, LOCALIZED_CONTENT, useQueryLocale(options.locale));
417
+ const localizedOptions = { ...options, locale };
418
+ return useSuspenseQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.document(localizedOptions), queryFn: () => get(localizedOptions) });
419
+ }
420
+
421
+ export function useUpdate() {
422
+ const qc = useQueryClient();
423
+ return useMutation<<%= model.pascal %>, Error, Partial<<%= model.pascal %>Input>>({
424
+ mutationFn: update,
425
+ onSuccess: () => qc.invalidateQueries({ queryKey: qk.root })
426
+ });
427
+ }
428
+ <% } -%>
367
429
 
368
430
  /*───────────────────────────────────────────────────────────────*/
369
431
  /* ──────────── 🎯 Default export ──────────── */
370
432
  /*───────────────────────────────────────────────────────────────*/
371
433
 
372
434
  export default {
435
+ <% if (!model.isSingleton) { -%>
373
436
  /* async helpers */
374
437
  getById,
375
438
  getBySlug,
@@ -391,4 +454,11 @@ export default {
391
454
  useCreate,
392
455
  useUpdate,
393
456
  useRemove
457
+ <% } else { -%>
458
+ get,
459
+ update,
460
+ useGet,
461
+ useSuspenseGet,
462
+ useUpdate
463
+ <% } -%>
394
464
  };
@@ -0,0 +1,162 @@
1
+ <%
2
+ /**
3
+ * INPUT ▸ model = {
4
+ * uid: "api::blog.blog", // UID in format "api::name.name"
5
+ * pascal: "Blog" // PascalCase for type names
6
+ * fields: {key: {type, required?, multiple?}} // Schema fields
7
+ * }
8
+ */
9
+ %>
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 WithRelations } from "fimo/ui";
12
+ <%
13
+ if (model.relationImports && model.relationImports.length > 0) {
14
+ model.relationImports.forEach((relationImport) => {
15
+ -%>
16
+ import type { <%= relationImport.pascal %> } from "./<%= relationImport.pascal %>.types";
17
+ <%
18
+ });
19
+ }
20
+ -%>
21
+
22
+ /*───────────────────────────────────────────────────────────────*/
23
+ /* ──────────── 📄 Type definitions ──────────── */
24
+ /*───────────────────────────────────────────────────────────────*/
25
+
26
+ /**
27
+ * <%= model.pascal %> entity interface
28
+ */
29
+ export interface <%= model.pascal %>FimoMeta {
30
+ contentType: string;
31
+ }
32
+
33
+ export interface <%= model.pascal %> extends WithRelations<<%= model.pascal %>Relations> {
34
+ id: string;
35
+ documentId: string;
36
+ slug: FimoString;
37
+ <% if (model.localized) { -%>
38
+ locale: string;
39
+ <% } -%>
40
+ createdAt: string;
41
+ updatedAt: string;
42
+ __fimo: <%= model.pascal %>FimoMeta;
43
+ <%
44
+ // Generate interface properties based on fields
45
+ if (model.fields) {
46
+ Object.entries(model.fields).forEach(([key, field]) => {
47
+ const type = getTypeForField(field);
48
+ -%>
49
+ <%= key %>: <%= type %>;
50
+ <%
51
+ });
52
+ }
53
+ -%>
54
+
55
+ export interface <%= model.pascal %>Input {
56
+ slug?: string;
57
+ <% if (model.localized) { -%>
58
+ locale?: string;
59
+ <% } -%>
60
+ <% if (!model.isSingleton) { -%>
61
+ documentId?: string;
62
+ <% } -%>
63
+ <%
64
+ if (model.fields) {
65
+ Object.entries(model.fields).forEach(([key, field]) => {
66
+ const type = getInputTypeForField(field);
67
+ -%>
68
+ <%= key %><%= field.required ? '' : '?' %>: <%= type %>;
69
+ <%
70
+ });
71
+ }
72
+ -%>
73
+ }
74
+
75
+ <%
76
+ /**
77
+ * Helper function to convert schema field types to TypeScript types
78
+ * Uses Fimo wrapper types for source tracking
79
+ */
80
+ function getTypeForField(field) {
81
+ switch(field.type) {
82
+ case 'string':
83
+ return 'FimoString';
84
+ case 'richtext':
85
+ return 'FimoRichText';
86
+ case 'number':
87
+ return 'number';
88
+ case 'boolean':
89
+ return 'FimoBoolean';
90
+ case 'date':
91
+ return 'string';
92
+ case 'json':
93
+ return 'Record<string, any>';
94
+ case 'media':
95
+ return field.multiple ? 'FimoMedia[]' : 'FimoMedia';
96
+ case 'relation':
97
+ return field.many ? 'FimoReference[]' : 'FimoReference | null';
98
+ default:
99
+ return 'any';
100
+ }
101
+ }
102
+
103
+ function getInputTypeForField(field) {
104
+ switch(field.type) {
105
+ case 'string':
106
+ return 'string';
107
+ case 'richtext':
108
+ return 'FimoRichTextContent';
109
+ case 'number':
110
+ return 'number';
111
+ case 'boolean':
112
+ return 'boolean';
113
+ case 'date':
114
+ return 'string';
115
+ case 'json':
116
+ return 'Record<string, unknown>';
117
+ case 'media': {
118
+ const media = 'Pick<FimoMedia, "url" | "alt" | "name" | "mime" | "width" | "height">';
119
+ return field.multiple ? `${media}[]` : media;
120
+ }
121
+ case 'relation':
122
+ return field.many ? 'FimoReference[]' : 'FimoReference | null';
123
+ default:
124
+ return 'unknown';
125
+ }
126
+ }
127
+ -%>
128
+ }
129
+
130
+ export interface <%= model.pascal %>Relations {
131
+ <%
132
+ if (model.fields) {
133
+ Object.entries(model.fields).forEach(([key, field]) => {
134
+ if (field.type !== 'relation' || typeof field.targetPascal !== 'string') {
135
+ return;
136
+ }
137
+ const type = field.many ? `${field.targetPascal}[]` : `${field.targetPascal} | null`;
138
+ -%>
139
+ <%= key %>: <%= type %>;
140
+ <%
141
+ });
142
+ }
143
+ -%>
144
+ }
145
+
146
+ export type <%= model.pascal %>Populate = Populate<<%= model.pascal %>>;
147
+
148
+ /*───────────────────────────────────────────────────────────────*/
149
+ /* ──────────── 🔎 Query types ──────────── */
150
+ /*───────────────────────────────────────────────────────────────*/
151
+
152
+ export type <%= model.pascal %>Where = Where<<%= model.pascal %>>;
153
+ export type <%= model.pascal %>Sort = Sort<<%= model.pascal %>>;
154
+ export type <%= model.pascal %>Fields = Fields<<%= model.pascal %>>;
155
+ export type <%= model.pascal %>Query<
156
+ TFields extends <%= model.pascal %>Fields | undefined = <%= model.pascal %>Fields | undefined,
157
+ TPopulate extends <%= model.pascal %>Populate | undefined = <%= model.pascal %>Populate | undefined,
158
+ > = Query<<%= model.pascal %>, TFields, TPopulate>;
159
+ export type <%= model.pascal %>Result<
160
+ TFields extends <%= model.pascal %>Fields | undefined = undefined,
161
+ TPopulate extends <%= model.pascal %>Populate | undefined = undefined,
162
+ > = ProjectedWithPopulate<PopulatedResult<<%= model.pascal %>, TPopulate>, TFields, TPopulate>;