fimo 0.4.1 → 0.5.0-experimental.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,8 +9,11 @@
9
9
  */
10
10
  %>
11
11
  // AUTO-GENERATED ⚠️ – do not edit by hand
12
- import { useSuspenseQuery, useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
13
- import { FimoString, FimoMedia, FimoBoolean, FimoRichText, useLocale } from "fimo/ui";
12
+ import { <% if (!model.isSingleton) { %>useInfiniteQuery, <% } %>useSuspenseQuery, useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
13
+ <% if (!model.isSingleton) { -%>
14
+ import type { InfiniteData, UseInfiniteQueryResult, UseQueryResult } from "@tanstack/react-query";
15
+ <% } -%>
16
+ import { FimoString, FimoMedia, FimoBoolean, FimoRichText, useLocale<% if (!model.isSingleton) { %>, parseCollectionResponse<% } %> } from "fimo/ui";
14
17
  // @ts-ignore - virtual module provided by Fimo's Vite plugin
15
18
  import fimoConfig from "virtual:fimo-config";
16
19
  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";
@@ -215,6 +218,37 @@ function splitWritePayload(payload: Partial<<%= model.pascal %>Input>): Record<s
215
218
  }
216
219
 
217
220
  <% if (!model.isSingleton) { -%>
221
+ export interface CollectionPageInfo {
222
+ readonly limit: number;
223
+ readonly offset: number;
224
+ readonly total: number;
225
+ readonly hasMore: boolean;
226
+ readonly nextOffset: number | null;
227
+ }
228
+
229
+ export interface CollectionPage<TItem> {
230
+ readonly items: TItem[];
231
+ readonly pageInfo: CollectionPageInfo;
232
+ }
233
+
234
+ export type InfiniteListQuery<
235
+ TFields extends <%= model.pascal %>Fields | undefined = undefined,
236
+ TPopulate extends <%= model.pascal %>Populate | undefined = undefined,
237
+ > = Omit<<%= model.pascal %>Query<TFields, TPopulate>, "offset">;
238
+
239
+ export type InfiniteListResult<TItem> = UseInfiniteQueryResult<
240
+ InfiniteData<CollectionPage<TItem>>,
241
+ Error
242
+ > & {
243
+ readonly items: TItem[];
244
+ readonly total: number;
245
+ readonly hasMore: boolean;
246
+ readonly loadMore: () => ReturnType<
247
+ UseInfiniteQueryResult<InfiniteData<CollectionPage<TItem>>, Error>["fetchNextPage"]
248
+ >;
249
+ readonly isLoadingMore: boolean;
250
+ };
251
+
218
252
  export async function getById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(
219
253
  id: string,
220
254
  options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}
@@ -282,15 +316,30 @@ function toQueryString<
282
316
  return query.toString();
283
317
  }
284
318
 
285
- export async function get<
319
+ export async function list<
286
320
  TFields extends <%= model.pascal %>Fields | undefined = undefined,
287
321
  TPopulate extends <%= model.pascal %>Populate | undefined = undefined,
288
- >(params: <%= model.pascal %>Query<TFields, TPopulate> = {} as <%= model.pascal %>Query<TFields, TPopulate>): Promise<<%= model.pascal %>Result<TFields, TPopulate>[]> {
322
+ >(params: <%= model.pascal %>Query<TFields, TPopulate> = {} as <%= model.pascal %>Query<TFields, TPopulate>): Promise<CollectionPage<<%= model.pascal %>Result<TFields, TPopulate>>> {
289
323
  const qs = toQueryString(params);
290
324
  const res = await fetch(`${base}/entries/<%= model.uid %>${qs ? "?" + qs : ""}`, { headers: fimoHeaders() });
291
325
  if (!res.ok) throw new Error(await res.text());
292
- const data = await res.json();
293
- return data.data.map(wrapWithSource) as <%= model.pascal %>Result<TFields, TPopulate>[];
326
+ const data = parseCollectionResponse(await res.json());
327
+ const items = data.data.map(wrapWithSource) as <%= model.pascal %>Result<TFields, TPopulate>[];
328
+ const pageInfo: CollectionPageInfo = {
329
+ limit: data.meta.limit,
330
+ offset: data.meta.offset,
331
+ total: data.meta.total,
332
+ hasMore: data.meta.hasMore,
333
+ nextOffset: data.meta.hasMore ? data.meta.offset + data.meta.limit : null,
334
+ };
335
+ return { items, pageInfo };
336
+ }
337
+
338
+ export async function get<
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>): Promise<<%= model.pascal %>Result<TFields, TPopulate>[]> {
342
+ return (await list(params)).items;
294
343
  }
295
344
 
296
345
  export async function create(payload: <%= model.pascal %>Input): Promise<<%= model.pascal %>> {
@@ -353,7 +402,11 @@ const qk = {
353
402
  byId : <TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) => ["<%= model.pascal %>", id, options.locale, options.populate] as const,
354
403
  bySlug : <TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(slug: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) => ["<%= model.pascal %>", "slug", slug, options.locale, options.populate] as const,
355
404
  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,
356
- list : (params: <%= model.pascal %>Query) => ["<%= model.pascal %>", params] as const
405
+ list : (params: <%= model.pascal %>Query) => ["<%= model.pascal %>", params] as const,
406
+ infiniteList: <
407
+ TFields extends <%= model.pascal %>Fields | undefined = undefined,
408
+ TPopulate extends <%= model.pascal %>Populate | undefined = undefined,
409
+ >(params: InfiniteListQuery<TFields, TPopulate>) => ["<%= model.pascal %>", "infinite", params] as const
357
410
  };
358
411
 
359
412
  export function useGetById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
@@ -380,7 +433,46 @@ export function useGet<
380
433
  >(params: <%= model.pascal %>Query<TFields, TPopulate> = {} as <%= model.pascal %>Query<TFields, TPopulate>) {
381
434
  const locale = readLocale(params, LOCALIZED_CONTENT, useQueryLocale(params.locale));
382
435
  const localizedParams = { ...params, locale };
383
- return useQuery<<%= model.pascal %>Result<TFields, TPopulate>[], Error>({ queryKey: qk.list(localizedParams), queryFn: () => get(localizedParams) });
436
+ return useQuery<CollectionPage<<%= model.pascal %>Result<TFields, TPopulate>>, Error, <%= model.pascal %>Result<TFields, TPopulate>[]>({
437
+ queryKey: qk.list(localizedParams),
438
+ queryFn: () => list(localizedParams),
439
+ select: (page) => page.items,
440
+ });
441
+ }
442
+
443
+ export function useList<
444
+ TFields extends <%= model.pascal %>Fields | undefined = undefined,
445
+ TPopulate extends <%= model.pascal %>Populate | undefined = undefined,
446
+ >(params: <%= model.pascal %>Query<TFields, TPopulate> = {} as <%= model.pascal %>Query<TFields, TPopulate>): UseQueryResult<CollectionPage<<%= model.pascal %>Result<TFields, TPopulate>>, Error> {
447
+ const locale = readLocale(params, LOCALIZED_CONTENT, useQueryLocale(params.locale));
448
+ const localizedParams = { ...params, locale };
449
+ return useQuery<CollectionPage<<%= model.pascal %>Result<TFields, TPopulate>>, Error>({
450
+ queryKey: qk.list(localizedParams),
451
+ queryFn: () => list(localizedParams),
452
+ });
453
+ }
454
+
455
+ export function useInfiniteList<
456
+ TFields extends <%= model.pascal %>Fields | undefined = undefined,
457
+ TPopulate extends <%= model.pascal %>Populate | undefined = undefined,
458
+ >(params: InfiniteListQuery<TFields, TPopulate> = {} as InfiniteListQuery<TFields, TPopulate>): InfiniteListResult<<%= model.pascal %>Result<TFields, TPopulate>> {
459
+ const locale = readLocale(params, LOCALIZED_CONTENT, useQueryLocale(params.locale));
460
+ const localizedParams = { ...params, locale };
461
+ const query = useInfiniteQuery({
462
+ queryKey: qk.infiniteList(localizedParams),
463
+ queryFn: ({ pageParam }) => list({ ...localizedParams, offset: pageParam }),
464
+ initialPageParam: 0,
465
+ getNextPageParam: (lastPage) => lastPage.pageInfo.nextOffset ?? undefined,
466
+ });
467
+
468
+ return {
469
+ ...query,
470
+ items: query.data?.pages.flatMap((page) => page.items) ?? [],
471
+ total: query.data?.pages[0]?.pageInfo.total ?? 0,
472
+ hasMore: query.hasNextPage,
473
+ loadMore: () => query.fetchNextPage(),
474
+ isLoadingMore: query.isFetchingNextPage,
475
+ };
384
476
  }
385
477
 
386
478
  export function useSuspenseGetById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
@@ -407,7 +499,11 @@ export function useSuspenseGet<
407
499
  >(params: <%= model.pascal %>Query<TFields, TPopulate> = {} as <%= model.pascal %>Query<TFields, TPopulate>) {
408
500
  const locale = readLocale(params, LOCALIZED_CONTENT, useQueryLocale(params.locale));
409
501
  const localizedParams = { ...params, locale };
410
- return useSuspenseQuery<<%= model.pascal %>Result<TFields, TPopulate>[], Error>({ queryKey: qk.list(localizedParams), queryFn: () => get(localizedParams) });
502
+ return useSuspenseQuery<CollectionPage<<%= model.pascal %>Result<TFields, TPopulate>>, Error, <%= model.pascal %>Result<TFields, TPopulate>[]>({
503
+ queryKey: qk.list(localizedParams),
504
+ queryFn: () => list(localizedParams),
505
+ select: (page) => page.items,
506
+ });
411
507
  }
412
508
 
413
509
  export function useCreate() {
@@ -476,6 +572,7 @@ export default {
476
572
  getById,
477
573
  getBySlug,
478
574
  getByField,
575
+ list,
479
576
  get,
480
577
  create,
481
578
  update,
@@ -485,6 +582,8 @@ export default {
485
582
  useGetById,
486
583
  useGetBySlug,
487
584
  useGetByField,
585
+ useList,
586
+ useInfiniteList,
488
587
  useGet,
489
588
  useSuspenseGetById,
490
589
  useSuspenseGet,
@@ -20,7 +20,7 @@ For CLI commands and workflows (creating a project, pushing schemas/forms, deplo
20
20
 
21
21
  This is the load-bearing rule for every code change. **Fimo's whole point is that content editors edit content in the dashboard, not in code.** Anything hardcoded breaks that.
22
22
 
23
- - **Content** (posts, products, pages, lists, anything structured) → declare a schema, render with the generated `useGet()` hook + `fimo/ui` components. **Never** write `const posts = [...]` in JSX.
23
+ - **Content** (posts, products, pages, lists, anything structured) → declare a schema, render collections with generated `useList()` or `useInfiniteList()` hooks + `fimo/ui` components. Use `useGet()` only for legacy fixed array queries. **Never** write `const posts = [...]` in JSX.
24
24
  - **Strings** (labels, CTAs, headings, descriptions, any visible text) → wrap in `t('key')` and create the default-locale value with `fimo labels set`. Use `--locale <locale>` only when intentionally writing a non-default locale. **Never** write `<h1>Welcome</h1>` directly — write `<Text value={t('home.title')} as="h1" />`.
25
25
  - **Media** (images, videos, files) → use `<Image>` / `<Video>` from `fimo/ui` for schema media, `<StaticImage>` for hard-coded URLs. **Never** use `<img src={...}>`.
26
26
  - **Forms** → use the generated Zod schema + `submitX` helper from `@/forms/<name>`. Never hand-roll fetch or validation.
@@ -250,6 +250,7 @@ Filtering on polymorphic fields is not supported yet.
250
250
  getById<TPopulate extends BlogPostPopulate | undefined>(id: string, options?: { populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate>>
251
251
  getBySlug<TPopulate extends BlogPostPopulate | undefined>(slug: string, options?: { populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate> | null>
252
252
  getByField<TPopulate extends BlogPostPopulate | undefined>(fieldName: string, value: string, options?: { populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate> | null>
253
+ list<TFields extends BlogPostFields | undefined, TPopulate extends BlogPostPopulate | undefined>(params?: BlogPostQuery<TFields, TPopulate>): Promise<CollectionPage<BlogPostResult<TFields, TPopulate>>>
253
254
  get<TFields extends BlogPostFields | undefined, TPopulate extends BlogPostPopulate | undefined>(params?: BlogPostQuery<TFields, TPopulate>): Promise<BlogPostResult<TFields, TPopulate>[]>
254
255
  create(payload: BlogPostInput): Promise<BlogPost>
255
256
  update(id: string, payload: Partial<BlogPostInput>): Promise<BlogPost>
@@ -259,7 +260,9 @@ remove(id: string): Promise<void>
259
260
  useGetById(id, { populate? }) // → { data: BlogPostResult<undefined, TPopulate> | undefined, isLoading, ... }
260
261
  useGetBySlug(slug, { populate? }) // → { data: BlogPostResult<undefined, TPopulate> | null | undefined, ... } — ideal for /:slug detail routes
261
262
  useGetByField(fieldName, value, { populate? })
262
- useGet<TFields, TPopulate>(params?) // → { data: BlogPostResult<TFields, TPopulate>[] | undefined, isLoading, ... }
263
+ useList<TFields, TPopulate>(params?) // → { data: CollectionPage<BlogPostResult<TFields, TPopulate>> | undefined, ... }
264
+ useInfiniteList<TFields, TPopulate>(params?) // → { items, total, hasMore, loadMore, isLoadingMore, ... }
265
+ useGet<TFields, TPopulate>(params?) // → legacy array-shaped { data: BlogPostResult<TFields, TPopulate>[] | undefined, ... }
263
266
  useCreate() / useUpdate() / useRemove()
264
267
 
265
268
  // Suspense variants (same signatures, throw to a <Suspense> boundary instead of returning isLoading)
@@ -278,12 +281,37 @@ useGet(options?) / useSuspenseGet(options?) / useUpdate()
278
281
 
279
282
  Singleton clients do not expose `create`, `remove`, `getById`, or list queries. Localized singletons have one variant per locale; non-localized singletons have one locale-free row.
280
283
 
281
- Access pattern — the hooks **unwrap** the API envelope, so `get()` / `useGet()` resolve to a `BlogPost[]` directly (not a `{ data, meta }` wrapper):
284
+ Choose the collection interface from the UI behavior. Use `useList` when the UI controls a page, and read pagination state from `pageInfo`:
282
285
 
283
286
  ```tsx
284
- const { data: posts, isLoading } = useGet({ where: { featured: true }, sort: '-publishedAt', limit: 10 });
285
- if (isLoading || !posts) return null;
286
- posts.map((post) => /* ... */); // BlogPost[]
287
+ const { data: page, isLoading } = BlogPost.useList({ sort: '-publishedAt', limit: 12, offset: 24 });
288
+ if (isLoading || !page) return null;
289
+
290
+ page.items.map((post) => /* ... */);
291
+ page.pageInfo; // { limit, offset, total, hasMore, nextOffset }
292
+ ```
293
+
294
+ Use `useInfiniteList` for load-more and infinite-scroll views. It accumulates pages and exposes one typed `items` array, so UI code does not manage offsets or flatten pages:
295
+
296
+ ```tsx
297
+ const {
298
+ items: posts,
299
+ hasMore,
300
+ loadMore,
301
+ isLoadingMore,
302
+ } = BlogPost.useInfiniteList({
303
+ sort: '-publishedAt',
304
+ limit: 12,
305
+ });
306
+ ```
307
+
308
+ `get()` and `useGet()` remain available as legacy array-shaped helpers for small, deliberately bounded subsets:
309
+
310
+ ```tsx
311
+ const { data: featured = [] } = BlogPost.useGet({
312
+ where: { featured: true },
313
+ limit: 3,
314
+ });
287
315
  ```
288
316
 
289
317
  Query params supported: `limit` (1-100, default 20), `offset` (default 0), `search`, `sort`, `where`, `fields`, and `populate`. Use these canonical names in new code.
@@ -350,7 +378,7 @@ Do not type populated hook data as `BlogPost[]`: plain `BlogPost` keeps referenc
350
378
 
351
379
  Populate is depth-one. It only works for reference fields; trying to populate a scalar field returns 400. To-many reference order is preserved.
352
380
 
353
- The generated hooks do **not** surface pagination meta (`total`, `hasMore`, `limit`, `offset`). If you need it, read it from a raw fetch to `/entries/<typeName>` (see the **Read path** section below), which returns the full `{ data, meta }` envelope.
381
+ `list()` and `useList()` surface pagination metadata as `pageInfo`. `useInfiniteList()` consumes `nextOffset` internally and exposes `hasMore` plus `loadMore()`.
354
382
 
355
383
  ## Fimo primitives (what the field values are)
356
384
 
@@ -397,15 +425,15 @@ class FimoBoolean {
397
425
  ## UI code pattern
398
426
 
399
427
  ```tsx
400
- import { useGet } from '@/schemas/BlogPost';
428
+ import { useList } from '@/schemas/BlogPost';
401
429
  import { Text, RichText, Image, Date } from 'fimo/ui';
402
430
 
403
431
  export function BlogList() {
404
- const { data: posts, isLoading } = useGet({ limit: 10 });
405
- if (isLoading || !posts) return null;
432
+ const { data: page, isLoading } = useList({ limit: 10 });
433
+ if (isLoading || !page) return null;
406
434
  return (
407
435
  <ul>
408
- {posts.map((post) => (
436
+ {page.items.map((post) => (
409
437
  <li key={post.id}>
410
438
  <Text value={post.title} as="h2" />
411
439
  <Image value={post.coverImage} width={800} height={400} fit="cover" />
@@ -429,7 +457,7 @@ const { data, meta } = await res.json();
429
457
  // meta: { limit, offset, total, hasMore }
430
458
  ```
431
459
 
432
- Prefer the generated React Query hooks from `@/schemas/{Uid}` over hand-rolled fetch — they handle caching and types (returning typed `BlogPostResult<TFields, TPopulate>[]` results). Reach for this raw fetch only when you need the pagination `meta`, which the hooks don't surface.
460
+ Prefer the generated `useList` or `useInfiniteList` hooks from `@/schemas/{Uid}` over hand-rolled fetches. They preserve projected/populated item types, cache results, and expose the pagination state regular list UIs need. Use the raw endpoint only for lower-level integrations outside the generated client.
433
461
 
434
462
  ## Forms vs. schemas
435
463
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "bundled": true,
3
3
  "bundler": "esbuild",
4
- "bundledAt": "2026-07-14T07:18:58.576Z",
5
- "cliVersion": "0.4.1",
4
+ "bundledAt": "2026-07-14T07:07:58.432Z",
5
+ "cliVersion": "0.4.0",
6
6
  "external": [
7
7
  "oxc-parser",
8
8
  "fsevents"
@@ -0,0 +1,11 @@
1
+ export interface CollectionResponse {
2
+ readonly data: Record<string, unknown>[];
3
+ readonly meta: {
4
+ readonly limit: number;
5
+ readonly offset: number;
6
+ readonly total: number;
7
+ readonly hasMore: boolean;
8
+ };
9
+ }
10
+ export declare function parseCollectionResponse(value: unknown): CollectionResponse;
11
+ //# sourceMappingURL=parseCollectionResponse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseCollectionResponse.d.ts","sourceRoot":"","sources":["../../../src/runtime/content/parseCollectionResponse.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACzC,QAAQ,CAAC,IAAI,EAAE;QACb,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;KAC3B,CAAC;CACH;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,CAE1E"}
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ const collectionResponseSchema = z.object({
3
+ data: z.array(z.record(z.string(), z.unknown())),
4
+ meta: z.object({
5
+ limit: z.number().int().positive(),
6
+ offset: z.number().int().nonnegative(),
7
+ total: z.number().int().nonnegative(),
8
+ hasMore: z.boolean(),
9
+ }),
10
+ });
11
+ export function parseCollectionResponse(value) {
12
+ return collectionResponseSchema.parse(value);
13
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=parseCollectionResponse.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseCollectionResponse.test.d.ts","sourceRoot":"","sources":["../../../src/runtime/content/parseCollectionResponse.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parseCollectionResponse } from './parseCollectionResponse.js';
3
+ describe('parseCollectionResponse', () => {
4
+ it('accepts the collection envelope used by generated clients', () => {
5
+ const response = parseCollectionResponse({
6
+ data: [{ id: 'post-1', title: 'Hello' }],
7
+ meta: { limit: 12, offset: 24, total: 40, hasMore: true },
8
+ });
9
+ expect(response).toEqual({
10
+ data: [{ id: 'post-1', title: 'Hello' }],
11
+ meta: { limit: 12, offset: 24, total: 40, hasMore: true },
12
+ });
13
+ });
14
+ it('rejects invalid pagination metadata', () => {
15
+ expect(() => parseCollectionResponse({
16
+ data: [],
17
+ meta: { limit: 0, offset: -1, total: '40', hasMore: 'yes' },
18
+ })).toThrow();
19
+ });
20
+ });
@@ -6,6 +6,8 @@ export type { FimoLocaleContextValue } from './i18n/FimoLocaleContext.js';
6
6
  export type { FimoLocaleResolution } from './i18n/locale.js';
7
7
  export { DataSourcesProvider } from './content/DataSourcesProvider.js';
8
8
  export { RefreshContentProvider } from './content/RefreshContentProvider.js';
9
+ export { parseCollectionResponse } from './content/parseCollectionResponse.js';
10
+ export type { CollectionResponse } from './content/parseCollectionResponse.js';
9
11
  export { AppErrorBoundary } from './core/AppErrorBoundary.js';
10
12
  export { ViteLifecycles } from './core/ViteLifecycles.js';
11
13
  export { Pageview } from './t/Pageview.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AACvF,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACvE,YAAY,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAC1E,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAG7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAG7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG1D,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAG3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAEpE,cAAc,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AACvF,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACvE,YAAY,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAC1E,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAG7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AAC/E,YAAY,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAG/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG1D,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAG3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAEpE,cAAc,uBAAuB,CAAC"}
@@ -5,6 +5,7 @@ export { localizeFimoPath, resolveFimoLocale } from './i18n/locale.js';
5
5
  // Content
6
6
  export { DataSourcesProvider } from './content/DataSourcesProvider.js';
7
7
  export { RefreshContentProvider } from './content/RefreshContentProvider.js';
8
+ export { parseCollectionResponse } from './content/parseCollectionResponse.js';
8
9
  // Core
9
10
  export { AppErrorBoundary } from './core/AppErrorBoundary.js';
10
11
  export { ViteLifecycles } from './core/ViteLifecycles.js';
@@ -75,6 +75,7 @@ const SCHEMAS = {
75
75
  };
76
76
  const USAGE = `// Agent-style usage of the generated types — compile-time contract.
77
77
  import type { PageResult, PageWhere } from "./Page";
78
+ import Post from "./Post";
78
79
  import type { PostResult } from "./Post";
79
80
 
80
81
  export function renderFeatured(page: PageResult<undefined, readonly ["featured", "hero"]>): string[] {
@@ -112,6 +113,45 @@ export function whereContract(): PageWhere {
112
113
  featured: "some-document-id",
113
114
  };
114
115
  }
116
+
117
+ export function paginatedPostContract(): void {
118
+ const legacy = Post.useGet({ limit: 3 });
119
+ const page = Post.useList({
120
+ fields: ["title", "author"] as const,
121
+ populate: ["author"] as const,
122
+ limit: 12,
123
+ offset: 24,
124
+ });
125
+ const infinite = Post.useInfiniteList({
126
+ fields: ["title", "author"] as const,
127
+ populate: ["author"] as const,
128
+ limit: 12,
129
+ });
130
+
131
+ const legacyPosts: PostResult[] = legacy.data ?? [];
132
+ const pagePosts: PostResult<readonly ["title", "author"], readonly ["author"]>[] = page.data?.items ?? [];
133
+ const total: number = page.data?.pageInfo.total ?? 0;
134
+ const nextOffset: number | null = page.data?.pageInfo.nextOffset ?? null;
135
+ const infinitePosts: PostResult<readonly ["title", "author"], readonly ["author"]>[] = infinite.items;
136
+ const firstAuthorName: string = infinitePosts[0]?.author ? String(infinitePosts[0].author.name) : "";
137
+ const hasMore: boolean = infinite.hasMore;
138
+ const isLoadingMore: boolean = infinite.isLoadingMore;
139
+
140
+ void infinite.loadMore();
141
+ // @ts-expect-error infinite lists own offset progression
142
+ Post.useInfiniteList({ offset: 12 });
143
+
144
+ void [legacyPosts, pagePosts, total, nextOffset, infinitePosts, firstAuthorName, hasMore, isLoadingMore];
145
+ }
146
+
147
+ export async function paginatedPostFetchContract(): Promise<void> {
148
+ const page = await Post.list({ limit: 12, offset: 24 });
149
+ const legacyPosts: PostResult[] = await Post.get({ limit: 3 });
150
+ const pagePosts: PostResult[] = page.items;
151
+ const hasMore: boolean = page.pageInfo.hasMore;
152
+
153
+ void [legacyPosts, pagePosts, hasMore];
154
+ }
115
155
  `;
116
156
  const SHIMS = `declare module "virtual:fimo-config" {
117
157
  const config: Record<string, unknown>;
@@ -95,7 +95,7 @@ describe('syncSchemas', () => {
95
95
  expect(settings).toContain('const LOCALIZED_REFERENCE_FIELDS = new Set<string>(["homepage"]);');
96
96
  expect(settings).not.toContain(' locale: entry.locale,');
97
97
  expect(settings).toContain('readLocale(params, LOCALIZED_CONTENT, useQueryLocale(params.locale))');
98
- expect(settings.match(/readLocale\(params, LOCALIZED_CONTENT, useQueryLocale\(params\.locale\)\)/g)?.length).toBe(2);
98
+ expect(settings.match(/readLocale\(params, LOCALIZED_CONTENT, useQueryLocale\(params\.locale\)\)/g)?.length).toBe(4);
99
99
  expect(settings.match(/readLocale\(options, false, useQueryLocale\(options\.locale\)\)/g)?.length).toBe(2);
100
100
  });
101
101
  it('generates singleton document APIs without collection mutations', () => {
@@ -115,6 +115,9 @@ describe('syncSchemas', () => {
115
115
  expect(settings).not.toContain('export async function create(');
116
116
  expect(settings).not.toContain('export async function remove(');
117
117
  expect(settings).not.toContain('export function useCreate(');
118
+ expect(settings).not.toContain('export function useList(');
119
+ expect(settings).not.toContain('export function useInfiniteList(');
120
+ expect(settings).not.toContain('useInfiniteQuery');
118
121
  expect(settingsTypes).toContain('siteName: string;');
119
122
  expect(settingsTypes).not.toContain('documentId?: string;');
120
123
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fimo",
3
- "version": "0.4.1",
3
+ "version": "0.5.0-experimental.1",
4
4
  "description": "Fimo CLI - create, deploy, and manage Fimo projects",
5
5
  "bin": {
6
6
  "fimo": "dist/cli/index.js"
package/release.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "source": "npm",
3
- "apiUrl": "https://api.fimo.ai",
4
- "webUrl": "https://fimo.ai"
3
+ "apiUrl": "https://api.staging.fimo.team",
4
+ "webUrl": "https://staging.fimo.team"
5
5
  }
@@ -21,7 +21,7 @@
21
21
  "cmdk": "^1.1.1",
22
22
  "date-fns": "^4.1.0",
23
23
  "embla-carousel-react": "^8.6.0",
24
- "fimo": "0.4.1",
24
+ "fimo": "0.5.0-experimental.1",
25
25
  "input-otp": "^1.4.2",
26
26
  "isbot": "^5",
27
27
  "lucide-react": "^0.577.0",