@zenstackhq/client-helpers 3.1.1 → 3.2.1

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.
@@ -1,602 +0,0 @@
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
- });