@declaro/data 2.0.0-beta.11 → 2.0.0-beta.111

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 (124) hide show
  1. package/{LICENSE → LICENSE.md} +1 -1
  2. package/README.md +0 -0
  3. package/dist/browser/index.js +26 -0
  4. package/dist/browser/index.js.map +93 -0
  5. package/dist/node/index.cjs +13121 -0
  6. package/dist/node/index.cjs.map +93 -0
  7. package/dist/node/index.js +13100 -0
  8. package/dist/node/index.js.map +93 -0
  9. package/dist/ts/application/model-controller.d.ts +29 -0
  10. package/dist/ts/application/model-controller.d.ts.map +1 -0
  11. package/dist/ts/application/model-controller.test.d.ts +2 -0
  12. package/dist/ts/application/model-controller.test.d.ts.map +1 -0
  13. package/dist/ts/application/read-only-model-controller.d.ts +20 -0
  14. package/dist/ts/application/read-only-model-controller.d.ts.map +1 -0
  15. package/dist/ts/application/read-only-model-controller.test.d.ts +2 -0
  16. package/dist/ts/application/read-only-model-controller.test.d.ts.map +1 -0
  17. package/dist/ts/domain/events/domain-event.d.ts +41 -0
  18. package/dist/ts/domain/events/domain-event.d.ts.map +1 -0
  19. package/dist/ts/domain/events/domain-event.test.d.ts +2 -0
  20. package/dist/ts/domain/events/domain-event.test.d.ts.map +1 -0
  21. package/dist/ts/domain/events/event-types.d.ts +21 -0
  22. package/dist/ts/domain/events/event-types.d.ts.map +1 -0
  23. package/dist/ts/domain/events/mutation-event.d.ts +6 -0
  24. package/dist/ts/domain/events/mutation-event.d.ts.map +1 -0
  25. package/dist/ts/domain/events/mutation-event.test.d.ts +2 -0
  26. package/dist/ts/domain/events/mutation-event.test.d.ts.map +1 -0
  27. package/dist/ts/domain/events/query-event.d.ts +6 -0
  28. package/dist/ts/domain/events/query-event.d.ts.map +1 -0
  29. package/dist/ts/domain/events/query-event.test.d.ts +2 -0
  30. package/dist/ts/domain/events/query-event.test.d.ts.map +1 -0
  31. package/dist/ts/domain/events/request-event.d.ts +11 -0
  32. package/dist/ts/domain/events/request-event.d.ts.map +1 -0
  33. package/dist/ts/domain/events/request-event.test.d.ts +2 -0
  34. package/dist/ts/domain/events/request-event.test.d.ts.map +1 -0
  35. package/dist/ts/domain/interfaces/repository.d.ts +84 -0
  36. package/dist/ts/domain/interfaces/repository.d.ts.map +1 -0
  37. package/dist/ts/domain/models/pagination.d.ts +28 -0
  38. package/dist/ts/domain/models/pagination.d.ts.map +1 -0
  39. package/dist/ts/domain/services/base-model-service.d.ts +22 -0
  40. package/dist/ts/domain/services/base-model-service.d.ts.map +1 -0
  41. package/dist/ts/domain/services/model-service-args.d.ts +9 -0
  42. package/dist/ts/domain/services/model-service-args.d.ts.map +1 -0
  43. package/dist/ts/domain/services/model-service.d.ts +50 -0
  44. package/dist/ts/domain/services/model-service.d.ts.map +1 -0
  45. package/dist/ts/domain/services/model-service.test.d.ts +2 -0
  46. package/dist/ts/domain/services/model-service.test.d.ts.map +1 -0
  47. package/dist/ts/domain/services/read-only-model-service.d.ts +57 -0
  48. package/dist/ts/domain/services/read-only-model-service.d.ts.map +1 -0
  49. package/dist/ts/domain/services/read-only-model-service.test.d.ts +2 -0
  50. package/dist/ts/domain/services/read-only-model-service.test.d.ts.map +1 -0
  51. package/dist/ts/index.d.ts +18 -0
  52. package/dist/ts/index.d.ts.map +1 -0
  53. package/dist/ts/shared/utils/schema-inference.d.ts +23 -0
  54. package/dist/ts/shared/utils/schema-inference.d.ts.map +1 -0
  55. package/dist/ts/shared/utils/schema-inheritance.d.ts +24 -0
  56. package/dist/ts/shared/utils/schema-inheritance.d.ts.map +1 -0
  57. package/dist/ts/test/mock/models/mock-book-models.d.ts +42 -0
  58. package/dist/ts/test/mock/models/mock-book-models.d.ts.map +1 -0
  59. package/dist/ts/test/mock/repositories/mock-memory-repository.assign.test.d.ts +2 -0
  60. package/dist/ts/test/mock/repositories/mock-memory-repository.assign.test.d.ts.map +1 -0
  61. package/dist/ts/test/mock/repositories/mock-memory-repository.basic.test.d.ts +2 -0
  62. package/dist/ts/test/mock/repositories/mock-memory-repository.basic.test.d.ts.map +1 -0
  63. package/dist/ts/test/mock/repositories/mock-memory-repository.bulk-upsert.test.d.ts +2 -0
  64. package/dist/ts/test/mock/repositories/mock-memory-repository.bulk-upsert.test.d.ts.map +1 -0
  65. package/dist/ts/test/mock/repositories/mock-memory-repository.count.test.d.ts +2 -0
  66. package/dist/ts/test/mock/repositories/mock-memory-repository.count.test.d.ts.map +1 -0
  67. package/dist/ts/test/mock/repositories/mock-memory-repository.custom-lookup.test.d.ts +1 -0
  68. package/dist/ts/test/mock/repositories/mock-memory-repository.custom-lookup.test.d.ts.map +1 -0
  69. package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts +44 -0
  70. package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts.map +1 -0
  71. package/dist/ts/test/mock/repositories/mock-memory-repository.search.test.d.ts +2 -0
  72. package/dist/ts/test/mock/repositories/mock-memory-repository.search.test.d.ts.map +1 -0
  73. package/dist/ts/test/mock/repositories/mock-memory-repository.upsert.test.d.ts +2 -0
  74. package/dist/ts/test/mock/repositories/mock-memory-repository.upsert.test.d.ts.map +1 -0
  75. package/package.json +45 -42
  76. package/src/application/model-controller.test.ts +503 -0
  77. package/src/application/model-controller.ts +92 -0
  78. package/src/application/read-only-model-controller.test.ts +335 -0
  79. package/src/application/read-only-model-controller.ts +61 -0
  80. package/src/domain/events/domain-event.test.ts +82 -0
  81. package/src/domain/events/domain-event.ts +69 -0
  82. package/src/domain/events/event-types.ts +21 -0
  83. package/src/domain/events/mutation-event.test.ts +38 -0
  84. package/src/domain/events/mutation-event.ts +8 -0
  85. package/src/domain/events/query-event.test.ts +28 -0
  86. package/src/domain/events/query-event.ts +8 -0
  87. package/src/domain/events/request-event.test.ts +38 -0
  88. package/src/domain/events/request-event.ts +32 -0
  89. package/src/domain/interfaces/repository.ts +107 -0
  90. package/src/domain/models/pagination.ts +28 -0
  91. package/src/domain/services/base-model-service.ts +50 -0
  92. package/src/domain/services/model-service-args.ts +9 -0
  93. package/src/domain/services/model-service.test.ts +1021 -0
  94. package/src/domain/services/model-service.ts +345 -0
  95. package/src/domain/services/read-only-model-service.test.ts +396 -0
  96. package/src/domain/services/read-only-model-service.ts +177 -0
  97. package/src/index.ts +17 -4
  98. package/src/shared/utils/schema-inference.ts +26 -0
  99. package/src/shared/utils/schema-inheritance.ts +28 -0
  100. package/src/test/mock/models/mock-book-models.ts +78 -0
  101. package/src/test/mock/repositories/mock-memory-repository.assign.test.ts +215 -0
  102. package/src/test/mock/repositories/mock-memory-repository.basic.test.ts +129 -0
  103. package/src/test/mock/repositories/mock-memory-repository.bulk-upsert.test.ts +159 -0
  104. package/src/test/mock/repositories/mock-memory-repository.count.test.ts +98 -0
  105. package/src/test/mock/repositories/mock-memory-repository.custom-lookup.test.ts +0 -0
  106. package/src/test/mock/repositories/mock-memory-repository.search.test.ts +265 -0
  107. package/src/test/mock/repositories/mock-memory-repository.ts +301 -0
  108. package/src/test/mock/repositories/mock-memory-repository.upsert.test.ts +108 -0
  109. package/dist/databaseConnection.d.ts +0 -24
  110. package/dist/datastoreAbstract.d.ts +0 -37
  111. package/dist/declaro-data.cjs +0 -1
  112. package/dist/declaro-data.mjs +0 -250
  113. package/dist/hydrateEntity.d.ts +0 -8
  114. package/dist/index.d.ts +0 -4
  115. package/dist/serverConnection.d.ts +0 -15
  116. package/dist/trackedStatus.d.ts +0 -15
  117. package/src/databaseConnection.ts +0 -137
  118. package/src/datastoreAbstract.ts +0 -190
  119. package/src/hydrateEntity.ts +0 -36
  120. package/src/placeholder.test.ts +0 -7
  121. package/src/serverConnection.ts +0 -74
  122. package/src/trackedStatus.ts +0 -35
  123. package/tsconfig.json +0 -10
  124. package/vite.config.ts +0 -23
