@classytic/arc-next 0.5.0 → 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/README.md +7 -5
- package/dist/api.d.ts +59 -75
- package/dist/api.js +25 -6
- package/dist/cache.d.ts +20 -3
- package/dist/cache.js +31 -17
- package/dist/client.d.ts +64 -69
- package/dist/client.js +115 -82
- package/dist/hooks.d.ts +72 -8
- package/dist/hooks.js +54 -8
- package/dist/prefetch.d.ts +23 -0
- package/dist/prefetch.js +20 -0
- package/dist/presets/bulk.d.ts +5 -4
- package/dist/presets/search.d.ts +5 -4
- package/dist/presets/slug.d.ts +2 -2
- package/dist/presets/soft-delete.d.ts +4 -3
- package/dist/presets/tree.d.ts +4 -3
- package/dist/query.d.ts +20 -20
- package/dist/query.js +12 -22
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -97,19 +97,19 @@ All mutations are optimistic with automatic rollback on error. Lists prefill the
|
|
|
97
97
|
|
|
98
98
|
### `useApiQuery` — non-CRUD reads
|
|
99
99
|
|
|
100
|
-
For reports, aggregates, RPC-style endpoints.
|
|
100
|
+
For reports, aggregates, RPC-style endpoints. Response IS the data — arc 2.13+ has no envelope:
|
|
101
101
|
|
|
102
102
|
```ts
|
|
103
103
|
import { useApiQuery } from "@classytic/arc-next/query";
|
|
104
104
|
|
|
105
|
-
const { data, isLoading } = useApiQuery<
|
|
105
|
+
const { data, isLoading } = useApiQuery<DashboardStats>({
|
|
106
106
|
queryKey: ["dashboard", "stats"],
|
|
107
107
|
queryFn: ({ signal }) => api.request("GET", "/dashboard/stats", { options: { signal } }),
|
|
108
108
|
freshness: "realtime", // 'realtime' | 'frequent' | 'stable' | 'static'
|
|
109
109
|
});
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
Pass a custom `select` to
|
|
112
|
+
Pass a custom `select` to project a sub-field from the response.
|
|
113
113
|
|
|
114
114
|
## Actions & Custom Routes
|
|
115
115
|
|
|
@@ -126,14 +126,16 @@ const stats = await api.invokeRoute<{ data: { total: number } }>({
|
|
|
126
126
|
method: "GET",
|
|
127
127
|
path: "/stats",
|
|
128
128
|
});
|
|
129
|
-
|
|
129
|
+
import type { OffsetPaginationResult } from "@classytic/repo-core/pagination";
|
|
130
|
+
|
|
131
|
+
const recent = await api.invokeRoute<OffsetPaginationResult<Todo>>({
|
|
130
132
|
method: "GET",
|
|
131
133
|
path: "/recent",
|
|
132
134
|
params: { limit: 5 },
|
|
133
135
|
});
|
|
134
136
|
```
|
|
135
137
|
|
|
136
|
-
The `useAction` hook (returned from `createCrudHooks`) wraps `api.dispatchAction()` with toast + invalidation. For custom GETs, compose `api.invokeRoute()` with `useApiQuery` —
|
|
138
|
+
The `useAction` hook (returned from `createCrudHooks`) wraps `api.dispatchAction()` with toast + invalidation. For custom GETs, compose `api.invokeRoute()` with `useApiQuery` — the response IS the data (no envelope since arc 2.13):
|
|
137
139
|
|
|
138
140
|
```ts
|
|
139
141
|
const { data } = useApiQuery({
|
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { ArcClient } from "./client.js";
|
|
2
|
+
import { AggregatePaginationResult, KeysetPaginationResult, OffsetPaginationResult, PaginatedResult } from "@classytic/repo-core/pagination";
|
|
3
|
+
import { AggResult, AggRow, BulkCreateResult, DeleteManyResult, DeleteResult, UpdateManyResult } from "@classytic/repo-core/repository";
|
|
4
|
+
import { BracketOperator, BracketOperator as BracketOperator$1 } from "@classytic/repo-core/query-parser";
|
|
2
5
|
|
|
3
6
|
//#region src/api.d.ts
|
|
4
7
|
interface PopulateOption {
|
|
@@ -6,68 +9,24 @@ interface PopulateOption {
|
|
|
6
9
|
select?: string;
|
|
7
10
|
match?: Record<string, unknown>;
|
|
8
11
|
}
|
|
9
|
-
interface ApiResponse<T = unknown> {
|
|
10
|
-
success: boolean;
|
|
11
|
-
data?: T;
|
|
12
|
-
message?: string;
|
|
13
|
-
}
|
|
14
|
-
interface OffsetPaginationResponse<T = unknown> {
|
|
15
|
-
success: boolean;
|
|
16
|
-
method: 'offset';
|
|
17
|
-
docs: T[];
|
|
18
|
-
page: number;
|
|
19
|
-
limit: number;
|
|
20
|
-
total: number;
|
|
21
|
-
pages: number;
|
|
22
|
-
hasNext: boolean;
|
|
23
|
-
hasPrev: boolean;
|
|
24
|
-
warning?: string;
|
|
25
|
-
}
|
|
26
|
-
interface KeysetPaginationResponse<T = unknown> {
|
|
27
|
-
success: boolean;
|
|
28
|
-
method: 'keyset';
|
|
29
|
-
docs: T[];
|
|
30
|
-
limit: number;
|
|
31
|
-
hasMore: boolean;
|
|
32
|
-
next: string | null;
|
|
33
|
-
}
|
|
34
|
-
interface AggregatePaginationResponse<T = unknown> {
|
|
35
|
-
success: boolean;
|
|
36
|
-
method: 'aggregate';
|
|
37
|
-
docs: T[];
|
|
38
|
-
page: number;
|
|
39
|
-
limit: number;
|
|
40
|
-
total: number;
|
|
41
|
-
pages: number;
|
|
42
|
-
hasNext: boolean;
|
|
43
|
-
hasPrev: boolean;
|
|
44
|
-
warning?: string;
|
|
45
|
-
}
|
|
46
|
-
type PaginatedResponse<T = unknown> = OffsetPaginationResponse<T> | KeysetPaginationResponse<T> | AggregatePaginationResponse<T>;
|
|
47
|
-
interface DeleteResponse {
|
|
48
|
-
success: boolean;
|
|
49
|
-
data?: {
|
|
50
|
-
message?: string;
|
|
51
|
-
id?: string;
|
|
52
|
-
soft?: boolean;
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
interface BulkCreateResponse<T = unknown> {
|
|
56
|
-
success: boolean;
|
|
57
|
-
data?: T[];
|
|
58
|
-
count?: number;
|
|
59
|
-
}
|
|
60
|
-
interface BulkUpdateResponse {
|
|
61
|
-
success: boolean;
|
|
62
|
-
modifiedCount?: number;
|
|
63
|
-
}
|
|
64
|
-
interface BulkDeleteResponse {
|
|
65
|
-
success: boolean;
|
|
66
|
-
deletedCount?: number;
|
|
67
|
-
}
|
|
68
12
|
type SortDirection = 1 | -1 | 'asc' | 'desc';
|
|
69
13
|
type SortSpec = Record<string, SortDirection> | string;
|
|
70
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Filter operators supported by arc-next URL emission.
|
|
16
|
+
*
|
|
17
|
+
* Composes:
|
|
18
|
+
* - **Canonical** ({@link BracketOperator}) — every operator repo-core's
|
|
19
|
+
* `parseUrl` reverses. Cross-kit portable: mongokit, sqlitekit, prismakit,
|
|
20
|
+
* and any future kit that consumes the canonical Filter IR all support
|
|
21
|
+
* these out of the box.
|
|
22
|
+
* - **Driver-specific extensions** — operators that require kit-native
|
|
23
|
+
* support. Geo (`near`, `nearSphere`, `geoWithin`, `withinRadius`) is
|
|
24
|
+
* mongokit + sqlitekit-spatialite. `size` / `type` are mongokit
|
|
25
|
+
* array/BSON helpers. Hosts using kits without these features just
|
|
26
|
+
* don't emit them; the union stays open with `(string & {})` so custom
|
|
27
|
+
* domain operators still satisfy the type.
|
|
28
|
+
*/
|
|
29
|
+
type FilterOperator = BracketOperator$1 | 'size' | 'type' | 'near' | 'nearSphere' | 'geoWithin' | 'withinRadius' | (string & {});
|
|
71
30
|
interface QueryParams {
|
|
72
31
|
page?: number;
|
|
73
32
|
limit?: number;
|
|
@@ -140,7 +99,7 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
|
|
|
140
99
|
organizationId?: string | null;
|
|
141
100
|
params?: QueryParams;
|
|
142
101
|
options?: Omit<RequestOptions, 'token' | 'organizationId'>;
|
|
143
|
-
}): Promise<
|
|
102
|
+
}): Promise<PaginatedResult<TDoc>>;
|
|
144
103
|
getById({
|
|
145
104
|
token,
|
|
146
105
|
organizationId,
|
|
@@ -156,7 +115,7 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
|
|
|
156
115
|
populate?: string | string[];
|
|
157
116
|
};
|
|
158
117
|
options?: Omit<RequestOptions, 'token' | 'organizationId'>;
|
|
159
|
-
}): Promise<
|
|
118
|
+
}): Promise<TDoc>;
|
|
160
119
|
create({
|
|
161
120
|
token,
|
|
162
121
|
organizationId,
|
|
@@ -167,7 +126,7 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
|
|
|
167
126
|
organizationId?: string | null;
|
|
168
127
|
data: TCreate;
|
|
169
128
|
options?: Omit<RequestOptions, 'token' | 'organizationId'>;
|
|
170
|
-
}): Promise<
|
|
129
|
+
}): Promise<TDoc>;
|
|
171
130
|
update({
|
|
172
131
|
token,
|
|
173
132
|
organizationId,
|
|
@@ -180,7 +139,7 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
|
|
|
180
139
|
id: string;
|
|
181
140
|
data: TUpdate;
|
|
182
141
|
options?: Omit<RequestOptions, 'token' | 'organizationId'>;
|
|
183
|
-
}): Promise<
|
|
142
|
+
}): Promise<TDoc>;
|
|
184
143
|
delete({
|
|
185
144
|
token,
|
|
186
145
|
organizationId,
|
|
@@ -191,7 +150,7 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
|
|
|
191
150
|
organizationId?: string | null;
|
|
192
151
|
id: string;
|
|
193
152
|
options?: Omit<RequestOptions, 'token' | 'organizationId'>;
|
|
194
|
-
}): Promise<
|
|
153
|
+
}): Promise<DeleteResult>;
|
|
195
154
|
upload({
|
|
196
155
|
token,
|
|
197
156
|
organizationId,
|
|
@@ -203,7 +162,7 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
|
|
|
203
162
|
data: FormData; /** Resource ID — shorthand for path, appended as `baseUrl/{id}/upload` */
|
|
204
163
|
id?: string; /** Custom sub-path appended as `baseUrl/{path}`. Takes precedence over `id`. */
|
|
205
164
|
path?: string;
|
|
206
|
-
}): Promise<
|
|
165
|
+
}): Promise<TDoc>;
|
|
207
166
|
request<TResponse = unknown>(method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE', endpoint: string, {
|
|
208
167
|
token,
|
|
209
168
|
organizationId,
|
|
@@ -223,17 +182,18 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
|
|
|
223
182
|
* Arc's escape hatch for endpoints that don't fit CRUD or actions.
|
|
224
183
|
*
|
|
225
184
|
* For aggregates / reports, prefer the response-aware {@link useApiQuery} hook
|
|
226
|
-
*
|
|
185
|
+
* and pass `invokeRoute` as the queryFn — arc 2.13+ emits raw payloads, so
|
|
186
|
+
* the response IS the data.
|
|
227
187
|
*
|
|
228
188
|
* @example
|
|
229
|
-
* // GET /todos/stats → {
|
|
189
|
+
* // GET /todos/stats → { total, byStatus }
|
|
230
190
|
* const stats = await api.invokeRoute<{ total: number; byStatus: Record<string, number> }>({
|
|
231
191
|
* method: 'GET',
|
|
232
192
|
* path: '/stats',
|
|
233
193
|
* });
|
|
234
194
|
*
|
|
235
195
|
* // GET /todos/recent?limit=5 → paginated shape spread to root
|
|
236
|
-
* const recent = await api.invokeRoute<
|
|
196
|
+
* const recent = await api.invokeRoute<PaginatedResult<Todo>>({
|
|
237
197
|
* method: 'GET',
|
|
238
198
|
* path: '/recent',
|
|
239
199
|
* params: { limit: 5 },
|
|
@@ -260,6 +220,30 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
|
|
|
260
220
|
data?: unknown;
|
|
261
221
|
params?: QueryParams;
|
|
262
222
|
}): Promise<TResponse>;
|
|
223
|
+
/**
|
|
224
|
+
* Fetch a declared aggregation by name.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* const { rows } = await api.aggregate<{ day: string; total: number }>({
|
|
228
|
+
* name: 'salesByDay',
|
|
229
|
+
* filter: { from: '2025-01-01', to: '2025-12-31' },
|
|
230
|
+
* });
|
|
231
|
+
*/
|
|
232
|
+
aggregate<TRow extends AggRow = AggRow>({
|
|
233
|
+
token,
|
|
234
|
+
organizationId,
|
|
235
|
+
name,
|
|
236
|
+
filter,
|
|
237
|
+
options
|
|
238
|
+
}: ScopedArgs & {
|
|
239
|
+
/** Aggregation name as declared on the resource. */name: string;
|
|
240
|
+
/**
|
|
241
|
+
* URL-encoded filter narrows + dimension args. Reserved keys (`page`,
|
|
242
|
+
* `limit`, etc.) are stripped server-side; everything else flows into
|
|
243
|
+
* the AggRequest filter via shallow merge with the host's base filter.
|
|
244
|
+
*/
|
|
245
|
+
filter?: Record<string, unknown>;
|
|
246
|
+
}): Promise<AggResult<TRow>>;
|
|
263
247
|
dispatchAction<TResult = unknown, TBody extends Record<string, unknown> = Record<string, unknown>>({
|
|
264
248
|
token,
|
|
265
249
|
organizationId,
|
|
@@ -271,12 +255,12 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
|
|
|
271
255
|
id: string;
|
|
272
256
|
action: string;
|
|
273
257
|
data?: TBody;
|
|
274
|
-
}): Promise<
|
|
258
|
+
}): Promise<TResult>;
|
|
275
259
|
}
|
|
276
260
|
declare function createCrudApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, TUpdate = Partial<TDoc>>(entity: string, config?: BaseApiConfig): BaseApi<TDoc, TCreate, TUpdate>;
|
|
277
|
-
type ExtractDoc<T> = T extends
|
|
278
|
-
declare function isOffsetPagination<T>(response:
|
|
279
|
-
declare function isKeysetPagination<T>(response:
|
|
280
|
-
declare function isAggregatePagination<T>(response:
|
|
261
|
+
type ExtractDoc<T> = T extends PaginatedResult<infer D> ? D : never;
|
|
262
|
+
declare function isOffsetPagination<T>(response: PaginatedResult<T>): response is OffsetPaginationResult<T>;
|
|
263
|
+
declare function isKeysetPagination<T>(response: PaginatedResult<T>): response is KeysetPaginationResult<T>;
|
|
264
|
+
declare function isAggregatePagination<T>(response: PaginatedResult<T>): response is AggregatePaginationResult<T>;
|
|
281
265
|
//#endregion
|
|
282
|
-
export {
|
|
266
|
+
export { type AggResult, type AggRow, BaseApi, BaseApiConfig, type BracketOperator, type BulkCreateResult, type DeleteManyResult, type DeleteResult, ExtractDoc, FilterOperator, PopulateOption, QueryParams, RequestOptions, ScopedArgs, SortDirection, SortSpec, type UpdateManyResult, createCrudApi, isAggregatePagination, isKeysetPagination, isOffsetPagination };
|
package/dist/api.js
CHANGED
|
@@ -158,17 +158,18 @@ var BaseApi = class {
|
|
|
158
158
|
* Arc's escape hatch for endpoints that don't fit CRUD or actions.
|
|
159
159
|
*
|
|
160
160
|
* For aggregates / reports, prefer the response-aware {@link useApiQuery} hook
|
|
161
|
-
*
|
|
161
|
+
* and pass `invokeRoute` as the queryFn — arc 2.13+ emits raw payloads, so
|
|
162
|
+
* the response IS the data.
|
|
162
163
|
*
|
|
163
164
|
* @example
|
|
164
|
-
* // GET /todos/stats → {
|
|
165
|
+
* // GET /todos/stats → { total, byStatus }
|
|
165
166
|
* const stats = await api.invokeRoute<{ total: number; byStatus: Record<string, number> }>({
|
|
166
167
|
* method: 'GET',
|
|
167
168
|
* path: '/stats',
|
|
168
169
|
* });
|
|
169
170
|
*
|
|
170
171
|
* // GET /todos/recent?limit=5 → paginated shape spread to root
|
|
171
|
-
* const recent = await api.invokeRoute<
|
|
172
|
+
* const recent = await api.invokeRoute<PaginatedResult<Todo>>({
|
|
172
173
|
* method: 'GET',
|
|
173
174
|
* path: '/recent',
|
|
174
175
|
* params: { limit: 5 },
|
|
@@ -193,6 +194,24 @@ var BaseApi = class {
|
|
|
193
194
|
options
|
|
194
195
|
});
|
|
195
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Fetch a declared aggregation by name.
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* const { rows } = await api.aggregate<{ day: string; total: number }>({
|
|
202
|
+
* name: 'salesByDay',
|
|
203
|
+
* filter: { from: '2025-01-01', to: '2025-12-31' },
|
|
204
|
+
* });
|
|
205
|
+
*/
|
|
206
|
+
async aggregate({ token = null, organizationId = null, name, filter, options = {} }) {
|
|
207
|
+
if (!name) throw new Error("Aggregation name is required");
|
|
208
|
+
const queryString = filter ? this.createQueryString(filter) : "";
|
|
209
|
+
const endpoint = `${this.baseUrl}/aggregations/${name}${queryString ? `?${queryString}` : ""}`;
|
|
210
|
+
const requestOptions = { ...options };
|
|
211
|
+
if (token) requestOptions.token = token;
|
|
212
|
+
if (organizationId) requestOptions.organizationId = organizationId;
|
|
213
|
+
return this.requestFn("GET", endpoint, this.withHeaders(requestOptions));
|
|
214
|
+
}
|
|
196
215
|
async dispatchAction({ token = null, organizationId = null, id, action, data, options = {} }) {
|
|
197
216
|
if (!id) throw new Error("ID is required");
|
|
198
217
|
if (!action) throw new Error("Action name is required");
|
|
@@ -212,13 +231,13 @@ function createCrudApi(entity, config = {}) {
|
|
|
212
231
|
return new BaseApi(entity, config);
|
|
213
232
|
}
|
|
214
233
|
function isOffsetPagination(response) {
|
|
215
|
-
return response.method === "offset";
|
|
234
|
+
return "method" in response && response.method === "offset";
|
|
216
235
|
}
|
|
217
236
|
function isKeysetPagination(response) {
|
|
218
|
-
return response.method === "keyset";
|
|
237
|
+
return "method" in response && response.method === "keyset";
|
|
219
238
|
}
|
|
220
239
|
function isAggregatePagination(response) {
|
|
221
|
-
return response.method === "aggregate";
|
|
240
|
+
return "method" in response && response.method === "aggregate";
|
|
222
241
|
}
|
|
223
242
|
|
|
224
243
|
//#endregion
|
package/dist/cache.d.ts
CHANGED
|
@@ -53,9 +53,10 @@ declare function normalizePagination(data: unknown): PaginationData | null;
|
|
|
53
53
|
*/
|
|
54
54
|
declare function extractItems<T>(data: unknown): T[];
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
56
|
+
* Detail extractor. Arc emits the doc directly (no envelope wrapper) — this
|
|
57
|
+
* function is identity-with-null-guard. Kept as a named helper so callers
|
|
58
|
+
* have a stable seam if a future backend ever ships an envelope, and so
|
|
59
|
+
* `null` / `undefined` responses normalize to `null` consistently.
|
|
59
60
|
*/
|
|
60
61
|
declare function extractItem<T>(data: unknown): T | null;
|
|
61
62
|
/**
|
|
@@ -74,6 +75,14 @@ interface QueryKeys {
|
|
|
74
75
|
scopedDetail: (id: string, organizationId: string | null) => QueryKey;
|
|
75
76
|
custom: (key: string, ...args: unknown[]) => QueryKey;
|
|
76
77
|
scopedList: (scope: string, params?: unknown) => QueryKey;
|
|
78
|
+
/** Prefix for every aggregation on this resource — invalidate all at once. */
|
|
79
|
+
aggregations: () => QueryKey;
|
|
80
|
+
/**
|
|
81
|
+
* Aggregation key (`arc 2.13+ /aggregations/:name`). The `filter` arg is
|
|
82
|
+
* structurally hashed by TanStack — pass the same object identity (or
|
|
83
|
+
* structurally identical) you pass to `useAggregation` so the cache hits.
|
|
84
|
+
*/
|
|
85
|
+
aggregation: (name: string, filter?: unknown) => QueryKey;
|
|
77
86
|
}
|
|
78
87
|
/**
|
|
79
88
|
* Build a hierarchical query-key factory for a resource. The returned shape
|
|
@@ -97,6 +106,14 @@ interface CacheUtils<T> {
|
|
|
97
106
|
getScopedDetail: (client: QueryClient, id: string, organizationId: string | null) => T | undefined;
|
|
98
107
|
/** Remove tenant-scoped detail from cache. */
|
|
99
108
|
removeScopedDetail: (client: QueryClient, id: string, organizationId: string | null) => void;
|
|
109
|
+
/**
|
|
110
|
+
* Invalidate every aggregation for this resource. Call from mutation
|
|
111
|
+
* `onSuccess` so dashboards refresh after CRUD writes.
|
|
112
|
+
*
|
|
113
|
+
* For targeted invalidation of a single aggregation pass the name —
|
|
114
|
+
* prefix-matches every parameterized variant.
|
|
115
|
+
*/
|
|
116
|
+
invalidateAggregations: (client: QueryClient, name?: string) => Promise<void>;
|
|
100
117
|
}
|
|
101
118
|
/**
|
|
102
119
|
* Build cache read/write/invalidate helpers bound to the given key factory.
|
package/dist/cache.js
CHANGED
|
@@ -7,28 +7,32 @@ const DEFAULT_QUERY_CONFIG = {
|
|
|
7
7
|
};
|
|
8
8
|
/** Pre-built query config presets for common data freshness patterns. */
|
|
9
9
|
const QUERY_CONFIGS = {
|
|
10
|
+
/** Live data: 20s stale, 30s polling */
|
|
10
11
|
realtime: {
|
|
11
12
|
staleTime: 2e4,
|
|
12
13
|
refetchInterval: 3e4
|
|
13
14
|
},
|
|
15
|
+
/** Frequently updated: 60s stale */
|
|
14
16
|
frequent: { staleTime: 6e4 },
|
|
17
|
+
/** Stable data: 5min stale (same as default) */
|
|
15
18
|
stable: { staleTime: 3e5 },
|
|
19
|
+
/** Rarely changes: 10min stale */
|
|
16
20
|
static: { staleTime: 6e5 }
|
|
17
21
|
};
|
|
18
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Well-known keys checked in order for list responses.
|
|
24
|
+
*
|
|
25
|
+
* Arc emits `{data: T[]}` for both paginated and bare-list endpoints, so
|
|
26
|
+
* `docs` is the canonical key. `items` / `results` cover non-arc backends
|
|
27
|
+
* the permissive detector still supports — the any-array fallback below
|
|
28
|
+
* keeps `{products: [...]}` / `{users: [...]}` working without per-resource
|
|
29
|
+
* configuration.
|
|
30
|
+
*/
|
|
19
31
|
const LIST_KEYS = [
|
|
20
|
-
"docs",
|
|
21
32
|
"data",
|
|
22
33
|
"items",
|
|
23
34
|
"results"
|
|
24
35
|
];
|
|
25
|
-
/** Well-known keys checked in order for detail responses. */
|
|
26
|
-
const DETAIL_KEYS = [
|
|
27
|
-
"data",
|
|
28
|
-
"doc",
|
|
29
|
-
"item",
|
|
30
|
-
"result"
|
|
31
|
-
];
|
|
32
36
|
/**
|
|
33
37
|
* Extract `_id` or `id` from any item. Returns `null` if neither exists.
|
|
34
38
|
* Coerces numeric IDs to strings so cache keys stay consistent.
|
|
@@ -78,16 +82,14 @@ function extractItems(data) {
|
|
|
78
82
|
return [];
|
|
79
83
|
}
|
|
80
84
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
85
|
+
* Detail extractor. Arc emits the doc directly (no envelope wrapper) — this
|
|
86
|
+
* function is identity-with-null-guard. Kept as a named helper so callers
|
|
87
|
+
* have a stable seam if a future backend ever ships an envelope, and so
|
|
88
|
+
* `null` / `undefined` responses normalize to `null` consistently.
|
|
84
89
|
*/
|
|
85
90
|
function extractItem(data) {
|
|
86
91
|
if (data == null) return null;
|
|
87
|
-
|
|
88
|
-
const d = data;
|
|
89
|
-
for (const key of DETAIL_KEYS) if (d[key] != null) return d[key];
|
|
90
|
-
return d;
|
|
92
|
+
return data;
|
|
91
93
|
}
|
|
92
94
|
/**
|
|
93
95
|
* Optimistic-update helper that mutates the items array of a list cache
|
|
@@ -166,6 +168,17 @@ function createQueryKeys(entityKey) {
|
|
|
166
168
|
_scope: scope,
|
|
167
169
|
...params
|
|
168
170
|
}
|
|
171
|
+
],
|
|
172
|
+
aggregations: () => [entityKey, "aggregation"],
|
|
173
|
+
aggregation: (name, filter) => filter !== void 0 ? [
|
|
174
|
+
entityKey,
|
|
175
|
+
"aggregation",
|
|
176
|
+
name,
|
|
177
|
+
filter
|
|
178
|
+
] : [
|
|
179
|
+
entityKey,
|
|
180
|
+
"aggregation",
|
|
181
|
+
name
|
|
169
182
|
]
|
|
170
183
|
};
|
|
171
184
|
}
|
|
@@ -189,7 +202,8 @@ function createCacheUtils(KEYS) {
|
|
|
189
202
|
getScopedDetail: (client, id, organizationId) => {
|
|
190
203
|
return client.getQueryData(KEYS.scopedDetail(id, organizationId))?.data;
|
|
191
204
|
},
|
|
192
|
-
removeScopedDetail: (client, id, organizationId) => client.removeQueries({ queryKey: KEYS.scopedDetail(id, organizationId) })
|
|
205
|
+
removeScopedDetail: (client, id, organizationId) => client.removeQueries({ queryKey: KEYS.scopedDetail(id, organizationId) }),
|
|
206
|
+
invalidateAggregations: (client, name) => client.invalidateQueries({ queryKey: name ? KEYS.aggregation(name) : KEYS.aggregations() })
|
|
193
207
|
};
|
|
194
208
|
}
|
|
195
209
|
|