@declaro/data 2.0.0-beta.98 → 2.0.0-beta.99

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 (33) hide show
  1. package/dist/browser/index.js +7 -7
  2. package/dist/browser/index.js.map +3 -3
  3. package/dist/node/index.cjs +59 -17
  4. package/dist/node/index.cjs.map +3 -3
  5. package/dist/node/index.js +59 -17
  6. package/dist/node/index.js.map +3 -3
  7. package/dist/ts/test/mock/repositories/mock-memory-repository.assign.test.d.ts +2 -0
  8. package/dist/ts/test/mock/repositories/mock-memory-repository.assign.test.d.ts.map +1 -0
  9. package/dist/ts/test/mock/repositories/mock-memory-repository.basic.test.d.ts +2 -0
  10. package/dist/ts/test/mock/repositories/mock-memory-repository.basic.test.d.ts.map +1 -0
  11. package/dist/ts/test/mock/repositories/mock-memory-repository.bulk-upsert.test.d.ts +2 -0
  12. package/dist/ts/test/mock/repositories/mock-memory-repository.bulk-upsert.test.d.ts.map +1 -0
  13. package/dist/ts/test/mock/repositories/mock-memory-repository.count.test.d.ts +2 -0
  14. package/dist/ts/test/mock/repositories/mock-memory-repository.count.test.d.ts.map +1 -0
  15. package/dist/ts/test/mock/repositories/mock-memory-repository.custom-lookup.test.d.ts +1 -0
  16. package/dist/ts/test/mock/repositories/mock-memory-repository.custom-lookup.test.d.ts.map +1 -0
  17. package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts.map +1 -1
  18. package/dist/ts/test/mock/repositories/mock-memory-repository.search.test.d.ts +2 -0
  19. package/dist/ts/test/mock/repositories/mock-memory-repository.search.test.d.ts.map +1 -0
  20. package/dist/ts/test/mock/repositories/mock-memory-repository.upsert.test.d.ts +2 -0
  21. package/dist/ts/test/mock/repositories/mock-memory-repository.upsert.test.d.ts.map +1 -0
  22. package/package.json +5 -5
  23. package/src/test/mock/repositories/mock-memory-repository.assign.test.ts +215 -0
  24. package/src/test/mock/repositories/mock-memory-repository.basic.test.ts +129 -0
  25. package/src/test/mock/repositories/mock-memory-repository.bulk-upsert.test.ts +159 -0
  26. package/src/test/mock/repositories/mock-memory-repository.count.test.ts +98 -0
  27. package/src/test/mock/repositories/mock-memory-repository.custom-lookup.test.ts +0 -0
  28. package/src/test/mock/repositories/mock-memory-repository.search.test.ts +265 -0
  29. package/src/test/mock/repositories/mock-memory-repository.ts +67 -16
  30. package/src/test/mock/repositories/mock-memory-repository.upsert.test.ts +108 -0
  31. package/dist/ts/test/mock/repositories/mock-memory-repository.test.d.ts +0 -2
  32. package/dist/ts/test/mock/repositories/mock-memory-repository.test.d.ts.map +0 -1
  33. package/src/test/mock/repositories/mock-memory-repository.test.ts +0 -919
