@zenstackhq/client-helpers 3.1.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/.turbo/turbo-build.log +24 -0
- package/LICENSE +21 -0
- package/dist/fetch.d.ts +35 -0
- package/dist/fetch.js +101 -0
- package/dist/fetch.js.map +1 -0
- package/dist/index.d.ts +278 -0
- package/dist/index.js +822 -0
- package/dist/index.js.map +1 -0
- package/eslint.config.js +4 -0
- package/package.json +40 -0
- package/src/constants.ts +4 -0
- package/src/fetch.ts +107 -0
- package/src/index.ts +9 -0
- package/src/invalidation.ts +89 -0
- package/src/logging.ts +15 -0
- package/src/mutator.ts +449 -0
- package/src/nested-read-visitor.ts +68 -0
- package/src/nested-write-visitor.ts +359 -0
- package/src/optimistic.ts +139 -0
- package/src/query-analysis.ts +111 -0
- package/src/types.ts +82 -0
- package/test/fetch.test.ts +423 -0
- package/test/invalidation.test.ts +602 -0
- package/test/mutator.test.ts +1533 -0
- package/test/nested-read-visitor.test.ts +949 -0
- package/test/nested-write-visitor.test.ts +1244 -0
- package/test/optimistic.test.ts +743 -0
- package/test/query-analysis.test.ts +1399 -0
- package/test/test-helpers.ts +37 -0
- package/tsconfig.json +4 -0
- package/tsconfig.test.json +7 -0
- package/tsup.config.ts +14 -0
- package/vitest.config.ts +4 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createInvalidator } from '../src/invalidation';
|
|
3
|
+
import type { Logger } from '../src/logging';
|
|
4
|
+
import { createField, createRelationField, createSchema } from './test-helpers';
|
|
5
|
+
|
|
6
|
+
describe('Invalidation tests', () => {
|
|
7
|
+
describe('createInvalidator', () => {
|
|
8
|
+
it('creates an invalidator function that invalidates the mutated model', async () => {
|
|
9
|
+
const schema = createSchema({
|
|
10
|
+
User: {
|
|
11
|
+
name: 'User',
|
|
12
|
+
fields: {
|
|
13
|
+
id: createField('id', 'String'),
|
|
14
|
+
name: createField('name', 'String'),
|
|
15
|
+
},
|
|
16
|
+
uniqueFields: {},
|
|
17
|
+
idFields: ['id'],
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
let capturedPredicate: any;
|
|
22
|
+
const invalidatorMock = vi.fn((predicate) => {
|
|
23
|
+
capturedPredicate = predicate;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const invalidator = createInvalidator('User', 'create', schema, invalidatorMock, undefined);
|
|
27
|
+
|
|
28
|
+
// Call the invalidator with mutation result and variables
|
|
29
|
+
const result = { id: '1', name: 'John' };
|
|
30
|
+
const variables = { data: { name: 'John' } };
|
|
31
|
+
await invalidator(result, variables);
|
|
32
|
+
|
|
33
|
+
// Invalidator should have been called
|
|
34
|
+
expect(invalidatorMock).toHaveBeenCalledTimes(1);
|
|
35
|
+
expect(invalidatorMock).toHaveBeenCalledWith(expect.any(Function));
|
|
36
|
+
|
|
37
|
+
// Test the predicate
|
|
38
|
+
expect(capturedPredicate({ model: 'User', args: {} })).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('invalidates nested models from mutation', async () => {
|
|
42
|
+
const schema = createSchema({
|
|
43
|
+
User: {
|
|
44
|
+
name: 'User',
|
|
45
|
+
fields: {
|
|
46
|
+
id: createField('id', 'String'),
|
|
47
|
+
posts: createRelationField('posts', 'Post'),
|
|
48
|
+
},
|
|
49
|
+
uniqueFields: {},
|
|
50
|
+
idFields: ['id'],
|
|
51
|
+
},
|
|
52
|
+
Post: {
|
|
53
|
+
name: 'Post',
|
|
54
|
+
fields: {
|
|
55
|
+
id: createField('id', 'String'),
|
|
56
|
+
title: createField('title', 'String'),
|
|
57
|
+
},
|
|
58
|
+
uniqueFields: {},
|
|
59
|
+
idFields: ['id'],
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let capturedPredicate: any;
|
|
64
|
+
const invalidatorMock = vi.fn((predicate) => {
|
|
65
|
+
capturedPredicate = predicate;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const invalidator = createInvalidator('User', 'create', schema, invalidatorMock, undefined);
|
|
69
|
+
|
|
70
|
+
// Create user with nested post
|
|
71
|
+
await invalidator(
|
|
72
|
+
{},
|
|
73
|
+
{
|
|
74
|
+
data: {
|
|
75
|
+
name: 'John',
|
|
76
|
+
posts: {
|
|
77
|
+
create: { title: 'My Post' },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Should invalidate both User and Post
|
|
84
|
+
expect(capturedPredicate({ model: 'User', args: {} })).toBe(true);
|
|
85
|
+
expect(capturedPredicate({ model: 'Post', args: {} })).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('works with undefined logging', async () => {
|
|
89
|
+
const schema = createSchema({
|
|
90
|
+
User: {
|
|
91
|
+
name: 'User',
|
|
92
|
+
fields: {
|
|
93
|
+
id: createField('id', 'String'),
|
|
94
|
+
},
|
|
95
|
+
uniqueFields: {},
|
|
96
|
+
idFields: ['id'],
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const invalidatorMock = vi.fn();
|
|
101
|
+
const invalidator = createInvalidator('User', 'create', schema, invalidatorMock, undefined);
|
|
102
|
+
|
|
103
|
+
await invalidator({}, { data: {} });
|
|
104
|
+
|
|
105
|
+
expect(invalidatorMock).toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('logs when logger is provided', async () => {
|
|
109
|
+
const schema = createSchema({
|
|
110
|
+
User: {
|
|
111
|
+
name: 'User',
|
|
112
|
+
fields: {
|
|
113
|
+
id: createField('id', 'String'),
|
|
114
|
+
},
|
|
115
|
+
uniqueFields: {},
|
|
116
|
+
idFields: ['id'],
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const loggerMock = vi.fn() as Logger;
|
|
121
|
+
let capturedPredicate: any;
|
|
122
|
+
const invalidatorMock = vi.fn((predicate) => {
|
|
123
|
+
capturedPredicate = predicate;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const invalidator = createInvalidator('User', 'create', schema, invalidatorMock, loggerMock);
|
|
127
|
+
|
|
128
|
+
await invalidator({}, { data: { name: 'John' } });
|
|
129
|
+
|
|
130
|
+
// Execute the predicate to trigger logging
|
|
131
|
+
capturedPredicate({ model: 'User', args: {} });
|
|
132
|
+
|
|
133
|
+
// Logger should have been called
|
|
134
|
+
expect(loggerMock).toHaveBeenCalledWith(expect.stringContaining('Marking "User" query for invalidation'));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('handles multiple mutations with different operations', async () => {
|
|
138
|
+
const schema = createSchema({
|
|
139
|
+
User: {
|
|
140
|
+
name: 'User',
|
|
141
|
+
fields: {
|
|
142
|
+
id: createField('id', 'String'),
|
|
143
|
+
name: createField('name', 'String'),
|
|
144
|
+
},
|
|
145
|
+
uniqueFields: {},
|
|
146
|
+
idFields: ['id'],
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const capturedPredicates: any[] = [];
|
|
151
|
+
const invalidatorMock = vi.fn((predicate) => {
|
|
152
|
+
capturedPredicates.push(predicate);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Create invalidators for different operations
|
|
156
|
+
const createInvalidatorFn = createInvalidator('User', 'create', schema, invalidatorMock, undefined);
|
|
157
|
+
const updateInvalidatorFn = createInvalidator('User', 'update', schema, invalidatorMock, undefined);
|
|
158
|
+
const deleteInvalidatorFn = createInvalidator('User', 'delete', schema, invalidatorMock, undefined);
|
|
159
|
+
|
|
160
|
+
// Execute each invalidator
|
|
161
|
+
await createInvalidatorFn({}, { data: { name: 'John' } });
|
|
162
|
+
await updateInvalidatorFn({}, { where: { id: '1' }, data: { name: 'Jane' } });
|
|
163
|
+
await deleteInvalidatorFn({}, { where: { id: '1' } });
|
|
164
|
+
|
|
165
|
+
// All should invalidate User queries
|
|
166
|
+
expect(capturedPredicates).toHaveLength(3);
|
|
167
|
+
capturedPredicates.forEach((predicate) => {
|
|
168
|
+
expect(predicate({ model: 'User', args: {} })).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('handles cascade deletes correctly', async () => {
|
|
173
|
+
const schema = createSchema({
|
|
174
|
+
User: {
|
|
175
|
+
name: 'User',
|
|
176
|
+
fields: {
|
|
177
|
+
id: createField('id', 'String'),
|
|
178
|
+
},
|
|
179
|
+
uniqueFields: {},
|
|
180
|
+
idFields: ['id'],
|
|
181
|
+
},
|
|
182
|
+
Post: {
|
|
183
|
+
name: 'Post',
|
|
184
|
+
fields: {
|
|
185
|
+
id: createField('id', 'String'),
|
|
186
|
+
user: {
|
|
187
|
+
name: 'user',
|
|
188
|
+
type: 'User',
|
|
189
|
+
optional: false,
|
|
190
|
+
relation: {
|
|
191
|
+
opposite: 'posts',
|
|
192
|
+
onDelete: 'Cascade',
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
uniqueFields: {},
|
|
197
|
+
idFields: ['id'],
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
let capturedPredicate: any;
|
|
202
|
+
const invalidatorMock = vi.fn((predicate) => {
|
|
203
|
+
capturedPredicate = predicate;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const invalidator = createInvalidator('User', 'delete', schema, invalidatorMock, undefined);
|
|
207
|
+
|
|
208
|
+
await invalidator({}, { where: { id: '1' } });
|
|
209
|
+
|
|
210
|
+
// Should invalidate both User and Post (cascade)
|
|
211
|
+
expect(capturedPredicate({ model: 'User', args: {} })).toBe(true);
|
|
212
|
+
expect(capturedPredicate({ model: 'Post', args: {} })).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('handles base model inheritance', async () => {
|
|
216
|
+
const schema = createSchema({
|
|
217
|
+
Animal: {
|
|
218
|
+
name: 'Animal',
|
|
219
|
+
fields: {
|
|
220
|
+
id: createField('id', 'String'),
|
|
221
|
+
name: createField('name', 'String'),
|
|
222
|
+
},
|
|
223
|
+
uniqueFields: {},
|
|
224
|
+
idFields: ['id'],
|
|
225
|
+
},
|
|
226
|
+
Dog: {
|
|
227
|
+
name: 'Dog',
|
|
228
|
+
baseModel: 'Animal',
|
|
229
|
+
fields: {
|
|
230
|
+
id: createField('id', 'String'),
|
|
231
|
+
breed: createField('breed', 'String'),
|
|
232
|
+
},
|
|
233
|
+
uniqueFields: {},
|
|
234
|
+
idFields: ['id'],
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
let capturedPredicate: any;
|
|
239
|
+
const invalidatorMock = vi.fn((predicate) => {
|
|
240
|
+
capturedPredicate = predicate;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const invalidator = createInvalidator('Dog', 'create', schema, invalidatorMock, undefined);
|
|
244
|
+
|
|
245
|
+
await invalidator({}, { data: { breed: 'Labrador' } });
|
|
246
|
+
|
|
247
|
+
// Should invalidate both Dog and Animal (base)
|
|
248
|
+
expect(capturedPredicate({ model: 'Dog', args: {} })).toBe(true);
|
|
249
|
+
expect(capturedPredicate({ model: 'Animal', args: {} })).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('handles async invalidator function', async () => {
|
|
253
|
+
const schema = createSchema({
|
|
254
|
+
User: {
|
|
255
|
+
name: 'User',
|
|
256
|
+
fields: {
|
|
257
|
+
id: createField('id', 'String'),
|
|
258
|
+
},
|
|
259
|
+
uniqueFields: {},
|
|
260
|
+
idFields: ['id'],
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const invalidatorMock = vi.fn(async () => {
|
|
265
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const invalidator = createInvalidator('User', 'create', schema, invalidatorMock, undefined);
|
|
269
|
+
|
|
270
|
+
await invalidator({}, { data: {} });
|
|
271
|
+
|
|
272
|
+
expect(invalidatorMock).toHaveBeenCalled();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('passes correct predicate for nested reads', async () => {
|
|
276
|
+
const schema = createSchema({
|
|
277
|
+
User: {
|
|
278
|
+
name: 'User',
|
|
279
|
+
fields: {
|
|
280
|
+
id: createField('id', 'String'),
|
|
281
|
+
posts: createRelationField('posts', 'Post'),
|
|
282
|
+
},
|
|
283
|
+
uniqueFields: {},
|
|
284
|
+
idFields: ['id'],
|
|
285
|
+
},
|
|
286
|
+
Post: {
|
|
287
|
+
name: 'Post',
|
|
288
|
+
fields: {
|
|
289
|
+
id: createField('id', 'String'),
|
|
290
|
+
title: createField('title', 'String'),
|
|
291
|
+
},
|
|
292
|
+
uniqueFields: {},
|
|
293
|
+
idFields: ['id'],
|
|
294
|
+
},
|
|
295
|
+
Profile: {
|
|
296
|
+
name: 'Profile',
|
|
297
|
+
fields: {
|
|
298
|
+
id: createField('id', 'String'),
|
|
299
|
+
bio: createField('bio', 'String'),
|
|
300
|
+
},
|
|
301
|
+
uniqueFields: {},
|
|
302
|
+
idFields: ['id'],
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
let capturedPredicate: any;
|
|
307
|
+
const invalidatorMock = vi.fn((predicate) => {
|
|
308
|
+
capturedPredicate = predicate;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const invalidator = createInvalidator('Post', 'create', schema, invalidatorMock, undefined);
|
|
312
|
+
|
|
313
|
+
await invalidator({}, { data: { title: 'New Post' } });
|
|
314
|
+
|
|
315
|
+
// Should invalidate User queries that include posts
|
|
316
|
+
expect(
|
|
317
|
+
capturedPredicate({
|
|
318
|
+
model: 'User',
|
|
319
|
+
args: {
|
|
320
|
+
include: { posts: true },
|
|
321
|
+
},
|
|
322
|
+
}),
|
|
323
|
+
).toBe(true);
|
|
324
|
+
|
|
325
|
+
// Should not invalidate User queries without posts
|
|
326
|
+
expect(
|
|
327
|
+
capturedPredicate({
|
|
328
|
+
model: 'User',
|
|
329
|
+
args: {
|
|
330
|
+
select: { id: true },
|
|
331
|
+
},
|
|
332
|
+
}),
|
|
333
|
+
).toBe(false);
|
|
334
|
+
|
|
335
|
+
// Should not invalidate unrelated Profile queries
|
|
336
|
+
expect(capturedPredicate({ model: 'Profile', args: {} })).toBe(false);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('handles undefined mutation variables', async () => {
|
|
340
|
+
const schema = createSchema({
|
|
341
|
+
User: {
|
|
342
|
+
name: 'User',
|
|
343
|
+
fields: {
|
|
344
|
+
id: createField('id', 'String'),
|
|
345
|
+
},
|
|
346
|
+
uniqueFields: {},
|
|
347
|
+
idFields: ['id'],
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
let capturedPredicate: any;
|
|
352
|
+
const invalidatorMock = vi.fn((predicate) => {
|
|
353
|
+
capturedPredicate = predicate;
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const invalidator = createInvalidator('User', 'create', schema, invalidatorMock, undefined);
|
|
357
|
+
|
|
358
|
+
await invalidator({}, undefined);
|
|
359
|
+
|
|
360
|
+
// Should still invalidate User queries
|
|
361
|
+
expect(capturedPredicate({ model: 'User', args: {} })).toBe(true);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('uses the second argument as variables', async () => {
|
|
365
|
+
const schema = createSchema({
|
|
366
|
+
User: {
|
|
367
|
+
name: 'User',
|
|
368
|
+
fields: {
|
|
369
|
+
id: createField('id', 'String'),
|
|
370
|
+
posts: createRelationField('posts', 'Post'),
|
|
371
|
+
},
|
|
372
|
+
uniqueFields: {},
|
|
373
|
+
idFields: ['id'],
|
|
374
|
+
},
|
|
375
|
+
Post: {
|
|
376
|
+
name: 'Post',
|
|
377
|
+
fields: {
|
|
378
|
+
id: createField('id', 'String'),
|
|
379
|
+
title: createField('title', 'String'),
|
|
380
|
+
},
|
|
381
|
+
uniqueFields: {},
|
|
382
|
+
idFields: ['id'],
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
let capturedPredicate: any;
|
|
387
|
+
const invalidatorMock = vi.fn((predicate) => {
|
|
388
|
+
capturedPredicate = predicate;
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const invalidator = createInvalidator('User', 'create', schema, invalidatorMock, undefined);
|
|
392
|
+
|
|
393
|
+
// First argument is typically the mutation result, second is variables
|
|
394
|
+
const result = { id: '1', name: 'John' };
|
|
395
|
+
const variables = {
|
|
396
|
+
data: {
|
|
397
|
+
name: 'John',
|
|
398
|
+
posts: {
|
|
399
|
+
create: { title: 'Post' },
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
await invalidator(result, variables);
|
|
405
|
+
|
|
406
|
+
// Should pick up the nested Post from variables
|
|
407
|
+
expect(capturedPredicate({ model: 'Post', args: {} })).toBe(true);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe('real-world scenarios', () => {
|
|
412
|
+
it('handles blog post creation with multiple relations', async () => {
|
|
413
|
+
const schema = createSchema({
|
|
414
|
+
User: {
|
|
415
|
+
name: 'User',
|
|
416
|
+
fields: {
|
|
417
|
+
id: createField('id', 'String'),
|
|
418
|
+
posts: createRelationField('posts', 'Post'),
|
|
419
|
+
},
|
|
420
|
+
uniqueFields: {},
|
|
421
|
+
idFields: ['id'],
|
|
422
|
+
},
|
|
423
|
+
Post: {
|
|
424
|
+
name: 'Post',
|
|
425
|
+
fields: {
|
|
426
|
+
id: createField('id', 'String'),
|
|
427
|
+
author: createRelationField('author', 'User'),
|
|
428
|
+
tags: createRelationField('tags', 'Tag'),
|
|
429
|
+
comments: createRelationField('comments', 'Comment'),
|
|
430
|
+
},
|
|
431
|
+
uniqueFields: {},
|
|
432
|
+
idFields: ['id'],
|
|
433
|
+
},
|
|
434
|
+
Tag: {
|
|
435
|
+
name: 'Tag',
|
|
436
|
+
fields: {
|
|
437
|
+
id: createField('id', 'String'),
|
|
438
|
+
name: createField('name', 'String'),
|
|
439
|
+
},
|
|
440
|
+
uniqueFields: {},
|
|
441
|
+
idFields: ['id'],
|
|
442
|
+
},
|
|
443
|
+
Comment: {
|
|
444
|
+
name: 'Comment',
|
|
445
|
+
fields: {
|
|
446
|
+
id: createField('id', 'String'),
|
|
447
|
+
text: createField('text', 'String'),
|
|
448
|
+
},
|
|
449
|
+
uniqueFields: {},
|
|
450
|
+
idFields: ['id'],
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
let capturedPredicate: any;
|
|
455
|
+
const invalidatorMock = vi.fn((predicate) => {
|
|
456
|
+
capturedPredicate = predicate;
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const invalidator = createInvalidator('Post', 'create', schema, invalidatorMock, undefined);
|
|
460
|
+
|
|
461
|
+
await invalidator(
|
|
462
|
+
{},
|
|
463
|
+
{
|
|
464
|
+
data: {
|
|
465
|
+
title: 'My Post',
|
|
466
|
+
author: { connect: { id: '1' } },
|
|
467
|
+
tags: {
|
|
468
|
+
create: [{ name: 'tech' }],
|
|
469
|
+
},
|
|
470
|
+
comments: {
|
|
471
|
+
create: { text: 'First!' },
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
// Should invalidate all involved models
|
|
478
|
+
expect(capturedPredicate({ model: 'Post', args: {} })).toBe(true);
|
|
479
|
+
expect(capturedPredicate({ model: 'User', args: { include: { posts: true } } })).toBe(true);
|
|
480
|
+
expect(capturedPredicate({ model: 'Tag', args: {} })).toBe(true);
|
|
481
|
+
expect(capturedPredicate({ model: 'Comment', args: {} })).toBe(true);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('handles complex update with disconnect and delete', async () => {
|
|
485
|
+
const schema = createSchema({
|
|
486
|
+
User: {
|
|
487
|
+
name: 'User',
|
|
488
|
+
fields: {
|
|
489
|
+
id: createField('id', 'String'),
|
|
490
|
+
posts: createRelationField('posts', 'Post'),
|
|
491
|
+
},
|
|
492
|
+
uniqueFields: {},
|
|
493
|
+
idFields: ['id'],
|
|
494
|
+
},
|
|
495
|
+
Post: {
|
|
496
|
+
name: 'Post',
|
|
497
|
+
fields: {
|
|
498
|
+
id: createField('id', 'String'),
|
|
499
|
+
user: {
|
|
500
|
+
name: 'user',
|
|
501
|
+
type: 'User',
|
|
502
|
+
optional: false,
|
|
503
|
+
relation: {
|
|
504
|
+
opposite: 'posts',
|
|
505
|
+
onDelete: 'Cascade',
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
comments: createRelationField('comments', 'Comment'),
|
|
509
|
+
},
|
|
510
|
+
uniqueFields: {},
|
|
511
|
+
idFields: ['id'],
|
|
512
|
+
},
|
|
513
|
+
Comment: {
|
|
514
|
+
name: 'Comment',
|
|
515
|
+
fields: {
|
|
516
|
+
id: createField('id', 'String'),
|
|
517
|
+
post: {
|
|
518
|
+
name: 'post',
|
|
519
|
+
type: 'Post',
|
|
520
|
+
optional: false,
|
|
521
|
+
relation: {
|
|
522
|
+
opposite: 'comments',
|
|
523
|
+
onDelete: 'Cascade',
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
uniqueFields: {},
|
|
528
|
+
idFields: ['id'],
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
let capturedPredicate: any;
|
|
533
|
+
const invalidatorMock = vi.fn((predicate) => {
|
|
534
|
+
capturedPredicate = predicate;
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const invalidator = createInvalidator('User', 'update', schema, invalidatorMock, undefined);
|
|
538
|
+
|
|
539
|
+
await invalidator(
|
|
540
|
+
{},
|
|
541
|
+
{
|
|
542
|
+
where: { id: '1' },
|
|
543
|
+
data: {
|
|
544
|
+
posts: {
|
|
545
|
+
disconnect: { id: '1' },
|
|
546
|
+
delete: { id: '2' }, // Will cascade to comments
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
// Should invalidate all three models
|
|
553
|
+
expect(capturedPredicate({ model: 'User', args: {} })).toBe(true);
|
|
554
|
+
expect(capturedPredicate({ model: 'Post', args: {} })).toBe(true);
|
|
555
|
+
expect(capturedPredicate({ model: 'Comment', args: {} })).toBe(true); // cascade delete
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('integrates with query library invalidation flow', async () => {
|
|
559
|
+
const schema = createSchema({
|
|
560
|
+
User: {
|
|
561
|
+
name: 'User',
|
|
562
|
+
fields: {
|
|
563
|
+
id: createField('id', 'String'),
|
|
564
|
+
name: createField('name', 'String'),
|
|
565
|
+
},
|
|
566
|
+
uniqueFields: {},
|
|
567
|
+
idFields: ['id'],
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Simulate a query library's invalidation mechanism
|
|
572
|
+
const queries = [
|
|
573
|
+
{ queryKey: ['User', 'findMany', {}], model: 'User', args: {} },
|
|
574
|
+
{
|
|
575
|
+
queryKey: ['User', 'findUnique', { where: { id: '1' } }],
|
|
576
|
+
model: 'User',
|
|
577
|
+
args: { where: { id: '1' } },
|
|
578
|
+
},
|
|
579
|
+
{ queryKey: ['Post', 'findMany', {}], model: 'Post', args: {} },
|
|
580
|
+
];
|
|
581
|
+
|
|
582
|
+
const invalidatedQueries: any[] = [];
|
|
583
|
+
const queryLibraryInvalidate = vi.fn((predicate) => {
|
|
584
|
+
queries.forEach((query) => {
|
|
585
|
+
if (predicate({ model: query.model, args: query.args })) {
|
|
586
|
+
invalidatedQueries.push(query.queryKey);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
const invalidator = createInvalidator('User', 'create', schema, queryLibraryInvalidate, undefined);
|
|
592
|
+
|
|
593
|
+
await invalidator({}, { data: { name: 'John' } });
|
|
594
|
+
|
|
595
|
+
// Should only invalidate User queries
|
|
596
|
+
expect(invalidatedQueries).toHaveLength(2);
|
|
597
|
+
expect(invalidatedQueries).toContainEqual(['User', 'findMany', {}]);
|
|
598
|
+
expect(invalidatedQueries).toContainEqual(['User', 'findUnique', { where: { id: '1' } }]);
|
|
599
|
+
expect(invalidatedQueries).not.toContainEqual(['Post', 'findMany', {}]);
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
});
|