@zenstackhq/tanstack-query 2.20.1 → 3.0.0-beta.16

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 (94) hide show
  1. package/.turbo/turbo-build.log +32 -0
  2. package/.turbo/turbo-lint.log +5 -0
  3. package/LICENSE +1 -1
  4. package/dist/react.cjs +1238 -0
  5. package/dist/react.cjs.map +1 -0
  6. package/dist/react.d.cts +696 -0
  7. package/dist/react.d.ts +696 -0
  8. package/dist/react.js +1195 -0
  9. package/dist/react.js.map +1 -0
  10. package/eslint.config.js +4 -0
  11. package/package.json +56 -109
  12. package/scripts/generate.ts +27 -0
  13. package/src/react.ts +531 -0
  14. package/src/utils/common.ts +457 -0
  15. package/src/utils/mutator.ts +441 -0
  16. package/src/utils/nested-read-visitor.ts +61 -0
  17. package/src/utils/nested-write-visitor.ts +359 -0
  18. package/src/utils/query-analysis.ts +116 -0
  19. package/src/utils/serialization.ts +39 -0
  20. package/src/utils/types.ts +19 -0
  21. package/test/react-query.test.tsx +1787 -0
  22. package/test/schemas/basic/input.ts +70 -0
  23. package/test/schemas/basic/models.ts +12 -0
  24. package/test/schemas/basic/schema-lite.ts +124 -0
  25. package/test/schemas/basic/schema.zmodel +25 -0
  26. package/tsconfig.json +7 -0
  27. package/tsconfig.test.json +8 -0
  28. package/tsup.config.ts +13 -0
  29. package/vitest.config.ts +11 -0
  30. package/README.md +0 -5
  31. package/generator.d.ts +0 -6
  32. package/generator.js +0 -578
  33. package/generator.js.map +0 -1
  34. package/index.d.ts +0 -4
  35. package/index.js +0 -22
  36. package/index.js.map +0 -1
  37. package/runtime/common-CXlL7vTW.d.mts +0 -121
  38. package/runtime/common-CXlL7vTW.d.ts +0 -121
  39. package/runtime/index.d.mts +0 -20
  40. package/runtime/index.d.ts +0 -20
  41. package/runtime/index.js +0 -44
  42. package/runtime/index.js.map +0 -1
  43. package/runtime/index.mjs +0 -21
  44. package/runtime/index.mjs.map +0 -1
  45. package/runtime/react.d.mts +0 -322
  46. package/runtime/react.d.ts +0 -322
  47. package/runtime/react.js +0 -408
  48. package/runtime/react.js.map +0 -1
  49. package/runtime/react.mjs +0 -380
  50. package/runtime/react.mjs.map +0 -1
  51. package/runtime/svelte.d.mts +0 -322
  52. package/runtime/svelte.d.ts +0 -322
  53. package/runtime/svelte.js +0 -407
  54. package/runtime/svelte.js.map +0 -1
  55. package/runtime/svelte.mjs +0 -379
  56. package/runtime/svelte.mjs.map +0 -1
  57. package/runtime/vue.d.mts +0 -330
  58. package/runtime/vue.d.ts +0 -330
  59. package/runtime/vue.js +0 -418
  60. package/runtime/vue.js.map +0 -1
  61. package/runtime/vue.mjs +0 -390
  62. package/runtime/vue.mjs.map +0 -1
  63. package/runtime-v5/angular.d.mts +0 -59
  64. package/runtime-v5/angular.d.ts +0 -59
  65. package/runtime-v5/angular.js +0 -425
  66. package/runtime-v5/angular.js.map +0 -1
  67. package/runtime-v5/angular.mjs +0 -397
  68. package/runtime-v5/angular.mjs.map +0 -1
  69. package/runtime-v5/common-CXlL7vTW.d.mts +0 -121
  70. package/runtime-v5/common-CXlL7vTW.d.ts +0 -121
  71. package/runtime-v5/index.d.mts +0 -20
  72. package/runtime-v5/index.d.ts +0 -20
  73. package/runtime-v5/index.js +0 -44
  74. package/runtime-v5/index.js.map +0 -1
  75. package/runtime-v5/index.mjs +0 -21
  76. package/runtime-v5/index.mjs.map +0 -1
  77. package/runtime-v5/react.d.mts +0 -474
  78. package/runtime-v5/react.d.ts +0 -474
  79. package/runtime-v5/react.js +0 -440
  80. package/runtime-v5/react.js.map +0 -1
  81. package/runtime-v5/react.mjs +0 -412
  82. package/runtime-v5/react.mjs.map +0 -1
  83. package/runtime-v5/svelte.d.mts +0 -386
  84. package/runtime-v5/svelte.d.ts +0 -386
  85. package/runtime-v5/svelte.js +0 -436
  86. package/runtime-v5/svelte.js.map +0 -1
  87. package/runtime-v5/svelte.mjs +0 -408
  88. package/runtime-v5/svelte.mjs.map +0 -1
  89. package/runtime-v5/vue.d.mts +0 -330
  90. package/runtime-v5/vue.d.ts +0 -330
  91. package/runtime-v5/vue.js +0 -420
  92. package/runtime-v5/vue.js.map +0 -1
  93. package/runtime-v5/vue.mjs +0 -392
  94. package/runtime-v5/vue.mjs.map +0 -1
