@deessejs/collections 0.0.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.
@@ -0,0 +1,1076 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { collection } from '../src/collection'
3
+ import { field } from '../src/field'
4
+ import { f } from '../src'
5
+ import { createCollectionOperations } from '../src/operations/collection-operations'
6
+
7
+ /**
8
+ * Mock table schema for testing
9
+ */
10
+ const mockTable = {
11
+ id: { name: 'id' },
12
+ name: { name: 'name' },
13
+ email: { name: 'email' },
14
+ active: { name: 'active' }
15
+ }
16
+
17
+ /**
18
+ * Create a mock database query builder that properly handles chaining
19
+ */
20
+ const createQueryBuilder = (baseReturn: unknown[], options: { limit?: number } = {}) => {
21
+ let returnValue = [...baseReturn]
22
+
23
+ const builder: any = {
24
+ where: vi.fn(() => builder),
25
+ limit: vi.fn((n: number) => {
26
+ returnValue = returnValue.slice(0, n)
27
+ return builder
28
+ }),
29
+ orderBy: vi.fn(() => builder),
30
+ offset: vi.fn((n: number) => {
31
+ returnValue = returnValue.slice(n)
32
+ return builder
33
+ }),
34
+ returning: vi.fn(() => returnValue)
35
+ }
36
+
37
+ // Make the builder thenable (for Promise support)
38
+ builder.then = (resolve: any) => resolve(returnValue)
39
+
40
+ return builder
41
+ }
42
+
43
+ /**
44
+ * Create a mock database
45
+ */
46
+ const createMockDb = (overrides: {
47
+ selectReturn?: unknown[]
48
+ insertReturn?: unknown[]
49
+ updateReturn?: unknown[]
50
+ deleteReturn?: unknown[]
51
+ } = {}) => {
52
+ const selectResult = overrides.selectReturn || []
53
+
54
+ return {
55
+ select: vi.fn(() => ({
56
+ from: vi.fn(() => createQueryBuilder(selectResult))
57
+ })),
58
+ insert: vi.fn(() => ({
59
+ values: vi.fn(() => createQueryBuilder(overrides.insertReturn || []))
60
+ })),
61
+ update: vi.fn(() => ({
62
+ set: vi.fn(() => ({
63
+ where: vi.fn(() => createQueryBuilder(overrides.updateReturn || []))
64
+ }))
65
+ })),
66
+ delete: vi.fn(() => ({
67
+ where: vi.fn(() => createQueryBuilder(overrides.deleteReturn || []))
68
+ }))
69
+ }
70
+ }
71
+
72
+ describe('hooks execution', () => {
73
+ describe('create operations', () => {
74
+ it('executes hooks in correct order for create operation', async () => {
75
+ const executionOrder: string[] = []
76
+
77
+ const users = collection({
78
+ slug: 'users',
79
+ fields: {
80
+ name: field({ fieldType: f.text() }),
81
+ email: field({ fieldType: f.text() })
82
+ },
83
+ hooks: {
84
+ beforeOperation: [async () => { executionOrder.push('beforeOperation') }],
85
+ beforeCreate: [async () => { executionOrder.push('beforeCreate') }],
86
+ afterCreate: [async () => { executionOrder.push('afterCreate') }],
87
+ afterOperation: [async () => { executionOrder.push('afterOperation') }]
88
+ }
89
+ })
90
+
91
+ const mockDb = createMockDb({
92
+ insertReturn: [{ id: 1, name: 'John', email: 'john@example.com' }]
93
+ })
94
+
95
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
96
+
97
+ await operations.create({
98
+ data: { name: 'John', email: 'john@example.com' },
99
+ returning: true
100
+ })
101
+
102
+ expect(executionOrder).toEqual([
103
+ 'beforeOperation',
104
+ 'beforeCreate',
105
+ 'afterCreate',
106
+ 'afterOperation'
107
+ ])
108
+ })
109
+
110
+ it('passes correct context to beforeOperation hook on create', async () => {
111
+ let receivedContext: any = null
112
+
113
+ const users = collection({
114
+ slug: 'users',
115
+ fields: {
116
+ name: field({ fieldType: f.text() })
117
+ },
118
+ hooks: {
119
+ beforeOperation: [async (context) => { receivedContext = context }]
120
+ }
121
+ })
122
+
123
+ const mockDb = createMockDb({
124
+ insertReturn: [{ id: 1, name: 'John' }]
125
+ })
126
+
127
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
128
+
129
+ await operations.create({
130
+ data: { name: 'John' }
131
+ })
132
+
133
+ expect(receivedContext).toEqual({
134
+ collection: 'users',
135
+ operation: 'create',
136
+ data: { name: 'John' },
137
+ where: undefined
138
+ })
139
+ })
140
+
141
+ it('passes correct context to beforeCreate hook on create', async () => {
142
+ let receivedContext: any = null
143
+
144
+ const users = collection({
145
+ slug: 'users',
146
+ fields: {
147
+ name: field({ fieldType: f.text() })
148
+ },
149
+ hooks: {
150
+ beforeCreate: [async (context) => { receivedContext = context }]
151
+ }
152
+ })
153
+
154
+ const mockDb = createMockDb({
155
+ insertReturn: [{ id: 1, name: 'John' }]
156
+ })
157
+
158
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
159
+
160
+ await operations.create({
161
+ data: { name: 'John' }
162
+ })
163
+
164
+ expect(receivedContext).toMatchObject({
165
+ collection: 'users',
166
+ operation: 'create',
167
+ data: { name: 'John' }
168
+ })
169
+ expect(receivedContext.db).toBeDefined()
170
+ })
171
+
172
+ it('passes correct context to afterCreate hook on create', async () => {
173
+ let receivedContext: any = null
174
+
175
+ const users = collection({
176
+ slug: 'users',
177
+ fields: {
178
+ name: field({ fieldType: f.text() })
179
+ },
180
+ hooks: {
181
+ afterCreate: [async (context) => { receivedContext = context }]
182
+ }
183
+ })
184
+
185
+ const mockDb = createMockDb({
186
+ insertReturn: [{ id: 1, name: 'John' }]
187
+ })
188
+
189
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
190
+
191
+ await operations.create({
192
+ data: { name: 'John' },
193
+ returning: true
194
+ })
195
+
196
+ expect(receivedContext).toMatchObject({
197
+ collection: 'users',
198
+ operation: 'create',
199
+ data: { name: 'John' }
200
+ })
201
+ expect(receivedContext.result).toEqual({ id: 1, name: 'John' })
202
+ })
203
+
204
+ it('executes multiple hooks in order', async () => {
205
+ const executionOrder: string[] = []
206
+
207
+ const users = collection({
208
+ slug: 'users',
209
+ fields: {
210
+ name: field({ fieldType: f.text() })
211
+ },
212
+ hooks: {
213
+ beforeCreate: [
214
+ async () => { executionOrder.push('hook1') },
215
+ async () => { executionOrder.push('hook2') },
216
+ async () => { executionOrder.push('hook3') }
217
+ ]
218
+ }
219
+ })
220
+
221
+ const mockDb = createMockDb({
222
+ insertReturn: [{ id: 1, name: 'John' }]
223
+ })
224
+
225
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
226
+
227
+ await operations.create({
228
+ data: { name: 'John' }
229
+ })
230
+
231
+ expect(executionOrder).toEqual(['hook1', 'hook2', 'hook3'])
232
+ })
233
+
234
+ it('supports synchronous hooks', async () => {
235
+ let hookCalled = false
236
+
237
+ const users = collection({
238
+ slug: 'users',
239
+ fields: {
240
+ name: field({ fieldType: f.text() })
241
+ },
242
+ hooks: {
243
+ beforeCreate: [() => { hookCalled = true }]
244
+ }
245
+ })
246
+
247
+ const mockDb = createMockDb({
248
+ insertReturn: [{ id: 1, name: 'John' }]
249
+ })
250
+
251
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
252
+
253
+ await operations.create({
254
+ data: { name: 'John' }
255
+ })
256
+
257
+ expect(hookCalled).toBe(true)
258
+ })
259
+
260
+ it('allows hooks to modify data before create', async () => {
261
+ let capturedData: any = null
262
+
263
+ const users = collection({
264
+ slug: 'users',
265
+ fields: {
266
+ name: field({ fieldType: f.text() }),
267
+ email: field({ fieldType: f.text() })
268
+ },
269
+ hooks: {
270
+ beforeCreate: [async (context) => {
271
+ context.data.email = 'modified@example.com'
272
+ capturedData = context.data
273
+ }]
274
+ }
275
+ })
276
+
277
+ const mockDb = createMockDb({
278
+ insertReturn: [{ id: 1, name: 'John', email: 'modified@example.com' }]
279
+ })
280
+
281
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
282
+
283
+ await operations.create({
284
+ data: { name: 'John', email: 'original@example.com' }
285
+ })
286
+
287
+ expect(capturedData.email).toBe('modified@example.com')
288
+ })
289
+ })
290
+
291
+ describe('update operations', () => {
292
+ it('executes hooks in correct order for update operation', async () => {
293
+ const executionOrder: string[] = []
294
+
295
+ const users = collection({
296
+ slug: 'users',
297
+ fields: {
298
+ name: field({ fieldType: f.text() })
299
+ },
300
+ hooks: {
301
+ beforeOperation: [async () => { executionOrder.push('beforeOperation') }],
302
+ beforeUpdate: [async () => { executionOrder.push('beforeUpdate') }],
303
+ afterUpdate: [async () => { executionOrder.push('afterUpdate') }],
304
+ afterOperation: [async () => { executionOrder.push('afterOperation') }]
305
+ }
306
+ })
307
+
308
+ const mockDb = createMockDb({
309
+ selectReturn: [{ id: 1, name: 'Old Name' }],
310
+ updateReturn: [{ id: 1, name: 'New Name' }]
311
+ })
312
+
313
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
314
+
315
+ await operations.update({
316
+ where: { id: 1 },
317
+ data: { name: 'New Name' },
318
+ returning: true
319
+ })
320
+
321
+ expect(executionOrder).toEqual([
322
+ 'beforeOperation',
323
+ 'beforeUpdate',
324
+ 'afterUpdate',
325
+ 'afterOperation'
326
+ ])
327
+ })
328
+
329
+ it('passes previousData to beforeUpdate hook', async () => {
330
+ let receivedContext: any = null
331
+
332
+ const users = collection({
333
+ slug: 'users',
334
+ fields: {
335
+ name: field({ fieldType: f.text() })
336
+ },
337
+ hooks: {
338
+ beforeUpdate: [async (context) => { receivedContext = context }]
339
+ }
340
+ })
341
+
342
+ const mockDb = createMockDb({
343
+ selectReturn: [{ id: 1, name: 'Old Name' }],
344
+ updateReturn: [{ id: 1, name: 'New Name' }]
345
+ })
346
+
347
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
348
+
349
+ await operations.update({
350
+ where: { id: 1 },
351
+ data: { name: 'New Name' }
352
+ })
353
+
354
+ expect(receivedContext.previousData).toEqual({ id: 1, name: 'Old Name' })
355
+ })
356
+
357
+ it('passes previousData to afterUpdate hook', async () => {
358
+ let receivedContext: any = null
359
+
360
+ const users = collection({
361
+ slug: 'users',
362
+ fields: {
363
+ name: field({ fieldType: f.text() })
364
+ },
365
+ hooks: {
366
+ afterUpdate: [async (context) => { receivedContext = context }]
367
+ }
368
+ })
369
+
370
+ const mockDb = createMockDb({
371
+ selectReturn: [{ id: 1, name: 'Old Name' }],
372
+ updateReturn: [{ id: 1, name: 'New Name' }]
373
+ })
374
+
375
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
376
+
377
+ await operations.update({
378
+ where: { id: 1 },
379
+ data: { name: 'New Name' },
380
+ returning: true
381
+ })
382
+
383
+ expect(receivedContext.previousData).toEqual({ id: 1, name: 'Old Name' })
384
+ })
385
+
386
+ it('passes correct context to beforeOperation hook on update', async () => {
387
+ let receivedContext: any = null
388
+
389
+ const users = collection({
390
+ slug: 'users',
391
+ fields: {
392
+ name: field({ fieldType: f.text() })
393
+ },
394
+ hooks: {
395
+ beforeOperation: [async (context) => { receivedContext = context }]
396
+ }
397
+ })
398
+
399
+ const mockDb = createMockDb({
400
+ selectReturn: [{ id: 1, name: 'Old' }],
401
+ updateReturn: [{ id: 1, name: 'New' }]
402
+ })
403
+
404
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
405
+
406
+ await operations.update({
407
+ where: { id: 1 },
408
+ data: { name: 'New' }
409
+ })
410
+
411
+ expect(receivedContext).toEqual({
412
+ collection: 'users',
413
+ operation: 'update',
414
+ data: { name: 'New' },
415
+ where: { id: 1 }
416
+ })
417
+ })
418
+ })
419
+
420
+ describe('delete operations', () => {
421
+ it('executes hooks in correct order for delete operation', async () => {
422
+ const executionOrder: string[] = []
423
+
424
+ const users = collection({
425
+ slug: 'users',
426
+ fields: {
427
+ name: field({ fieldType: f.text() })
428
+ },
429
+ hooks: {
430
+ beforeOperation: [async () => { executionOrder.push('beforeOperation') }],
431
+ beforeDelete: [async () => { executionOrder.push('beforeDelete') }],
432
+ afterDelete: [async () => { executionOrder.push('afterDelete') }],
433
+ afterOperation: [async () => { executionOrder.push('afterOperation') }]
434
+ }
435
+ })
436
+
437
+ const mockDb = createMockDb({
438
+ selectReturn: [{ id: 1, name: 'John' }],
439
+ deleteReturn: [{ id: 1, name: 'John' }]
440
+ })
441
+
442
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
443
+
444
+ await operations.delete({
445
+ where: { id: 1 },
446
+ returning: true
447
+ })
448
+
449
+ expect(executionOrder).toEqual([
450
+ 'beforeOperation',
451
+ 'beforeDelete',
452
+ 'afterDelete',
453
+ 'afterOperation'
454
+ ])
455
+ })
456
+
457
+ it('passes previousData to beforeDelete hook', async () => {
458
+ let receivedContext: any = null
459
+
460
+ const users = collection({
461
+ slug: 'users',
462
+ fields: {
463
+ name: field({ fieldType: f.text() })
464
+ },
465
+ hooks: {
466
+ beforeDelete: [async (context) => { receivedContext = context }]
467
+ }
468
+ })
469
+
470
+ const mockDb = createMockDb({
471
+ selectReturn: [{ id: 1, name: 'John' }],
472
+ deleteReturn: [{ id: 1, name: 'John' }]
473
+ })
474
+
475
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
476
+
477
+ await operations.delete({
478
+ where: { id: 1 }
479
+ })
480
+
481
+ expect(receivedContext.previousData).toEqual({ id: 1, name: 'John' })
482
+ })
483
+
484
+ it('passes previousData to afterDelete hook', async () => {
485
+ let receivedContext: any = null
486
+
487
+ const users = collection({
488
+ slug: 'users',
489
+ fields: {
490
+ name: field({ fieldType: f.text() })
491
+ },
492
+ hooks: {
493
+ afterDelete: [async (context) => { receivedContext = context }]
494
+ }
495
+ })
496
+
497
+ const mockDb = createMockDb({
498
+ selectReturn: [{ id: 1, name: 'John' }],
499
+ deleteReturn: [{ id: 1, name: 'John' }]
500
+ })
501
+
502
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
503
+
504
+ await operations.delete({
505
+ where: { id: 1 },
506
+ returning: true
507
+ })
508
+
509
+ expect(receivedContext.previousData).toEqual({ id: 1, name: 'John' })
510
+ })
511
+
512
+ it('passes correct context to beforeOperation hook on delete', async () => {
513
+ let receivedContext: any = null
514
+
515
+ const users = collection({
516
+ slug: 'users',
517
+ fields: {
518
+ name: field({ fieldType: f.text() })
519
+ },
520
+ hooks: {
521
+ beforeOperation: [async (context) => { receivedContext = context }]
522
+ }
523
+ })
524
+
525
+ const mockDb = createMockDb({
526
+ selectReturn: [{ id: 1, name: 'John' }],
527
+ deleteReturn: [{ id: 1, name: 'John' }]
528
+ })
529
+
530
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
531
+
532
+ await operations.delete({
533
+ where: { id: 1 }
534
+ })
535
+
536
+ expect(receivedContext).toEqual({
537
+ collection: 'users',
538
+ operation: 'delete',
539
+ where: { id: 1 },
540
+ data: undefined
541
+ })
542
+ })
543
+ })
544
+
545
+ describe('read operations', () => {
546
+ it('executes hooks in correct order for findMany operation', async () => {
547
+ const executionOrder: string[] = []
548
+
549
+ const users = collection({
550
+ slug: 'users',
551
+ fields: {
552
+ name: field({ fieldType: f.text() })
553
+ },
554
+ hooks: {
555
+ beforeOperation: [async () => { executionOrder.push('beforeOperation') }],
556
+ beforeRead: [async () => { executionOrder.push('beforeRead') }],
557
+ afterRead: [async () => { executionOrder.push('afterRead') }],
558
+ afterOperation: [async () => { executionOrder.push('afterOperation') }]
559
+ }
560
+ })
561
+
562
+ const mockDb = createMockDb({
563
+ selectReturn: [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
564
+ })
565
+
566
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
567
+
568
+ await operations.findMany()
569
+
570
+ expect(executionOrder).toEqual([
571
+ 'beforeOperation',
572
+ 'beforeRead',
573
+ 'afterRead',
574
+ 'afterOperation'
575
+ ])
576
+ })
577
+
578
+ it('passes correct context to beforeOperation hook on read', async () => {
579
+ let receivedContext: any = null
580
+
581
+ const users = collection({
582
+ slug: 'users',
583
+ fields: {
584
+ name: field({ fieldType: f.text() })
585
+ },
586
+ hooks: {
587
+ beforeOperation: [async (context) => { receivedContext = context }]
588
+ }
589
+ })
590
+
591
+ const mockDb = createMockDb({
592
+ selectReturn: [{ id: 1, name: 'John' }]
593
+ })
594
+
595
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
596
+
597
+ await operations.findMany({
598
+ where: { name: { contains: 'John' } }
599
+ })
600
+
601
+ expect(receivedContext).toEqual({
602
+ collection: 'users',
603
+ operation: 'read',
604
+ where: { name: { contains: 'John' } }
605
+ })
606
+ })
607
+
608
+ it('passes query to beforeRead hook', async () => {
609
+ let receivedContext: any = null
610
+
611
+ const users = collection({
612
+ slug: 'users',
613
+ fields: {
614
+ name: field({ fieldType: f.text() })
615
+ },
616
+ hooks: {
617
+ beforeRead: [async (context) => { receivedContext = context }]
618
+ }
619
+ })
620
+
621
+ const mockDb = createMockDb({
622
+ selectReturn: [{ id: 1, name: 'John' }]
623
+ })
624
+
625
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
626
+
627
+ await operations.findMany({
628
+ where: { name: 'John' },
629
+ limit: 10,
630
+ offset: 5
631
+ })
632
+
633
+ expect(receivedContext.query).toMatchObject({
634
+ where: { name: 'John' },
635
+ limit: 10,
636
+ offset: 5
637
+ })
638
+ })
639
+
640
+ it('passes result to afterRead hook', async () => {
641
+ let receivedContext: any = null
642
+
643
+ const users = collection({
644
+ slug: 'users',
645
+ fields: {
646
+ name: field({ fieldType: f.text() })
647
+ },
648
+ hooks: {
649
+ afterRead: [async (context) => { receivedContext = context }]
650
+ }
651
+ })
652
+
653
+ const mockDb = createMockDb({
654
+ selectReturn: [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
655
+ })
656
+
657
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
658
+
659
+ const result = await operations.findMany()
660
+
661
+ expect(receivedContext.result).toEqual([
662
+ { id: 1, name: 'John' },
663
+ { id: 2, name: 'Jane' }
664
+ ])
665
+ })
666
+
667
+ it('executes hooks for findUnique operation', async () => {
668
+ const executionOrder: string[] = []
669
+
670
+ const users = collection({
671
+ slug: 'users',
672
+ fields: {
673
+ name: field({ fieldType: f.text() })
674
+ },
675
+ hooks: {
676
+ beforeOperation: [async () => { executionOrder.push('beforeOperation') }],
677
+ beforeRead: [async () => { executionOrder.push('beforeRead') }],
678
+ afterRead: [async () => { executionOrder.push('afterRead') }],
679
+ afterOperation: [async () => { executionOrder.push('afterOperation') }]
680
+ }
681
+ })
682
+
683
+ const mockDb = createMockDb({
684
+ selectReturn: [{ id: 1, name: 'John' }]
685
+ })
686
+
687
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
688
+
689
+ await operations.findUnique({
690
+ where: { id: 1 }
691
+ })
692
+
693
+ expect(executionOrder).toEqual([
694
+ 'beforeOperation',
695
+ 'beforeRead',
696
+ 'afterRead',
697
+ 'afterOperation'
698
+ ])
699
+ })
700
+
701
+ it('executes hooks for findFirst operation', async () => {
702
+ const executionOrder: string[] = []
703
+
704
+ const users = collection({
705
+ slug: 'users',
706
+ fields: {
707
+ name: field({ fieldType: f.text() })
708
+ },
709
+ hooks: {
710
+ beforeOperation: [async () => { executionOrder.push('beforeOperation') }],
711
+ beforeRead: [async () => { executionOrder.push('beforeRead') }],
712
+ afterRead: [async () => { executionOrder.push('afterRead') }],
713
+ afterOperation: [async () => { executionOrder.push('afterOperation') }]
714
+ }
715
+ })
716
+
717
+ const mockDb = createMockDb({
718
+ selectReturn: [{ id: 1, name: 'John' }]
719
+ })
720
+
721
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
722
+
723
+ await operations.findFirst({
724
+ where: { name: 'John' }
725
+ })
726
+
727
+ expect(executionOrder).toEqual([
728
+ 'beforeOperation',
729
+ 'beforeRead',
730
+ 'afterRead',
731
+ 'afterOperation'
732
+ ])
733
+ })
734
+
735
+ it('executes hooks for count operation', async () => {
736
+ const executionOrder: string[] = []
737
+
738
+ const users = collection({
739
+ slug: 'users',
740
+ fields: {
741
+ name: field({ fieldType: f.text() })
742
+ },
743
+ hooks: {
744
+ beforeOperation: [async () => { executionOrder.push('beforeOperation') }],
745
+ beforeRead: [async () => { executionOrder.push('beforeRead') }],
746
+ afterRead: [async () => { executionOrder.push('afterRead') }],
747
+ afterOperation: [async () => { executionOrder.push('afterOperation') }]
748
+ }
749
+ })
750
+
751
+ const mockDb = createMockDb({
752
+ selectReturn: [{ id: 1 }, { id: 2 }, { id: 3 }]
753
+ })
754
+
755
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
756
+
757
+ await operations.count()
758
+
759
+ expect(executionOrder).toEqual([
760
+ 'beforeOperation',
761
+ 'beforeRead',
762
+ 'afterRead',
763
+ 'afterOperation'
764
+ ])
765
+ })
766
+
767
+ it('executes hooks for exists operation', async () => {
768
+ const executionOrder: string[] = []
769
+
770
+ const users = collection({
771
+ slug: 'users',
772
+ fields: {
773
+ name: field({ fieldType: f.text() })
774
+ },
775
+ hooks: {
776
+ beforeOperation: [async () => { executionOrder.push('beforeOperation') }],
777
+ beforeRead: [async () => { executionOrder.push('beforeRead') }],
778
+ afterRead: [async () => { executionOrder.push('afterRead') }],
779
+ afterOperation: [async () => { executionOrder.push('afterOperation') }]
780
+ }
781
+ })
782
+
783
+ const mockDb = createMockDb({
784
+ selectReturn: [{ id: 1, name: 'John' }]
785
+ })
786
+
787
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
788
+
789
+ await operations.exists({
790
+ where: { id: 1 }
791
+ })
792
+
793
+ expect(executionOrder).toEqual([
794
+ 'beforeOperation',
795
+ 'beforeRead',
796
+ 'afterRead',
797
+ 'afterOperation'
798
+ ])
799
+ })
800
+ })
801
+
802
+ describe('createMany operations', () => {
803
+ it('executes hooks for each item in createMany', async () => {
804
+ const executionOrder: string[] = []
805
+
806
+ const users = collection({
807
+ slug: 'users',
808
+ fields: {
809
+ name: field({ fieldType: f.text() })
810
+ },
811
+ hooks: {
812
+ beforeOperation: [async (context) => { executionOrder.push(`beforeOperation-${context.data.name}`) }],
813
+ beforeCreate: [async (context) => { executionOrder.push(`beforeCreate-${context.data.name}`) }],
814
+ afterCreate: [async (context) => { executionOrder.push(`afterCreate-${context.data.name}`) }],
815
+ afterOperation: [async (context) => { executionOrder.push(`afterOperation-${context.data.name}`) }]
816
+ }
817
+ })
818
+
819
+ const mockDb = createMockDb({
820
+ insertReturn: [
821
+ { id: 1, name: 'John' },
822
+ { id: 2, name: 'Jane' }
823
+ ]
824
+ })
825
+
826
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
827
+
828
+ await operations.createMany({
829
+ data: [
830
+ { name: 'John' },
831
+ { name: 'Jane' }
832
+ ]
833
+ })
834
+
835
+ expect(executionOrder).toEqual([
836
+ 'beforeOperation-John',
837
+ 'beforeCreate-John',
838
+ 'beforeOperation-Jane',
839
+ 'beforeCreate-Jane',
840
+ 'afterCreate-John',
841
+ 'afterOperation-John',
842
+ 'afterCreate-Jane',
843
+ 'afterOperation-Jane'
844
+ ])
845
+ })
846
+ })
847
+
848
+ describe('updateMany operations', () => {
849
+ it('executes hooks for each item in updateMany', async () => {
850
+ const executionOrder: string[] = []
851
+
852
+ const users = collection({
853
+ slug: 'users',
854
+ fields: {
855
+ name: field({ fieldType: f.text() })
856
+ },
857
+ hooks: {
858
+ beforeUpdate: [async (context) => {
859
+ executionOrder.push(`beforeUpdate-${context.previousData?.name}`)
860
+ }],
861
+ afterUpdate: [async (context) => {
862
+ executionOrder.push(`afterUpdate-${context.previousData?.name}`)
863
+ }]
864
+ }
865
+ })
866
+
867
+ const mockDb = createMockDb({
868
+ selectReturn: [
869
+ { id: 1, name: 'John' },
870
+ { id: 2, name: 'Jane' }
871
+ ]
872
+ })
873
+
874
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
875
+
876
+ await operations.updateMany({
877
+ where: { active: true },
878
+ data: { active: false }
879
+ })
880
+
881
+ expect(executionOrder).toEqual([
882
+ 'beforeUpdate-John',
883
+ 'beforeUpdate-Jane',
884
+ 'afterUpdate-John',
885
+ 'afterUpdate-Jane'
886
+ ])
887
+ })
888
+ })
889
+
890
+ describe('deleteMany operations', () => {
891
+ it('executes hooks for each item in deleteMany', async () => {
892
+ const executionOrder: string[] = []
893
+
894
+ const users = collection({
895
+ slug: 'users',
896
+ fields: {
897
+ name: field({ fieldType: f.text() })
898
+ },
899
+ hooks: {
900
+ beforeDelete: [async (context) => {
901
+ executionOrder.push(`beforeDelete-${context.previousData?.name}`)
902
+ }],
903
+ afterDelete: [async (context) => {
904
+ executionOrder.push(`afterDelete-${context.previousData?.name}`)
905
+ }]
906
+ }
907
+ })
908
+
909
+ const mockDb = createMockDb({
910
+ selectReturn: [
911
+ { id: 1, name: 'John' },
912
+ { id: 2, name: 'Jane' }
913
+ ]
914
+ })
915
+
916
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
917
+
918
+ await operations.deleteMany({
919
+ where: { active: false }
920
+ })
921
+
922
+ expect(executionOrder).toEqual([
923
+ 'beforeDelete-John',
924
+ 'beforeDelete-Jane',
925
+ 'afterDelete-John',
926
+ 'afterDelete-Jane'
927
+ ])
928
+ })
929
+ })
930
+
931
+ describe('hooks without db', () => {
932
+ it('returns placeholder operations when db is not provided', async () => {
933
+ const users = collection({
934
+ slug: 'users',
935
+ fields: {
936
+ name: field({ fieldType: f.text() })
937
+ },
938
+ hooks: {
939
+ beforeCreate: [async () => { throw new Error('Should not be called') }]
940
+ }
941
+ })
942
+
943
+ const operations = createCollectionOperations(users, 'users', null, mockTable, users.hooks)
944
+
945
+ const result = await operations.findMany()
946
+ expect(result).toEqual([])
947
+
948
+ const unique = await operations.findUnique({ where: { id: 1 } })
949
+ expect(unique).toBeUndefined()
950
+
951
+ const created = await operations.create({ data: { name: 'John' } })
952
+ expect(created).toBeUndefined()
953
+
954
+ const count = await operations.count()
955
+ expect(count).toBe(0)
956
+
957
+ const exists = await operations.exists({ where: { id: 1 } })
958
+ expect(exists).toBe(false)
959
+ })
960
+ })
961
+
962
+ describe('error handling in hooks', () => {
963
+ it('throws error when beforeOperation hook throws', async () => {
964
+ const users = collection({
965
+ slug: 'users',
966
+ fields: {
967
+ name: field({ fieldType: f.text() })
968
+ },
969
+ hooks: {
970
+ beforeOperation: [async () => { throw new Error('Hook error') }]
971
+ }
972
+ })
973
+
974
+ const mockDb = createMockDb({
975
+ selectReturn: [{ id: 1, name: 'John' }]
976
+ })
977
+
978
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
979
+
980
+ await expect(operations.findMany()).rejects.toThrow('Hook error')
981
+ })
982
+
983
+ it('throws error when beforeCreate hook throws', async () => {
984
+ const users = collection({
985
+ slug: 'users',
986
+ fields: {
987
+ name: field({ fieldType: f.text() })
988
+ },
989
+ hooks: {
990
+ beforeCreate: [async () => { throw new Error('Create hook error') }]
991
+ }
992
+ })
993
+
994
+ const mockDb = createMockDb({
995
+ insertReturn: [{ id: 1, name: 'John' }]
996
+ })
997
+
998
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
999
+
1000
+ await expect(operations.create({ data: { name: 'John' } })).rejects.toThrow('Create hook error')
1001
+ })
1002
+
1003
+ it('throws error when afterOperation hook throws', async () => {
1004
+ const users = collection({
1005
+ slug: 'users',
1006
+ fields: {
1007
+ name: field({ fieldType: f.text() })
1008
+ },
1009
+ hooks: {
1010
+ afterOperation: [async () => { throw new Error('After hook error') }]
1011
+ }
1012
+ })
1013
+
1014
+ const mockDb = createMockDb({
1015
+ selectReturn: [{ id: 1, name: 'John' }]
1016
+ })
1017
+
1018
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
1019
+
1020
+ await expect(operations.findMany()).rejects.toThrow('After hook error')
1021
+ })
1022
+ })
1023
+
1024
+ describe('afterOperation hook context', () => {
1025
+ it('passes result to afterOperation hook on create', async () => {
1026
+ let receivedContext: any = null
1027
+
1028
+ const users = collection({
1029
+ slug: 'users',
1030
+ fields: {
1031
+ name: field({ fieldType: f.text() })
1032
+ },
1033
+ hooks: {
1034
+ afterOperation: [async (context) => { receivedContext = context }]
1035
+ }
1036
+ })
1037
+
1038
+ const mockDb = createMockDb({
1039
+ insertReturn: [{ id: 1, name: 'John' }]
1040
+ })
1041
+
1042
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
1043
+
1044
+ await operations.create({
1045
+ data: { name: 'John' },
1046
+ returning: true
1047
+ })
1048
+
1049
+ expect(receivedContext.result).toEqual({ id: 1, name: 'John' })
1050
+ })
1051
+
1052
+ it('passes result to afterOperation hook on read', async () => {
1053
+ let receivedContext: any = null
1054
+
1055
+ const users = collection({
1056
+ slug: 'users',
1057
+ fields: {
1058
+ name: field({ fieldType: f.text() })
1059
+ },
1060
+ hooks: {
1061
+ afterOperation: [async (context) => { receivedContext = context }]
1062
+ }
1063
+ })
1064
+
1065
+ const mockDb = createMockDb({
1066
+ selectReturn: [{ id: 1, name: 'John' }]
1067
+ })
1068
+
1069
+ const operations = createCollectionOperations(users, 'users', mockDb, mockTable, users.hooks)
1070
+
1071
+ await operations.findMany()
1072
+
1073
+ expect(receivedContext.result).toEqual([{ id: 1, name: 'John' }])
1074
+ })
1075
+ })
1076
+ })