fimo 0.4.0-experimental.2 → 0.4.0-experimental.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,79 +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
- }
26
-
27
- export interface <%= model.pascal %> {
28
- id: string;
29
- documentId: string;
30
- slug: FimoString;
31
- locale: string;
32
- createdAt: string;
33
- updatedAt: string;
34
- __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";
35
16
  <%
36
- // Generate interface properties based on fields
37
- if (model.fields) {
38
- Object.entries(model.fields).forEach(([key, field]) => {
39
- const type = getTypeForField(field);
17
+ if (model.relationImports && model.relationImports.length > 0) {
18
+ model.relationImports.forEach((relationImport) => {
40
19
  -%>
41
- <%= key %>: <%= type %>;
20
+ import { wrapWithSource as wrap<%= relationImport.pascal %>WithSource } from "./<%= relationImport.pascal %>";
42
21
  <%
43
22
  });
44
23
  }
45
-
46
- /**
47
- * Helper function to convert schema field types to TypeScript types
48
- * Uses Fimo wrapper types for source tracking
49
- */
50
- function getTypeForField(field) {
51
- switch(field.type) {
52
- case 'string':
53
- return 'FimoString';
54
- case 'richtext':
55
- return 'FimoRichText';
56
- case 'number':
57
- return 'number';
58
- case 'boolean':
59
- return 'FimoBoolean';
60
- case 'date':
61
- return 'string';
62
- case 'json':
63
- return 'Record<string, any>';
64
- case 'media':
65
- return field.multiple ? 'FimoMedia[]' : 'FimoMedia';
66
- case 'relation':
67
- return field.many ? 'Array<string | Record<string, any>>' : 'string | Record<string, any> | null';
68
- default:
69
- return 'any';
70
- }
71
- }
72
24
  -%>
73
- }
74
-
75
- /*───────────────────────────────────────────────────────────────*/
76
- /* ──────────── 🔎 Query types ──────────── */
77
- /*───────────────────────────────────────────────────────────────*/
78
-
79
- export type <%= model.pascal %>Where = Where<<%= model.pascal %>>;
80
- export type <%= model.pascal %>Sort = Sort<<%= model.pascal %>>;
81
- export type <%= model.pascal %>Fields = Fields<<%= model.pascal %>>;
82
- export type <%= model.pascal %>Query<TFields extends <%= model.pascal %>Fields | undefined = <%= model.pascal %>Fields | undefined> = Query<<%= model.pascal %>, TFields>;
83
- 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";
84
26
 
85
27
  /*───────────────────────────────────────────────────────────────*/
86
28
  /* ──────────── 🎁 Source wrapping ──────────── */
@@ -89,7 +31,7 @@ export type <%= model.pascal %>Result<TFields extends <%= model.pascal %>Fields
89
31
  /**
90
32
  * Wraps raw API data with Fimo types for source tracking
91
33
  */
92
- function wrapWithSource(entry: Record<string, any>): <%= model.pascal %> {
34
+ export function wrapWithSource(entry: Record<string, any>): <%= model.pascal %> {
93
35
  const id = entry.id;
94
36
  const sourcePrefix = `<%= model.pascal %>.${id}`;
95
37
  const entryFimo = entry.__fimo && typeof entry.__fimo === "object" ? entry.__fimo : {};
@@ -98,7 +40,9 @@ function wrapWithSource(entry: Record<string, any>): <%= model.pascal %> {
98
40
  id,
99
41
  documentId: entry.documentId,
100
42
  slug: entry.slug != null ? new FimoString(entry.slug, `${sourcePrefix}.slug`) : entry.slug,
43
+ <% if (model.localized) { -%>
101
44
  locale: entry.locale,
45
+ <% } -%>
102
46
  createdAt: entry.createdAt,
103
47
  updatedAt: entry.updatedAt,
104
48
  __fimo: {
@@ -135,6 +79,19 @@ if (model.fields) {
135
79
  <%
136
80
  }
137
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
+ }
138
95
  default:
139
96
  -%>
140
97
  <%= key %>: entry.<%= key %>,
@@ -157,7 +114,25 @@ function defaultLocale(): string {
157
114
  }
158
115
 
159
116
  function useQueryLocale(locale?: string): string {
160
- 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;
161
136
  }
162
137
 
163
138
  function fimoHeaders(extra?: Record<string, string>): Record<string, string> {
@@ -168,13 +143,21 @@ function fimoHeaders(extra?: Record<string, string>): Record<string, string> {
168
143
  return h;
169
144
  }
170
145
 
171
- 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 {
172
151
  const u = new URL(url, "http://fimo.local");
173
- 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
+ }
174
157
  return `${u.pathname}${u.search}`;
175
158
  }
176
159
 
177
- function splitWritePayload(payload: Partial<<%= model.pascal %>>): Record<string, unknown> {
160
+ function splitWritePayload(payload: Partial<<%= model.pascal %>Input>): Record<string, unknown> {
178
161
  const {
179
162
  id,
180
163
  documentId,
@@ -192,31 +175,45 @@ function splitWritePayload(payload: Partial<<%= model.pascal %>>): Record<string
192
175
  };
193
176
  }
194
177
 
195
- export async function getById(id: string, options: { locale?: string } = {}): Promise<<%= model.pascal %>> {
196
- 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() });
197
184
  if (!res.ok) throw new Error(await res.text());
198
185
  const data = await res.json();
199
- return wrapWithSource(data.data);
186
+ return wrapWithSource(data.data) as <%= model.pascal %>Result<undefined, TPopulate>;
200
187
  }
201
188
 
202
- export async function getBySlug(slug: string, options: { locale?: string } = {}): Promise<<%= model.pascal %> | null> {
203
- 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() });
204
194
  if (res.status === 404) return null;
205
195
  if (!res.ok) throw new Error(await res.text());
206
196
  const data = await res.json();
207
- return wrapWithSource(data.data);
197
+ return wrapWithSource(data.data) as <%= model.pascal %>Result<undefined, TPopulate>;
208
198
  }
209
199
 
210
- export async function getByField(fieldName: string, value: string, options: { locale?: string } = {}): Promise<<%= model.pascal %> | null> {
211
- 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() });
212
206
  if (res.status === 404) return null;
213
207
  if (!res.ok) throw new Error(await res.text());
214
208
  const data = await res.json();
215
- return wrapWithSource(data.data);
209
+ return wrapWithSource(data.data) as <%= model.pascal %>Result<undefined, TPopulate>;
216
210
  }
217
211
 
218
- function toQueryString<TFields extends <%= model.pascal %>Fields | undefined = undefined>(params: <%= model.pascal %>Query<TFields> = {} as <%= model.pascal %>Query<TFields>): string {
219
- 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;
220
217
  const query = new URLSearchParams();
221
218
 
222
219
  for (const [key, value] of Object.entries(flat)) {
@@ -240,22 +237,24 @@ function toQueryString<TFields extends <%= model.pascal %>Fields | undefined = u
240
237
  query.set("where", JSON.stringify(where));
241
238
  }
242
239
 
243
- if (!query.has("locale")) {
244
- query.set("locale", defaultLocale());
245
- }
240
+ const locale = readLocale({ locale: requestedLocale, populate }, LOCALIZED_CONTENT);
241
+ if (locale) query.set("locale", locale);
246
242
 
247
243
  return query.toString();
248
244
  }
249
245
 
250
- 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>[]> {
251
250
  const qs = toQueryString(params);
252
251
  const res = await fetch(`${base}/entries/<%= model.uid %>${qs ? "?" + qs : ""}`, { headers: fimoHeaders() });
253
252
  if (!res.ok) throw new Error(await res.text());
254
253
  const data = await res.json();
255
- return data.data.map(wrapWithSource) as <%= model.pascal %>Result<TFields>[];
254
+ return data.data.map(wrapWithSource) as <%= model.pascal %>Result<TFields, TPopulate>[];
256
255
  }
257
256
 
258
- export async function create(payload: Partial<<%= model.pascal %>>): Promise<<%= model.pascal %>> {
257
+ export async function create(payload: <%= model.pascal %>Input): Promise<<%= model.pascal %>> {
259
258
  const res = await fetch(`${base}/entries/<%= model.uid %>`, {
260
259
  method: "POST",
261
260
  headers: fimoHeaders({ "Content-Type": "application/json" }),
@@ -263,10 +262,10 @@ export async function create(payload: Partial<<%= model.pascal %>>): Promise<<%=
263
262
  });
264
263
  if (!res.ok) throw new Error(await res.text());
265
264
  const data = await res.json();
266
- return data.data;
265
+ return wrapWithSource(data.data);
267
266
  }
268
267
 
269
- 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 %>> {
270
269
  const res = await fetch(`${base}/entries/<%= model.uid %>/${id}`, {
271
270
  method: "PUT",
272
271
  headers: fimoHeaders({ "Content-Type": "application/json" }),
@@ -274,71 +273,107 @@ export async function update(id: string, payload: Partial<<%= model.pascal %>>):
274
273
  });
275
274
  if (!res.ok) throw new Error(await res.text());
276
275
  const data = await res.json();
277
- return data.data;
276
+ return wrapWithSource(data.data);
278
277
  }
279
278
 
280
279
  export async function remove(id: string): Promise<void> {
281
280
  const res = await fetch(`${base}/entries/<%= model.uid %>/${id}`, { method: "DELETE", headers: fimoHeaders() });
282
281
  if (!res.ok) throw new Error(await res.text());
283
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
+ <% } -%>
284
306
 
285
307
  /*───────────────────────────────────────────────────────────────*/
286
308
  /* ──────────── 🍯 React-Query hooks ──────────── */
287
309
  /*───────────────────────────────────────────────────────────────*/
288
310
 
311
+ <% if (!model.isSingleton) { -%>
289
312
  const qk = {
290
313
  root : ["<%= model.pascal %>"] as const,
291
- byId : (id: string, locale?: string) => ["<%= model.pascal %>", id, locale ?? defaultLocale()] as const,
292
- bySlug : (slug: string, locale?: string) => ["<%= model.pascal %>", "slug", slug, locale ?? defaultLocale()] as const,
293
- 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,
294
317
  list : (params: <%= model.pascal %>Query) => ["<%= model.pascal %>", params] as const
295
318
  };
296
319
 
297
- export function useGetById(id: string, options: { locale?: string } = {}) {
298
- const locale = useQueryLocale(options.locale);
299
- 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) });
300
324
  }
