@classytic/arc-next 0.4.0 → 0.5.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 +272 -616
- package/dist/api.d.ts +64 -127
- package/dist/api.js +55 -121
- package/dist/cache.d.ts +108 -0
- package/dist/cache.js +197 -0
- package/dist/client.d.ts +340 -8
- package/dist/client.js +427 -22
- package/dist/hooks.d.ts +96 -20
- package/dist/hooks.js +247 -115
- package/dist/mutation.d.ts +18 -4
- package/dist/mutation.js +20 -4
- package/dist/prefetch.d.ts +37 -3
- package/dist/prefetch.js +56 -17
- package/dist/presets/bulk.d.ts +42 -0
- package/dist/presets/bulk.js +50 -0
- package/dist/presets/search.d.ts +55 -0
- package/dist/presets/search.js +60 -0
- package/dist/presets/slug.d.ts +28 -0
- package/dist/presets/slug.js +27 -0
- package/dist/presets/soft-delete.d.ts +33 -0
- package/dist/presets/soft-delete.js +45 -0
- package/dist/presets/tree.d.ts +31 -0
- package/dist/presets/tree.js +47 -0
- package/dist/query.d.ts +86 -64
- package/dist/query.js +69 -151
- package/dist/sse.d.ts +116 -30
- package/dist/sse.js +179 -102
- package/dist/upload.d.ts +181 -0
- package/dist/upload.js +346 -0
- package/dist/ws.d.ts +167 -0
- package/dist/ws.js +274 -0
- package/package.json +37 -1
package/dist/api.d.ts
CHANGED
|
@@ -67,7 +67,7 @@ interface BulkDeleteResponse {
|
|
|
67
67
|
}
|
|
68
68
|
type SortDirection = 1 | -1 | 'asc' | 'desc';
|
|
69
69
|
type SortSpec = Record<string, SortDirection> | string;
|
|
70
|
-
type FilterOperator = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'contains' | 'startsWith' | 'endsWith' | 'regex' | 'like' | 'exists' | 'size' | 'type';
|
|
70
|
+
type FilterOperator = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'contains' | 'startsWith' | 'endsWith' | 'regex' | 'like' | 'exists' | 'size' | 'type' | 'between' | 'near' | 'nearSphere' | 'geoWithin' | 'withinRadius';
|
|
71
71
|
interface QueryParams {
|
|
72
72
|
page?: number;
|
|
73
73
|
limit?: number;
|
|
@@ -97,6 +97,18 @@ interface RequestOptions {
|
|
|
97
97
|
responseType?: 'json' | 'blob' | 'text';
|
|
98
98
|
signal?: AbortSignal;
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Common args every BaseApi-style method accepts: `token`, `organizationId`,
|
|
102
|
+
* and `options` (per-request RequestOptions minus the auth fields).
|
|
103
|
+
*
|
|
104
|
+
* Preset method signatures extend this so the auth-injection contract stays
|
|
105
|
+
* uniform across every call (BaseApi method, preset method, custom user wrapper).
|
|
106
|
+
*/
|
|
107
|
+
interface ScopedArgs {
|
|
108
|
+
token?: string | null;
|
|
109
|
+
organizationId?: string | null;
|
|
110
|
+
options?: Omit<RequestOptions, 'token' | 'organizationId'>;
|
|
111
|
+
}
|
|
100
112
|
interface BaseApiConfig {
|
|
101
113
|
basePath?: string;
|
|
102
114
|
defaultParams?: {
|
|
@@ -187,154 +199,79 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
|
|
|
187
199
|
id,
|
|
188
200
|
path,
|
|
189
201
|
options
|
|
190
|
-
}: {
|
|
191
|
-
token?: string | null;
|
|
192
|
-
organizationId?: string | null;
|
|
202
|
+
}: ScopedArgs & {
|
|
193
203
|
data: FormData; /** Resource ID — shorthand for path, appended as `baseUrl/{id}/upload` */
|
|
194
204
|
id?: string; /** Custom sub-path appended as `baseUrl/{path}`. Takes precedence over `id`. */
|
|
195
205
|
path?: string;
|
|
196
|
-
options?: Omit<RequestOptions, 'token' | 'organizationId'>;
|
|
197
206
|
}): Promise<ApiResponse<TDoc>>;
|
|
198
|
-
search({
|
|
199
|
-
token,
|
|
200
|
-
organizationId,
|
|
201
|
-
searchParams,
|
|
202
|
-
params,
|
|
203
|
-
options
|
|
204
|
-
}?: {
|
|
205
|
-
token?: string | null;
|
|
206
|
-
organizationId?: string | null;
|
|
207
|
-
searchParams?: Record<string, unknown>;
|
|
208
|
-
params?: QueryParams;
|
|
209
|
-
options?: Omit<RequestOptions, 'token' | 'organizationId'>;
|
|
210
|
-
}): Promise<PaginatedResponse<TDoc>>;
|
|
211
|
-
findBy({
|
|
212
|
-
token,
|
|
213
|
-
organizationId,
|
|
214
|
-
field,
|
|
215
|
-
value,
|
|
216
|
-
operator,
|
|
217
|
-
params,
|
|
218
|
-
options
|
|
219
|
-
}: {
|
|
220
|
-
token?: string | null;
|
|
221
|
-
organizationId?: string | null;
|
|
222
|
-
field: string;
|
|
223
|
-
value: unknown;
|
|
224
|
-
operator?: FilterOperator;
|
|
225
|
-
params?: QueryParams;
|
|
226
|
-
options?: Omit<RequestOptions, 'token' | 'organizationId'>;
|
|
227
|
-
}): Promise<PaginatedResponse<TDoc>>;
|
|
228
207
|
request<TResponse = unknown>(method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE', endpoint: string, {
|
|
229
208
|
token,
|
|
230
209
|
organizationId,
|
|
231
210
|
data,
|
|
232
211
|
params,
|
|
233
212
|
options
|
|
234
|
-
}?: {
|
|
235
|
-
token?: string;
|
|
236
|
-
organizationId?: string | null;
|
|
213
|
+
}?: ScopedArgs & {
|
|
237
214
|
data?: unknown;
|
|
238
215
|
params?: QueryParams;
|
|
239
|
-
options?: Omit<RequestOptions, 'token' | 'organizationId'>;
|
|
240
216
|
}): Promise<TResponse>;
|
|
241
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Invoke a custom route mounted on this resource (e.g. `/todos/stats`,
|
|
219
|
+
* `/todos/recent`). Resource-relative wrapper around {@link request} that
|
|
220
|
+
* prepends `this.baseUrl` so callers don't have to remember the prefix.
|
|
221
|
+
*
|
|
222
|
+
* Use this for `defineResource({ routes: [{ method, path, handler }] })` —
|
|
223
|
+
* Arc's escape hatch for endpoints that don't fit CRUD or actions.
|
|
224
|
+
*
|
|
225
|
+
* For aggregates / reports, prefer the response-aware {@link useApiQuery} hook
|
|
226
|
+
* (which auto-unwraps `{ success, data }`) and pass `invokeRoute` as the queryFn.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* // GET /todos/stats → { success, data: { total, byStatus } }
|
|
230
|
+
* const stats = await api.invokeRoute<{ total: number; byStatus: Record<string, number> }>({
|
|
231
|
+
* method: 'GET',
|
|
232
|
+
* path: '/stats',
|
|
233
|
+
* });
|
|
234
|
+
*
|
|
235
|
+
* // GET /todos/recent?limit=5 → paginated shape spread to root
|
|
236
|
+
* const recent = await api.invokeRoute<PaginatedResponse<Todo>>({
|
|
237
|
+
* method: 'GET',
|
|
238
|
+
* path: '/recent',
|
|
239
|
+
* params: { limit: 5 },
|
|
240
|
+
* });
|
|
241
|
+
*
|
|
242
|
+
* // POST /products/import — body + path
|
|
243
|
+
* await api.invokeRoute({
|
|
244
|
+
* method: 'POST',
|
|
245
|
+
* path: '/import',
|
|
246
|
+
* data: { source: 'csv', items: [...] },
|
|
247
|
+
* });
|
|
248
|
+
*/
|
|
249
|
+
invokeRoute<TResponse = unknown>({
|
|
242
250
|
token,
|
|
243
251
|
organizationId,
|
|
252
|
+
method,
|
|
253
|
+
path,
|
|
254
|
+
data,
|
|
244
255
|
params,
|
|
245
256
|
options
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
257
|
+
}: ScopedArgs & {
|
|
258
|
+
method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; /** Path relative to the resource baseUrl. Leading slash optional. */
|
|
259
|
+
path: string;
|
|
260
|
+
data?: unknown;
|
|
249
261
|
params?: QueryParams;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
restore({
|
|
262
|
+
}): Promise<TResponse>;
|
|
263
|
+
dispatchAction<TResult = unknown, TBody extends Record<string, unknown> = Record<string, unknown>>({
|
|
253
264
|
token,
|
|
254
265
|
organizationId,
|
|
255
266
|
id,
|
|
256
|
-
|
|
257
|
-
}: {
|
|
258
|
-
token?: string | null;
|
|
259
|
-
organizationId?: string | null;
|
|
260
|
-
id: string;
|
|
261
|
-
options?: Omit<RequestOptions, 'token' | 'organizationId'>;
|
|
262
|
-
}): Promise<ApiResponse<TDoc>>;
|
|
263
|
-
bulkCreate({
|
|
264
|
-
token,
|
|
265
|
-
organizationId,
|
|
267
|
+
action,
|
|
266
268
|
data,
|
|
267
269
|
options
|
|
268
|
-
}: {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
data
|
|
272
|
-
|
|
273
|
-
}): Promise<BulkCreateResponse<TDoc>>;
|
|
274
|
-
bulkUpdate({
|
|
275
|
-
token,
|
|
276
|
-
organizationId,
|
|
277
|
-
filter,
|
|
278
|
-
data,
|
|
279
|
-
options
|
|
280
|
-
}: {
|
|
281
|
-
token?: string | null;
|
|
282
|
-
organizationId?: string | null;
|
|
283
|
-
filter: Record<string, unknown>;
|
|
284
|
-
data: TUpdate;
|
|
285
|
-
options?: Omit<RequestOptions, 'token' | 'organizationId'>;
|
|
286
|
-
}): Promise<BulkUpdateResponse>;
|
|
287
|
-
bulkDelete({
|
|
288
|
-
token,
|
|
289
|
-
organizationId,
|
|
290
|
-
filter,
|
|
291
|
-
options
|
|
292
|
-
}: {
|
|
293
|
-
token?: string | null;
|
|
294
|
-
organizationId?: string | null;
|
|
295
|
-
filter: Record<string, unknown>;
|
|
296
|
-
options?: Omit<RequestOptions, 'token' | 'organizationId'>;
|
|
297
|
-
}): Promise<BulkDeleteResponse>;
|
|
298
|
-
getBySlug({
|
|
299
|
-
token,
|
|
300
|
-
organizationId,
|
|
301
|
-
slug,
|
|
302
|
-
params,
|
|
303
|
-
options
|
|
304
|
-
}: {
|
|
305
|
-
token?: string | null;
|
|
306
|
-
organizationId?: string | null;
|
|
307
|
-
slug: string;
|
|
308
|
-
params?: {
|
|
309
|
-
select?: string;
|
|
310
|
-
populate?: string | string[];
|
|
311
|
-
};
|
|
312
|
-
options?: Omit<RequestOptions, 'token' | 'organizationId'>;
|
|
313
|
-
}): Promise<ApiResponse<TDoc>>;
|
|
314
|
-
getTree({
|
|
315
|
-
token,
|
|
316
|
-
organizationId,
|
|
317
|
-
params,
|
|
318
|
-
options
|
|
319
|
-
}?: {
|
|
320
|
-
token?: string | null;
|
|
321
|
-
organizationId?: string | null;
|
|
322
|
-
params?: QueryParams;
|
|
323
|
-
options?: Omit<RequestOptions, 'token' | 'organizationId'>;
|
|
324
|
-
}): Promise<ApiResponse<TDoc[]>>;
|
|
325
|
-
getChildren({
|
|
326
|
-
token,
|
|
327
|
-
organizationId,
|
|
328
|
-
parentId,
|
|
329
|
-
params,
|
|
330
|
-
options
|
|
331
|
-
}: {
|
|
332
|
-
token?: string | null;
|
|
333
|
-
organizationId?: string | null;
|
|
334
|
-
parentId: string;
|
|
335
|
-
params?: QueryParams;
|
|
336
|
-
options?: Omit<RequestOptions, 'token' | 'organizationId'>;
|
|
337
|
-
}): Promise<PaginatedResponse<TDoc>>;
|
|
270
|
+
}: ScopedArgs & {
|
|
271
|
+
id: string;
|
|
272
|
+
action: string;
|
|
273
|
+
data?: TBody;
|
|
274
|
+
}): Promise<ApiResponse<TResult>>;
|
|
338
275
|
}
|
|
339
276
|
declare function createCrudApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, TUpdate = Partial<TDoc>>(entity: string, config?: BaseApiConfig): BaseApi<TDoc, TCreate, TUpdate>;
|
|
340
277
|
type ExtractDoc<T> = T extends PaginatedResponse<infer D> ? D : never;
|
|
@@ -342,4 +279,4 @@ declare function isOffsetPagination<T>(response: PaginatedResponse<T>): response
|
|
|
342
279
|
declare function isKeysetPagination<T>(response: PaginatedResponse<T>): response is KeysetPaginationResponse<T>;
|
|
343
280
|
declare function isAggregatePagination<T>(response: PaginatedResponse<T>): response is AggregatePaginationResponse<T>;
|
|
344
281
|
//#endregion
|
|
345
|
-
export { AggregatePaginationResponse, ApiResponse, BaseApi, BaseApiConfig, BulkCreateResponse, BulkDeleteResponse, BulkUpdateResponse, DeleteResponse, ExtractDoc, FilterOperator, KeysetPaginationResponse, OffsetPaginationResponse, PaginatedResponse, PopulateOption, QueryParams, RequestOptions, SortDirection, SortSpec, createCrudApi, isAggregatePagination, isKeysetPagination, isOffsetPagination };
|
|
282
|
+
export { AggregatePaginationResponse, ApiResponse, BaseApi, BaseApiConfig, BulkCreateResponse, BulkDeleteResponse, BulkUpdateResponse, DeleteResponse, ExtractDoc, FilterOperator, KeysetPaginationResponse, OffsetPaginationResponse, PaginatedResponse, PopulateOption, QueryParams, RequestOptions, ScopedArgs, SortDirection, SortSpec, createCrudApi, isAggregatePagination, isKeysetPagination, isOffsetPagination };
|
package/dist/api.js
CHANGED
|
@@ -63,7 +63,8 @@ var BaseApi = class {
|
|
|
63
63
|
}
|
|
64
64
|
if (value !== void 0 && value !== "") if (["page", "limit"].includes(key)) result[key] = parseInt(String(value)) || (key === "page" ? 1 : 10);
|
|
65
65
|
else if (Array.isArray(value)) {
|
|
66
|
-
if (
|
|
66
|
+
if (/\[([^\]]+)\]$/.test(key)) result[key] = value.join(",");
|
|
67
|
+
else if (value.length > 1) result[`${key}[in]`] = value.join(",");
|
|
67
68
|
else if (value.length === 1) result[key] = value[0];
|
|
68
69
|
} else result[key] = value;
|
|
69
70
|
});
|
|
@@ -96,7 +97,7 @@ var BaseApi = class {
|
|
|
96
97
|
if (organizationId) requestOptions.organizationId = organizationId;
|
|
97
98
|
return this.requestFn("GET", url, this.withHeaders(requestOptions));
|
|
98
99
|
}
|
|
99
|
-
async create({ token, organizationId = null, data, options = {} }) {
|
|
100
|
+
async create({ token = null, organizationId = null, data, options = {} }) {
|
|
100
101
|
const requestOptions = {
|
|
101
102
|
body: data,
|
|
102
103
|
...options
|
|
@@ -105,7 +106,7 @@ var BaseApi = class {
|
|
|
105
106
|
if (organizationId) requestOptions.organizationId = organizationId;
|
|
106
107
|
return this.requestFn("POST", this.baseUrl, this.withHeaders(requestOptions));
|
|
107
108
|
}
|
|
108
|
-
async update({ token, organizationId = null, id, data, options = {} }) {
|
|
109
|
+
async update({ token = null, organizationId = null, id, data, options = {} }) {
|
|
109
110
|
if (!id) throw new Error("ID is required");
|
|
110
111
|
const requestOptions = {
|
|
111
112
|
body: data,
|
|
@@ -115,14 +116,14 @@ var BaseApi = class {
|
|
|
115
116
|
if (organizationId) requestOptions.organizationId = organizationId;
|
|
116
117
|
return this.requestFn("PATCH", `${this.baseUrl}/${id}`, this.withHeaders(requestOptions));
|
|
117
118
|
}
|
|
118
|
-
async delete({ token, organizationId = null, id, options = {} }) {
|
|
119
|
+
async delete({ token = null, organizationId = null, id, options = {} }) {
|
|
119
120
|
if (!id) throw new Error("ID is required");
|
|
120
121
|
const requestOptions = { ...options };
|
|
121
122
|
if (token) requestOptions.token = token;
|
|
122
123
|
if (organizationId) requestOptions.organizationId = organizationId;
|
|
123
124
|
return this.requestFn("DELETE", `${this.baseUrl}/${id}`, this.withHeaders(requestOptions));
|
|
124
125
|
}
|
|
125
|
-
async upload({ token, organizationId = null, data, id, path, options = {} }) {
|
|
126
|
+
async upload({ token = null, organizationId = null, data, id, path, options = {} }) {
|
|
126
127
|
const suffix = path ?? (id ? `${id}/upload` : void 0);
|
|
127
128
|
const url = suffix ? `${this.baseUrl}/${suffix}` : this.baseUrl;
|
|
128
129
|
const requestOptions = {
|
|
@@ -133,41 +134,7 @@ var BaseApi = class {
|
|
|
133
134
|
if (organizationId) requestOptions.organizationId = organizationId;
|
|
134
135
|
return this.requestFn("POST", url, this.withHeaders(requestOptions));
|
|
135
136
|
}
|
|
136
|
-
async
|
|
137
|
-
const queryParams = {
|
|
138
|
-
...this.config.defaultParams,
|
|
139
|
-
...params,
|
|
140
|
-
...searchParams
|
|
141
|
-
};
|
|
142
|
-
const processedParams = this.prepareParams(queryParams);
|
|
143
|
-
const queryString = this.createQueryString(processedParams);
|
|
144
|
-
const requestOptions = {
|
|
145
|
-
cache: this.config.cache,
|
|
146
|
-
...options
|
|
147
|
-
};
|
|
148
|
-
if (token) requestOptions.token = token;
|
|
149
|
-
if (organizationId) requestOptions.organizationId = organizationId;
|
|
150
|
-
return this.requestFn("GET", `${this.baseUrl}?${queryString}`, this.withHeaders(requestOptions));
|
|
151
|
-
}
|
|
152
|
-
async findBy({ token = null, organizationId = null, field, value, operator, params = {}, options = {} }) {
|
|
153
|
-
if (!field || value === void 0) throw new Error("Field and value are required");
|
|
154
|
-
const queryParams = {
|
|
155
|
-
...this.config.defaultParams,
|
|
156
|
-
...params
|
|
157
|
-
};
|
|
158
|
-
if (operator) queryParams[`${field}[${operator}]`] = Array.isArray(value) ? value.join(",") : value;
|
|
159
|
-
else queryParams[field] = value;
|
|
160
|
-
const processedParams = this.prepareParams(queryParams);
|
|
161
|
-
const queryString = this.createQueryString(processedParams);
|
|
162
|
-
const requestOptions = {
|
|
163
|
-
cache: this.config.cache,
|
|
164
|
-
...options
|
|
165
|
-
};
|
|
166
|
-
if (token) requestOptions.token = token;
|
|
167
|
-
if (organizationId) requestOptions.organizationId = organizationId;
|
|
168
|
-
return this.requestFn("GET", `${this.baseUrl}?${queryString}`, this.withHeaders(requestOptions));
|
|
169
|
-
}
|
|
170
|
-
async request(method, endpoint, { token, organizationId = null, data, params, options = {} } = {}) {
|
|
137
|
+
async request(method, endpoint, { token = null, organizationId = null, data, params, options = {} } = {}) {
|
|
171
138
|
let url = endpoint;
|
|
172
139
|
if (params) {
|
|
173
140
|
const processedParams = this.prepareParams(params);
|
|
@@ -182,96 +149,63 @@ var BaseApi = class {
|
|
|
182
149
|
if (organizationId) requestOptions.organizationId = organizationId;
|
|
183
150
|
return this.requestFn(method, url, this.withHeaders(requestOptions));
|
|
184
151
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Invoke a custom route mounted on this resource (e.g. `/todos/stats`,
|
|
154
|
+
* `/todos/recent`). Resource-relative wrapper around {@link request} that
|
|
155
|
+
* prepends `this.baseUrl` so callers don't have to remember the prefix.
|
|
156
|
+
*
|
|
157
|
+
* Use this for `defineResource({ routes: [{ method, path, handler }] })` —
|
|
158
|
+
* Arc's escape hatch for endpoints that don't fit CRUD or actions.
|
|
159
|
+
*
|
|
160
|
+
* For aggregates / reports, prefer the response-aware {@link useApiQuery} hook
|
|
161
|
+
* (which auto-unwraps `{ success, data }`) and pass `invokeRoute` as the queryFn.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* // GET /todos/stats → { success, data: { total, byStatus } }
|
|
165
|
+
* const stats = await api.invokeRoute<{ total: number; byStatus: Record<string, number> }>({
|
|
166
|
+
* method: 'GET',
|
|
167
|
+
* path: '/stats',
|
|
168
|
+
* });
|
|
169
|
+
*
|
|
170
|
+
* // GET /todos/recent?limit=5 → paginated shape spread to root
|
|
171
|
+
* const recent = await api.invokeRoute<PaginatedResponse<Todo>>({
|
|
172
|
+
* method: 'GET',
|
|
173
|
+
* path: '/recent',
|
|
174
|
+
* params: { limit: 5 },
|
|
175
|
+
* });
|
|
176
|
+
*
|
|
177
|
+
* // POST /products/import — body + path
|
|
178
|
+
* await api.invokeRoute({
|
|
179
|
+
* method: 'POST',
|
|
180
|
+
* path: '/import',
|
|
181
|
+
* data: { source: 'csv', items: [...] },
|
|
182
|
+
* });
|
|
183
|
+
*/
|
|
184
|
+
async invokeRoute({ token = null, organizationId = null, method = "GET", path, data, params, options = {} }) {
|
|
185
|
+
if (!path) throw new Error("path is required");
|
|
186
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
187
|
+
const endpoint = `${this.baseUrl}${normalized}`;
|
|
188
|
+
return this.request(method, endpoint, {
|
|
189
|
+
token,
|
|
190
|
+
organizationId,
|
|
191
|
+
data,
|
|
192
|
+
params,
|
|
193
|
+
options
|
|
194
|
+
});
|
|
199
195
|
}
|
|
200
|
-
async
|
|
196
|
+
async dispatchAction({ token = null, organizationId = null, id, action, data, options = {} }) {
|
|
201
197
|
if (!id) throw new Error("ID is required");
|
|
202
|
-
|
|
203
|
-
if (token) requestOptions.token = token;
|
|
204
|
-
if (organizationId) requestOptions.organizationId = organizationId;
|
|
205
|
-
return this.requestFn("POST", `${this.baseUrl}/${id}/restore`, this.withHeaders(requestOptions));
|
|
206
|
-
}
|
|
207
|
-
async bulkCreate({ token, organizationId = null, data, options = {} }) {
|
|
208
|
-
const requestOptions = {
|
|
209
|
-
body: data,
|
|
210
|
-
...options
|
|
211
|
-
};
|
|
212
|
-
if (token) requestOptions.token = token;
|
|
213
|
-
if (organizationId) requestOptions.organizationId = organizationId;
|
|
214
|
-
return this.requestFn("POST", `${this.baseUrl}/bulk`, this.withHeaders(requestOptions));
|
|
215
|
-
}
|
|
216
|
-
async bulkUpdate({ token, organizationId = null, filter, data, options = {} }) {
|
|
198
|
+
if (!action) throw new Error("Action name is required");
|
|
217
199
|
const requestOptions = {
|
|
218
200
|
body: {
|
|
219
|
-
|
|
220
|
-
data
|
|
201
|
+
action,
|
|
202
|
+
...data ?? {}
|
|
221
203
|
},
|
|
222
204
|
...options
|
|
223
205
|
};
|
|
224
206
|
if (token) requestOptions.token = token;
|
|
225
207
|
if (organizationId) requestOptions.organizationId = organizationId;
|
|
226
|
-
return this.requestFn("
|
|
227
|
-
}
|
|
228
|
-
async bulkDelete({ token, organizationId = null, filter, options = {} }) {
|
|
229
|
-
const requestOptions = {
|
|
230
|
-
body: { filter },
|
|
231
|
-
...options
|
|
232
|
-
};
|
|
233
|
-
if (token) requestOptions.token = token;
|
|
234
|
-
if (organizationId) requestOptions.organizationId = organizationId;
|
|
235
|
-
return this.requestFn("DELETE", `${this.baseUrl}/bulk`, this.withHeaders(requestOptions));
|
|
236
|
-
}
|
|
237
|
-
async getBySlug({ token = null, organizationId = null, slug, params = {}, options = {} }) {
|
|
238
|
-
if (!slug) throw new Error("Slug is required");
|
|
239
|
-
const queryString = this.createQueryString(params);
|
|
240
|
-
const url = queryString ? `${this.baseUrl}/slug/${slug}?${queryString}` : `${this.baseUrl}/slug/${slug}`;
|
|
241
|
-
const requestOptions = {
|
|
242
|
-
cache: this.config.cache,
|
|
243
|
-
...options
|
|
244
|
-
};
|
|
245
|
-
if (token) requestOptions.token = token;
|
|
246
|
-
if (organizationId) requestOptions.organizationId = organizationId;
|
|
247
|
-
return this.requestFn("GET", url, this.withHeaders(requestOptions));
|
|
248
|
-
}
|
|
249
|
-
async getTree({ token = null, organizationId = null, params = {}, options = {} } = {}) {
|
|
250
|
-
const processedParams = this.prepareParams(params);
|
|
251
|
-
const queryString = this.createQueryString(processedParams);
|
|
252
|
-
const requestOptions = {
|
|
253
|
-
cache: this.config.cache,
|
|
254
|
-
...options
|
|
255
|
-
};
|
|
256
|
-
if (token) requestOptions.token = token;
|
|
257
|
-
if (organizationId) requestOptions.organizationId = organizationId;
|
|
258
|
-
return this.requestFn("GET", `${this.baseUrl}/tree?${queryString}`, this.withHeaders(requestOptions));
|
|
259
|
-
}
|
|
260
|
-
async getChildren({ token = null, organizationId = null, parentId, params = {}, options = {} }) {
|
|
261
|
-
if (!parentId) throw new Error("Parent ID is required");
|
|
262
|
-
const mergedParams = {
|
|
263
|
-
...this.config.defaultParams,
|
|
264
|
-
...params
|
|
265
|
-
};
|
|
266
|
-
const processedParams = this.prepareParams(mergedParams);
|
|
267
|
-
const queryString = this.createQueryString(processedParams);
|
|
268
|
-
const requestOptions = {
|
|
269
|
-
cache: this.config.cache,
|
|
270
|
-
...options
|
|
271
|
-
};
|
|
272
|
-
if (token) requestOptions.token = token;
|
|
273
|
-
if (organizationId) requestOptions.organizationId = organizationId;
|
|
274
|
-
return this.requestFn("GET", `${this.baseUrl}/${parentId}/children?${queryString}`, this.withHeaders(requestOptions));
|
|
208
|
+
return this.requestFn("POST", `${this.baseUrl}/${id}/action`, this.withHeaders(requestOptions));
|
|
275
209
|
}
|
|
276
210
|
};
|
|
277
211
|
function createCrudApi(entity, config = {}) {
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { QueryClient, QueryKey } from "@tanstack/react-query";
|
|
2
|
+
|
|
3
|
+
//#region src/cache.d.ts
|
|
4
|
+
interface PaginationData {
|
|
5
|
+
/** Pagination method detected from response (offset | keyset | aggregate) */
|
|
6
|
+
method: 'offset' | 'keyset' | 'aggregate' | null;
|
|
7
|
+
total: number;
|
|
8
|
+
pages: number;
|
|
9
|
+
page: number;
|
|
10
|
+
limit: number;
|
|
11
|
+
hasNext: boolean;
|
|
12
|
+
hasPrev: boolean;
|
|
13
|
+
/** Keyset cursor for next page (keyset pagination only) */
|
|
14
|
+
next?: string | null;
|
|
15
|
+
}
|
|
16
|
+
declare const DEFAULT_QUERY_CONFIG: {
|
|
17
|
+
readonly staleTime: number;
|
|
18
|
+
readonly gcTime: number;
|
|
19
|
+
readonly refetchOnWindowFocus: false;
|
|
20
|
+
readonly retry: 0;
|
|
21
|
+
};
|
|
22
|
+
/** Pre-built query config presets for common data freshness patterns. */
|
|
23
|
+
declare const QUERY_CONFIGS: {
|
|
24
|
+
/** Live data: 20s stale, 30s polling */readonly realtime: {
|
|
25
|
+
readonly staleTime: 20000;
|
|
26
|
+
readonly refetchInterval: 30000;
|
|
27
|
+
}; /** Frequently updated: 60s stale */
|
|
28
|
+
readonly frequent: {
|
|
29
|
+
readonly staleTime: 60000;
|
|
30
|
+
}; /** Stable data: 5min stale (same as default) */
|
|
31
|
+
readonly stable: {
|
|
32
|
+
readonly staleTime: 300000;
|
|
33
|
+
}; /** Rarely changes: 10min stale */
|
|
34
|
+
readonly static: {
|
|
35
|
+
readonly staleTime: 600000;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Extract `_id` or `id` from any item. Returns `null` if neither exists.
|
|
40
|
+
* Coerces numeric IDs to strings so cache keys stay consistent.
|
|
41
|
+
*/
|
|
42
|
+
declare function getItemId(item: unknown): string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Normalize any pagination response shape (offset / keyset / aggregate) to a
|
|
45
|
+
* uniform `PaginationData` object. Returns `null` when no pagination signal
|
|
46
|
+
* is present.
|
|
47
|
+
*/
|
|
48
|
+
declare function normalizePagination(data: unknown): PaginationData | null;
|
|
49
|
+
/**
|
|
50
|
+
* Permissive list extractor. Checks well-known keys (`docs`, `data`, `items`,
|
|
51
|
+
* `results`) then falls back to *any* top-level array — so `{ products: [...] }`
|
|
52
|
+
* and `{ users: [...] }` work without per-resource configuration.
|
|
53
|
+
*/
|
|
54
|
+
declare function extractItems<T>(data: unknown): T[];
|
|
55
|
+
/**
|
|
56
|
+
* Strict detail extractor. Checks well-known keys (`data`, `doc`, `item`,
|
|
57
|
+
* `result`); falls back to returning the response as-is. Primitive responses
|
|
58
|
+
* (string/number/boolean) pass through.
|
|
59
|
+
*/
|
|
60
|
+
declare function extractItem<T>(data: unknown): T | null;
|
|
61
|
+
/**
|
|
62
|
+
* Optimistic-update helper that mutates the items array of a list cache
|
|
63
|
+
* regardless of which key holds it. Auto-adjusts `total`/`totalDocs` when
|
|
64
|
+
* the array length changes.
|
|
65
|
+
*/
|
|
66
|
+
declare function updateListCache<T>(listData: unknown, updater: (items: T[]) => T[]): unknown;
|
|
67
|
+
interface QueryKeys {
|
|
68
|
+
all: string[];
|
|
69
|
+
lists: () => QueryKey;
|
|
70
|
+
list: (params?: unknown) => QueryKey;
|
|
71
|
+
details: () => QueryKey;
|
|
72
|
+
detail: (id: string) => QueryKey;
|
|
73
|
+
/** Tenant-scoped detail key. Use when IDs are only unique within an org. */
|
|
74
|
+
scopedDetail: (id: string, organizationId: string | null) => QueryKey;
|
|
75
|
+
custom: (key: string, ...args: unknown[]) => QueryKey;
|
|
76
|
+
scopedList: (scope: string, params?: unknown) => QueryKey;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Build a hierarchical query-key factory for a resource. The returned shape
|
|
80
|
+
* is identical between server (prefetch) and client (hooks), so RSC SSR
|
|
81
|
+
* hydration matches what client-side `useList`/`useDetail` produce.
|
|
82
|
+
*/
|
|
83
|
+
declare function createQueryKeys(entityKey: string): QueryKeys;
|
|
84
|
+
interface CacheUtils<T> {
|
|
85
|
+
invalidateAll: (client: QueryClient) => Promise<void>;
|
|
86
|
+
invalidateLists: (client: QueryClient) => Promise<void>;
|
|
87
|
+
/** Invalidate detail by ID (prefix-matches all scoped/parameterized variants). */
|
|
88
|
+
invalidateDetail: (client: QueryClient, id: string) => Promise<void>;
|
|
89
|
+
setDetail: (client: QueryClient, id: string, data: T) => void;
|
|
90
|
+
getDetail: (client: QueryClient, id: string) => T | undefined;
|
|
91
|
+
removeDetail: (client: QueryClient, id: string) => void;
|
|
92
|
+
/** Invalidate tenant-scoped detail (prefix-matches parameterized variants within org). */
|
|
93
|
+
invalidateScopedDetail: (client: QueryClient, id: string, organizationId: string | null) => Promise<void>;
|
|
94
|
+
/** Set tenant-scoped detail cache. */
|
|
95
|
+
setScopedDetail: (client: QueryClient, id: string, organizationId: string | null, data: T) => void;
|
|
96
|
+
/** Get tenant-scoped detail from cache. */
|
|
97
|
+
getScopedDetail: (client: QueryClient, id: string, organizationId: string | null) => T | undefined;
|
|
98
|
+
/** Remove tenant-scoped detail from cache. */
|
|
99
|
+
removeScopedDetail: (client: QueryClient, id: string, organizationId: string | null) => void;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Build cache read/write/invalidate helpers bound to the given key factory.
|
|
103
|
+
* Server-safe — operates on a `QueryClient` instance which can be a per-request
|
|
104
|
+
* server client (during prefetch) or the browser singleton.
|
|
105
|
+
*/
|
|
106
|
+
declare function createCacheUtils<T>(KEYS: QueryKeys): CacheUtils<T>;
|
|
107
|
+
//#endregion
|
|
108
|
+
export { CacheUtils, DEFAULT_QUERY_CONFIG, PaginationData, QUERY_CONFIGS, QueryKeys, createCacheUtils, createQueryKeys, extractItem, extractItems, getItemId, normalizePagination, updateListCache };
|