@zenstackhq/tanstack-query 2.21.0 → 3.0.0-beta.17

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.
Files changed (93) hide show
  1. package/.turbo/turbo-build.log +31 -0
  2. package/LICENSE +1 -1
  3. package/dist/react.cjs +1238 -0
  4. package/dist/react.cjs.map +1 -0
  5. package/dist/react.d.cts +696 -0
  6. package/dist/react.d.ts +696 -0
  7. package/dist/react.js +1195 -0
  8. package/dist/react.js.map +1 -0
  9. package/eslint.config.js +4 -0
  10. package/package.json +56 -109
  11. package/scripts/generate.ts +27 -0
  12. package/src/react.ts +531 -0
  13. package/src/utils/common.ts +457 -0
  14. package/src/utils/mutator.ts +441 -0
  15. package/src/utils/nested-read-visitor.ts +61 -0
  16. package/src/utils/nested-write-visitor.ts +359 -0
  17. package/src/utils/query-analysis.ts +116 -0
  18. package/src/utils/serialization.ts +39 -0
  19. package/src/utils/types.ts +19 -0
  20. package/test/react-query.test.tsx +1787 -0
  21. package/test/schemas/basic/input.ts +70 -0
  22. package/test/schemas/basic/models.ts +12 -0
  23. package/test/schemas/basic/schema-lite.ts +124 -0
  24. package/test/schemas/basic/schema.zmodel +25 -0
  25. package/tsconfig.json +7 -0
  26. package/tsconfig.test.json +8 -0
  27. package/tsup.config.ts +13 -0
  28. package/vitest.config.ts +11 -0
  29. package/README.md +0 -5
  30. package/generator.d.ts +0 -6
  31. package/generator.js +0 -578
  32. package/generator.js.map +0 -1
  33. package/index.d.ts +0 -4
  34. package/index.js +0 -22
  35. package/index.js.map +0 -1
  36. package/runtime/common-CXlL7vTW.d.mts +0 -121
  37. package/runtime/common-CXlL7vTW.d.ts +0 -121
  38. package/runtime/index.d.mts +0 -20
  39. package/runtime/index.d.ts +0 -20
  40. package/runtime/index.js +0 -44
  41. package/runtime/index.js.map +0 -1
  42. package/runtime/index.mjs +0 -21
  43. package/runtime/index.mjs.map +0 -1
  44. package/runtime/react.d.mts +0 -322
  45. package/runtime/react.d.ts +0 -322
  46. package/runtime/react.js +0 -408
  47. package/runtime/react.js.map +0 -1
  48. package/runtime/react.mjs +0 -380
  49. package/runtime/react.mjs.map +0 -1
  50. package/runtime/svelte.d.mts +0 -322
  51. package/runtime/svelte.d.ts +0 -322
  52. package/runtime/svelte.js +0 -407
  53. package/runtime/svelte.js.map +0 -1
  54. package/runtime/svelte.mjs +0 -379
  55. package/runtime/svelte.mjs.map +0 -1
  56. package/runtime/vue.d.mts +0 -330
  57. package/runtime/vue.d.ts +0 -330
  58. package/runtime/vue.js +0 -418
  59. package/runtime/vue.js.map +0 -1
  60. package/runtime/vue.mjs +0 -390
  61. package/runtime/vue.mjs.map +0 -1
  62. package/runtime-v5/angular.d.mts +0 -59
  63. package/runtime-v5/angular.d.ts +0 -59
  64. package/runtime-v5/angular.js +0 -425
  65. package/runtime-v5/angular.js.map +0 -1
  66. package/runtime-v5/angular.mjs +0 -397
  67. package/runtime-v5/angular.mjs.map +0 -1
  68. package/runtime-v5/common-CXlL7vTW.d.mts +0 -121
  69. package/runtime-v5/common-CXlL7vTW.d.ts +0 -121
  70. package/runtime-v5/index.d.mts +0 -20
  71. package/runtime-v5/index.d.ts +0 -20
  72. package/runtime-v5/index.js +0 -44
  73. package/runtime-v5/index.js.map +0 -1
  74. package/runtime-v5/index.mjs +0 -21
  75. package/runtime-v5/index.mjs.map +0 -1
  76. package/runtime-v5/react.d.mts +0 -474
  77. package/runtime-v5/react.d.ts +0 -474
  78. package/runtime-v5/react.js +0 -440
  79. package/runtime-v5/react.js.map +0 -1
  80. package/runtime-v5/react.mjs +0 -412
  81. package/runtime-v5/react.mjs.map +0 -1
  82. package/runtime-v5/svelte.d.mts +0 -386
  83. package/runtime-v5/svelte.d.ts +0 -386
  84. package/runtime-v5/svelte.js +0 -436
  85. package/runtime-v5/svelte.js.map +0 -1
  86. package/runtime-v5/svelte.mjs +0 -408
  87. package/runtime-v5/svelte.mjs.map +0 -1
  88. package/runtime-v5/vue.d.mts +0 -330
  89. package/runtime-v5/vue.d.ts +0 -330
  90. package/runtime-v5/vue.js +0 -420
  91. package/runtime-v5/vue.js.map +0 -1
  92. package/runtime-v5/vue.mjs +0 -392
  93. package/runtime-v5/vue.mjs.map +0 -1