@@ -0,0 +1,1021 @@
1
+ import { describe, it, expect, beforeEach } from 'bun:test'
2
+ import { ModelService } from './model-service'
3
+ import { MockMemoryRepository } from '../../test/mock/repositories/mock-memory-repository'
4
+ import { MockBookSchema, type MockBookInput } from '../../test/mock/models/mock-book-models'
5
+ import { EventManager } from '@declaro/core'
6
+ import { mock } from 'bun:test'
7
+
8
+ describe('ModelService', () => {
9
+ const namespace = 'books'
10
+ const mockSchema = MockBookSchema
11
+
12
+ let repository: MockMemoryRepository<typeof mockSchema>
13
+ let emitter: EventManager
14
+ let service: ModelService<typeof mockSchema>
15
+
16
+ beforeEach(() => {
17
+ repository = new MockMemoryRepository({ schema: mockSchema })
18
+ emitter = new EventManager()
19
+ service = new ModelService({ repository, emitter, schema: mockSchema, namespace })
20
+ })
21
+
22
+ const beforeCreateSpy = mock((event) => {})
23
+ const afterCreateSpy = mock((event) => {})
24
+ const beforeUpdateSpy = mock((event) => {})
25
+ const afterUpdateSpy = mock((event) => {})
26
+ const beforeRemoveSpy = mock((event) => {})
27
+ const afterRemoveSpy = mock((event) => {})
28
+ const beforeRestoreSpy = mock((event) => {})
29
+ const afterRestoreSpy = mock((event) => {})
30
+
31
+ beforeEach(() => {
32
+ emitter.on('books::book.beforeCreate', beforeCreateSpy)
33
+ emitter.on('books::book.afterCreate', afterCreateSpy)
34
+ emitter.on('books::book.beforeUpdate', beforeUpdateSpy)
35
+ emitter.on('books::book.afterUpdate', afterUpdateSpy)
36
+ emitter.on('books::book.beforeRemove', beforeRemoveSpy)
37
+ emitter.on('books::book.afterRemove', afterRemoveSpy)
38
+ emitter.on('books::book.beforeRestore', beforeRestoreSpy)
39
+ emitter.on('books::book.afterRestore', afterRestoreSpy)
40
+
41
+ beforeCreateSpy.mockClear()
42
+ afterCreateSpy.mockClear()
43
+ beforeUpdateSpy.mockClear()
44
+ afterUpdateSpy.mockClear()
45
+ beforeRemoveSpy.mockClear()
46
+ afterRemoveSpy.mockClear()
47
+ beforeRestoreSpy.mockClear()
48
+ afterRestoreSpy.mockClear()
49
+ })
50
+
51
+ it('should create a record', async () => {
52
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
53
+ const createdRecord = await service.create(input)
54
+
55
+ expect(createdRecord).toEqual(input)
56
+ })
57
+
58
+ it('should update a record', async () => {
59
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
60
+ await repository.create(input)
61
+
62
+ const updatedInput = { title: 'Updated Book', author: 'Updated Author', publishedDate: new Date() }
63
+ const updatedRecord = await service.update({ id: 42 }, updatedInput)
64
+
65
+ expect(updatedRecord).toEqual({ id: 42, ...updatedInput })
66
+ })
67
+
68
+ it('should remove a record', async () => {
69
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
70
+ await repository.create(input)
71
+
72
+ const removedRecord = await service.remove({ id: 42 })
73
+
74
+ expect(removedRecord).toEqual(input)
75
+ expect(await repository.load({ id: 42 })).toBeNull()
76
+ })
77
+
78
+ it('should restore a record', async () => {
79
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
80
+ await repository.create(input)
81
+ await repository.remove({ id: 42 })
82
+
83
+ const restoredRecord = await service.restore({ id: 42 })
84
+
85
+ expect(restoredRecord).toEqual(input)
86
+ expect(await repository.load({ id: 42 })).toEqual(input)
87
+ })
88
+
89
+ it('should trigger before and after events for create', async () => {
90
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
91
+ const createdRecord = await service.create(input)
92
+
93
+ expect(createdRecord).toEqual(input)
94
+ expect(beforeCreateSpy).toHaveBeenCalledTimes(1)
95
+ expect(beforeCreateSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.beforeCreate' }))
96
+ expect(afterCreateSpy).toHaveBeenCalledTimes(1)
97
+ expect(afterCreateSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.afterCreate' }))
98
+ })
99
+
100
+ it('should trigger before and after events for update', async () => {
101
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
102
+ await repository.create(input)
103
+
104
+ const updatedInput = { title: 'Updated Book', author: 'Updated Author', publishedDate: new Date() }
105
+ const updatedRecord = await service.update({ id: 42 }, updatedInput)
106
+
107
+ expect(updatedRecord).toEqual({ id: 42, ...updatedInput })
108
+ expect(beforeUpdateSpy).toHaveBeenCalledTimes(1)
109
+ expect(beforeUpdateSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.beforeUpdate' }))
110
+ expect(afterUpdateSpy).toHaveBeenCalledTimes(1)
111
+ expect(afterUpdateSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.afterUpdate' }))
112
+ })
113
+
114
+ it('should trigger before and after events for remove', async () => {
115
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
116
+ await repository.create(input)
117
+
118
+ const removedRecord = await service.remove({ id: 42 })
119
+
120
+ expect(removedRecord).toEqual(input)
121
+ expect(beforeRemoveSpy).toHaveBeenCalledTimes(1)
122
+ expect(beforeRemoveSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.beforeRemove' }))
123
+ expect(afterRemoveSpy).toHaveBeenCalledTimes(1)
124
+ expect(afterRemoveSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.afterRemove' }))
125
+ })
126
+
127
+ it('should trigger before and after events for restore', async () => {
128
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
129
+ await repository.create(input)
130
+ await repository.remove({ id: 42 })
131
+
132
+ const restoredRecord = await service.restore({ id: 42 })
133
+
134
+ expect(restoredRecord).toEqual(input)
135
+ expect(beforeRestoreSpy).toHaveBeenCalledTimes(1)
136
+ expect(beforeRestoreSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.beforeRestore' }))
137
+ expect(afterRestoreSpy).toHaveBeenCalledTimes(1)
138
+ expect(afterRestoreSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.afterRestore' }))
139
+ })
140
+
141
+ it('should throw an error when attempting to remove a non-existent record', async () => {
142
+ await expect(service.remove({ id: 999 })).rejects.toThrow('Item not found')
143
+ })
144
+
145
+ it('should throw an error when attempting to update a non-existent record', async () => {
146
+ const updatedInput = { title: 'Updated Book', author: 'Updated Author', publishedDate: new Date() }
147
+ await expect(service.update({ id: 999 }, updatedInput)).rejects.toThrow('Item not found')
148
+ })
149
+
150
+ describe('upsert functionality', () => {
151
+ it('should create a new record when no existing record with primary key exists', async () => {
152
+ const input = { id: 42, title: 'New Book', author: 'Author Name', publishedDate: new Date() }
153
+
154
+ const upsertedRecord = await service.upsert(input)
155
+
156
+ expect(upsertedRecord).toEqual(input)
157
+ expect(await repository.load({ id: 42 })).toEqual(input)
158
+ })
159
+
160
+ it('should update an existing record when primary key matches', async () => {
161
+ // Create initial record
162
+ const initial = {
163
+ id: 42,
164
+ title: 'Original Book',
165
+ author: 'Original Author',
166
+ publishedDate: new Date('2023-01-01'),
167
+ }
168
+ await repository.create(initial)
169
+
170
+ // Upsert with same ID but different data
171
+ const update = {
172
+ id: 42,
173
+ title: 'Updated Book',
174
+ author: 'Updated Author',
175
+ publishedDate: new Date('2023-12-01'),
176
+ }
177
+ const upsertedRecord = await service.upsert(update)
178
+
179
+ expect(upsertedRecord).toEqual(update)
180
+ expect(await repository.load({ id: 42 })).toEqual(update)
181
+ })
182
+
183
+ it('should create a new record when upserting without primary key', async () => {
184
+ const input = { title: 'Book Without ID', author: 'Author Name', publishedDate: new Date() }
185
+
186
+ const upsertedRecord = await service.upsert(input)
187
+
188
+ expect(upsertedRecord.id).toBeDefined()
189
+ expect(upsertedRecord.title).toBe(input.title)
190
+ expect(upsertedRecord.author).toBe(input.author)
191
+ expect(await repository.load({ id: upsertedRecord.id })).toEqual(upsertedRecord)
192
+ })
193
+
194
+ it('should create a new record when primary key is null', async () => {
195
+ const input = {
196
+ id: undefined,
197
+ title: 'Book With Undefined ID',
198
+ author: 'Author Name',
199
+ publishedDate: new Date(),
200
+ }
201
+
202
+ const upsertedRecord = await service.upsert(input)
203
+
204
+ expect(upsertedRecord.id).toBeDefined()
205
+ expect(upsertedRecord.id).not.toBeNull()
206
+ expect(upsertedRecord.title).toBe(input.title)
207
+ expect(upsertedRecord.author).toBe(input.author)
208
+ })
209
+
210
+ it('should trigger create events when upserting a new record', async () => {
211
+ const input = { id: 42, title: 'New Book', author: 'Author Name', publishedDate: new Date() }
212
+
213
+ const upsertedRecord = await service.upsert(input)
214
+
215
+ expect(upsertedRecord).toEqual(input)
216
+ expect(beforeCreateSpy).toHaveBeenCalledTimes(1)
217
+ expect(beforeCreateSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.beforeCreate' }))
218
+ expect(afterCreateSpy).toHaveBeenCalledTimes(1)
219
+ expect(afterCreateSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.afterCreate' }))
220
+ expect(beforeUpdateSpy).not.toHaveBeenCalled()
221
+ expect(afterUpdateSpy).not.toHaveBeenCalled()
222
+ })
223
+
224
+ it('should trigger update events when upserting an existing record', async () => {
225
+ // Create initial record
226
+ const initial = { id: 42, title: 'Original Book', author: 'Original Author', publishedDate: new Date() }
227
+ await repository.create(initial)
228
+
229
+ // Clear create spies since they were called during setup
230
+ beforeCreateSpy.mockClear()
231
+ afterCreateSpy.mockClear()
232
+
233
+ // Upsert existing record
234
+ const update = { id: 42, title: 'Updated Book', author: 'Updated Author', publishedDate: new Date() }
235
+ const upsertedRecord = await service.upsert(update)
236
+
237
+ expect(upsertedRecord).toEqual(update)
238
+ expect(beforeUpdateSpy).toHaveBeenCalledTimes(1)
239
+ expect(beforeUpdateSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.beforeUpdate' }))
240
+ expect(afterUpdateSpy).toHaveBeenCalledTimes(1)
241
+ expect(afterUpdateSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.afterUpdate' }))
242
+ expect(beforeCreateSpy).not.toHaveBeenCalled()
243
+ expect(afterCreateSpy).not.toHaveBeenCalled()
244
+ })
245
+
246
+ it('should trigger create events when upserting with non-existent primary key', async () => {
247
+ // Try to upsert with an ID that doesn't exist
248
+ const input = { id: 999, title: 'Non-existent Book', author: 'Author Name', publishedDate: new Date() }
249
+
250
+ const upsertedRecord = await service.upsert(input)
251
+
252
+ expect(upsertedRecord).toEqual(input)
253
+ expect(beforeCreateSpy).toHaveBeenCalledTimes(1)
254
+ expect(beforeCreateSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.beforeCreate' }))
255
+ expect(afterCreateSpy).toHaveBeenCalledTimes(1)
256
+ expect(afterCreateSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.afterCreate' }))
257
+ expect(beforeUpdateSpy).not.toHaveBeenCalled()
258
+ expect(afterUpdateSpy).not.toHaveBeenCalled()
259
+ })
260
+
261
+ it('should handle multiple upserts correctly', async () => {
262
+ // First upsert (create)
263
+ const input1 = { id: 1, title: 'Book 1', author: 'Author 1', publishedDate: new Date() }
264
+ const result1 = await service.upsert(input1)
265
+ expect(result1).toEqual(input1)
266
+ expect(await repository.load({ id: 1 })).toEqual(input1)
267
+
268
+ // Second upsert (update)
269
+ const input2 = { id: 1, title: 'Updated Book 1', author: 'Updated Author 1', publishedDate: new Date() }
270
+ const result2 = await service.upsert(input2)
271
+ expect(result2).toEqual(input2)
272
+ expect(await repository.load({ id: 1 })).toEqual(input2)
273
+
274
+ // Third upsert (create new)
275
+ const input3 = { id: 2, title: 'Book 2', author: 'Author 2', publishedDate: new Date() }
276
+ const result3 = await service.upsert(input3)
277
+ expect(result3).toEqual(input3)
278
+ expect(await repository.load({ id: 2 })).toEqual(input3)
279
+ })
280
+
281
+ it('should work with auto-generated IDs', async () => {
282
+ const input1 = { title: 'Auto ID Book 1', author: 'Author 1', publishedDate: new Date() }
283
+ const input2 = { title: 'Auto ID Book 2', author: 'Author 2', publishedDate: new Date() }
284
+
285
+ const result1 = await service.upsert(input1)
286
+ const result2 = await service.upsert(input2)
287
+
288
+ expect(result1.id).toBeDefined()
289
+ expect(result2.id).toBeDefined()
290
+ expect(result1.id).not.toBe(result2.id)
291
+ expect(result1.title).toBe(input1.title)
292
+ expect(result2.title).toBe(input2.title)
293
+ })
294
+ })
295
+
296
+ describe('bulkUpsert functionality', () => {
297
+ it('should handle empty input array', async () => {
298
+ const results = await service.bulkUpsert([])
299
+ expect(results).toEqual([])
300
+ })
301
+
302
+ it('should create multiple new records with explicit IDs', async () => {
303
+ const inputs = [
304
+ { id: 1, title: 'Book 1', author: 'Author 1', publishedDate: new Date('2023-01-01') },
305
+ { id: 2, title: 'Book 2', author: 'Author 2', publishedDate: new Date('2023-02-01') },
306
+ { id: 3, title: 'Book 3', author: 'Author 3', publishedDate: new Date('2023-03-01') },
307
+ ]
308
+
309
+ const results = await service.bulkUpsert(inputs)
310
+
311
+ expect(results).toEqual(inputs)
312
+ expect(results).toHaveLength(3)
313
+
314
+ // Verify all records were created
315
+ for (const input of inputs) {
316
+ const loaded = await repository.load({ id: input.id })
317
+ expect(loaded).toEqual(input)
318
+ }
319
+ })
320
+
321
+ it('should update multiple existing records', async () => {
322
+ // Create initial records
323
+ const initialRecords = [
324
+ { id: 1, title: 'Original Book 1', author: 'Original Author 1', publishedDate: new Date('2023-01-01') },
325
+ { id: 2, title: 'Original Book 2', author: 'Original Author 2', publishedDate: new Date('2023-02-01') },
326
+ { id: 3, title: 'Original Book 3', author: 'Original Author 3', publishedDate: new Date('2023-03-01') },
327
+ ]
328
+
329
+ for (const record of initialRecords) {
330
+ await repository.create(record)
331
+ }
332
+
333
+ // Clear spies from creation
334
+ beforeCreateSpy.mockClear()
335
+ afterCreateSpy.mockClear()
336
+
337
+ // Update all records
338
+ const updateInputs = [
339
+ { id: 1, title: 'Updated Book 1', author: 'Updated Author 1', publishedDate: new Date('2023-06-01') },
340
+ { id: 2, title: 'Updated Book 2', author: 'Updated Author 2', publishedDate: new Date('2023-07-01') },
341
+ { id: 3, title: 'Updated Book 3', author: 'Updated Author 3', publishedDate: new Date('2023-08-01') },
342
+ ]
343
+
344
+ const results = await service.bulkUpsert(updateInputs)
345
+
346
+ expect(results).toEqual(updateInputs)
347
+ expect(results).toHaveLength(3)
348
+
349
+ // Verify all records were updated
350
+ for (const input of updateInputs) {
351
+ const loaded = await repository.load({ id: input.id })
352
+ expect(loaded).toEqual(input)
353
+ }
354
+ })
355
+
356
+ it('should handle mixed create and update operations', async () => {
357
+ // Create some initial records
358
+ const initialRecords = [
359
+ { id: 1, title: 'Existing Book 1', author: 'Existing Author 1', publishedDate: new Date('2023-01-01') },
360
+ { id: 3, title: 'Existing Book 3', author: 'Existing Author 3', publishedDate: new Date('2023-03-01') },
361
+ ]
362
+
363
+ for (const record of initialRecords) {
364
+ await repository.create(record)
365
+ }
366
+
367
+ // Clear spies from creation
368
+ beforeCreateSpy.mockClear()
369
+ afterCreateSpy.mockClear()
370
+
371
+ // Mix of updates and creates
372
+ const mixedInputs = [
373
+ { id: 1, title: 'Updated Book 1', author: 'Updated Author 1', publishedDate: new Date('2023-06-01') }, // Update
374
+ { id: 2, title: 'New Book 2', author: 'New Author 2', publishedDate: new Date('2023-07-01') }, // Create
375
+ { id: 3, title: 'Updated Book 3', author: 'Updated Author 3', publishedDate: new Date('2023-08-01') }, // Update
376
+ { id: 4, title: 'New Book 4', author: 'New Author 4', publishedDate: new Date('2023-09-01') }, // Create
377
+ ]
378
+
379
+ const results = await service.bulkUpsert(mixedInputs)
380
+
381
+ expect(results).toEqual(mixedInputs)
382
+ expect(results).toHaveLength(4)
383
+
384
+ // Verify all records exist with correct data
385
+ for (const input of mixedInputs) {
386
+ const loaded = await repository.load({ id: input.id })
387
+ expect(loaded).toEqual(input)
388
+ }
389
+ })
390
+
391
+ it('should handle records without primary keys (auto-generated IDs)', async () => {
392
+ const inputsWithoutIds = [
393
+ { title: 'Auto Book 1', author: 'Auto Author 1', publishedDate: new Date('2023-01-01') },
394
+ { title: 'Auto Book 2', author: 'Auto Author 2', publishedDate: new Date('2023-02-01') },
395
+ ]
396
+
397
+ const results = await service.bulkUpsert(inputsWithoutIds)
398
+
399
+ expect(results).toHaveLength(2)
400
+ expect(results[0].id).toBeDefined()
401
+ expect(results[1].id).toBeDefined()
402
+ expect(results[0].id).not.toBe(results[1].id)
403
+ expect(results[0].title).toBe(inputsWithoutIds[0].title)
404
+ expect(results[1].title).toBe(inputsWithoutIds[1].title)
405
+
406
+ // Verify records were created
407
+ for (const result of results) {
408
+ const loaded = await repository.load({ id: result.id })
409
+ expect(loaded).toEqual(result)
410
+ }
411
+ })
412
+
413
+ it('should handle mixed records with and without primary keys', async () => {
414
+ // Create an existing record with a higher ID to avoid conflicts with auto-generated IDs
415
+ const existingRecord = {
416
+ id: 100,
417
+ title: 'Existing Book',
418
+ author: 'Existing Author',
419
+ publishedDate: new Date('2023-01-01'),
420
+ }
421
+ await repository.create(existingRecord)
422
+
423
+ // Clear spies from creation
424
+ beforeCreateSpy.mockClear()
425
+ afterCreateSpy.mockClear()
426
+
427
+ const mixedInputs = [
428
+ {
429
+ id: 100,
430
+ title: 'Updated Existing Book',
431
+ author: 'Updated Author',
432
+ publishedDate: new Date('2023-06-01'),
433
+ }, // Update
434
+ { id: 200, title: 'New Book with ID', author: 'New Author', publishedDate: new Date('2023-07-01') }, // Create with ID
435
+ { title: 'Auto Book 1', author: 'Auto Author 1', publishedDate: new Date('2023-08-01') }, // Create without ID
436
+ { title: 'Auto Book 2', author: 'Auto Author 2', publishedDate: new Date('2023-09-01') }, // Create without ID
437
+ ]
438
+
439
+ const results = await service.bulkUpsert(mixedInputs)
440
+
441
+ expect(results).toHaveLength(4)
442
+
443
+ // Results should be in the same order as inputs (repository preserves order)
444
+ expect(results[0].id).toBe(100)
445
+ expect(results[0].title).toBe('Updated Existing Book')
446
+
447
+ expect(results[1].id).toBe(200)
448
+ expect(results[1].title).toBe('New Book with ID')
449
+
450
+ expect(results[2].id).toBeDefined()
451
+ expect(results[2].title).toBe('Auto Book 1')
452
+
453
+ expect(results[3].id).toBeDefined()
454
+ expect(results[3].title).toBe('Auto Book 2')
455
+
456
+ // Verify all records exist in repository
457
+ const loadedRecord1 = await repository.load({ id: 100 })
458
+ expect(loadedRecord1?.title).toBe('Updated Existing Book')
459
+
460
+ const loadedRecord2 = await repository.load({ id: 200 })
461
+ expect(loadedRecord2?.title).toBe('New Book with ID')
462
+
463
+ const loadedRecord3 = await repository.load({ id: results[2].id })
464
+ expect(loadedRecord3?.title).toBe('Auto Book 1')
465
+
466
+ const loadedRecord4 = await repository.load({ id: results[3].id })
467
+ expect(loadedRecord4?.title).toBe('Auto Book 2')
468
+ })
469
+
470
+ it('should trigger correct before and after events for bulk create operations', async () => {
471
+ const inputs = [
472
+ { id: 1, title: 'Book 1', author: 'Author 1', publishedDate: new Date() },
473
+ { id: 2, title: 'Book 2', author: 'Author 2', publishedDate: new Date() },
474
+ ]
475
+
476
+ await service.bulkUpsert(inputs)
477
+
478
+ expect(beforeCreateSpy).toHaveBeenCalledTimes(2)
479
+ expect(afterCreateSpy).toHaveBeenCalledTimes(2)
480
+ expect(beforeUpdateSpy).not.toHaveBeenCalled()
481
+ expect(afterUpdateSpy).not.toHaveBeenCalled()
482
+
483
+ // Verify event details
484
+ expect(beforeCreateSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.beforeCreate' }))
485
+ expect(afterCreateSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.afterCreate' }))
486
+ })
487
+
488
+ it('should trigger correct before and after events for bulk update operations', async () => {
489
+ // Create initial records
490
+ const initialRecords = [
491
+ { id: 1, title: 'Original Book 1', author: 'Original Author 1', publishedDate: new Date() },
492
+ { id: 2, title: 'Original Book 2', author: 'Original Author 2', publishedDate: new Date() },
493
+ ]
494
+
495
+ for (const record of initialRecords) {
496
+ await repository.create(record)
497
+ }
498
+
499
+ // Clear spies from creation
500
+ beforeCreateSpy.mockClear()
501
+ afterCreateSpy.mockClear()
502
+
503
+ // Update records
504
+ const updateInputs = [
505
+ { id: 1, title: 'Updated Book 1', author: 'Updated Author 1', publishedDate: new Date() },
506
+ { id: 2, title: 'Updated Book 2', author: 'Updated Author 2', publishedDate: new Date() },
507
+ ]
508
+
509
+ await service.bulkUpsert(updateInputs)
510
+
511
+ expect(beforeUpdateSpy).toHaveBeenCalledTimes(2)
512
+ expect(afterUpdateSpy).toHaveBeenCalledTimes(2)
513
+ expect(beforeCreateSpy).not.toHaveBeenCalled()
514
+ expect(afterCreateSpy).not.toHaveBeenCalled()
515
+
516
+ // Verify event details
517
+ expect(beforeUpdateSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.beforeUpdate' }))
518
+ expect(afterUpdateSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.afterUpdate' }))
519
+ })
520
+
521
+ it('should trigger correct before and after events for mixed operations', async () => {
522
+ // Create one existing record
523
+ const existingRecord = {
524
+ id: 1,
525
+ title: 'Existing Book',
526
+ author: 'Existing Author',
527
+ publishedDate: new Date(),
528
+ }
529
+ await repository.create(existingRecord)
530
+
531
+ // Clear spies from creation
532
+ beforeCreateSpy.mockClear()
533
+ afterCreateSpy.mockClear()
534
+
535
+ // Mix of update and create
536
+ const mixedInputs = [
537
+ { id: 1, title: 'Updated Book', author: 'Updated Author', publishedDate: new Date() }, // Update
538
+ { id: 2, title: 'New Book', author: 'New Author', publishedDate: new Date() }, // Create
539
+ ]
540
+
541
+ await service.bulkUpsert(mixedInputs)
542
+
543
+ expect(beforeCreateSpy).toHaveBeenCalledTimes(1)
544
+ expect(afterCreateSpy).toHaveBeenCalledTimes(1)
545
+ expect(beforeUpdateSpy).toHaveBeenCalledTimes(1)
546
+ expect(afterUpdateSpy).toHaveBeenCalledTimes(1)
547
+ })
548
+
549
+ it('should trigger events for records without primary keys', async () => {
550
+ const inputsWithoutIds = [
551
+ { title: 'Auto Book 1', author: 'Auto Author 1', publishedDate: new Date() },
552
+ { title: 'Auto Book 2', author: 'Auto Author 2', publishedDate: new Date() },
553
+ ]
554
+
555
+ await service.bulkUpsert(inputsWithoutIds)
556
+
557
+ expect(beforeCreateSpy).toHaveBeenCalledTimes(2)
558
+ expect(afterCreateSpy).toHaveBeenCalledTimes(2)
559
+ expect(beforeUpdateSpy).not.toHaveBeenCalled()
560
+ expect(afterUpdateSpy).not.toHaveBeenCalled()
561
+ })
562
+
563
+ it('should handle large batches efficiently', async () => {
564
+ const batchSize = 100
565
+ const inputs = Array.from({ length: batchSize }, (_, index) => ({
566
+ id: index + 1,
567
+ title: `Book ${index + 1}`,
568
+ author: `Author ${index + 1}`,
569
+ publishedDate: new Date(`2023-${String((index % 12) + 1).padStart(2, '0')}-01`),
570
+ }))
571
+
572
+ const startTime = Date.now()
573
+ const results = await service.bulkUpsert(inputs)
574
+ const endTime = Date.now()
575
+
576
+ expect(results).toHaveLength(batchSize)
577
+ expect(results).toEqual(inputs)
578
+
579
+ // Verify events were triggered for all items
580
+ expect(beforeCreateSpy).toHaveBeenCalledTimes(batchSize)
581
+ expect(afterCreateSpy).toHaveBeenCalledTimes(batchSize)
582
+
583
+ // Basic performance check - should complete in reasonable time
584
+ expect(endTime - startTime).toBeLessThan(5000) // Less than 5 seconds
585
+
586
+ // Verify a few random records
587
+ const randomIndexes = [0, Math.floor(batchSize / 2), batchSize - 1]
588
+ for (const index of randomIndexes) {
589
+ const loaded = await repository.load({ id: inputs[index].id })
590
+ expect(loaded).toEqual(inputs[index])
591
+ }
592
+ })
593
+
594
+ it('should maintain data integrity when bulk upserting duplicate primary keys in input', async () => {
595
+ // Input with duplicate IDs - last one should win
596
+ const inputsWithDuplicates = [
597
+ { id: 1, title: 'First Book 1', author: 'First Author 1', publishedDate: new Date('2023-01-01') },
598
+ { id: 2, title: 'Book 2', author: 'Author 2', publishedDate: new Date('2023-02-01') },
599
+ { id: 1, title: 'Last Book 1', author: 'Last Author 1', publishedDate: new Date('2023-03-01') }, // Duplicate ID
600
+ ]
601
+
602
+ const results = await service.bulkUpsert(inputsWithDuplicates)
603
+
604
+ expect(results).toHaveLength(3)
605
+
606
+ // The repository should handle duplicates according to its implementation
607
+ // In our mock implementation, it processes them sequentially
608
+ const finalRecord1 = await repository.load({ id: 1 })
609
+ expect(finalRecord1?.title).toBe('Last Book 1') // Last write wins
610
+
611
+ const record2 = await repository.load({ id: 2 })
612
+ expect(record2?.title).toBe('Book 2')
613
+ })
614
+
615
+ it('should preserve order of results matching input order', async () => {
616
+ const inputs = [
617
+ { id: 3, title: 'Book 3', author: 'Author 3', publishedDate: new Date('2023-03-01') },
618
+ { id: 1, title: 'Book 1', author: 'Author 1', publishedDate: new Date('2023-01-01') },
619
+ { id: 2, title: 'Book 2', author: 'Author 2', publishedDate: new Date('2023-02-01') },
620
+ ]
621
+
622
+ const results = await service.bulkUpsert(inputs)
623
+
624
+ expect(results).toHaveLength(3)
625
+ expect(results[0].id).toBe(3)
626
+ expect(results[1].id).toBe(1)
627
+ expect(results[2].id).toBe(2)
628
+ expect(results).toEqual(inputs)
629
+ })
630
+ })
631
+
632
+ describe('normalizeInput functionality', () => {
633
+ class TestModelService extends ModelService<typeof mockSchema> {
634
+ public testNormalizeInput(input: any) {
635
+ return this.normalizeInput(input)
636
+ }
637
+
638
+ protected async normalizeInput(input: MockBookInput): Promise<MockBookInput> {
639
+ return {
640
+ ...input,
641
+ title: input.title?.trim(),
642
+ author: input.author?.trim(),
643
+ publishedDate: new Date('2023-01-01'),
644
+ }
645
+ }
646
+ }
647
+
648
+ let testService: TestModelService
649
+
650
+ beforeEach(() => {
651
+ testService = new TestModelService({ repository, emitter, schema: mockSchema, namespace })
652
+ })
653
+
654
+ it('should use default normalizeInput method (no changes) when not overridden', async () => {
655
+ const input = { title: ' Test Book ', author: ' Author Name ', publishedDate: new Date() }
656
+ const normalized = await service['normalizeInput'](input)
657
+
658
+ expect(normalized).toEqual(input)
659
+ expect(normalized).toBe(input) // Should be the exact same reference
660
+ })
661
+
662
+ it('should use custom normalizeInput method for create operation', async () => {
663
+ const input = { title: ' Test Book ', author: ' Author Name ', publishedDate: new Date() }
664
+ const createdItem = await testService.create(input)
665
+
666
+ expect(createdItem.title).toBe('Test Book')
667
+ expect(createdItem.author).toBe('Author Name')
668
+ expect(createdItem.publishedDate).toEqual(new Date('2023-01-01'))
669
+ })
670
+
671
+ it('should use custom normalizeInput method for update operation', async () => {
672
+ const input = { id: 42, title: 'Original Book', author: 'Original Author', publishedDate: new Date() }
673
+ const createdItem = await testService.create(input)
674
+
675
+ const updateInput = { title: ' Updated Book ', author: ' Updated Author ', publishedDate: new Date() }
676
+ const updatedItem = await testService.update({ id: createdItem.id }, updateInput)
677
+
678
+ expect(updatedItem.title).toBe('Updated Book')
679
+ expect(updatedItem.author).toBe('Updated Author')
680
+ expect(updatedItem.publishedDate).toEqual(new Date('2023-01-01'))
681
+ })
682
+
683
+ it('should use custom normalizeInput method for upsert operation', async () => {
684
+ const input = { id: 42, title: ' Test Book ', author: ' Author Name ', publishedDate: new Date() }
685
+ const upsertedItem = await testService.upsert(input)
686
+
687
+ expect(upsertedItem.title).toBe('Test Book')
688
+ expect(upsertedItem.author).toBe('Author Name')
689
+ expect(upsertedItem.publishedDate).toEqual(new Date('2023-01-01'))
690
+
691
+ // Upsert again with different data
692
+ const updateInput = {
693
+ id: 42,
694
+ title: ' Updated Book ',
695
+ author: ' Updated Author ',
696
+ publishedDate: new Date(),
697
+ }
698
+ const updatedItem = await testService.upsert(updateInput)
699
+
700
+ expect(updatedItem.title).toBe('Updated Book')
701
+ expect(updatedItem.author).toBe('Updated Author')
702
+ expect(updatedItem.publishedDate).toEqual(new Date('2023-01-01'))
703
+ })
704
+
705
+ it('should use custom normalizeInput method for bulkUpsert operation', async () => {
706
+ const inputs = [
707
+ { id: 1, title: ' Book One ', author: ' Author One ', publishedDate: new Date() },
708
+ { id: 2, title: ' Book Two ', author: ' Author Two ', publishedDate: new Date() },
709
+ { title: ' Book Three ', author: ' Author Three ', publishedDate: new Date() }, // No ID - will be created
710
+ ]
711
+
712
+ const results = await testService.bulkUpsert(inputs)
713
+
714
+ expect(results).toHaveLength(3)
715
+ expect(results[0].title).toBe('Book One')
716
+ expect(results[0].author).toBe('Author One')
717
+ expect(results[0].publishedDate).toEqual(new Date('2023-01-01'))
718
+
719
+ expect(results[1].title).toBe('Book Two')
720
+ expect(results[1].author).toBe('Author Two')
721
+ expect(results[1].publishedDate).toEqual(new Date('2023-01-01'))
722
+
723
+ expect(results[2].title).toBe('Book Three')
724
+ expect(results[2].author).toBe('Author Three')
725
+ expect(results[2].publishedDate).toEqual(new Date('2023-01-01'))
726
+ })
727
+
728
+ it('should preserve events order with normalized input in create operation', async () => {
729
+ const input = { title: ' Test Book ', author: ' Author Name ', publishedDate: new Date() }
730
+ await testService.create(input)
731
+
732
+ // Debug: Let's see what was actually called
733
+ const beforeCreateCall = beforeCreateSpy.mock.calls[0][0]
734
+ const afterCreateCall = afterCreateSpy.mock.calls[0][0]
735
+
736
+ expect(beforeCreateCall.meta.input.title).toBe('Test Book')
737
+ expect(beforeCreateCall.meta.input.author).toBe('Author Name')
738
+ expect(beforeCreateCall.meta.input.publishedDate).toEqual(new Date('2023-01-01'))
739
+
740
+ expect(afterCreateCall.meta.input.title).toBe('Test Book')
741
+ expect(afterCreateCall.meta.input.author).toBe('Author Name')
742
+ expect(afterCreateCall.meta.input.publishedDate).toEqual(new Date('2023-01-01'))
743
+ })
744
+
745
+ it('should call normalizeInput method exactly once per input during bulkUpsert with Promise.all', async () => {
746
+ const normalizeInputSpy = mock(async (input: any) => ({ ...input, normalized: true }))
747
+
748
+ class SpyService extends ModelService<typeof mockSchema> {
749
+ protected async normalizeInput(input: any) {
750
+ return normalizeInputSpy(input)
751
+ }
752
+ }
753
+
754
+ const spyService = new SpyService({ repository, emitter, schema: mockSchema, namespace })
755
+
756
+ const inputs = [
757
+ { id: 1, title: 'Book One', author: 'Author One', publishedDate: new Date() },
758
+ { id: 2, title: 'Book Two', author: 'Author Two', publishedDate: new Date() },
759
+ ]
760
+
761
+ await spyService.bulkUpsert(inputs)
762
+
763
+ expect(normalizeInputSpy).toHaveBeenCalledTimes(2)
764
+ expect(normalizeInputSpy).toHaveBeenNthCalledWith(1, inputs[0])
765
+ expect(normalizeInputSpy).toHaveBeenNthCalledWith(2, inputs[1])
766
+ })
767
+
768
+ it('should handle async normalization errors gracefully', async () => {
769
+ class ErrorNormalizationService extends ModelService<typeof mockSchema> {
770
+ protected async normalizeInput(input: any) {
771
+ if (input.title === 'ERROR') {
772
+ throw new Error('Normalization failed')
773
+ }
774
+ return input
775
+ }
776
+ }
777
+
778
+ const errorService = new ErrorNormalizationService({
779
+ repository,
780
+ emitter,
781
+ schema: mockSchema,
782
+ namespace,
783
+ })
784
+
785
+ const input = { title: 'ERROR', author: 'Author Name', publishedDate: new Date() }
786
+
787
+ await expect(errorService.create(input)).rejects.toThrow('Normalization failed')
788
+ })
789
+
790
+ it('should process bulk normalization in parallel for performance', async () => {
791
+ const processingTimes: number[] = []
792
+
793
+ class TimingNormalizationService extends ModelService<typeof mockSchema> {
794
+ protected async normalizeInput(input: any) {
795
+ const start = Date.now()
796
+ // Simulate some async work
797
+ await new Promise((resolve) => setTimeout(resolve, 50))
798
+ processingTimes.push(Date.now() - start)
799
+ return input
800
+ }
801
+ }
802
+
803
+ const timingService = new TimingNormalizationService({
804
+ repository,
805
+ emitter,
806
+ schema: mockSchema,
807
+ namespace,
808
+ })
809
+
810
+ const inputs = [
811
+ { id: 1, title: 'Book One', author: 'Author One', publishedDate: new Date() },
812
+ { id: 2, title: 'Book Two', author: 'Author Two', publishedDate: new Date() },
813
+ { id: 3, title: 'Book Three', author: 'Author Three', publishedDate: new Date() },
814
+ ]
815
+
816
+ const start = Date.now()
817
+ await timingService.bulkUpsert(inputs)
818
+ const totalTime = Date.now() - start
819
+
820
+ // With Promise.all, total time should be closer to single operation time rather than sum of all
821
+ // Allow some variance for test stability
822
+ expect(totalTime).toBeLessThan(150) // Much less than 3 * 50ms = 150ms
823
+ expect(processingTimes).toHaveLength(3)
824
+ })
825
+ })
826
+
827
+ describe('Response Normalization', () => {
828
+ class TestRepository extends MockMemoryRepository<typeof mockSchema> {
829
+ constructor() {
830
+ super({
831
+ schema: mockSchema,
832
+ })
833
+ }
834
+
835
+ async create(input: MockBookInput): Promise<any> {
836
+ const record = await super.create(input)
837
+ // Return with publishedDate as string to test normalization
838
+ record.publishedDate = '2024-01-01' as any
839
+ return record
840
+ }
841
+
842
+ async update(lookup: any, input: MockBookInput): Promise<any> {
843
+ const record = await super.update(lookup, input)
844
+ // Return with publishedDate as string to test normalization
845
+ record.publishedDate = '2024-01-01' as any
846
+ return record
847
+ }
848
+
849
+ async upsert(input: MockBookInput): Promise<any> {
850
+ const record = await super.upsert(input)
851
+ // Return with publishedDate as string to test normalization
852
+ record.publishedDate = '2024-01-01' as any
853
+ return record
854
+ }
855
+
856
+ async bulkUpsert(inputs: MockBookInput[]): Promise<any[]> {
857
+ const records = await super.bulkUpsert(inputs)
858
+ // Return with publishedDate as string to test normalization
859
+ for (const record of records) {
860
+ record.publishedDate = '2024-01-01' as any
861
+ }
862
+ return records
863
+ }
864
+
865
+ async remove(lookup: any): Promise<any> {
866
+ const record = await super.remove(lookup)
867
+ // Return with publishedDate as string to test normalization
868
+ if (record) {
869
+ record.publishedDate = '2024-01-01' as any
870
+ }
871
+ return record
872
+ }
873
+
874
+ async restore(lookup: any): Promise<any> {
875
+ const record = await super.restore(lookup)
876
+ // Return with publishedDate as string to test normalization
877
+ if (record) {
878
+ record.publishedDate = '2024-01-01' as any
879
+ }
880
+ return record
881
+ }
882
+ }
883
+
884
+ class TestService extends ModelService<typeof mockSchema> {}
885
+
886
+ let testRepository: TestRepository
887
+ let testService: TestService
888
+
889
+ beforeEach(() => {
890
+ testRepository = new TestRepository()
891
+ emitter = new EventManager()
892
+
893
+ testService = new TestService({
894
+ repository: testRepository,
895
+ emitter,
896
+ schema: mockSchema,
897
+ namespace,
898
+ })
899
+ })
900
+
901
+ it('should normalize details in the create response', async () => {
902
+ const input = { id: 200, title: 'Create Test', author: 'Creator', publishedDate: new Date() }
903
+ const record = await testService.create(input)
904
+
905
+ const expectedDate = new Date('2024-01-01')
906
+ const actualDate = record.publishedDate
907
+
908
+ expect(actualDate).toEqual(expectedDate)
909
+ expect(actualDate).toBeInstanceOf(Date)
910
+ })
911
+
912
+ it('should normalize details in the update response', async () => {
913
+ const input = { id: 201, title: 'Update Test', author: 'Updater', publishedDate: new Date() }
914
+ await testRepository.create(input)
915
+
916
+ const updatedInput = { title: 'Updated Test', author: 'Updated Author', publishedDate: new Date() }
917
+ const record = await testService.update({ id: 201 }, updatedInput)
918
+
919
+ const expectedDate = new Date('2024-01-01')
920
+ const actualDate = record.publishedDate
921
+
922
+ expect(actualDate).toEqual(expectedDate)
923
+ expect(actualDate).toBeInstanceOf(Date)
924
+ })
925
+
926
+ it('should normalize details in the upsert response when creating', async () => {
927
+ const input = { id: 202, title: 'Upsert Create Test', author: 'Upserter', publishedDate: new Date() }
928
+ const record = await testService.upsert(input)
929
+
930
+ const expectedDate = new Date('2024-01-01')
931
+ const actualDate = record.publishedDate
932
+
933
+ expect(actualDate).toEqual(expectedDate)
934
+ expect(actualDate).toBeInstanceOf(Date)
935
+ })
936
+
937
+ it('should normalize details in the upsert response when updating', async () => {
938
+ const input = { id: 203, title: 'Upsert Update Test', author: 'Upserter', publishedDate: new Date() }
939
+ await testRepository.create(input)
940
+
941
+ const updatedInput = {
942
+ id: 203,
943
+ title: 'Updated Upsert Test',
944
+ author: 'Updated Upserter',
945
+ publishedDate: new Date(),
946
+ }
947
+ const record = await testService.upsert(updatedInput)
948
+
949
+ const expectedDate = new Date('2024-01-01')
950
+ const actualDate = record.publishedDate
951
+
952
+ expect(actualDate).toEqual(expectedDate)
953
+ expect(actualDate).toBeInstanceOf(Date)
954
+ })
955
+
956
+ it('should normalize details in the bulkUpsert response', async () => {
957
+ const input1 = { id: 204, title: 'Bulk Test 1', author: 'Bulk Author 1', publishedDate: new Date() }
958
+ const input2 = { id: 205, title: 'Bulk Test 2', author: 'Bulk Author 2', publishedDate: new Date() }
959
+
960
+ const records = await testService.bulkUpsert([input1, input2])
961
+
962
+ const expectedDate = new Date('2024-01-01')
963
+
964
+ for (const record of records) {
965
+ const actualDate = record.publishedDate
966
+ expect(actualDate).toEqual(expectedDate)
967
+ expect(actualDate).toBeInstanceOf(Date)
968
+ }
969
+ })
970
+
971
+ it('should normalize details in the bulkUpsert response with mixed create and update', async () => {
972
+ const existingInput = { id: 206, title: 'Existing', author: 'Existing Author', publishedDate: new Date() }
973
+ await testRepository.create(existingInput)
974
+
975
+ const updateInput = {
976
+ id: 206,
977
+ title: 'Updated Existing',
978
+ author: 'Updated Author',
979
+ publishedDate: new Date(),
980
+ }
981
+ const createInput = { id: 207, title: 'New Record', author: 'New Author', publishedDate: new Date() }
982
+
983
+ const records = await testService.bulkUpsert([updateInput, createInput])
984
+
985
+ const expectedDate = new Date('2024-01-01')
986
+
987
+ for (const record of records) {
988
+ const actualDate = record.publishedDate
989
+ expect(actualDate).toEqual(expectedDate)
990
+ expect(actualDate).toBeInstanceOf(Date)
991
+ }
992
+ })
993
+
994
+ it('should normalize summaries in the remove response', async () => {
995
+ const input = { id: 208, title: 'Remove Test', author: 'Remover', publishedDate: new Date() }
996
+ await testRepository.create(input)
997
+
998
+ const record = await testService.remove({ id: 208 })
999
+
1000
+ const expectedDate = new Date('2024-01-01')
1001
+ const actualDate = record.publishedDate
1002
+
1003
+ expect(actualDate).toEqual(expectedDate)
1004
+ expect(actualDate).toBeInstanceOf(Date)
1005
+ })
1006
+
1007
+ it('should normalize summaries in the restore response', async () => {
1008
+ const input = { id: 209, title: 'Restore Test', author: 'Restorer', publishedDate: new Date() }
1009
+ await testRepository.create(input)
1010
+ await testRepository.remove({ id: 209 })
1011
+
1012
+ const record = await testService.restore({ id: 209 })
1013
+
1014
+ const expectedDate = new Date('2024-01-01')
1015
+ const actualDate = record.publishedDate
1016
+
1017
+ expect(actualDate).toEqual(expectedDate)
1018
+ expect(actualDate).toBeInstanceOf(Date)
1019
+ })
1020
+ })
1021
+ })