@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,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
+ }