@classytic/arc-next 0.9.1 → 0.11.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 +14 -0
- package/dist/api.d.ts +44 -0
- package/dist/api.js +66 -0
- package/dist/cache.d.ts +15 -1
- package/dist/cache.js +21 -1
- package/dist/client.d.ts +40 -1
- package/dist/client.js +30 -1
- package/dist/field-encryption.d.ts +86 -0
- package/dist/field-encryption.js +159 -0
- package/dist/hooks.d.ts +15 -1
- package/dist/hooks.js +38 -28
- package/dist/mutation.js +4 -2
- package/dist/prefetch.d.ts +23 -62
- package/dist/prefetch.js +25 -103
- package/dist/presets/history.d.ts +56 -0
- package/dist/presets/history.js +29 -0
- package/dist/query-client.js +2 -1
- package/dist/query-options.d.ts +165 -0
- package/dist/query-options.js +188 -0
- package/llms.txt +22 -0
- package/package.json +178 -165
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { QueryKeys } from "./cache.js";
|
|
2
|
+
import * as _$_tanstack_react_query0 from "@tanstack/react-query";
|
|
3
|
+
|
|
4
|
+
//#region src/query-options.d.ts
|
|
5
|
+
/** Per-call request context: auth + Next.js fetch caching passthrough. */
|
|
6
|
+
interface QueryFnContext {
|
|
7
|
+
/** Bearer token for protected endpoints (required server-side for non-public reads). */
|
|
8
|
+
token?: string | null;
|
|
9
|
+
/** Organization ID for multi-tenant reads. Also becomes part of the cache key. */
|
|
10
|
+
organizationId?: string | null;
|
|
11
|
+
/** Extra headers (e.g. x-api-key). */
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
/** Next.js fetch caching forwarded to the API call (ISR-friendly prefetch). */
|
|
14
|
+
cache?: RequestCache;
|
|
15
|
+
revalidate?: number | false;
|
|
16
|
+
tags?: string[];
|
|
17
|
+
}
|
|
18
|
+
type ForwardedApiOptions = {
|
|
19
|
+
headerOptions?: Record<string, string>;
|
|
20
|
+
cache?: RequestCache;
|
|
21
|
+
revalidate?: number | false;
|
|
22
|
+
tags?: string[];
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
};
|
|
25
|
+
/** Structural read-API contract (BaseApi satisfies this; presets add the optionals). */
|
|
26
|
+
interface EntityReadApi {
|
|
27
|
+
getAll: (opts: {
|
|
28
|
+
params?: Record<string, unknown>;
|
|
29
|
+
token?: string | null;
|
|
30
|
+
organizationId?: string | null;
|
|
31
|
+
options?: ForwardedApiOptions;
|
|
32
|
+
}) => Promise<unknown>;
|
|
33
|
+
getById: (opts: {
|
|
34
|
+
id: string;
|
|
35
|
+
token?: string | null;
|
|
36
|
+
organizationId?: string | null;
|
|
37
|
+
params?: Record<string, unknown>;
|
|
38
|
+
options?: ForwardedApiOptions;
|
|
39
|
+
}) => Promise<unknown>;
|
|
40
|
+
getBySlug?: (opts: {
|
|
41
|
+
slug: string;
|
|
42
|
+
token?: string | null;
|
|
43
|
+
organizationId?: string | null;
|
|
44
|
+
params?: Record<string, unknown>;
|
|
45
|
+
options?: ForwardedApiOptions;
|
|
46
|
+
}) => Promise<unknown>;
|
|
47
|
+
getDeleted?: (opts: {
|
|
48
|
+
params?: Record<string, unknown>;
|
|
49
|
+
token?: string | null;
|
|
50
|
+
organizationId?: string | null;
|
|
51
|
+
options?: ForwardedApiOptions;
|
|
52
|
+
}) => Promise<unknown>;
|
|
53
|
+
getTree?: (opts: {
|
|
54
|
+
params?: Record<string, unknown>;
|
|
55
|
+
token?: string | null;
|
|
56
|
+
organizationId?: string | null;
|
|
57
|
+
options?: ForwardedApiOptions;
|
|
58
|
+
}) => Promise<unknown>;
|
|
59
|
+
getChildren?: (opts: {
|
|
60
|
+
parentId: string;
|
|
61
|
+
params?: Record<string, unknown>;
|
|
62
|
+
token?: string | null;
|
|
63
|
+
organizationId?: string | null;
|
|
64
|
+
options?: ForwardedApiOptions;
|
|
65
|
+
}) => Promise<unknown>;
|
|
66
|
+
aggregate?: (opts: {
|
|
67
|
+
name: string;
|
|
68
|
+
filter?: Record<string, unknown>;
|
|
69
|
+
token?: string | null;
|
|
70
|
+
organizationId?: string | null;
|
|
71
|
+
options?: ForwardedApiOptions;
|
|
72
|
+
}) => Promise<unknown>;
|
|
73
|
+
}
|
|
74
|
+
interface DetailQueryOpts extends QueryFnContext {
|
|
75
|
+
/** Query params (select, populate) — becomes part of the key, matching useDetail. */
|
|
76
|
+
params?: {
|
|
77
|
+
select?: string;
|
|
78
|
+
populate?: string | string[];
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
type EntityQueries = ReturnType<typeof createEntityQueries>;
|
|
82
|
+
/**
|
|
83
|
+
* Build queryOptions factories for one entity. Server-safe; keys are
|
|
84
|
+
* hash-identical to the corresponding `createCrudHooks` hooks.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* // queries/products.ts — colocate next to the api definition
|
|
88
|
+
* export const productQueries = createEntityQueries(productApi, 'products');
|
|
89
|
+
*
|
|
90
|
+
* // RSC / route loader
|
|
91
|
+
* await queryClient.ensureQueryData(productQueries.detail(id, { token }));
|
|
92
|
+
*/
|
|
93
|
+
declare function createEntityQueries(api: EntityReadApi, entityKey: string): {
|
|
94
|
+
/** The entity's key factory — for invalidation / setQueryData at call sites. */keys: QueryKeys; /** GET /:resource — mirrors `useList`'s key (scoped, org-normalized). */
|
|
95
|
+
list(params?: Record<string, unknown>, ctx?: QueryFnContext): _$_tanstack_react_query0.OmitKeyof<_$_tanstack_react_query0.UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
|
|
96
|
+
queryFn?: _$_tanstack_react_query0.QueryFunction<unknown, readonly unknown[], never> | undefined;
|
|
97
|
+
} & {
|
|
98
|
+
queryKey: readonly unknown[] & {
|
|
99
|
+
[dataTagSymbol]: unknown;
|
|
100
|
+
[dataTagErrorSymbol]: Error;
|
|
101
|
+
};
|
|
102
|
+
}; /** GET /:resource/:id — mirrors `useDetail`'s scoped key (+ params variant). */
|
|
103
|
+
detail(id: string, opts?: DetailQueryOpts): _$_tanstack_react_query0.OmitKeyof<_$_tanstack_react_query0.UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
|
|
104
|
+
queryFn?: _$_tanstack_react_query0.QueryFunction<unknown, readonly unknown[], never> | undefined;
|
|
105
|
+
} & {
|
|
106
|
+
queryKey: readonly unknown[] & {
|
|
107
|
+
[dataTagSymbol]: unknown;
|
|
108
|
+
[dataTagErrorSymbol]: Error;
|
|
109
|
+
};
|
|
110
|
+
}; /** GET /:resource/slug/:slug — mirrors `useDetailBySlug`'s key. */
|
|
111
|
+
bySlug(slug: string, opts?: DetailQueryOpts): _$_tanstack_react_query0.OmitKeyof<_$_tanstack_react_query0.UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
|
|
112
|
+
queryFn?: _$_tanstack_react_query0.QueryFunction<unknown, readonly unknown[], never> | undefined;
|
|
113
|
+
} & {
|
|
114
|
+
queryKey: readonly unknown[] & {
|
|
115
|
+
[dataTagSymbol]: unknown;
|
|
116
|
+
[dataTagErrorSymbol]: Error;
|
|
117
|
+
};
|
|
118
|
+
}; /** GET /:resource/deleted — mirrors `useDeleted`'s key. */
|
|
119
|
+
deleted(params?: Record<string, unknown>, ctx?: QueryFnContext): _$_tanstack_react_query0.OmitKeyof<_$_tanstack_react_query0.UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
|
|
120
|
+
queryFn?: _$_tanstack_react_query0.QueryFunction<unknown, readonly unknown[], never> | undefined;
|
|
121
|
+
} & {
|
|
122
|
+
queryKey: readonly unknown[] & {
|
|
123
|
+
[dataTagSymbol]: unknown;
|
|
124
|
+
[dataTagErrorSymbol]: Error;
|
|
125
|
+
};
|
|
126
|
+
}; /** GET /:resource/tree — mirrors `useTree`'s key. */
|
|
127
|
+
tree(params?: Record<string, unknown>, ctx?: QueryFnContext): _$_tanstack_react_query0.OmitKeyof<_$_tanstack_react_query0.UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
|
|
128
|
+
queryFn?: _$_tanstack_react_query0.QueryFunction<unknown, readonly unknown[], never> | undefined;
|
|
129
|
+
} & {
|
|
130
|
+
queryKey: readonly unknown[] & {
|
|
131
|
+
[dataTagSymbol]: unknown;
|
|
132
|
+
[dataTagErrorSymbol]: Error;
|
|
133
|
+
};
|
|
134
|
+
}; /** GET /:resource/:parentId/children — mirrors `useChildren`'s key. */
|
|
135
|
+
children(parentId: string, params?: Record<string, unknown>, ctx?: QueryFnContext): _$_tanstack_react_query0.OmitKeyof<_$_tanstack_react_query0.UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
|
|
136
|
+
queryFn?: _$_tanstack_react_query0.QueryFunction<unknown, readonly unknown[], never> | undefined;
|
|
137
|
+
} & {
|
|
138
|
+
queryKey: readonly unknown[] & {
|
|
139
|
+
[dataTagSymbol]: unknown;
|
|
140
|
+
[dataTagErrorSymbol]: Error;
|
|
141
|
+
};
|
|
142
|
+
}; /** GET /:resource/aggregations/:name — mirrors `useAggregation`'s tenant-scoped key. */
|
|
143
|
+
aggregation(name: string, filter?: Record<string, unknown>, ctx?: QueryFnContext): _$_tanstack_react_query0.OmitKeyof<_$_tanstack_react_query0.UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
|
|
144
|
+
queryFn?: _$_tanstack_react_query0.QueryFunction<unknown, readonly unknown[], never> | undefined;
|
|
145
|
+
} & {
|
|
146
|
+
queryKey: readonly unknown[] & {
|
|
147
|
+
[dataTagSymbol]: unknown;
|
|
148
|
+
[dataTagErrorSymbol]: Error;
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Infinite list — mirrors `useInfiniteList`'s key (`scopedList + 'infinite'`)
|
|
153
|
+
* and its page-param semantics (keyset cursor or offset page + 1).
|
|
154
|
+
*/
|
|
155
|
+
infiniteList(params?: Record<string, unknown>, ctx?: QueryFnContext): _$_tanstack_react_query0.OmitKeyof<_$_tanstack_react_query0.UseInfiniteQueryOptions<unknown, Error, _$_tanstack_react_query0.InfiniteData<unknown, unknown>, unknown[], unknown>, "queryFn"> & {
|
|
156
|
+
queryFn?: _$_tanstack_react_query0.QueryFunction<unknown, unknown[], unknown> | undefined;
|
|
157
|
+
} & {
|
|
158
|
+
queryKey: unknown[] & {
|
|
159
|
+
[dataTagSymbol]: _$_tanstack_react_query0.InfiniteData<unknown, unknown>;
|
|
160
|
+
[dataTagErrorSymbol]: Error;
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
//#endregion
|
|
165
|
+
export { DetailQueryOpts, EntityQueries, EntityReadApi, QueryFnContext, createEntityQueries };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { isKeysetPagination, isOffsetPagination } from "./api.js";
|
|
2
|
+
import { createQueryKeys, withOrgParams } from "./cache.js";
|
|
3
|
+
import { infiniteQueryOptions, queryOptions } from "@tanstack/react-query";
|
|
4
|
+
|
|
5
|
+
//#region src/query-options.ts
|
|
6
|
+
/**
|
|
7
|
+
* Build queryOptions factories for one entity. Server-safe; keys are
|
|
8
|
+
* hash-identical to the corresponding `createCrudHooks` hooks.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // queries/products.ts — colocate next to the api definition
|
|
12
|
+
* export const productQueries = createEntityQueries(productApi, 'products');
|
|
13
|
+
*
|
|
14
|
+
* // RSC / route loader
|
|
15
|
+
* await queryClient.ensureQueryData(productQueries.detail(id, { token }));
|
|
16
|
+
*/
|
|
17
|
+
function createEntityQueries(api, entityKey) {
|
|
18
|
+
const KEYS = createQueryKeys(entityKey);
|
|
19
|
+
const fwd = (ctx, signal) => {
|
|
20
|
+
const opt = {};
|
|
21
|
+
if (ctx.headers) opt.headerOptions = ctx.headers;
|
|
22
|
+
if (ctx.cache !== void 0) opt.cache = ctx.cache;
|
|
23
|
+
if (ctx.revalidate !== void 0) opt.revalidate = ctx.revalidate;
|
|
24
|
+
if (ctx.tags !== void 0) opt.tags = ctx.tags;
|
|
25
|
+
if (signal) opt.signal = signal;
|
|
26
|
+
return Object.keys(opt).length ? { options: opt } : {};
|
|
27
|
+
};
|
|
28
|
+
const resolveOrg = (params, ctx) => {
|
|
29
|
+
const { organizationId: paramOrg, ...rest } = params;
|
|
30
|
+
return {
|
|
31
|
+
org: paramOrg ?? ctx.organizationId ?? null,
|
|
32
|
+
rest
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
/** The entity's key factory — for invalidation / setQueryData at call sites. */
|
|
37
|
+
keys: KEYS,
|
|
38
|
+
/** GET /:resource — mirrors `useList`'s key (scoped, org-normalized). */
|
|
39
|
+
list(params = {}, ctx = {}) {
|
|
40
|
+
const { org, rest } = resolveOrg(params, ctx);
|
|
41
|
+
const scope = org ? "tenant" : "super-admin";
|
|
42
|
+
return queryOptions({
|
|
43
|
+
queryKey: KEYS.scopedList(scope, withOrgParams(org, rest)),
|
|
44
|
+
queryFn: ({ signal }) => api.getAll({
|
|
45
|
+
params: rest,
|
|
46
|
+
token: ctx.token ?? null,
|
|
47
|
+
organizationId: org,
|
|
48
|
+
...fwd(ctx, signal)
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
/** GET /:resource/:id — mirrors `useDetail`'s scoped key (+ params variant). */
|
|
53
|
+
detail(id, opts = {}) {
|
|
54
|
+
const { params, ...ctx } = opts;
|
|
55
|
+
const baseKey = KEYS.scopedDetail(id, ctx.organizationId ?? null);
|
|
56
|
+
return queryOptions({
|
|
57
|
+
queryKey: params ? [...baseKey, params] : baseKey,
|
|
58
|
+
queryFn: ({ signal }) => api.getById({
|
|
59
|
+
id,
|
|
60
|
+
token: ctx.token ?? null,
|
|
61
|
+
organizationId: ctx.organizationId ?? null,
|
|
62
|
+
...params ? { params } : {},
|
|
63
|
+
...fwd(ctx, signal)
|
|
64
|
+
})
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
/** GET /:resource/slug/:slug — mirrors `useDetailBySlug`'s key. */
|
|
68
|
+
bySlug(slug, opts = {}) {
|
|
69
|
+
const { params, ...ctx } = opts;
|
|
70
|
+
return queryOptions({
|
|
71
|
+
queryKey: params ? KEYS.custom("slug", slug, params) : KEYS.custom("slug", slug),
|
|
72
|
+
queryFn: ({ signal }) => {
|
|
73
|
+
if (!api.getBySlug) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define getBySlug (slugLookup preset)`));
|
|
74
|
+
return api.getBySlug({
|
|
75
|
+
slug,
|
|
76
|
+
token: ctx.token ?? null,
|
|
77
|
+
organizationId: ctx.organizationId ?? null,
|
|
78
|
+
...params ? { params } : {},
|
|
79
|
+
...fwd(ctx, signal)
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
/** GET /:resource/deleted — mirrors `useDeleted`'s key. */
|
|
85
|
+
deleted(params = {}, ctx = {}) {
|
|
86
|
+
const { org, rest } = resolveOrg(params, ctx);
|
|
87
|
+
return queryOptions({
|
|
88
|
+
queryKey: KEYS.custom("deleted", withOrgParams(org, rest)),
|
|
89
|
+
queryFn: ({ signal }) => {
|
|
90
|
+
if (!api.getDeleted) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define getDeleted (softDelete preset)`));
|
|
91
|
+
return api.getDeleted({
|
|
92
|
+
params: rest,
|
|
93
|
+
token: ctx.token ?? null,
|
|
94
|
+
organizationId: org,
|
|
95
|
+
...fwd(ctx, signal)
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
/** GET /:resource/tree — mirrors `useTree`'s key. */
|
|
101
|
+
tree(params = {}, ctx = {}) {
|
|
102
|
+
const { org, rest } = resolveOrg(params, ctx);
|
|
103
|
+
return queryOptions({
|
|
104
|
+
queryKey: KEYS.custom("tree", withOrgParams(org, rest)),
|
|
105
|
+
queryFn: ({ signal }) => {
|
|
106
|
+
if (!api.getTree) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define getTree (tree preset)`));
|
|
107
|
+
return api.getTree({
|
|
108
|
+
params: rest,
|
|
109
|
+
token: ctx.token ?? null,
|
|
110
|
+
organizationId: org,
|
|
111
|
+
...fwd(ctx, signal)
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
/** GET /:resource/:parentId/children — mirrors `useChildren`'s key. */
|
|
117
|
+
children(parentId, params = {}, ctx = {}) {
|
|
118
|
+
const { org, rest } = resolveOrg(params, ctx);
|
|
119
|
+
return queryOptions({
|
|
120
|
+
queryKey: KEYS.custom("children", parentId, withOrgParams(org, rest)),
|
|
121
|
+
queryFn: ({ signal }) => {
|
|
122
|
+
if (!api.getChildren) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define getChildren (tree preset)`));
|
|
123
|
+
return api.getChildren({
|
|
124
|
+
parentId,
|
|
125
|
+
params: rest,
|
|
126
|
+
token: ctx.token ?? null,
|
|
127
|
+
organizationId: org,
|
|
128
|
+
...fwd(ctx, signal)
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
/** GET /:resource/aggregations/:name — mirrors `useAggregation`'s tenant-scoped key. */
|
|
134
|
+
aggregation(name, filter, ctx = {}) {
|
|
135
|
+
const org = ctx.organizationId ?? null;
|
|
136
|
+
const filterKey = org ? {
|
|
137
|
+
_org: org,
|
|
138
|
+
...filter ?? {}
|
|
139
|
+
} : filter ?? {};
|
|
140
|
+
return queryOptions({
|
|
141
|
+
queryKey: KEYS.aggregation(name, filterKey),
|
|
142
|
+
queryFn: ({ signal }) => {
|
|
143
|
+
if (!api.aggregate) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define aggregate (arc 2.13+)`));
|
|
144
|
+
return api.aggregate({
|
|
145
|
+
name,
|
|
146
|
+
filter,
|
|
147
|
+
token: ctx.token ?? null,
|
|
148
|
+
organizationId: org,
|
|
149
|
+
...fwd(ctx, signal)
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
/**
|
|
155
|
+
* Infinite list — mirrors `useInfiniteList`'s key (`scopedList + 'infinite'`)
|
|
156
|
+
* and its page-param semantics (keyset cursor or offset page + 1).
|
|
157
|
+
*/
|
|
158
|
+
infiniteList(params = {}, ctx = {}) {
|
|
159
|
+
const { org, rest } = resolveOrg(params, ctx);
|
|
160
|
+
const scope = org ? "tenant" : "super-admin";
|
|
161
|
+
return infiniteQueryOptions({
|
|
162
|
+
queryKey: [...KEYS.scopedList(scope, withOrgParams(org, rest)), "infinite"],
|
|
163
|
+
queryFn: ({ pageParam, signal }) => api.getAll({
|
|
164
|
+
params: {
|
|
165
|
+
...rest,
|
|
166
|
+
...pageParam ? { page: pageParam } : {}
|
|
167
|
+
},
|
|
168
|
+
token: ctx.token ?? null,
|
|
169
|
+
organizationId: org,
|
|
170
|
+
...fwd(ctx, signal)
|
|
171
|
+
}),
|
|
172
|
+
initialPageParam: 1,
|
|
173
|
+
getNextPageParam: (lastPage) => {
|
|
174
|
+
if (isKeysetPagination(lastPage)) return lastPage.hasMore ? lastPage.next : void 0;
|
|
175
|
+
if (isOffsetPagination(lastPage)) {
|
|
176
|
+
const p = lastPage;
|
|
177
|
+
return p.hasNext ? p.page + 1 : void 0;
|
|
178
|
+
}
|
|
179
|
+
const p = lastPage;
|
|
180
|
+
if (p && typeof p.hasNext === "boolean" && typeof p.page === "number") return p.hasNext ? p.page + 1 : void 0;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
//#endregion
|
|
188
|
+
export { createEntityQueries };
|
package/llms.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @classytic/arc-next
|
|
2
|
+
|
|
3
|
+
> React + TanStack Query SDK for @classytic/arc backends. Typed CRUD hooks,
|
|
4
|
+
> optimistic updates with rollback, tenant-scoped cache keys, pagination
|
|
5
|
+
> normalization (offset + keyset), SSE, uploads.
|
|
6
|
+
|
|
7
|
+
IMPORTANT for coding agents: hand-rolled `fetch()`/axios against an arc API is a
|
|
8
|
+
bug in apps using this package. Before writing any data-fetching code, check:
|
|
9
|
+
|
|
10
|
+
- CRUD + queries → createCrudApi<T>('resource') + createCrudHooks (useList, useDetail, useInfiniteList, useSuspense*)
|
|
11
|
+
- "How many?" → api.count() / useCount() — arc's ?_count=true verb, zero documents fetched (also api.exists / api.distinct)
|
|
12
|
+
- Mutations → useActions/useBulkActions/useCustomMutation (optimistic updates + rollback + toast built in)
|
|
13
|
+
- Custom actions → useAction (arc's POST /:id/action router)
|
|
14
|
+
- Server presets → compose the matching wrapper: withSoftDelete/withBulk/withTree/withSlugLookup/withSearch/withHistory
|
|
15
|
+
- Record timeline → withHistory(api) + api.history(id) — arc 2.22 `history: true` audit timeline
|
|
16
|
+
- Quota 429s → isQuotaExceeded(err) + getQuotaDetails(err) → render "X of Y {kind}, resets {date}"; NEVER auto-retry (the shared query client already refuses)
|
|
17
|
+
- Aggregations → useAggregation(name, filter)
|
|
18
|
+
- Realtime → SSE/WS modules; uploads → useUpload/useFileUpload
|
|
19
|
+
- Auth/org context → configureClient/configureAuth once at app init; hooks read it — never thread tokens by hand
|
|
20
|
+
|
|
21
|
+
Type flow: the generic T in createCrudApi<T> should be the kernel/module's exported
|
|
22
|
+
WIRE type (plain JSON shape) — never a mongoose-flavored document type.
|