@@ -0,0 +1,457 @@
1
+ import { lowerCaseFirst } from '@zenstackhq/common-helpers';
2
+ import type { SchemaDef } from '@zenstackhq/schema';
3
+ import { applyMutation } from './mutator';
4
+ import { getMutatedModels, getReadModels } from './query-analysis';
5
+ import { deserialize, serialize } from './serialization';
6
+ import type { ORMWriteActionType } from './types';
7
+
8
+ /**
9
+ * The default query endpoint.
10
+ */
11
+ export const DEFAULT_QUERY_ENDPOINT = '/api/model';
12
+
13
+ /**
14
+ * Prefix for react-query keys.
15
+ */
16
+ export const QUERY_KEY_PREFIX = 'zenstack';
17
+
18
+ /**
19
+ * Function signature for `fetch`.
20
+ */
21
+ export type FetchFn = (url: string, options?: RequestInit) => Promise<Response>;
22
+
23
+ /**
24
+ * Type for query and mutation errors.
25
+ */
26
+ export type QueryError = Error & {
27
+ /**
28
+ * Additional error information.
29
+ */
30
+ info?: unknown;
31
+
32
+ /**
33
+ * HTTP status code.
34
+ */
35
+ status?: number;
36
+ };
37
+
38
+ /**
39
+ * Result of optimistic data provider.
40
+ */
41
+ export type OptimisticDataProviderResult = {
42
+ /**
43
+ * Kind of the result.
44
+ * - Update: use the `data` field to update the query cache.
45
+ * - Skip: skip the optimistic update for this query.
46
+ * - ProceedDefault: proceed with the default optimistic update.
47
+ */
48
+ kind: 'Update' | 'Skip' | 'ProceedDefault';
49
+
50
+ /**
51
+ * Data to update the query cache. Only applicable if `kind` is 'Update'.
52
+ *
53
+ * If the data is an object with fields updated, it should have a `$optimistic`
54
+ * field set to `true`. If it's an array and an element object is created or updated,
55
+ * the element should have a `$optimistic` field set to `true`.
56
+ */
57
+ data?: any;
58
+ };
59
+
60
+ /**
61
+ * Optimistic data provider.
62
+ *
63
+ * @param args Arguments.
64
+ * @param args.queryModel The model of the query.
65
+ * @param args.queryOperation The operation of the query, `findMany`, `count`, etc.
66
+ * @param args.queryArgs The arguments of the query.
67
+ * @param args.currentData The current cache data for the query.
68
+ * @param args.mutationArgs The arguments of the mutation.
69
+ */
70
+ export type OptimisticDataProvider = (args: {
71
+ queryModel: string;
72
+ queryOperation: string;
73
+ queryArgs: any;
74
+ currentData: any;
75
+ mutationArgs: any;
76
+ }) => OptimisticDataProviderResult | Promise<OptimisticDataProviderResult>;
77
+
78
+ /**
79
+ * Extra mutation options.
80
+ */
81
+ export type ExtraMutationOptions = {
82
+ /**
83
+ * Whether to automatically invalidate queries potentially affected by the mutation. Defaults to `true`.
84
+ */
85
+ invalidateQueries?: boolean;
86
+
87
+ /**
88
+ * Whether to optimistically update queries potentially affected by the mutation. Defaults to `false`.
89
+ */
90
+ optimisticUpdate?: boolean;
91
+
92
+ /**
93
+ * A callback for computing optimistic update data for each query cache entry.
94
+ */
95
+ optimisticDataProvider?: OptimisticDataProvider;
96
+ };
97
+
98
+ /**
99
+ * Extra query options.
100
+ */
101
+ export type ExtraQueryOptions = {
102
+ /**
103
+ * Whether to opt-in to optimistic updates for this query. Defaults to `true`.
104
+ */
105
+ optimisticUpdate?: boolean;
106
+ };
107
+
108
+ /**
109
+ * Context type for configuring the hooks.
110
+ */
111
+ export type APIContext = {
112
+ /**
113
+ * The endpoint to use for the queries.
114
+ */
115
+ endpoint?: string;
116
+
117
+ /**
118
+ * A custom fetch function for sending the HTTP requests.
119
+ */
120
+ fetch?: FetchFn;
121
+
122
+ /**
123
+ * If logging is enabled.
124
+ */
125
+ logging?: boolean;
126
+ };
127
+
128
+ export async function fetcher<R, C extends boolean>(
129
+ url: string,
130
+ options?: RequestInit,
131
+ customFetch?: FetchFn,
132
+ checkReadBack?: C,
133
+ ): Promise<C extends true ? R | undefined : R> {
134
+ const _fetch = customFetch ?? fetch;
135
+ const res = await _fetch(url, options);
136
+ if (!res.ok) {
137
+ const errData = unmarshal(await res.text());
138
+ if (
139
+ checkReadBack !== false &&
140
+ errData.error?.rejectedByPolicy &&
141
+ errData.error?.rejectReason === 'cannot-read-back'
142
+ ) {
143
+ // policy doesn't allow mutation result to be read back, just return undefined
144
+ return undefined as any;
145
+ }
146
+ const error: QueryError = new Error('An error occurred while fetching the data.');
147
+ error.info = errData.error;
148
+ error.status = res.status;
149
+ throw error;
150
+ }
151
+
152
+ const textResult = await res.text();
153
+ try {
154
+ return unmarshal(textResult).data as R;
155
+ } catch (err) {
156
+ console.error(`Unable to deserialize data:`, textResult);
157
+ throw err;
158
+ }
159
+ }
160
+
161
+ type QueryKey = [
162
+ string /* prefix */,
163
+ string /* model */,
164
+ string /* operation */,
165
+ unknown /* args */,
166
+ {
167
+ infinite: boolean;
168
+ optimisticUpdate: boolean;
169
+ } /* flags */,
170
+ ];
171
+
172
+ /**
173
+ * Computes query key for the given model, operation and query args.
174
+ * @param model Model name.
175
+ * @param operation Query operation (e.g, `findMany`) or request URL. If it's a URL, the last path segment will be used as the operation name.
176
+ * @param args Query arguments.
177
+ * @param options Query options, including `infinite` indicating if it's an infinite query (defaults to false), and `optimisticUpdate` indicating if optimistic updates are enabled (defaults to true).
178
+ * @returns Query key
179
+ */
180
+ export function getQueryKey(
181
+ model: string,
182
+ operation: string,
183
+ args: unknown,
184
+ options: { infinite: boolean; optimisticUpdate: boolean } = { infinite: false, optimisticUpdate: true },
185
+ ): QueryKey {
186
+ const infinite = options.infinite;
187
+ // infinite query doesn't support optimistic updates
188
+ const optimisticUpdate = options.infinite ? false : options.optimisticUpdate;
189
+ return [QUERY_KEY_PREFIX, model, operation!, args, { infinite, optimisticUpdate }];
190
+ }
191
+
192
+ export function marshal(value: unknown) {
193
+ const { data, meta } = serialize(value);
194
+ if (meta) {
195
+ return JSON.stringify({ ...(data as any), meta: { serialization: meta } });
196
+ } else {
197
+ return JSON.stringify(data);
198
+ }
199
+ }
200
+
201
+ export function unmarshal(value: string) {
202
+ const parsed = JSON.parse(value);
203
+ if (typeof parsed === 'object' && parsed?.data && parsed?.meta?.serialization) {
204
+ const deserializedData = deserialize(parsed.data, parsed.meta.serialization);
205
+ return { ...parsed, data: deserializedData };
206
+ } else {
207
+ return parsed;
208
+ }
209
+ }
210
+
211
+ export function makeUrl(url: string, model: string, operation: string, args?: unknown) {
212
+ const baseUrl = `${url}/${lowerCaseFirst(model)}/${operation}`;
213
+ if (!args) {
214
+ return baseUrl;
215
+ }
216
+
217
+ const { data, meta } = serialize(args);
218
+ let result = `${baseUrl}?q=${encodeURIComponent(JSON.stringify(data))}`;
219
+ if (meta) {
220
+ result += `&meta=${encodeURIComponent(JSON.stringify({ serialization: meta }))}`;
221
+ }
222
+ return result;
223
+ }
224
+
225
+ type InvalidationPredicate = ({ queryKey }: { queryKey: readonly unknown[] }) => boolean;
226
+ type InvalidateFunc = (predicate: InvalidationPredicate) => Promise<void>;
227
+ type MutationOptions = {
228
+ onMutate?: (...args: any[]) => any;
229
+ onSuccess?: (...args: any[]) => any;
230
+ onSettled?: (...args: any[]) => any;
231
+ };
232
+
233
+ // sets up invalidation hook for a mutation
234
+ export function setupInvalidation(
235
+ model: string,
236
+ operation: string,
237
+ schema: SchemaDef,
238
+ options: MutationOptions,
239
+ invalidate: InvalidateFunc,
240
+ logging = false,
241
+ ) {
242
+ const origOnSuccess = options?.onSuccess;
243
+ options.onSuccess = async (...args: unknown[]) => {
244
+ const [_, variables] = args;
245
+ const predicate = await getInvalidationPredicate(
246
+ model,
247
+ operation as ORMWriteActionType,
248
+ variables,
249
+ schema,
250
+ logging,
251
+ );
252
+ await invalidate(predicate);
253
+ return origOnSuccess?.(...args);
254
+ };
255
+ }
256
+
257
+ // gets a predicate for evaluating whether a query should be invalidated
258
+ async function getInvalidationPredicate(
259
+ model: string,
260
+ operation: ORMWriteActionType,
261
+ mutationArgs: any,
262
+ schema: SchemaDef,
263
+ logging = false,
264
+ ) {
265
+ const mutatedModels = await getMutatedModels(model, operation, mutationArgs, schema);
266
+
267
+ return ({ queryKey }: { queryKey: readonly unknown[] }) => {
268
+ const [_, queryModel, , args] = queryKey as QueryKey;
269
+
270
+ if (mutatedModels.includes(queryModel)) {
271
+ // direct match
272
+ if (logging) {
273
+ console.log(`Invalidating query ${JSON.stringify(queryKey)} due to mutation "${model}.${operation}"`);
274
+ }
275
+ return true;
276
+ }
277
+
278
+ if (args) {
279
+ // traverse query args to find nested reads that match the model under mutation
280
+ if (findNestedRead(queryModel, mutatedModels, schema, args)) {
281
+ if (logging) {
282
+ console.log(
283
+ `Invalidating query ${JSON.stringify(queryKey)} due to mutation "${model}.${operation}"`,
284
+ );
285
+ }
286
+ return true;
287
+ }
288
+ }
289
+
290
+ return false;
291
+ };
292
+ }
293
+
294
+ // find nested reads that match the given models
295
+ function findNestedRead(visitingModel: string, targetModels: string[], schema: SchemaDef, args: any) {
296
+ const modelsRead = getReadModels(visitingModel, schema, args);
297
+ return targetModels.some((m) => modelsRead.includes(m));
298
+ }
299
+
300
+ type QueryCache = {
301
+ queryKey: readonly unknown[];
302
+ state: {
303
+ data: unknown;
304
+ error: unknown;
305
+ };
306
+ }[];
307
+
308
+ type SetCacheFunc = (queryKey: readonly unknown[], data: unknown) => void;
309
+
310
+ /**
311
+ * Sets up optimistic update and invalidation (after settled) for a mutation.
312
+ */
313
+ export function setupOptimisticUpdate(
314
+ model: string,
315
+ operation: string,
316
+ schema: SchemaDef,
317
+ options: MutationOptions & ExtraMutationOptions,
318
+ queryCache: QueryCache,
319
+ setCache: SetCacheFunc,
320
+ invalidate?: InvalidateFunc,
321
+ logging = false,
322
+ ) {
323
+ const origOnMutate = options?.onMutate;
324
+ const origOnSettled = options?.onSettled;
325
+
326
+ // optimistic update on mutate
327
+ options.onMutate = async (...args: unknown[]) => {
328
+ const [variables] = args;
329
+ await optimisticUpdate(
330
+ model,
331
+ operation as ORMWriteActionType,
332
+ variables,
333
+ options,
334
+ schema,
335
+ queryCache,
336
+ setCache,
337
+ logging,
338
+ );
339
+ return origOnMutate?.(...args);
340
+ };
341
+
342
+ // invalidate on settled
343
+ options.onSettled = async (...args: unknown[]) => {
344
+ if (invalidate) {
345
+ const [, , variables] = args;
346
+ const predicate = await getInvalidationPredicate(
347
+ model,
348
+ operation as ORMWriteActionType,
349
+ variables,
350
+ schema,
351
+ logging,
352
+ );
353
+ await invalidate(predicate);
354
+ }
355
+ return origOnSettled?.(...args);
356
+ };
357
+ }
358
+
359
+ // optimistically updates query cache
360
+ async function optimisticUpdate(
361
+ mutationModel: string,
362
+ mutationOp: string,
363
+ mutationArgs: any,
364
+ options: MutationOptions & ExtraMutationOptions,
365
+ schema: SchemaDef,
366
+ queryCache: QueryCache,
367
+ setCache: SetCacheFunc,
368
+ logging = false,
369
+ ) {
370
+ for (const cacheItem of queryCache) {
371
+ const {
372
+ queryKey,
373
+ state: { data, error },
374
+ } = cacheItem;
375
+
376
+ if (!isZenStackQueryKey(queryKey)) {
377
+ // skip non-zenstack queries
378
+ continue;
379
+ }
380
+
381
+ if (error) {
382
+ if (logging) {
383
+ console.warn(`Skipping optimistic update for ${JSON.stringify(queryKey)} due to error:`, error);
384
+ }
385
+ continue;
386
+ }
387
+
388
+ const [_, queryModel, queryOperation, queryArgs, queryOptions] = queryKey;
389
+ if (!queryOptions?.optimisticUpdate) {
390
+ if (logging) {
391
+ console.log(`Skipping optimistic update for ${JSON.stringify(queryKey)} due to opt-out`);
392
+ }
393
+ continue;
394
+ }
395
+
396
+ if (options.optimisticDataProvider) {
397
+ const providerResult = await options.optimisticDataProvider({
398
+ queryModel,
399
+ queryOperation,
400
+ queryArgs,
401
+ currentData: data,
402
+ mutationArgs,
403
+ });
404
+
405
+ if (providerResult?.kind === 'Skip') {
406
+ // skip
407
+ if (logging) {
408
+ console.log(`Skipping optimistic update for ${JSON.stringify(queryKey)} due to provider`);
409
+ }
410
+ continue;
411
+ } else if (providerResult?.kind === 'Update') {
412
+ // update cache
413
+ if (logging) {
414
+ console.log(`Optimistically updating query ${JSON.stringify(queryKey)} due to provider`);
415
+ }
416
+ setCache(queryKey, providerResult.data);
417
+ continue;
418
+ }
419
+ }
420
+
421
+ // proceed with default optimistic update
422
+ const mutatedData = await applyMutation(
423
+ queryModel,
424
+ queryOperation,
425
+ data,
426
+ mutationModel,
427
+ mutationOp as ORMWriteActionType,
428
+ mutationArgs,
429
+ schema,
430
+ logging,
431
+ );
432
+
433
+ if (mutatedData !== undefined) {
434
+ // mutation applicable to this query, update cache
435
+ if (logging) {
436
+ console.log(
437
+ `Optimistically updating query ${JSON.stringify(
438
+ queryKey,
439
+ )} due to mutation "${mutationModel}.${mutationOp}"`,
440
+ );
441
+ }
442
+ setCache(queryKey, mutatedData);
443
+ }
444
+ }
445
+ }
446
+
447
+ function isZenStackQueryKey(queryKey: readonly unknown[]): queryKey is QueryKey {
448
+ if (queryKey.length < 5) {
449
+ return false;
450
+ }
451
+
452
+ if (queryKey[0] !== QUERY_KEY_PREFIX) {
453
+ return false;
454
+ }
455
+
456
+ return true;
457
+ }