@@ -1,919 +0,0 @@
1
- import { beforeEach, describe, expect, it, mock } from 'bun:test'
2
- import { MockBookSchema } from '../models/mock-book-models'
3
- import { MockMemoryRepository } from './mock-memory-repository'
4
- import { z } from 'zod/v4'
5
- import { ZodModel } from '@declaro/zod'
6
-
7
- describe('MockMemoryRepository', () => {
8
- const mockSchema = MockBookSchema
9
-
10
- let repository: MockMemoryRepository<typeof mockSchema>
11
-
12
- beforeEach(() => {
13
- repository = new MockMemoryRepository({ schema: mockSchema })
14
- })
15
-
16
- it('should create an item', async () => {
17
- const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
18
- const createdItem = await repository.create(input)
19
-
20
- expect(createdItem).toEqual(input)
21
- expect(await repository.load({ id: createdItem.id })).toEqual(createdItem)
22
- })
23
-
24
- it('should throw an error when creating an item with duplicate primary key', async () => {
25
- const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
26
- const createdItem = await repository.create(input)
27
-
28
- await expect(repository.create({ ...input, id: createdItem.id })).rejects.toThrow(
29
- 'Item with the same primary key already exists',
30
- )
31
- })
32
-
33
- it('should update an existing item', async () => {
34
- const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
35
- const createdItem = await repository.create(input)
36
-
37
- const updatedInput = { title: 'Updated Book', author: 'Updated Author', publishedDate: new Date() }
38
- const updatedItem = await repository.update({ id: createdItem.id }, updatedInput)
39
-
40
- expect(updatedItem).toEqual({ id: createdItem.id, ...updatedInput })
41
- expect(await repository.load({ id: createdItem.id })).toEqual({ id: createdItem.id, ...updatedInput })
42
- })
43
-
44
- it('should throw an error when updating a non-existent item', async () => {
45
- const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
46
-
47
- await expect(repository.update({ id: 999 }, input)).rejects.toThrow('Item not found')
48
- })
49
-
50
- it('should remove an item', async () => {
51
- const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
52
- const createdItem = await repository.create(input)
53
-
54
- const removedItem = await repository.remove({ id: createdItem.id })
55
-
56
- expect(removedItem).toEqual(input)
57
- const loadedItem = await repository.load({ id: createdItem.id })
58
- expect(loadedItem).toBeNull()
59
- })
60
-
61
- it('should restore a removed item', async () => {
62
- const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
63
- const createdItem = await repository.create(input)
64
- await repository.remove({ id: createdItem.id })
65
-
66
- const restoredItem = await repository.restore({ id: createdItem.id })
67
-
68
- expect(restoredItem).toEqual(input)
69
- expect(await repository.load({ id: createdItem.id })).toEqual(input)
70
- })
71
-
72
- it('should throw an error when restoring a non-existent item', async () => {
73
- await expect(repository.restore({ id: 999 })).rejects.toThrow('Item not found in trash')
74
- })
75
-
76
- it('should allow me to create an item without a primary key', async () => {
77
- const input = { title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
78
- const createdItem = await repository.create(input)
79
-
80
- expect(createdItem.id).toBeDefined()
81
- expect(createdItem.title).toBe(input.title)
82
- expect(createdItem.author).toBe(input.author)
83
- expect(await repository.load({ id: createdItem.id })).toEqual(createdItem)
84
- })
85
-
86
- it('should increment ids when creating items without a primary key', async () => {
87
- const input1 = { title: 'Test Book 1', author: 'Author Name 1', publishedDate: new Date() }
88
- const input2 = { title: 'Test Book 2', author: 'Author Name 2', publishedDate: new Date() }
89
-
90
- const createdItem1 = await repository.create(input1)
91
- const createdItem2 = await repository.create(input2)
92
-
93
- expect(createdItem1.id).toBe(1)
94
- expect(createdItem2.id).toBe(2)
95
- expect(await repository.load({ id: createdItem1.id })).toEqual(createdItem1)
96
- expect(await repository.load({ id: createdItem2.id })).toEqual(createdItem2)
97
- })
98
-
99
- it('should return null when loading a non-existent item', async () => {
100
- const result = await repository.load({ id: 999 })
101
- expect(result).toBeNull()
102
- })
103
-
104
- it('should be able to load items from a custom filter', async () => {
105
- // Creating a hypothetical schema with a custom filter, and a title lookup attribute
106
- const repository = new MockMemoryRepository({
107
- schema: mockSchema.custom({
108
- lookup: (h) =>
109
- new ZodModel(
110
- h.name,
111
- z.object({
112
- id: z.number().optional(),
113
- title: z.string().optional(),
114
- }),
115
- ),
116
- }),
117
- lookup: (data, lookup) =>
118
- data.id === lookup.id || data.title?.toLowerCase() === lookup.title?.toLowerCase(),
119
- })
120
-
121
- const input = { title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
122
- const createdItem = await repository.create(input)
123
-
124
- const loadedItem = await repository.load({ id: createdItem.id })
125
- const titleLoadedItem = await repository.load({ title: createdItem.title })
126
- expect(loadedItem).toEqual(createdItem)
127
- expect(titleLoadedItem).toEqual(createdItem)
128
- })
129
-
130
- describe('search functionality', () => {
131
- it('should search for all items when no filter is provided', async () => {
132
- const input1 = { title: 'Book One', author: 'Author A', publishedDate: new Date('2023-01-01') }
133
- const input2 = { title: 'Book Two', author: 'Author B', publishedDate: new Date('2023-02-01') }
134
- const input3 = { title: 'Book Three', author: 'Author C', publishedDate: new Date('2023-03-01') }
135
-
136
- const item1 = await repository.create(input1)
137
- const item2 = await repository.create(input2)
138
- const item3 = await repository.create(input3)
139
-
140
- const results = await repository.search({})
141
-
142
- expect(results.results).toHaveLength(3)
143
- expect(results.results).toEqual(expect.arrayContaining([item1, item2, item3]))
144
- expect(results.pagination.total).toBe(3)
145
- expect(results.pagination.page).toBe(1)
146
- expect(results.pagination.pageSize).toBe(25)
147
- expect(results.pagination.totalPages).toBe(1)
148
- })
149
-
150
- it('should filter items using custom filter function', async () => {
151
- const repositoryWithFilter = new MockMemoryRepository({
152
- schema: mockSchema,
153
- filter: (data, filters) => {
154
- if (filters.text) {
155
- return (
156
- data.title.toLowerCase().includes(filters.text.toLowerCase()) ||
157
- data.author.toLowerCase().includes(filters.text.toLowerCase())
158
- )
159
- }
160
- return true
161
- },
162
- })
163
-
164
- const input1 = { title: 'JavaScript Guide', author: 'John Doe', publishedDate: new Date() }
165
- const input2 = { title: 'Python Handbook', author: 'Jane Smith', publishedDate: new Date() }
166
- const input3 = { title: 'Java Programming', author: 'John Johnson', publishedDate: new Date() }
167
-
168
- const item1 = await repositoryWithFilter.create(input1)
169
- const item2 = await repositoryWithFilter.create(input2)
170
- const item3 = await repositoryWithFilter.create(input3)
171
-
172
- // Search by title
173
- const titleResults = await repositoryWithFilter.search({ text: 'JavaScript' })
174
- expect(titleResults.results).toEqual([item1])
175
-
176
- // Search by author
177
- const authorResults = await repositoryWithFilter.search({ text: 'John' })
178
- expect(authorResults.results).toHaveLength(2)
179
- expect(authorResults.results).toEqual(expect.arrayContaining([item1, item3]))
180
-
181
- // Search with no matches
182
- const noResults = await repositoryWithFilter.search({ text: 'Ruby' })
183
- expect(noResults.results).toEqual([])
184
- expect(noResults.pagination.total).toBe(0)
185
- })
186
-
187
- it('should handle pagination correctly', async () => {
188
- // Create 10 items
189
- for (let i = 1; i <= 10; i++) {
190
- const input = { title: `Book ${i}`, author: `Author ${i}`, publishedDate: new Date() }
191
- await repository.create(input)
192
- }
193
-
194
- // Test first page with pageSize 3
195
- const page1 = await repository.search(
196
- {},
197
- {
198
- pagination: { page: 1, pageSize: 3 },
199
- },
200
- )
201
- expect(page1.results).toHaveLength(3)
202
- expect(page1.pagination.page).toBe(1)
203
- expect(page1.pagination.pageSize).toBe(3)
204
- expect(page1.pagination.total).toBe(10)
205
- expect(page1.pagination.totalPages).toBe(4)
206
-
207
- // Test second page
208
- const page2 = await repository.search(
209
- {},
210
- {
211
- pagination: { page: 2, pageSize: 3 },
212
- },
213
- )
214
- expect(page2.results).toHaveLength(3)
215
- expect(page2.pagination.page).toBe(2)
216
- expect(page2.pagination.pageSize).toBe(3)
217
-
218
- // Test last page (should have 1 item)
219
- const page4 = await repository.search(
220
- {},
221
- {
222
- pagination: { page: 4, pageSize: 3 },
223
- },
224
- )
225
- expect(page4.results).toHaveLength(1)
226
- expect(page4.pagination.page).toBe(4)
227
-
228
- // Test page beyond available data
229
- const pageEmpty = await repository.search(
230
- {},
231
- {
232
- pagination: { page: 5, pageSize: 3 },
233
- },
234
- )
235
- expect(pageEmpty.results).toHaveLength(0)
236
- })
237
-
238
- it('should use default pagination when not provided', async () => {
239
- // Create 30 items to test default pagination
240
- for (let i = 1; i <= 30; i++) {
241
- await repository.create({ title: `Book ${i}`, author: `Author ${i}`, publishedDate: new Date() })
242
- }
243
-
244
- const results = await repository.search({})
245
- expect(results.pagination.page).toBe(1)
246
- expect(results.pagination.pageSize).toBe(25)
247
- expect(results.pagination.total).toBe(30)
248
- expect(results.pagination.totalPages).toBe(2)
249
- expect(results.results).toHaveLength(25)
250
- })
251
-
252
- it('should handle edge cases with pagination', async () => {
253
- // Test with empty repository
254
- const emptyResults = await repository.search(
255
- {},
256
- {
257
- pagination: { page: 1, pageSize: 10 },
258
- },
259
- )
260
- expect(emptyResults.results).toEqual([])
261
- expect(emptyResults.pagination.total).toBe(0)
262
- expect(emptyResults.pagination.totalPages).toBe(0)
263
-
264
- // Test with page 0 (results in empty due to slice calculation)
265
- await repository.create({ title: 'Test Book', author: 'Test Author', publishedDate: new Date() })
266
- const page0Results = await repository.search(
267
- {},
268
- {
269
- pagination: { page: 0, pageSize: 10 },
270
- },
271
- )
272
- expect(page0Results.pagination.page).toBe(0)
273
- expect(page0Results.results).toHaveLength(0) // slice(-10, 0) returns empty array
274
- })
275
-
276
- it('should handle sorting correctly', async () => {
277
- const input1 = { title: 'C Book', author: 'Author Z', publishedDate: new Date('2023-03-01') }
278
- const input2 = { title: 'A Book', author: 'Author Y', publishedDate: new Date('2023-01-01') }
279
- const input3 = { title: 'B Book', author: 'Author X', publishedDate: new Date('2023-02-01') }
280
-
281
- const item1 = await repository.create(input1)
282
- const item2 = await repository.create(input2)
283
- const item3 = await repository.create(input3)
284
-
285
- // Sort by title ascending
286
- const titleAscResults = await repository.search(
287
- {},
288
- {
289
- sort: [{ title: 'asc' }],
290
- },
291
- )
292
- expect(titleAscResults.results.map((r) => r.title)).toEqual(['A Book', 'B Book', 'C Book'])
293
-
294
- // Sort by title descending
295
- const titleDescResults = await repository.search(
296
- {},
297
- {
298
- sort: [{ title: 'desc' }],
299
- },
300
- )
301
- expect(titleDescResults.results.map((r) => r.title)).toEqual(['C Book', 'B Book', 'A Book'])
302
-
303
- // Sort by author ascending
304
- const authorAscResults = await repository.search(
305
- {},
306
- {
307
- sort: [{ author: 'asc' }],
308
- },
309
- )
310
- expect(authorAscResults.results.map((r) => r.author)).toEqual(['Author X', 'Author Y', 'Author Z'])
311
-
312
- // Multiple field sort: title asc, then author desc
313
- const multiSortResults = await repository.search(
314
- {},
315
- {
316
- sort: [{ title: 'asc' }, { author: 'desc' }],
317
- },
318
- )
319
- expect(multiSortResults.results.map((r) => r.title)).toEqual(['A Book', 'B Book', 'C Book'])
320
- })
321
-
322
- it('should handle sorting with pagination', async () => {
323
- for (let i = 1; i <= 5; i++) {
324
- await repository.create({
325
- title: `Book ${String.fromCharCode(69 - i)}`, // D, C, B, A, @
326
- author: `Author ${i}`,
327
- publishedDate: new Date(),
328
- })
329
- }
330
-
331
- // Sort by title ascending and get first page
332
- const sortedPage1 = await repository.search(
333
- {},
334
- {
335
- sort: [{ title: 'asc' }],
336
- pagination: { page: 1, pageSize: 2 },
337
- },
338
- )
339
- expect(sortedPage1.results.map((r) => r.title)).toEqual(['Book @', 'Book A'])
340
- expect(sortedPage1.pagination.totalPages).toBe(3)
341
-
342
- // Get second page with same sort
343
- const sortedPage2 = await repository.search(
344
- {},
345
- {
346
- sort: [{ title: 'asc' }],
347
- pagination: { page: 2, pageSize: 2 },
348
- },
349
- )
350
- expect(sortedPage2.results.map((r) => r.title)).toEqual(['Book B', 'Book C'])
351
- })
352
-
353
- it('should handle combined filtering, sorting, and pagination', async () => {
354
- const repositoryWithFilter = new MockMemoryRepository({
355
- schema: mockSchema,
356
- filter: (data, filters) => {
357
- if (filters.text) {
358
- return data.title.toLowerCase().includes(filters.text.toLowerCase())
359
- }
360
- return true
361
- },
362
- })
363
-
364
- await repositoryWithFilter.create({ title: 'Test Z Book', author: 'Author 1', publishedDate: new Date() })
365
- await repositoryWithFilter.create({ title: 'Test A Book', author: 'Author 2', publishedDate: new Date() })
366
- await repositoryWithFilter.create({ title: 'Other Book', author: 'Author 3', publishedDate: new Date() })
367
- await repositoryWithFilter.create({ title: 'Test B Book', author: 'Author 4', publishedDate: new Date() })
368
-
369
- const results = await repositoryWithFilter.search(
370
- { text: 'Test' },
371
- {
372
- sort: [{ title: 'asc' }],
373
- pagination: { page: 1, pageSize: 2 },
374
- },
375
- )
376
-
377
- expect(results.results).toHaveLength(2)
378
- expect(results.results.map((r) => r.title)).toEqual(['Test A Book', 'Test B Book'])
379
- expect(results.pagination.total).toBe(3) // 3 "Test" books total
380
- expect(results.pagination.totalPages).toBe(2)
381
- })
382
- })
383
-
384
- describe('count functionality', () => {
385
- it('should count all items when no filter is provided', async () => {
386
- // Create test data
387
- await repository.create({ title: 'Book 1', author: 'Author 1', publishedDate: new Date() })
388
- await repository.create({ title: 'Book 2', author: 'Author 2', publishedDate: new Date() })
389
- await repository.create({ title: 'Book 3', author: 'Author 3', publishedDate: new Date() })
390
-
391
- const count = await repository.count({})
392
- expect(count).toBe(3)
393
- })
394
-
395
- it('should count filtered items when filter function is provided', async () => {
396
- const repositoryWithFilter = new MockMemoryRepository({
397
- schema: mockSchema,
398
- filter: (data, filters) => {
399
- if (filters.text) {
400
- return data.title.toLowerCase().includes(filters.text.toLowerCase())
401
- }
402
- return true
403
- },
404
- })
405
-
406
- // Create test data
407
- await repositoryWithFilter.create({ title: 'Test Book A', author: 'Author 1', publishedDate: new Date() })
408
- await repositoryWithFilter.create({ title: 'Test Book B', author: 'Author 2', publishedDate: new Date() })
409
- await repositoryWithFilter.create({ title: 'Other Book', author: 'Author 3', publishedDate: new Date() })
410
- await repositoryWithFilter.create({ title: 'Another Book', author: 'Author 4', publishedDate: new Date() })
411
-
412
- // Count all items
413
- const totalCount = await repositoryWithFilter.count({})
414
- expect(totalCount).toBe(4)
415
-
416
- // Count filtered items
417
- const filteredCount = await repositoryWithFilter.count({ text: 'Test' })
418
- expect(filteredCount).toBe(2)
419
-
420
- // Count with no matches
421
- const noMatchCount = await repositoryWithFilter.count({ text: 'Ruby' })
422
- expect(noMatchCount).toBe(0)
423
- })
424
-
425
- it('should count items correctly after CRUD operations', async () => {
426
- const repositoryWithFilter = new MockMemoryRepository({
427
- schema: mockSchema,
428
- filter: (data, filters) => {
429
- if (filters.text) {
430
- return data.title.toLowerCase().includes(filters.text.toLowerCase())
431
- }
432
- return true
433
- },
434
- })
435
-
436
- // Initial count should be 0
437
- expect(await repositoryWithFilter.count({})).toBe(0)
438
-
439
- // Create items
440
- const book1 = await repositoryWithFilter.create({
441
- title: 'Test Book 1',
442
- author: 'Author 1',
443
- publishedDate: new Date(),
444
- })
445
- const book2 = await repositoryWithFilter.create({
446
- title: 'Other Book',
447
- author: 'Author 2',
448
- publishedDate: new Date(),
449
- })
450
-
451
- // Count after creation
452
- expect(await repositoryWithFilter.count({})).toBe(2)
453
- expect(await repositoryWithFilter.count({ text: 'Test' })).toBe(1)
454
-
455
- // Remove an item
456
- await repositoryWithFilter.remove({ id: book1.id })
457
-
458
- // Count after removal
459
- expect(await repositoryWithFilter.count({})).toBe(1)
460
- expect(await repositoryWithFilter.count({ text: 'Test' })).toBe(0)
461
-
462
- // Restore the item
463
- await repositoryWithFilter.restore({ id: book1.id })
464
-
465
- // Count after restore
466
- expect(await repositoryWithFilter.count({})).toBe(2)
467
- expect(await repositoryWithFilter.count({ text: 'Test' })).toBe(1)
468
- })
469
- })
470
-
471
- describe('upsert functionality', () => {
472
- it('should create a new item when no existing item with primary key exists', async () => {
473
- const input = { id: 42, title: 'New Book', author: 'Author Name', publishedDate: new Date() }
474
-
475
- const upsertedItem = await repository.upsert(input)
476
-
477
- expect(upsertedItem).toEqual(input)
478
- expect(await repository.load({ id: 42 })).toEqual(input)
479
- })
480
-
481
- it('should update an existing item when primary key matches', async () => {
482
- // Create initial item
483
- const initial = {
484
- id: 42,
485
- title: 'Original Book',
486
- author: 'Original Author',
487
- publishedDate: new Date('2023-01-01'),
488
- }
489
- await repository.create(initial)
490
-
491
- // Upsert with same ID but different data
492
- const update = {
493
- id: 42,
494
- title: 'Updated Book',
495
- author: 'Updated Author',
496
- publishedDate: new Date('2023-12-01'),
497
- }
498
- const upsertedItem = await repository.upsert(update)
499
-
500
- expect(upsertedItem).toEqual(update)
501
- expect(await repository.load({ id: 42 })).toEqual(update)
502
- })
503
-
504
- it('should generate primary key when upserting without one', async () => {
505
- const input = { title: 'Book Without ID', author: 'Author Name', publishedDate: new Date() }
506
-
507
- const upsertedItem = await repository.upsert(input)
508
-
509
- expect(upsertedItem.id).toBeDefined()
510
- expect(upsertedItem.title).toBe(input.title)
511
- expect(upsertedItem.author).toBe(input.author)
512
- expect(await repository.load({ id: upsertedItem.id })).toEqual(upsertedItem)
513
- })
514
-
515
- it('should merge with existing item properties when updating', async () => {
516
- // Create initial item with multiple properties
517
- const initial = {
518
- id: 42,
519
- title: 'Original Book',
520
- author: 'Original Author',
521
- publishedDate: new Date('2023-01-01'),
522
- }
523
- await repository.create(initial)
524
-
525
- // Upsert with partial update (only title) - need to provide required fields
526
- const partialUpdate = {
527
- id: 42,
528
- title: 'Updated Title',
529
- author: 'Original Author', // Keep original
530
- publishedDate: new Date('2023-01-01'), // Keep original
531
- }
532
- const upsertedItem = await repository.upsert(partialUpdate)
533
-
534
- // Should have updated title but kept other properties
535
- expect(upsertedItem.id).toBe(42)
536
- expect(upsertedItem.title).toBe('Updated Title')
537
- expect(upsertedItem.author).toBe('Original Author')
538
- expect(upsertedItem.publishedDate).toEqual(initial.publishedDate)
539
- })
540
-
541
- it('should handle upsert with null/undefined primary key', async () => {
542
- const input = {
543
- id: undefined,
544
- title: 'Book With Undefined ID',
545
- author: 'Author Name',
546
- publishedDate: new Date(),
547
- }
548
-
549
- const upsertedItem = await repository.upsert(input)
550
-
551
- expect(upsertedItem.id).toBeDefined()
552
- expect(typeof upsertedItem.id).toBe('number')
553
- expect(upsertedItem.title).toBe(input.title)
554
- })
555
-
556
- it('should increment auto-generated IDs correctly', async () => {
557
- const input1 = { title: 'Book 1', author: 'Author 1', publishedDate: new Date() }
558
- const input2 = { title: 'Book 2', author: 'Author 2', publishedDate: new Date() }
559
-
560
- const item1 = await repository.upsert(input1)
561
- const item2 = await repository.upsert(input2)
562
-
563
- expect(item1.id).toBe(1)
564
- expect(item2.id).toBe(2)
565
- })
566
- })
567
-
568
- describe('bulkUpsert functionality', () => {
569
- it('should upsert multiple new items', async () => {
570
- const inputs = [
571
- { id: 1, title: 'Book 1', author: 'Author 1', publishedDate: new Date('2023-01-01') },
572
- { id: 2, title: 'Book 2', author: 'Author 2', publishedDate: new Date('2023-02-01') },
573
- { id: 3, title: 'Book 3', author: 'Author 3', publishedDate: new Date('2023-03-01') },
574
- ]
575
-
576
- const upsertedItems = await repository.bulkUpsert(inputs)
577
-
578
- expect(upsertedItems).toHaveLength(3)
579
- expect(upsertedItems).toEqual(inputs)
580
-
581
- // Verify all items were created
582
- for (const input of inputs) {
583
- expect(await repository.load({ id: input.id })).toEqual(input)
584
- }
585
- })
586
-
587
- it('should update existing items and create new ones in same operation', async () => {
588
- // Create some initial items
589
- const existing1 = {
590
- id: 1,
591
- title: 'Original Book 1',
592
- author: 'Original Author 1',
593
- publishedDate: new Date('2023-01-01'),
594
- }
595
- const existing2 = {
596
- id: 2,
597
- title: 'Original Book 2',
598
- author: 'Original Author 2',
599
- publishedDate: new Date('2023-02-01'),
600
- }
601
- await repository.create(existing1)
602
- await repository.create(existing2)
603
-
604
- // Bulk upsert with mix of updates and new items
605
- const upsertInputs = [
606
- { id: 1, title: 'Updated Book 1', author: 'Updated Author 1', publishedDate: new Date('2023-06-01') }, // Update
607
- { id: 2, title: 'Updated Book 2', author: 'Updated Author 2', publishedDate: new Date('2023-07-01') }, // Update
608
- { id: 3, title: 'New Book 3', author: 'New Author 3', publishedDate: new Date('2023-08-01') }, // Create
609
- { id: 4, title: 'New Book 4', author: 'New Author 4', publishedDate: new Date('2023-09-01') }, // Create
610
- ]
611
-
612
- const upsertedItems = await repository.bulkUpsert(upsertInputs)
613
-
614
- expect(upsertedItems).toHaveLength(4)
615
- expect(upsertedItems).toEqual(upsertInputs)
616
-
617
- // Verify all items have the updated/new values
618
- for (const input of upsertInputs) {
619
- expect(await repository.load({ id: input.id })).toEqual(input)
620
- }
621
- })
622
-
623
- it('should handle bulk upsert with items without primary keys', async () => {
624
- const inputs = [
625
- { title: 'Book Without ID 1', author: 'Author 1', publishedDate: new Date() },
626
- { title: 'Book Without ID 2', author: 'Author 2', publishedDate: new Date() },
627
- { title: 'Book Without ID 3', author: 'Author 3', publishedDate: new Date() },
628
- ]
629
-
630
- const upsertedItems = await repository.bulkUpsert(inputs)
631
-
632
- expect(upsertedItems).toHaveLength(3)
633
-
634
- // All items should have generated IDs
635
- expect(upsertedItems[0].id).toBe(1)
636
- expect(upsertedItems[1].id).toBe(2)
637
- expect(upsertedItems[2].id).toBe(3)
638
-
639
- // Verify content is preserved
640
- for (let i = 0; i < inputs.length; i++) {
641
- expect(upsertedItems[i].title).toBe(inputs[i].title)
642
- expect(upsertedItems[i].author).toBe(inputs[i].author)
643
- expect(await repository.load({ id: upsertedItems[i].id })).toEqual(upsertedItems[i])
644
- }
645
- })
646
-
647
- it('should handle empty bulk upsert', async () => {
648
- const upsertedItems = await repository.bulkUpsert([])
649
-
650
- expect(upsertedItems).toEqual([])
651
- })
652
-
653
- it('should handle bulk upsert with partial updates', async () => {
654
- // Create initial items
655
- const initial1 = {
656
- id: 1,
657
- title: 'Original Book 1',
658
- author: 'Original Author 1',
659
- publishedDate: new Date('2023-01-01'),
660
- }
661
- const initial2 = {
662
- id: 2,
663
- title: 'Original Book 2',
664
- author: 'Original Author 2',
665
- publishedDate: new Date('2023-02-01'),
666
- }
667
- await repository.create(initial1)
668
- await repository.create(initial2)
669
-
670
- // Updates with all required fields but only changing title
671
- const partialUpdates = [
672
- { id: 1, title: 'Updated Title 1', author: 'Original Author 1', publishedDate: new Date('2023-01-01') },
673
- { id: 2, title: 'Updated Title 2', author: 'Original Author 2', publishedDate: new Date('2023-02-01') },
674
- ]
675
-
676
- const upsertedItems = await repository.bulkUpsert(partialUpdates)
677
-
678
- expect(upsertedItems).toHaveLength(2)
679
-
680
- // Should have updated titles but kept other properties
681
- expect(upsertedItems[0].title).toBe('Updated Title 1')
682
- expect(upsertedItems[0].author).toBe('Original Author 1')
683
- expect(upsertedItems[0].publishedDate).toEqual(initial1.publishedDate)
684
-
685
- expect(upsertedItems[1].title).toBe('Updated Title 2')
686
- expect(upsertedItems[1].author).toBe('Original Author 2')
687
- expect(upsertedItems[1].publishedDate).toEqual(initial2.publishedDate)
688
- })
689
-
690
- it('should handle large bulk operations efficiently', async () => {
691
- // Create a large number of items to test performance
692
- const inputs: Array<{ id: number; title: string; author: string; publishedDate: Date }> = []
693
- for (let i = 1; i <= 100; i++) {
694
- inputs.push({
695
- id: i,
696
- title: `Book ${i}`,
697
- author: `Author ${i}`,
698
- publishedDate: new Date(`2023-${String((i % 12) + 1).padStart(2, '0')}-01`),
699
- })
700
- }
701
-
702
- const startTime = Date.now()
703
- const upsertedItems = await repository.bulkUpsert(inputs)
704
- const endTime = Date.now()
705
-
706
- expect(upsertedItems).toHaveLength(100)
707
- expect(endTime - startTime).toBeLessThan(1000) // Should complete in under 1 second
708
-
709
- // Verify a few random items
710
- expect(await repository.load({ id: 1 })).toEqual(inputs[0])
711
- expect(await repository.load({ id: 50 })).toEqual(inputs[49])
712
- expect(await repository.load({ id: 100 })).toEqual(inputs[99])
713
- })
714
- })
715
-
716
- describe('assign functionality', () => {
717
- it('should use default Object.assign for create when no custom assign function is provided', async () => {
718
- const input = { title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
719
- const createdItem = await repository.create(input)
720
-
721
- expect(createdItem).toMatchObject(input)
722
- expect(createdItem.id).toBeDefined()
723
- })
724
-
725
- it('should use default Object.assign for update when no custom assign function is provided', async () => {
726
- const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
727
- const createdItem = await repository.create(input)
728
-
729
- const updateInput = { title: 'Updated Book', author: 'Updated Author', publishedDate: new Date() }
730
- const updatedItem = await repository.update({ id: createdItem.id }, updateInput)
731
-
732
- expect(updatedItem).toEqual({
733
- id: createdItem.id,
734
- title: 'Updated Book',
735
- author: 'Updated Author',
736
- publishedDate: updateInput.publishedDate,
737
- })
738
- })
739
-
740
- it('should use default Object.assign for upsert when no custom assign function is provided', async () => {
741
- const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
742
- const upsertedItem = await repository.upsert(input)
743
-
744
- expect(upsertedItem).toEqual(input)
745
-
746
- const updateInput = { id: 42, title: 'Updated Book', author: 'Updated Author', publishedDate: new Date() }
747
- const updatedItem = await repository.upsert(updateInput)
748
-
749
- expect(updatedItem).toEqual(updateInput)
750
- })
751
-
752
- it('should use custom assign function for create operation', async () => {
753
- const customAssignMock = mock((existing: any, input: any) => ({
754
- ...existing,
755
- ...input,
756
- customField: 'custom_create_value',
757
- }))
758
-
759
- const customRepository = new MockMemoryRepository({
760
- schema: mockSchema,
761
- assign: customAssignMock,
762
- })
763
-
764
- const input = { title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
765
- const createdItem = await customRepository.create(input)
766
-
767
- expect(customAssignMock).toHaveBeenCalledWith({}, input)
768
- expect((createdItem as any).customField).toBe('custom_create_value')
769
- expect(createdItem).toMatchObject(input)
770
- })
771
-
772
- it('should use custom assign function for update operation', async () => {
773
- const customAssignMock = mock((existing: any, input: any) => ({
774
- ...existing,
775
- ...input,
776
- customField: 'custom_update_value',
777
- lastModified: new Date('2023-01-01'),
778
- }))
779
-
780
- const customRepository = new MockMemoryRepository({
781
- schema: mockSchema,
782
- assign: customAssignMock,
783
- })
784
-
785
- const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
786
- const createdItem = await customRepository.create(input)
787
-
788
- const updateInput = { title: 'Updated Book', author: 'Updated Author', publishedDate: new Date() }
789
- const updatedItem = await customRepository.update({ id: createdItem.id }, updateInput)
790
-
791
- expect(customAssignMock).toHaveBeenCalledWith(createdItem, updateInput)
792
- expect((updatedItem as any).customField).toBe('custom_update_value')
793
- expect((updatedItem as any).lastModified).toEqual(new Date('2023-01-01'))
794
- expect(updatedItem.title).toBe('Updated Book')
795
- })
796
-
797
- it('should use custom assign function for upsert operation on existing item', async () => {
798
- const customAssignMock = mock((existing: any, input: any) => ({
799
- ...existing,
800
- ...input,
801
- mergeTimestamp: new Date('2023-01-01'),
802
- isUpserted: true,
803
- }))
804
-
805
- const customRepository = new MockMemoryRepository({
806
- schema: mockSchema,
807
- assign: customAssignMock,
808
- })
809
-
810
- const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
811
- const createdItem = await customRepository.create(input)
812
-
813
- const upsertInput = { id: 42, title: 'Upserted Book', author: 'Upserted Author', publishedDate: new Date() }
814
- const upsertedItem = await customRepository.upsert(upsertInput)
815
-
816
- // Should have been called twice: once for create, once for upsert
817
- expect(customAssignMock).toHaveBeenCalledTimes(2)
818
- expect(customAssignMock).toHaveBeenLastCalledWith(createdItem, upsertInput)
819
- expect((upsertedItem as any).mergeTimestamp).toEqual(new Date('2023-01-01'))
820
- expect((upsertedItem as any).isUpserted).toBe(true)
821
- expect(upsertedItem.title).toBe('Upserted Book')
822
- })
823
-
824
- it('should use custom assign function for upsert operation on new item', async () => {
825
- const customAssignMock = mock((existing: any, input: any) => ({
826
- ...existing,
827
- ...input,
828
- createdViaUpsert: true,
829
- }))
830
-
831
- const customRepository = new MockMemoryRepository({
832
- schema: mockSchema,
833
- assign: customAssignMock,
834
- })
835
-
836
- const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
837
- const upsertedItem = await customRepository.upsert(input)
838
-
839
- expect(customAssignMock).toHaveBeenCalledWith({}, input)
840
- expect((upsertedItem as any).createdViaUpsert).toBe(true)
841
- expect(upsertedItem).toMatchObject(input)
842
- })
843
-
844
- it('should use custom assign function for bulkUpsert operation', async () => {
845
- const customAssignMock = mock((existing: any, input: any) => ({
846
- ...existing,
847
- ...input,
848
- bulkProcessed: true,
849
- }))
850
-
851
- const customRepository = new MockMemoryRepository({
852
- schema: mockSchema,
853
- assign: customAssignMock,
854
- })
855
-
856
- const inputs = [
857
- { id: 1, title: 'Book 1', author: 'Author 1', publishedDate: new Date() },
858
- { id: 2, title: 'Book 2', author: 'Author 2', publishedDate: new Date() },
859
- ]
860
-
861
- const results = await customRepository.bulkUpsert(inputs)
862
-
863
- expect(customAssignMock).toHaveBeenCalledTimes(2)
864
- expect((results[0] as any).bulkProcessed).toBe(true)
865
- expect((results[1] as any).bulkProcessed).toBe(true)
866
- expect(results[0]).toMatchObject(inputs[0])
867
- expect(results[1]).toMatchObject(inputs[1])
868
- })
869
-
870
- it('should handle complex custom assign logic with conditional merging', async () => {
871
- const customAssign = (existing: any, input: any) => {
872
- const result = { ...existing, ...input }
873
-
874
- // Custom logic: preserve original author if input doesn't have one
875
- if (!input.author && existing.author) {
876
- result.author = existing.author
877
- }
878
-
879
- // Custom logic: track modification count
880
- result.modificationCount = (existing.modificationCount || 0) + 1
881
-
882
- return result
883
- }
884
-
885
- const customRepository = new MockMemoryRepository({
886
- schema: mockSchema,
887
- assign: customAssign,
888
- })
889
-
890
- const input = { id: 42, title: 'Test Book', author: 'Original Author', publishedDate: new Date() }
891
- const createdItem = await customRepository.create(input)
892
-
893
- expect((createdItem as any).modificationCount).toBe(1)
894
-
895
- // Update with partial data - use existing values for required fields
896
- const updateInput = {
897
- title: 'Updated Book',
898
- author: createdItem.author,
899
- publishedDate: createdItem.publishedDate,
900
- }
901
- const updatedItem = await customRepository.update({ id: createdItem.id }, updateInput)
902
-
903
- expect(updatedItem.author).toBe('Original Author')
904
- expect(updatedItem.title).toBe('Updated Book')
905
- expect((updatedItem as any).modificationCount).toBe(2)
906
-
907
- // Update with new author
908
- const updateWithAuthor = {
909
- title: updatedItem.title,
910
- author: 'New Author',
911
- publishedDate: updatedItem.publishedDate,
912
- }
913
- const finalItem = await customRepository.update({ id: createdItem.id }, updateWithAuthor)
914
-
915
- expect(finalItem.author).toBe('New Author')
916
- expect((finalItem as any).modificationCount).toBe(3)
917
- })
918
- })
919
- })