@@ -0,0 +1,441 @@
1
+ import { clone, enumerate, invariant, zip } from '@zenstackhq/common-helpers';
2
+ import type { FieldDef, SchemaDef } from '@zenstackhq/schema';
3
+ import { NestedWriteVisitor } from './nested-write-visitor';
4
+ import type { ORMWriteActionType } from './types';
5
+
6
+ /**
7
+ * Tries to apply a mutation to a query result.
8
+ *
9
+ * @param queryModel the model of the query
10
+ * @param queryOp the operation of the query
11
+ * @param queryData the result data of the query
12
+ * @param mutationModel the model of the mutation
13
+ * @param mutationOp the operation of the mutation
14
+ * @param mutationArgs the arguments of the mutation
15
+ * @param schema the model metadata
16
+ * @param logging whether to log the mutation application
17
+ * @returns the updated query data if the mutation is applicable, otherwise undefined
18
+ */
19
+ export async function applyMutation(
20
+ queryModel: string,
21
+ queryOp: string,
22
+ queryData: any,
23
+ mutationModel: string,
24
+ mutationOp: ORMWriteActionType,
25
+ mutationArgs: any,
26
+ schema: SchemaDef,
27
+ logging: boolean,
28
+ ) {
29
+ if (!queryData || (typeof queryData !== 'object' && !Array.isArray(queryData))) {
30
+ return undefined;
31
+ }
32
+
33
+ if (!queryOp.startsWith('find')) {
34
+ // only findXXX results are applicable
35
+ return undefined;
36
+ }
37
+
38
+ return await doApplyMutation(queryModel, queryData, mutationModel, mutationOp, mutationArgs, schema, logging);
39
+ }
40
+
41
+ async function doApplyMutation(
42
+ queryModel: string,
43
+ queryData: any,
44
+ mutationModel: string,
45
+ mutationOp: ORMWriteActionType,
46
+ mutationArgs: any,
47
+ schema: SchemaDef,
48
+ logging: boolean,
49
+ ) {
50
+ let resultData = queryData;
51
+ let updated = false;
52
+
53
+ const visitor = new NestedWriteVisitor(schema, {
54
+ create: (model, args) => {
55
+ if (
56
+ model === queryModel &&
57
+ Array.isArray(resultData) // "create" mutation is only relevant for arrays
58
+ ) {
59
+ const r = createMutate(queryModel, resultData, args, schema, logging);
60
+ if (r) {
61
+ resultData = r;
62
+ updated = true;
63
+ }
64
+ }
65
+ },
66
+
67
+ createMany: (model, args) => {
68
+ if (
69
+ model === queryModel &&
70
+ args?.data &&
71
+ Array.isArray(resultData) // "createMany" mutation is only relevant for arrays
72
+ ) {
73
+ for (const oneArg of enumerate(args.data)) {
74
+ const r = createMutate(queryModel, resultData, oneArg, schema, logging);
75
+ if (r) {
76
+ resultData = r;
77
+ updated = true;
78
+ }
79
+ }
80
+ }
81
+ },
82
+
83
+ update: (model, args) => {
84
+ if (
85
+ model === queryModel &&
86
+ !Array.isArray(resultData) // array elements will be handled with recursion
87
+ ) {
88
+ const r = updateMutate(queryModel, resultData, model, args, schema, logging);
89
+ if (r) {
90
+ resultData = r;
91
+ updated = true;
92
+ }
93
+ }
94
+ },
95
+
96
+ upsert: (model, args) => {
97
+ if (model === queryModel && args?.where && args?.create && args?.update) {
98
+ const r = upsertMutate(queryModel, resultData, model, args, schema, logging);
99
+ if (r) {
100
+ resultData = r;
101
+ updated = true;
102
+ }
103
+ }
104
+ },
105
+
106
+ delete: (model, args) => {
107
+ if (model === queryModel) {
108
+ const r = deleteMutate(queryModel, resultData, model, args, schema, logging);
109
+ if (r) {
110
+ resultData = r;
111
+ updated = true;
112
+ }
113
+ }
114
+ },
115
+ });
116
+
117
+ await visitor.visit(mutationModel, mutationOp, mutationArgs);
118
+
119
+ const modelFields = schema.models[queryModel]?.fields;
120
+ invariant(modelFields, `Model ${queryModel} not found in schema`);
121
+
122
+ if (Array.isArray(resultData)) {
123
+ // try to apply mutation to each item in the array, replicate the entire
124
+ // array if any item is updated
125
+
126
+ let arrayCloned = false;
127
+ for (let i = 0; i < resultData.length; i++) {
128
+ const item = resultData[i];
129
+ if (
130
+ !item ||
131
+ typeof item !== 'object' ||
132
+ item.$optimistic // skip items already optimistically updated
133
+ ) {
134
+ continue;
135
+ }
136
+
137
+ const r = await doApplyMutation(queryModel, item, mutationModel, mutationOp, mutationArgs, schema, logging);
138
+
139
+ if (r && typeof r === 'object') {
140
+ if (!arrayCloned) {
141
+ resultData = [...resultData];
142
+ arrayCloned = true;
143
+ }
144
+ resultData[i] = r;
145
+ updated = true;
146
+ }
147
+ }
148
+ } else if (resultData !== null && typeof resultData === 'object') {
149
+ // Clone resultData to prevent mutations affecting the loop
150
+ const currentData = { ...resultData };
151
+
152
+ // iterate over each field and apply mutation to nested data models
153
+ for (const [key, value] of Object.entries(currentData)) {
154
+ const fieldDef = modelFields[key];
155
+ if (!fieldDef?.relation) {
156
+ continue;
157
+ }
158
+
159
+ const r = await doApplyMutation(
160
+ fieldDef.type,
161
+ value,
162
+ mutationModel,
163
+ mutationOp,
164
+ mutationArgs,
165
+ schema,
166
+ logging,
167
+ );
168
+
169
+ if (r && typeof r === 'object') {
170
+ resultData = { ...resultData, [key]: r };
171
+ updated = true;
172
+ }
173
+ }
174
+ }
175
+
176
+ return updated ? resultData : undefined;
177
+ }
178
+
179
+ function createMutate(queryModel: string, currentData: any, newData: any, schema: SchemaDef, logging: boolean) {
180
+ if (!newData) {
181
+ return undefined;
182
+ }
183
+
184
+ const modelFields = schema.models[queryModel]?.fields;
185
+ if (!modelFields) {
186
+ return undefined;
187
+ }
188
+
189
+ const insert: any = {};
190
+ const newDataFields = Object.keys(newData);
191
+
192
+ Object.entries(modelFields).forEach(([name, field]) => {
193
+ if (field.relation && newData[name]) {
194
+ // deal with "connect"
195
+ assignForeignKeyFields(field, insert, newData[name]);
196
+ return;
197
+ }
198
+
199
+ if (newDataFields.includes(name)) {
200
+ insert[name] = clone(newData[name]);
201
+ } else {
202
+ const defaultAttr = field.attributes?.find((attr) => attr.name === '@default');
203
+ if (field.type === 'DateTime') {
204
+ // default value for DateTime field
205
+ if (defaultAttr || field.attributes?.some((attr) => attr.name === '@updatedAt')) {
206
+ insert[name] = new Date();
207
+ return;
208
+ }
209
+ }
210
+
211
+ const defaultArg = defaultAttr?.args?.[0]?.value;
212
+ if (defaultArg?.kind === 'literal') {
213
+ // other default value
214
+ insert[name] = defaultArg.value;
215
+ }
216
+ }
217
+ });
218
+
219
+ // add temp id value
220
+ const idFields = getIdFields(schema, queryModel);
221
+ idFields.forEach((f) => {
222
+ if (insert[f.name] === undefined) {
223
+ if (f.type === 'Int' || f.type === 'BigInt') {
224
+ const currMax = Array.isArray(currentData)
225
+ ? Math.max(
226
+ ...[...currentData].map((item) => {
227
+ const idv = parseInt(item[f.name]);
228
+ return isNaN(idv) ? 0 : idv;
229
+ }),
230
+ )
231
+ : 0;
232
+ insert[f.name] = currMax + 1;
233
+ } else {
234
+ insert[f.name] = crypto.randomUUID();
235
+ }
236
+ }
237
+ });
238
+
239
+ insert.$optimistic = true;
240
+
241
+ if (logging) {
242
+ console.log(`Optimistic create for ${queryModel}:`, insert);
243
+ }
244
+ return [insert, ...(Array.isArray(currentData) ? currentData : [])];
245
+ }
246
+
247
+ function updateMutate(
248
+ queryModel: string,
249
+ currentData: any,
250
+ mutateModel: string,
251
+ mutateArgs: any,
252
+ schema: SchemaDef,
253
+ logging: boolean,
254
+ ) {
255
+ if (!currentData || typeof currentData !== 'object') {
256
+ return undefined;
257
+ }
258
+
259
+ if (!mutateArgs?.where || typeof mutateArgs.where !== 'object') {
260
+ return undefined;
261
+ }
262
+
263
+ if (!mutateArgs?.data || typeof mutateArgs.data !== 'object') {
264
+ return undefined;
265
+ }
266
+
267
+ if (!idFieldsMatch(mutateModel, currentData, mutateArgs.where, schema)) {
268
+ return undefined;
269
+ }
270
+
271
+ const modelFields = schema.models[queryModel]?.fields;
272
+ if (!modelFields) {
273
+ return undefined;
274
+ }
275
+
276
+ let updated = false;
277
+ let resultData = currentData;
278
+
279
+ for (const [key, value] of Object.entries<any>(mutateArgs.data)) {
280
+ const fieldInfo = modelFields[key];
281
+ if (!fieldInfo) {
282
+ continue;
283
+ }
284
+
285
+ if (fieldInfo.relation && !value?.connect) {
286
+ // relation field but without "connect"
287
+ continue;
288
+ }
289
+
290
+ if (!updated) {
291
+ // clone
292
+ resultData = { ...currentData };
293
+ }
294
+
295
+ if (fieldInfo.relation) {
296
+ // deal with "connect"
297
+ assignForeignKeyFields(fieldInfo, resultData, value);
298
+ } else {
299
+ resultData[key] = clone(value);
300
+ }
301
+ resultData.$optimistic = true;
302
+ updated = true;
303
+
304
+ if (logging) {
305
+ console.log(`Optimistic update for ${queryModel}:`, resultData);
306
+ }
307
+ }
308
+
309
+ return updated ? resultData : undefined;
310
+ }
311
+
312
+ function upsertMutate(
313
+ queryModel: string,
314
+ currentData: any,
315
+ model: string,
316
+ args: { where: object; create: any; update: any },
317
+ schema: SchemaDef,
318
+ logging: boolean,
319
+ ) {
320
+ let updated = false;
321
+ let resultData = currentData;
322
+
323
+ if (Array.isArray(resultData)) {
324
+ // check if we should create or update
325
+ const foundIndex = resultData.findIndex((x) => idFieldsMatch(model, x, args.where, schema));
326
+ if (foundIndex >= 0) {
327
+ const updateResult = updateMutate(
328
+ queryModel,
329
+ resultData[foundIndex],
330
+ model,
331
+ { where: args.where, data: args.update },
332
+ schema,
333
+ logging,
334
+ );
335
+ if (updateResult) {
336
+ // replace the found item with updated item
337
+ resultData = [...resultData.slice(0, foundIndex), updateResult, ...resultData.slice(foundIndex + 1)];
338
+ updated = true;
339
+ }
340
+ } else {
341
+ const createResult = createMutate(queryModel, resultData, args.create, schema, logging);
342
+ if (createResult) {
343
+ resultData = createResult;
344
+ updated = true;
345
+ }
346
+ }
347
+ } else {
348
+ // try update only
349
+ const updateResult = updateMutate(
350
+ queryModel,
351
+ resultData,
352
+ model,
353
+ { where: args.where, data: args.update },
354
+ schema,
355
+ logging,
356
+ );
357
+ if (updateResult) {
358
+ resultData = updateResult;
359
+ updated = true;
360
+ }
361
+ }
362
+
363
+ return updated ? resultData : undefined;
364
+ }
365
+
366
+ function deleteMutate(
367
+ queryModel: string,
368
+ currentData: any,
369
+ mutateModel: string,
370
+ mutateArgs: any,
371
+ schema: SchemaDef,
372
+ logging: boolean,
373
+ ) {
374
+ // TODO: handle mutation of nested reads?
375
+
376
+ if (!currentData || !mutateArgs) {
377
+ return undefined;
378
+ }
379
+
380
+ if (queryModel !== mutateModel) {
381
+ return undefined;
382
+ }
383
+
384
+ let updated = false;
385
+ let result = currentData;
386
+
387
+ if (Array.isArray(currentData)) {
388
+ for (const item of currentData) {
389
+ if (idFieldsMatch(mutateModel, item, mutateArgs, schema)) {
390
+ result = (result as unknown[]).filter((x) => x !== item);
391
+ updated = true;
392
+ if (logging) {
393
+ console.log(`Optimistic delete for ${queryModel}:`, item);
394
+ }
395
+ }
396
+ }
397
+ } else {
398
+ if (idFieldsMatch(mutateModel, currentData, mutateArgs, schema)) {
399
+ result = null;
400
+ updated = true;
401
+ if (logging) {
402
+ console.log(`Optimistic delete for ${queryModel}:`, currentData);
403
+ }
404
+ }
405
+ }
406
+
407
+ return updated ? result : undefined;
408
+ }
409
+
410
+ function idFieldsMatch(model: string, x: any, y: any, schema: SchemaDef) {
411
+ if (!x || !y || typeof x !== 'object' || typeof y !== 'object') {
412
+ return false;
413
+ }
414
+ const idFields = getIdFields(schema, model);
415
+ if (idFields.length === 0) {
416
+ return false;
417
+ }
418
+ return idFields.every((f) => x[f.name] === y[f.name]);
419
+ }
420
+
421
+ function assignForeignKeyFields(field: FieldDef, resultData: any, mutationData: any) {
422
+ // convert "connect" like `{ connect: { id: '...' } }` to foreign key fields
423
+ // assignment: `{ userId: '...' }`
424
+ if (!mutationData?.connect) {
425
+ return;
426
+ }
427
+
428
+ if (!field.relation?.fields || !field.relation.references) {
429
+ return;
430
+ }
431
+
432
+ for (const [idField, fkField] of zip(field.relation.references, field.relation.fields)) {
433
+ if (idField in mutationData.connect) {
434
+ resultData[fkField] = mutationData.connect[idField];
435
+ }
436
+ }
437
+ }
438
+
439
+ function getIdFields(schema: SchemaDef, model: string) {
440
+ return (schema.models[model]?.idFields ?? []).map((f) => schema.models[model]!.fields[f]!);
441
+ }
@@ -0,0 +1,61 @@
1
+ import type { FieldDef, SchemaDef } from '@zenstackhq/schema';
2
+
3
+ export type NestedReadVisitorCallback = {
4
+ field?: (
5
+ model: string,
6
+ field: FieldDef | undefined,
7
+ kind: 'include' | 'select' | undefined,
8
+ args: unknown,
9
+ ) => void | boolean;
10
+ };
11
+
12
+ /**
13
+ * Visitor for nested read payload.
14
+ */
15
+ export class NestedReadVisitor {
16
+ constructor(
17
+ private readonly schema: SchemaDef,
18
+ private readonly callback: NestedReadVisitorCallback,
19
+ ) {}
20
+
21
+ doVisit(model: string, field: FieldDef | undefined, kind: 'include' | 'select' | undefined, args: unknown) {
22
+ if (this.callback.field) {
23
+ const r = this.callback.field(model, field, kind, args);
24
+ if (r === false) {
25
+ return;
26
+ }
27
+ }
28
+
29
+ if (!args || typeof args !== 'object') {
30
+ return;
31
+ }
32
+
33
+ let selectInclude: any;
34
+ let nextKind: 'select' | 'include' | undefined;
35
+ if ((args as any).select) {
36
+ selectInclude = (args as any).select;
37
+ nextKind = 'select';
38
+ } else if ((args as any).include) {
39
+ selectInclude = (args as any).include;
40
+ nextKind = 'include';
41
+ }
42
+
43
+ if (selectInclude && typeof selectInclude === 'object') {
44
+ for (const [k, v] of Object.entries(selectInclude)) {
45
+ if (k === '_count' && typeof v === 'object' && v) {
46
+ // recurse into { _count: { ... } }
47
+ this.doVisit(model, field, kind, v);
48
+ } else {
49
+ const field = this.schema.models[model]?.fields[k];
50
+ if (field) {
51
+ this.doVisit(field.type, field, nextKind, v);
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ visit(model: string, args: unknown) {
59
+ this.doVisit(model, undefined, undefined, args);
60
+ }
61
+ }