@classytic/arc-next 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mutation.js CHANGED
@@ -21,6 +21,25 @@ let toastHandler = {
21
21
  function configureToast(handler) {
22
22
  toastHandler = handler;
23
23
  }
24
+ /**
25
+ * Get the configured toast handler. Returns the console-based default when
26
+ * no handler has been configured yet.
27
+ *
28
+ * Use this when domain code outside the react-query lifecycle needs to fire
29
+ * ad-hoc success/error toasts using the same handler the SDK uses internally
30
+ * — avoids the need for consumer SDKs to keep a parallel cache of the handler.
31
+ *
32
+ * @example
33
+ * // In a domain helper outside any mutation lifecycle:
34
+ * import { getToastHandler } from '@classytic/arc-next/mutation';
35
+ *
36
+ * function notifySaved(label: string) {
37
+ * getToastHandler().success(`${label} saved`);
38
+ * }
39
+ */
40
+ function getToastHandler() {
41
+ return toastHandler;
42
+ }
24
43
  function showToast(type, messages, data, variables, error, handler) {
25
44
  const activeHandler = handler ?? toastHandler;
26
45
  if (type === "success") {
@@ -163,4 +182,4 @@ function useOptimisticMutation(config) {
163
182
  }
164
183
 
165
184
  //#endregion
166
- export { configureToast, useMutationWithOptimistic, useMutationWithTransition, useOptimisticMutation };
185
+ export { configureToast, getToastHandler, useMutationWithOptimistic, useMutationWithTransition, useOptimisticMutation };
@@ -1,4 +1,4 @@
1
- import { QueryClient, dehydrate } from "@tanstack/react-query";
1
+ import { HydrationBoundary, InfiniteData, QueryClient, dehydrate } from "@tanstack/react-query";
2
2
 
3
3
  //#region src/prefetch.d.ts
4
4
  interface PrefetchAuthContext {
@@ -51,6 +51,31 @@ interface CrudPrefetcher {
51
51
  * Only available when the API has a `getTree` method (tree preset).
52
52
  */
53
53
  prefetchTree: (queryClient: QueryClient, params?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
54
+ /**
55
+ * Prefetch an infinite list query (cursor / page-based pagination). Uses the
56
+ * same query keys as `useInfiniteList` and seeds the `{ pages, pageParams }`
57
+ * shape TanStack Query expects for `useInfiniteQuery` — a flat
58
+ * `prefetchQuery` would NOT match the cache shape and the client hook would
59
+ * re-fetch from scratch, defeating the prefetch.
60
+ *
61
+ * @example
62
+ * await productsPrefetcher.prefetchInfiniteList(queryClient, { limit: 20 });
63
+ */
64
+ /**
65
+ * Prefetch a declared aggregation (arc 2.13+). Uses the same query key as
66
+ * `useAggregation` so RSC-pre-rendered dashboard rows hydrate without a
67
+ * client refetch.
68
+ *
69
+ * @example
70
+ * await ordersPrefetcher.prefetchAggregation(
71
+ * queryClient,
72
+ * 'salesByDay',
73
+ * { from: '2025-01-01', to: '2025-12-31' },
74
+ * { token: jwt, organizationId: orgId, staleTime: 60_000 },
75
+ * );
76
+ */
77
+ prefetchAggregation: (queryClient: QueryClient, name: string, filter?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
78
+ prefetchInfiniteList: (queryClient: QueryClient, params?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
54
79
  }
55
80
  /**
56
81
  * Create server-safe prefetch helpers for CRUD queries.
@@ -111,6 +136,15 @@ declare function createCrudPrefetcher(api: {
111
136
  headerOptions?: Record<string, string>;
112
137
  };
113
138
  }) => Promise<unknown>;
139
+ aggregate?: (opts: {
140
+ name: string;
141
+ filter?: Record<string, unknown>;
142
+ token?: string | null;
143
+ organizationId?: string | null;
144
+ options?: {
145
+ headerOptions?: Record<string, string>;
146
+ };
147
+ }) => Promise<unknown>;
114
148
  getTree?: (opts: {
115
149
  params?: Record<string, unknown>;
116
150
  token?: string | null;
@@ -121,4 +155,4 @@ declare function createCrudPrefetcher(api: {
121
155
  }) => Promise<unknown>;
122
156
  }, entityKey: string): CrudPrefetcher;
123
157
  //#endregion
124
- export { CrudPrefetcher, PrefetchAuthContext, PrefetchDetailOptions, PrefetchOptions, createCrudPrefetcher, dehydrate };
158
+ export { CrudPrefetcher, HydrationBoundary, type InfiniteData, PrefetchAuthContext, PrefetchDetailOptions, PrefetchOptions, createCrudPrefetcher, dehydrate };
package/dist/prefetch.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createQueryKeys } from "./query.js";
2
- import { dehydrate } from "@tanstack/react-query";
1
+ import { createQueryKeys } from "./cache.js";
2
+ import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
3
3
 
4
4
  //#region src/prefetch.ts
5
5
  /**
@@ -100,6 +100,26 @@ function createCrudPrefetcher(api, entityKey) {
100
100
  staleTime: options.staleTime
101
101
  });
102
102
  },
103
+ async prefetchAggregation(queryClient, name, filter, options = {}) {
104
+ if (!api.aggregate) throw new Error(`[arc-next] prefetchAggregation requires an api with aggregate (arc 2.13+)`);
105
+ if (!name) throw new Error("[arc-next] prefetchAggregation: aggregation name is required");
106
+ const orgId = options.organizationId ?? null;
107
+ const filterKey = orgId ? {
108
+ _org: orgId,
109
+ ...filter ?? {}
110
+ } : filter ?? {};
111
+ await queryClient.prefetchQuery({
112
+ queryKey: KEYS.aggregation(name, filterKey),
113
+ queryFn: () => api.aggregate({
114
+ name,
115
+ filter,
116
+ token: options.token ?? null,
117
+ organizationId: orgId,
118
+ ...options.headers ? { options: { headerOptions: options.headers } } : {}
119
+ }),
120
+ staleTime: options.staleTime
121
+ });
122
+ },
103
123
  async prefetchTree(queryClient, params = {}, options = {}) {
104
124
  if (!api.getTree) throw new Error(`[arc-next] prefetchTree requires an api with getTree (tree preset)`);
105
125
  const { organizationId: paramOrgId, ...restParams } = params;
@@ -118,9 +138,33 @@ function createCrudPrefetcher(api, entityKey) {
118
138
  }),
119
139
  staleTime: options.staleTime
120
140
  });
141
+ },
142
+ async prefetchInfiniteList(queryClient, params = {}, options = {}) {
143
+ const { organizationId: paramOrgId, ...restParams } = params;
144
+ const orgId = paramOrgId ?? options.organizationId ?? null;
145
+ const scope = orgId ? "tenant" : "super-admin";
146
+ const queryKey = [...KEYS.scopedList(scope, {
147
+ ...orgId ? { organizationId: orgId } : {},
148
+ ...restParams
149
+ }), "infinite"];
150
+ await queryClient.prefetchInfiniteQuery({
151
+ queryKey,
152
+ queryFn: ({ pageParam }) => api.getAll({
153
+ params: {
154
+ ...restParams,
155
+ ...pageParam ? { page: pageParam } : {}
156
+ },
157
+ token: options.token ?? null,
158
+ organizationId: orgId,
159
+ ...options.headers ? { options: { headerOptions: options.headers } } : {}
160
+ }),
161
+ initialPageParam: 1,
162
+ getNextPageParam: () => void 0,
163
+ staleTime: options.staleTime
164
+ });
121
165
  }
122
166
  };
123
167
  }
124
168
 
125
169
  //#endregion
126
- export { createCrudPrefetcher, dehydrate };
170
+ export { HydrationBoundary, createCrudPrefetcher, dehydrate };
@@ -0,0 +1,43 @@
1
+ import { BaseApi, ScopedArgs } from "../api.js";
2
+ import { BulkCreateResult, DeleteManyResult, UpdateManyResult } from "@classytic/repo-core/repository";
3
+
4
+ //#region src/presets/bulk.d.ts
5
+ interface BulkMethods<TDoc, TCreate = Partial<TDoc>, TUpdate = Partial<TDoc>> {
6
+ /** Insert many docs in one round-trip. Backend mounts `POST /:resource/bulk`. */
7
+ bulkCreate(args: ScopedArgs & {
8
+ data: TCreate[];
9
+ }): Promise<BulkCreateResult<TDoc>>;
10
+ /**
11
+ * Update all docs matching `filter` with `data`.
12
+ * Backend mounts `PATCH /:resource/bulk`.
13
+ */
14
+ bulkUpdate(args: ScopedArgs & {
15
+ filter: Record<string, unknown>;
16
+ data: TUpdate;
17
+ }): Promise<UpdateManyResult>;
18
+ /**
19
+ * Delete all docs matching `filter`.
20
+ * Backend mounts `DELETE /:resource/bulk`.
21
+ */
22
+ bulkDelete(args: ScopedArgs & {
23
+ filter: Record<string, unknown>;
24
+ }): Promise<DeleteManyResult>;
25
+ }
26
+ /**
27
+ * Adds bulk preset methods to a BaseApi.
28
+ *
29
+ * Mirrors arc's `bulk` preset — three /bulk routes for batch ops. Bulk endpoints
30
+ * require explicit per-resource permissions on the backend (the standard CRUD
31
+ * `allowPublic()` doesn't cover them).
32
+ *
33
+ * @example
34
+ * import { withBulk } from '@classytic/arc-next/presets/bulk';
35
+ * const todos = withBulk(createCrudApi<Todo>('todos'));
36
+ *
37
+ * await todos.bulkCreate({ data: [{ title: 'A' }, { title: 'B' }] });
38
+ * await todos.bulkUpdate({ filter: { status: 'pending' }, data: { status: 'archived' } });
39
+ * await todos.bulkDelete({ filter: { archivedBefore: '2024-01-01' } });
40
+ */
41
+ declare function withBulk<TDoc, TCreate, TUpdate>(api: BaseApi<TDoc, TCreate, TUpdate>): BaseApi<TDoc, TCreate, TUpdate> & BulkMethods<TDoc, TCreate, TUpdate>;
42
+ //#endregion
43
+ export { BulkMethods, withBulk };
@@ -0,0 +1,50 @@
1
+ //#region src/presets/bulk.ts
2
+ /**
3
+ * Adds bulk preset methods to a BaseApi.
4
+ *
5
+ * Mirrors arc's `bulk` preset — three /bulk routes for batch ops. Bulk endpoints
6
+ * require explicit per-resource permissions on the backend (the standard CRUD
7
+ * `allowPublic()` doesn't cover them).
8
+ *
9
+ * @example
10
+ * import { withBulk } from '@classytic/arc-next/presets/bulk';
11
+ * const todos = withBulk(createCrudApi<Todo>('todos'));
12
+ *
13
+ * await todos.bulkCreate({ data: [{ title: 'A' }, { title: 'B' }] });
14
+ * await todos.bulkUpdate({ filter: { status: 'pending' }, data: { status: 'archived' } });
15
+ * await todos.bulkDelete({ filter: { archivedBefore: '2024-01-01' } });
16
+ */
17
+ function withBulk(api) {
18
+ return Object.assign(api, {
19
+ async bulkCreate({ token = null, organizationId = null, data, options = {} }) {
20
+ return api.request("POST", `${api.baseUrl}/bulk`, {
21
+ token,
22
+ organizationId,
23
+ data: { items: data },
24
+ options
25
+ });
26
+ },
27
+ async bulkUpdate({ token = null, organizationId = null, filter, data, options = {} }) {
28
+ return api.request("PATCH", `${api.baseUrl}/bulk`, {
29
+ token,
30
+ organizationId,
31
+ data: {
32
+ filter,
33
+ data
34
+ },
35
+ options
36
+ });
37
+ },
38
+ async bulkDelete({ token = null, organizationId = null, filter, options = {} }) {
39
+ return api.request("DELETE", `${api.baseUrl}/bulk`, {
40
+ token,
41
+ organizationId,
42
+ data: { filter },
43
+ options
44
+ });
45
+ }
46
+ });
47
+ }
48
+
49
+ //#endregion
50
+ export { withBulk };
@@ -0,0 +1,56 @@
1
+ import { BaseApi, ScopedArgs } from "../api.js";
2
+ import { PaginatedResult } from "@classytic/repo-core/pagination";
3
+
4
+ //#region src/presets/search.d.ts
5
+ interface SearchPresetMethods<TDoc> {
6
+ /**
7
+ * Engine-backed full-text search (Elastic, Algolia, Typesense...).
8
+ * Backend mounts `POST /:resource/search` via `searchPreset()`.
9
+ */
10
+ searchEngine<TResult = TDoc, TBody extends Record<string, unknown> = Record<string, unknown>>(args?: ScopedArgs & {
11
+ /** Free-text query forwarded as `body.query`. */query?: string; /** Engine-specific options merged into the request body. */
12
+ body?: TBody; /** Override path (default `/search`). */
13
+ path?: string;
14
+ }): Promise<TResult[] | PaginatedResult<TResult>>;
15
+ /**
16
+ * Vector / semantic similarity (Atlas, Pinecone, Qdrant...).
17
+ * Backend mounts `POST /:resource/search-similar`.
18
+ */
19
+ searchSimilar<TResult = TDoc, TBody extends Record<string, unknown> = Record<string, unknown>>(args?: ScopedArgs & {
20
+ /** Text query — backend embeds and searches for nearest neighbors. */query?: string; /** Pre-computed embedding vector — used directly for similarity search. */
21
+ vector?: number[]; /** Vector-engine options (`topK`, `filter`, `index`, ...). */
22
+ body?: TBody; /** Override path (default `/search-similar`). */
23
+ path?: string;
24
+ }): Promise<TResult[]>;
25
+ /**
26
+ * Convert text/media to a vector embedding via the engine the resource is wired to.
27
+ * Backend mounts `POST /:resource/embed`.
28
+ */
29
+ embed(args: ScopedArgs & {
30
+ /** Text or array of texts to embed. */input: string | string[]; /** Embed-engine options (`model`, `dimensions`, ...). */
31
+ body?: Record<string, unknown>; /** Override path (default `/embed`). */
32
+ path?: string;
33
+ }): Promise<number[] | number[][]>;
34
+ }
35
+ /**
36
+ * Adds search preset methods to a BaseApi.
37
+ *
38
+ * Mirrors arc's `searchPreset()` — three POST routes for engine-backed search,
39
+ * vector similarity, and embedding generation. Each route is independently
40
+ * permission-gated on the backend.
41
+ *
42
+ * Differs from {@link BaseApi.search} (a GET against the list endpoint with
43
+ * filter params) — that's the legacy filtered-list pattern. These methods hit
44
+ * the new POST routes the preset mounts.
45
+ *
46
+ * @example
47
+ * import { withSearchPreset } from '@classytic/arc-next/presets/search';
48
+ * const places = withSearchPreset(createCrudApi<Place>('places'));
49
+ *
50
+ * await places.searchEngine({ query: 'park', body: { filter: { category: 'park' } } });
51
+ * await places.searchSimilar({ vector: [0.1, 0.2, ...], body: { topK: 5 } });
52
+ * await places.embed({ input: 'hello world' });
53
+ */
54
+ declare function withSearchPreset<TDoc, TCreate, TUpdate>(api: BaseApi<TDoc, TCreate, TUpdate>): BaseApi<TDoc, TCreate, TUpdate> & SearchPresetMethods<TDoc>;
55
+ //#endregion
56
+ export { SearchPresetMethods, withSearchPreset };
@@ -0,0 +1,60 @@
1
+ //#region src/presets/search.ts
2
+ /**
3
+ * Adds search preset methods to a BaseApi.
4
+ *
5
+ * Mirrors arc's `searchPreset()` — three POST routes for engine-backed search,
6
+ * vector similarity, and embedding generation. Each route is independently
7
+ * permission-gated on the backend.
8
+ *
9
+ * Differs from {@link BaseApi.search} (a GET against the list endpoint with
10
+ * filter params) — that's the legacy filtered-list pattern. These methods hit
11
+ * the new POST routes the preset mounts.
12
+ *
13
+ * @example
14
+ * import { withSearchPreset } from '@classytic/arc-next/presets/search';
15
+ * const places = withSearchPreset(createCrudApi<Place>('places'));
16
+ *
17
+ * await places.searchEngine({ query: 'park', body: { filter: { category: 'park' } } });
18
+ * await places.searchSimilar({ vector: [0.1, 0.2, ...], body: { topK: 5 } });
19
+ * await places.embed({ input: 'hello world' });
20
+ */
21
+ function withSearchPreset(api) {
22
+ return Object.assign(api, {
23
+ async searchEngine({ token = null, organizationId = null, query, body, path = "/search", options = {} } = {}) {
24
+ const requestBody = { ...body ?? {} };
25
+ if (query !== void 0) requestBody.query = query;
26
+ return api.request("POST", `${api.baseUrl}${path}`, {
27
+ token,
28
+ organizationId,
29
+ data: requestBody,
30
+ options
31
+ });
32
+ },
33
+ async searchSimilar({ token = null, organizationId = null, query, vector, body, path = "/search-similar", options = {} } = {}) {
34
+ const requestBody = { ...body ?? {} };
35
+ if (query !== void 0) requestBody.query = query;
36
+ if (vector !== void 0) requestBody.vector = vector;
37
+ return api.request("POST", `${api.baseUrl}${path}`, {
38
+ token,
39
+ organizationId,
40
+ data: requestBody,
41
+ options
42
+ });
43
+ },
44
+ async embed({ token = null, organizationId = null, input, body, path = "/embed", options = {} }) {
45
+ const requestBody = {
46
+ input,
47
+ ...body ?? {}
48
+ };
49
+ return api.request("POST", `${api.baseUrl}${path}`, {
50
+ token,
51
+ organizationId,
52
+ data: requestBody,
53
+ options
54
+ });
55
+ }
56
+ });
57
+ }
58
+
59
+ //#endregion
60
+ export { withSearchPreset };
@@ -0,0 +1,28 @@
1
+ import { BaseApi, ScopedArgs } from "../api.js";
2
+
3
+ //#region src/presets/slug.d.ts
4
+ interface SlugLookupMethods<TDoc> {
5
+ /** Fetch a single doc by slug. Backend mounts `GET /:resource/slug/:slug`. */
6
+ getBySlug(args: ScopedArgs & {
7
+ slug: string;
8
+ params?: {
9
+ select?: string;
10
+ populate?: string | string[];
11
+ };
12
+ }): Promise<TDoc>;
13
+ }
14
+ /**
15
+ * Adds slug-lookup preset methods to a BaseApi.
16
+ *
17
+ * Mirrors arc's `slugLookup` preset — exposes `getBySlug` for resources keyed
18
+ * by URL-friendly slugs alongside the canonical id.
19
+ *
20
+ * @example
21
+ * import { withSlugLookup } from '@classytic/arc-next/presets/slug';
22
+ * const categories = withSlugLookup(createCrudApi<Category>('categories'));
23
+ *
24
+ * const cat = await categories.getBySlug({ slug: 'engineering' });
25
+ */
26
+ declare function withSlugLookup<TDoc, TCreate, TUpdate>(api: BaseApi<TDoc, TCreate, TUpdate>): BaseApi<TDoc, TCreate, TUpdate> & SlugLookupMethods<TDoc>;
27
+ //#endregion
28
+ export { SlugLookupMethods, withSlugLookup };
@@ -0,0 +1,27 @@
1
+ //#region src/presets/slug.ts
2
+ /**
3
+ * Adds slug-lookup preset methods to a BaseApi.
4
+ *
5
+ * Mirrors arc's `slugLookup` preset — exposes `getBySlug` for resources keyed
6
+ * by URL-friendly slugs alongside the canonical id.
7
+ *
8
+ * @example
9
+ * import { withSlugLookup } from '@classytic/arc-next/presets/slug';
10
+ * const categories = withSlugLookup(createCrudApi<Category>('categories'));
11
+ *
12
+ * const cat = await categories.getBySlug({ slug: 'engineering' });
13
+ */
14
+ function withSlugLookup(api) {
15
+ return Object.assign(api, { async getBySlug({ token = null, organizationId = null, slug, params = {}, options = {} }) {
16
+ if (!slug) throw new Error("Slug is required");
17
+ return api.request("GET", `${api.baseUrl}/slug/${slug}`, {
18
+ token,
19
+ organizationId,
20
+ params,
21
+ options
22
+ });
23
+ } });
24
+ }
25
+
26
+ //#endregion
27
+ export { withSlugLookup };
@@ -0,0 +1,34 @@
1
+ import { BaseApi, QueryParams, ScopedArgs } from "../api.js";
2
+ import { PaginatedResult } from "@classytic/repo-core/pagination";
3
+
4
+ //#region src/presets/soft-delete.d.ts
5
+ interface SoftDeleteMethods<TDoc> {
6
+ /** List soft-deleted docs. Backend mounts `GET /:resource/deleted`. */
7
+ getDeleted(args?: ScopedArgs & {
8
+ params?: QueryParams;
9
+ }): Promise<PaginatedResult<TDoc>>;
10
+ /** Undo a soft-delete. Backend mounts `POST /:resource/:id/restore`. */
11
+ restore(args: ScopedArgs & {
12
+ id: string;
13
+ }): Promise<TDoc>;
14
+ }
15
+ /**
16
+ * Adds soft-delete preset methods to a BaseApi.
17
+ *
18
+ * Mirrors arc's server-side `softDelete` preset routes. Use when your resource
19
+ * is registered with `presets: ['softDelete']`. With this preset, `delete()`
20
+ * becomes soft-delete on the backend and {@link SoftDeleteMethods.restore}
21
+ * undoes it; {@link SoftDeleteMethods.getDeleted} lists the tombstones.
22
+ *
23
+ * @example
24
+ * import { createCrudApi } from '@classytic/arc-next/api';
25
+ * import { withSoftDelete } from '@classytic/arc-next/presets/soft-delete';
26
+ *
27
+ * const todos = withSoftDelete(createCrudApi<Todo>('todos'));
28
+ * await todos.delete({ id }); // soft delete (backend)
29
+ * await todos.restore({ id }); // undo
30
+ * const trash = await todos.getDeleted();
31
+ */
32
+ declare function withSoftDelete<TDoc, TCreate, TUpdate>(api: BaseApi<TDoc, TCreate, TUpdate>): BaseApi<TDoc, TCreate, TUpdate> & SoftDeleteMethods<TDoc>;
33
+ //#endregion
34
+ export { SoftDeleteMethods, withSoftDelete };
@@ -0,0 +1,45 @@
1
+ //#region src/presets/soft-delete.ts
2
+ /**
3
+ * Adds soft-delete preset methods to a BaseApi.
4
+ *
5
+ * Mirrors arc's server-side `softDelete` preset routes. Use when your resource
6
+ * is registered with `presets: ['softDelete']`. With this preset, `delete()`
7
+ * becomes soft-delete on the backend and {@link SoftDeleteMethods.restore}
8
+ * undoes it; {@link SoftDeleteMethods.getDeleted} lists the tombstones.
9
+ *
10
+ * @example
11
+ * import { createCrudApi } from '@classytic/arc-next/api';
12
+ * import { withSoftDelete } from '@classytic/arc-next/presets/soft-delete';
13
+ *
14
+ * const todos = withSoftDelete(createCrudApi<Todo>('todos'));
15
+ * await todos.delete({ id }); // soft delete (backend)
16
+ * await todos.restore({ id }); // undo
17
+ * const trash = await todos.getDeleted();
18
+ */
19
+ function withSoftDelete(api) {
20
+ return Object.assign(api, {
21
+ async getDeleted({ token = null, organizationId = null, params = {}, options = {} } = {}) {
22
+ const merged = {
23
+ ...api.config.defaultParams,
24
+ ...params
25
+ };
26
+ return api.request("GET", `${api.baseUrl}/deleted`, {
27
+ token,
28
+ organizationId,
29
+ params: merged,
30
+ options
31
+ });
32
+ },
33
+ async restore({ token = null, organizationId = null, id, options = {} }) {
34
+ if (!id) throw new Error("ID is required");
35
+ return api.request("POST", `${api.baseUrl}/${id}/restore`, {
36
+ token,
37
+ organizationId,
38
+ options
39
+ });
40
+ }
41
+ });
42
+ }
43
+
44
+ //#endregion
45
+ export { withSoftDelete };
@@ -0,0 +1,32 @@
1
+ import { BaseApi, QueryParams, ScopedArgs } from "../api.js";
2
+ import { PaginatedResult } from "@classytic/repo-core/pagination";
3
+
4
+ //#region src/presets/tree.d.ts
5
+ interface TreeMethods<TDoc> {
6
+ /** Fetch the full hierarchy. Backend mounts `GET /:resource/tree`. */
7
+ getTree(args?: ScopedArgs & {
8
+ params?: QueryParams;
9
+ }): Promise<TDoc[]>;
10
+ /** Fetch direct children of a node. Backend mounts `GET /:resource/:id/children`. */
11
+ getChildren(args: ScopedArgs & {
12
+ parentId: string;
13
+ params?: QueryParams;
14
+ }): Promise<PaginatedResult<TDoc>>;
15
+ }
16
+ /**
17
+ * Adds tree preset methods to a BaseApi.
18
+ *
19
+ * Mirrors arc's `tree` preset — for resources with a `parentId` field, exposes
20
+ * `getTree` (full hierarchy) and `getChildren` (one level deep). Backend caps
21
+ * tree depth via the preset config.
22
+ *
23
+ * @example
24
+ * import { withTree } from '@classytic/arc-next/presets/tree';
25
+ * const categories = withTree(createCrudApi<Category>('categories'));
26
+ *
27
+ * const root = await categories.getTree();
28
+ * const kids = await categories.getChildren({ parentId: 'engineering' });
29
+ */
30
+ declare function withTree<TDoc, TCreate, TUpdate>(api: BaseApi<TDoc, TCreate, TUpdate>): BaseApi<TDoc, TCreate, TUpdate> & TreeMethods<TDoc>;
31
+ //#endregion
32
+ export { TreeMethods, withTree };
@@ -0,0 +1,47 @@
1
+ //#region src/presets/tree.ts
2
+ /**
3
+ * Adds tree preset methods to a BaseApi.
4
+ *
5
+ * Mirrors arc's `tree` preset — for resources with a `parentId` field, exposes
6
+ * `getTree` (full hierarchy) and `getChildren` (one level deep). Backend caps
7
+ * tree depth via the preset config.
8
+ *
9
+ * @example
10
+ * import { withTree } from '@classytic/arc-next/presets/tree';
11
+ * const categories = withTree(createCrudApi<Category>('categories'));
12
+ *
13
+ * const root = await categories.getTree();
14
+ * const kids = await categories.getChildren({ parentId: 'engineering' });
15
+ */
16
+ function withTree(api) {
17
+ return Object.assign(api, {
18
+ async getTree({ token = null, organizationId = null, params = {}, options = {} } = {}) {
19
+ const merged = {
20
+ ...api.config.defaultParams,
21
+ ...params
22
+ };
23
+ return api.request("GET", `${api.baseUrl}/tree`, {
24
+ token,
25
+ organizationId,
26
+ params: merged,
27
+ options
28
+ });
29
+ },
30
+ async getChildren({ token = null, organizationId = null, parentId, params = {}, options = {} }) {
31
+ if (!parentId) throw new Error("Parent ID is required");
32
+ const merged = {
33
+ ...api.config.defaultParams,
34
+ ...params
35
+ };
36
+ return api.request("GET", `${api.baseUrl}/${parentId}/children`, {
37
+ token,
38
+ organizationId,
39
+ params: merged,
40
+ options
41
+ });
42
+ }
43
+ });
44
+ }
45
+
46
+ //#endregion
47
+ export { withTree };