301
325
 
302
- export function useGetBySlug(slug: string, options: { locale?: string } = {}) {
303
- const locale = useQueryLocale(options.locale);
304
- 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 });
305
330
  }
306
331
 
307
- export function useGetByField(fieldName: string, value: string, options: { locale?: string } = {}) {
308
- const locale = useQueryLocale(options.locale);
309
- 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 });
310
336
  }
311
337
 
312
- export function useGet<TFields extends <%= model.pascal %>Fields | undefined = undefined>(params: <%= model.pascal %>Query<TFields> = {} as <%= model.pascal %>Query<TFields>) {
313
- 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));
314
343
  const localizedParams = { ...params, locale };
315
- 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) });
316
345
  }
317
346
 
318
- export function useSuspenseGetById(id: string, options: { locale?: string } = {}) {
319
- const locale = useQueryLocale(options.locale);
320
- 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) });
321
351
  }
322
352
 
323
- export function useSuspenseGetBySlug(slug: string, options: { locale?: string } = {}) {
324
- const locale = useQueryLocale(options.locale);
325
- 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) });
326
357
  }
327
358
 
328
- export function useSuspenseGetByField(fieldName: string, value: string, options: { locale?: string } = {}) {
329
- const locale = useQueryLocale(options.locale);
330
- 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) });
331
363
  }
