@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/README.md +272 -660
- package/dist/api.d.ts +115 -194
- package/dist/api.js +69 -116
- package/dist/cache.d.ts +125 -0
- package/dist/cache.js +211 -0
- package/dist/client.d.ts +295 -5
- package/dist/client.js +392 -12
- package/dist/hooks.d.ts +162 -22
- package/dist/hooks.js +249 -89
- package/dist/mutation.d.ts +18 -1
- package/dist/mutation.js +20 -1
- package/dist/prefetch.d.ts +36 -2
- package/dist/prefetch.js +47 -3
- package/dist/presets/bulk.d.ts +43 -0
- package/dist/presets/bulk.js +50 -0
- package/dist/presets/search.d.ts +56 -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 +34 -0
- package/dist/presets/soft-delete.js +45 -0
- package/dist/presets/tree.d.ts +32 -0
- package/dist/presets/tree.js +47 -0
- package/dist/query.d.ts +86 -69
- package/dist/query.js +59 -161
- package/dist/sse.d.ts +115 -36
- package/dist/sse.js +179 -103
- 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 +39 -1
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
* 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.
|
|
60
|
+
*/
|
|
61
|
+
declare function extractItem<T>(data: unknown): T | null;
|
|
62
|
+
/**
|
|
63
|
+
* Optimistic-update helper that mutates the items array of a list cache
|
|
64
|
+
* regardless of which key holds it. Auto-adjusts `total`/`totalDocs` when
|
|
65
|
+
* the array length changes.
|
|
66
|
+
*/
|
|
67
|
+
declare function updateListCache<T>(listData: unknown, updater: (items: T[]) => T[]): unknown;
|
|
68
|
+
interface QueryKeys {
|
|
69
|
+
all: string[];
|
|
70
|
+
lists: () => QueryKey;
|
|
71
|
+
list: (params?: unknown) => QueryKey;
|
|
72
|
+
details: () => QueryKey;
|
|
73
|
+
detail: (id: string) => QueryKey;
|
|
74
|
+
/** Tenant-scoped detail key. Use when IDs are only unique within an org. */
|
|
75
|
+
scopedDetail: (id: string, organizationId: string | null) => QueryKey;
|
|
76
|
+
custom: (key: string, ...args: unknown[]) => QueryKey;
|
|
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;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Build a hierarchical query-key factory for a resource. The returned shape
|
|
89
|
+
* is identical between server (prefetch) and client (hooks), so RSC SSR
|
|
90
|
+
* hydration matches what client-side `useList`/`useDetail` produce.
|
|
91
|
+
*/
|
|
92
|
+
declare function createQueryKeys(entityKey: string): QueryKeys;
|
|
93
|
+
interface CacheUtils<T> {
|
|
94
|
+
invalidateAll: (client: QueryClient) => Promise<void>;
|
|
95
|
+
invalidateLists: (client: QueryClient) => Promise<void>;
|
|
96
|
+
/** Invalidate detail by ID (prefix-matches all scoped/parameterized variants). */
|
|
97
|
+
invalidateDetail: (client: QueryClient, id: string) => Promise<void>;
|
|
98
|
+
setDetail: (client: QueryClient, id: string, data: T) => void;
|
|
99
|
+
getDetail: (client: QueryClient, id: string) => T | undefined;
|
|
100
|
+
removeDetail: (client: QueryClient, id: string) => void;
|
|
101
|
+
/** Invalidate tenant-scoped detail (prefix-matches parameterized variants within org). */
|
|
102
|
+
invalidateScopedDetail: (client: QueryClient, id: string, organizationId: string | null) => Promise<void>;
|
|
103
|
+
/** Set tenant-scoped detail cache. */
|
|
104
|
+
setScopedDetail: (client: QueryClient, id: string, organizationId: string | null, data: T) => void;
|
|
105
|
+
/** Get tenant-scoped detail from cache. */
|
|
106
|
+
getScopedDetail: (client: QueryClient, id: string, organizationId: string | null) => T | undefined;
|
|
107
|
+
/** Remove tenant-scoped detail from cache. */
|
|
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>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build cache read/write/invalidate helpers bound to the given key factory.
|
|
120
|
+
* Server-safe — operates on a `QueryClient` instance which can be a per-request
|
|
121
|
+
* server client (during prefetch) or the browser singleton.
|
|
122
|
+
*/
|
|
123
|
+
declare function createCacheUtils<T>(KEYS: QueryKeys): CacheUtils<T>;
|
|
124
|
+
//#endregion
|
|
125
|
+
export { CacheUtils, DEFAULT_QUERY_CONFIG, PaginationData, QUERY_CONFIGS, QueryKeys, createCacheUtils, createQueryKeys, extractItem, extractItems, getItemId, normalizePagination, updateListCache };
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
//#region src/cache.ts
|
|
2
|
+
const DEFAULT_QUERY_CONFIG = {
|
|
3
|
+
staleTime: 300 * 1e3,
|
|
4
|
+
gcTime: 1800 * 1e3,
|
|
5
|
+
refetchOnWindowFocus: false,
|
|
6
|
+
retry: 0
|
|
7
|
+
};
|
|
8
|
+
/** Pre-built query config presets for common data freshness patterns. */
|
|
9
|
+
const QUERY_CONFIGS = {
|
|
10
|
+
/** Live data: 20s stale, 30s polling */
|
|
11
|
+
realtime: {
|
|
12
|
+
staleTime: 2e4,
|
|
13
|
+
refetchInterval: 3e4
|
|
14
|
+
},
|
|
15
|
+
/** Frequently updated: 60s stale */
|
|
16
|
+
frequent: { staleTime: 6e4 },
|
|
17
|
+
/** Stable data: 5min stale (same as default) */
|
|
18
|
+
stable: { staleTime: 3e5 },
|
|
19
|
+
/** Rarely changes: 10min stale */
|
|
20
|
+
static: { staleTime: 6e5 }
|
|
21
|
+
};
|
|
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
|
+
*/
|
|
31
|
+
const LIST_KEYS = [
|
|
32
|
+
"data",
|
|
33
|
+
"items",
|
|
34
|
+
"results"
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Extract `_id` or `id` from any item. Returns `null` if neither exists.
|
|
38
|
+
* Coerces numeric IDs to strings so cache keys stay consistent.
|
|
39
|
+
*/
|
|
40
|
+
function getItemId(item) {
|
|
41
|
+
if (!item || typeof item !== "object") return null;
|
|
42
|
+
const obj = item;
|
|
43
|
+
const id = obj._id ?? obj.id;
|
|
44
|
+
return typeof id === "string" ? id : id ? String(id) : null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Normalize any pagination response shape (offset / keyset / aggregate) to a
|
|
48
|
+
* uniform `PaginationData` object. Returns `null` when no pagination signal
|
|
49
|
+
* is present.
|
|
50
|
+
*/
|
|
51
|
+
function normalizePagination(data) {
|
|
52
|
+
if (!data || typeof data !== "object") return null;
|
|
53
|
+
const d = data;
|
|
54
|
+
const method = d.method ?? null;
|
|
55
|
+
const isKeyset = method === "keyset" || d.hasMore != null && d.total == null && d.pages == null;
|
|
56
|
+
const hasTotal = d.total != null || d.totalDocs != null;
|
|
57
|
+
const hasPages = d.pages != null || d.totalPages != null;
|
|
58
|
+
if (!hasTotal && !hasPages && !isKeyset) return null;
|
|
59
|
+
return {
|
|
60
|
+
method,
|
|
61
|
+
total: Number(d.total ?? d.totalDocs ?? 0),
|
|
62
|
+
pages: Number(d.pages ?? d.totalPages ?? (isKeyset ? 0 : 1)),
|
|
63
|
+
page: Number(d.page ?? d.currentPage ?? (isKeyset ? 0 : 1)),
|
|
64
|
+
limit: Number(d.limit ?? 10),
|
|
65
|
+
hasNext: Boolean(d.hasNext ?? d.hasNextPage ?? d.hasMore ?? false),
|
|
66
|
+
hasPrev: Boolean(d.hasPrev ?? d.hasPrevPage ?? false),
|
|
67
|
+
...isKeyset ? { next: d.next ?? null } : {}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Permissive list extractor. Checks well-known keys (`docs`, `data`, `items`,
|
|
72
|
+
* `results`) then falls back to *any* top-level array — so `{ products: [...] }`
|
|
73
|
+
* and `{ users: [...] }` work without per-resource configuration.
|
|
74
|
+
*/
|
|
75
|
+
function extractItems(data) {
|
|
76
|
+
if (!data) return [];
|
|
77
|
+
if (Array.isArray(data)) return data;
|
|
78
|
+
if (typeof data !== "object") return [];
|
|
79
|
+
const d = data;
|
|
80
|
+
for (const key of LIST_KEYS) if (Array.isArray(d[key])) return d[key];
|
|
81
|
+
for (const value of Object.values(d)) if (Array.isArray(value)) return value;
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
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.
|
|
89
|
+
*/
|
|
90
|
+
function extractItem(data) {
|
|
91
|
+
if (data == null) return null;
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Optimistic-update helper that mutates the items array of a list cache
|
|
96
|
+
* regardless of which key holds it. Auto-adjusts `total`/`totalDocs` when
|
|
97
|
+
* the array length changes.
|
|
98
|
+
*/
|
|
99
|
+
function updateListCache(listData, updater) {
|
|
100
|
+
if (!listData) return listData;
|
|
101
|
+
if (Array.isArray(listData)) return updater(listData);
|
|
102
|
+
if (typeof listData !== "object") return listData;
|
|
103
|
+
const d = listData;
|
|
104
|
+
let arrayField = null;
|
|
105
|
+
for (const key of LIST_KEYS) if (Array.isArray(d[key])) {
|
|
106
|
+
arrayField = key;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
if (!arrayField) {
|
|
110
|
+
for (const [key, value] of Object.entries(d)) if (Array.isArray(value)) {
|
|
111
|
+
arrayField = key;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!arrayField) return listData;
|
|
116
|
+
const updated = updater(d[arrayField]);
|
|
117
|
+
const original = d[arrayField];
|
|
118
|
+
const delta = updated.length - original.length;
|
|
119
|
+
const result = {
|
|
120
|
+
...d,
|
|
121
|
+
[arrayField]: updated
|
|
122
|
+
};
|
|
123
|
+
if (delta !== 0) {
|
|
124
|
+
if (d.total != null) result.total = Math.max(0, Number(d.total) + delta);
|
|
125
|
+
if (d.totalDocs != null) result.totalDocs = Math.max(0, Number(d.totalDocs) + delta);
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Build a hierarchical query-key factory for a resource. The returned shape
|
|
131
|
+
* is identical between server (prefetch) and client (hooks), so RSC SSR
|
|
132
|
+
* hydration matches what client-side `useList`/`useDetail` produce.
|
|
133
|
+
*/
|
|
134
|
+
function createQueryKeys(entityKey) {
|
|
135
|
+
return {
|
|
136
|
+
all: [entityKey],
|
|
137
|
+
lists: () => [entityKey, "list"],
|
|
138
|
+
list: (params) => [
|
|
139
|
+
entityKey,
|
|
140
|
+
"list",
|
|
141
|
+
params
|
|
142
|
+
],
|
|
143
|
+
details: () => [entityKey, "detail"],
|
|
144
|
+
detail: (id) => [
|
|
145
|
+
entityKey,
|
|
146
|
+
"detail",
|
|
147
|
+
id
|
|
148
|
+
],
|
|
149
|
+
scopedDetail: (id, organizationId) => organizationId ? [
|
|
150
|
+
entityKey,
|
|
151
|
+
"detail",
|
|
152
|
+
id,
|
|
153
|
+
{ _org: organizationId }
|
|
154
|
+
] : [
|
|
155
|
+
entityKey,
|
|
156
|
+
"detail",
|
|
157
|
+
id
|
|
158
|
+
],
|
|
159
|
+
custom: (key, ...args) => [
|
|
160
|
+
entityKey,
|
|
161
|
+
key,
|
|
162
|
+
...args
|
|
163
|
+
],
|
|
164
|
+
scopedList: (scope, params) => [
|
|
165
|
+
entityKey,
|
|
166
|
+
"list",
|
|
167
|
+
{
|
|
168
|
+
_scope: scope,
|
|
169
|
+
...params
|
|
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
|
|
182
|
+
]
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Build cache read/write/invalidate helpers bound to the given key factory.
|
|
187
|
+
* Server-safe — operates on a `QueryClient` instance which can be a per-request
|
|
188
|
+
* server client (during prefetch) or the browser singleton.
|
|
189
|
+
*/
|
|
190
|
+
function createCacheUtils(KEYS) {
|
|
191
|
+
return {
|
|
192
|
+
invalidateAll: (client) => client.invalidateQueries({ queryKey: KEYS.all }),
|
|
193
|
+
invalidateLists: (client) => client.invalidateQueries({ queryKey: KEYS.lists() }),
|
|
194
|
+
invalidateDetail: (client, id) => client.invalidateQueries({ queryKey: KEYS.detail(id) }),
|
|
195
|
+
setDetail: (client, id, data) => client.setQueryData(KEYS.detail(id), { data }),
|
|
196
|
+
getDetail: (client, id) => {
|
|
197
|
+
return client.getQueryData(KEYS.detail(id))?.data;
|
|
198
|
+
},
|
|
199
|
+
removeDetail: (client, id) => client.removeQueries({ queryKey: KEYS.detail(id) }),
|
|
200
|
+
invalidateScopedDetail: (client, id, organizationId) => client.invalidateQueries({ queryKey: KEYS.scopedDetail(id, organizationId) }),
|
|
201
|
+
setScopedDetail: (client, id, organizationId, data) => client.setQueryData(KEYS.scopedDetail(id, organizationId), { data }),
|
|
202
|
+
getScopedDetail: (client, id, organizationId) => {
|
|
203
|
+
return client.getQueryData(KEYS.scopedDetail(id, organizationId))?.data;
|
|
204
|
+
},
|
|
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() })
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
//#endregion
|
|
211
|
+
export { DEFAULT_QUERY_CONFIG, QUERY_CONFIGS, createCacheUtils, createQueryKeys, extractItem, extractItems, getItemId, normalizePagination, updateListCache };
|