332
364
 
333
- export function useSuspenseGet<TFields extends <%= model.pascal %>Fields | undefined = undefined>(params: <%= model.pascal %>Query<TFields> = {} as <%= model.pascal %>Query<TFields>) {
334
- 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));
335
370
  const localizedParams = { ...params, locale };
336
- 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) });
337
372
  }
338
373
 
339
374
  export function useCreate() {
340
375
  const qc = useQueryClient();
341
- return useMutation<<%= model.pascal %>, Error, Partial<<%= model.pascal %>>>({
376
+ return useMutation<<%= model.pascal %>, Error, <%= model.pascal %>Input>({
342
377
  mutationFn : create,
343
378
  onSuccess : () => qc.invalidateQueries({ queryKey: qk.root })
344
379
  });
@@ -346,9 +381,12 @@ export function useCreate() {
346
381
 
347
382
  export function useUpdate() {
348
383
  const qc = useQueryClient();
349
- return useMutation<<%= model.pascal %>, Error, { id: string } & Partial<<%= model.pascal %>>>({
384
+ return useMutation<<%= model.pascal %>, Error, { id: string } & Partial<<%= model.pascal %>Input>>({
350
385
  mutationFn : ({ id, ...patch }) => update(id, patch),
351
- 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
+ }
352
390
  });
353
391
  }
354
392
 
@@ -358,16 +396,43 @@ export function useRemove() {
358
396
  mutationFn : remove,
359
397
  onSuccess : (_data, id) => {
360
398
  qc.invalidateQueries({ queryKey: qk.root });
361
- qc.removeQueries({ queryKey: qk.byId(id) as unknown as QueryKey });
399
+ qc.removeQueries({ queryKey: ["<%= model.pascal %>", id], exact: false });
362
400
  }
363
401
  });
364
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
+ <% } -%>
365
429
 
366
430
  /*───────────────────────────────────────────────────────────────*/
367
431
  /* ──────────── 🎯 Default export ──────────── */
368
432
  /*───────────────────────────────────────────────────────────────*/
369
433
 
370
434
  export default {
435
+ <% if (!model.isSingleton) { -%>
371
436
  /* async helpers */
372
437
  getById,
373
438
  getBySlug,
@@ -389,4 +454,11 @@ export default {
389
454
  useCreate,
390
455
  useUpdate,
391
456
  useRemove
457
+ <% } else { -%>
458
+ get,
459
+ update,
460
+ useGet,
461
+ useSuspenseGet,
462
+ useUpdate
463
+ <% } -%>
392
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>;