@declaro/data 2.0.0-beta.14 → 2.0.0-beta.140

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 (142) 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 +13372 -0
  6. package/dist/node/index.cjs.map +93 -0
  7. package/dist/node/index.js +13351 -0
  8. package/dist/node/index.js.map +93 -0
  9. package/dist/ts/application/model-controller.d.ts +60 -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 +24 -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 +37 -0
  22. package/dist/ts/domain/events/event-types.d.ts.map +1 -0
  23. package/dist/ts/domain/events/mutation-event.d.ts +41 -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 +8 -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 +26 -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 +110 -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 +23 -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 +99 -0
  44. package/dist/ts/domain/services/model-service.d.ts.map +1 -0
  45. package/dist/ts/domain/services/model-service.normalization.test.d.ts +2 -0
  46. package/dist/ts/domain/services/model-service.normalization.test.d.ts.map +1 -0
  47. package/dist/ts/domain/services/model-service.test.d.ts +2 -0
  48. package/dist/ts/domain/services/model-service.test.d.ts.map +1 -0
  49. package/dist/ts/domain/services/read-only-model-service.d.ts +90 -0
  50. package/dist/ts/domain/services/read-only-model-service.d.ts.map +1 -0
  51. package/dist/ts/domain/services/read-only-model-service.test.d.ts +2 -0
  52. package/dist/ts/domain/services/read-only-model-service.test.d.ts.map +1 -0
  53. package/dist/ts/index.d.ts +18 -0
  54. package/dist/ts/index.d.ts.map +1 -0
  55. package/dist/ts/shared/utils/schema-inference.d.ts +23 -0
  56. package/dist/ts/shared/utils/schema-inference.d.ts.map +1 -0
  57. package/dist/ts/shared/utils/schema-inheritance.d.ts +24 -0
  58. package/dist/ts/shared/utils/schema-inheritance.d.ts.map +1 -0
  59. package/dist/ts/shared/utils/schema-inheritance.test.d.ts +2 -0
  60. package/dist/ts/shared/utils/schema-inheritance.test.d.ts.map +1 -0
  61. package/dist/ts/shared/utils/test/animal-schema.d.ts +57 -0
  62. package/dist/ts/shared/utils/test/animal-schema.d.ts.map +1 -0
  63. package/dist/ts/shared/utils/test/animal-trait-schema.d.ts +55 -0
  64. package/dist/ts/shared/utils/test/animal-trait-schema.d.ts.map +1 -0
  65. package/dist/ts/shared/utils/test/elephant-schema.d.ts +30 -0
  66. package/dist/ts/shared/utils/test/elephant-schema.d.ts.map +1 -0
  67. package/dist/ts/shared/utils/test/elephant-trait-schema.d.ts +26 -0
  68. package/dist/ts/shared/utils/test/elephant-trait-schema.d.ts.map +1 -0
  69. package/dist/ts/test/mock/models/mock-book-models.d.ts +42 -0
  70. package/dist/ts/test/mock/models/mock-book-models.d.ts.map +1 -0
  71. package/dist/ts/test/mock/repositories/mock-memory-repository.assign.test.d.ts +2 -0
  72. package/dist/ts/test/mock/repositories/mock-memory-repository.assign.test.d.ts.map +1 -0
  73. package/dist/ts/test/mock/repositories/mock-memory-repository.basic.test.d.ts +2 -0
  74. package/dist/ts/test/mock/repositories/mock-memory-repository.basic.test.d.ts.map +1 -0
  75. package/dist/ts/test/mock/repositories/mock-memory-repository.bulk-upsert.test.d.ts +2 -0
  76. package/dist/ts/test/mock/repositories/mock-memory-repository.bulk-upsert.test.d.ts.map +1 -0
  77. package/dist/ts/test/mock/repositories/mock-memory-repository.count.test.d.ts +2 -0
  78. package/dist/ts/test/mock/repositories/mock-memory-repository.count.test.d.ts.map +1 -0
  79. package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts +62 -0
  80. package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts.map +1 -0
  81. package/dist/ts/test/mock/repositories/mock-memory-repository.search.test.d.ts +2 -0
  82. package/dist/ts/test/mock/repositories/mock-memory-repository.search.test.d.ts.map +1 -0
  83. package/dist/ts/test/mock/repositories/mock-memory-repository.trash.test.d.ts +2 -0
  84. package/dist/ts/test/mock/repositories/mock-memory-repository.trash.test.d.ts.map +1 -0
  85. package/dist/ts/test/mock/repositories/mock-memory-repository.upsert.test.d.ts +2 -0
  86. package/dist/ts/test/mock/repositories/mock-memory-repository.upsert.test.d.ts.map +1 -0
  87. package/package.json +46 -42
  88. package/src/application/model-controller.test.ts +694 -0
  89. package/src/application/model-controller.ts +186 -0
  90. package/src/application/read-only-model-controller.test.ts +335 -0
  91. package/src/application/read-only-model-controller.ts +79 -0
  92. package/src/domain/events/domain-event.test.ts +82 -0
  93. package/src/domain/events/domain-event.ts +69 -0
  94. package/src/domain/events/event-types.ts +37 -0
  95. package/src/domain/events/mutation-event.test.ts +390 -0
  96. package/src/domain/events/mutation-event.ts +53 -0
  97. package/src/domain/events/query-event.test.ts +228 -0
  98. package/src/domain/events/query-event.ts +14 -0
  99. package/src/domain/events/request-event.test.ts +38 -0
  100. package/src/domain/events/request-event.ts +47 -0
  101. package/src/domain/interfaces/repository.ts +136 -0
  102. package/src/domain/models/pagination.ts +28 -0
  103. package/src/domain/services/base-model-service.ts +54 -0
  104. package/src/domain/services/model-service-args.ts +9 -0
  105. package/src/domain/services/model-service.normalization.test.ts +704 -0
  106. package/src/domain/services/model-service.test.ts +1616 -0
  107. package/src/domain/services/model-service.ts +597 -0
  108. package/src/domain/services/read-only-model-service.test.ts +1130 -0
  109. package/src/domain/services/read-only-model-service.ts +211 -0
  110. package/src/index.ts +17 -4
  111. package/src/shared/utils/schema-inference.ts +26 -0
  112. package/src/shared/utils/schema-inheritance.test.ts +295 -0
  113. package/src/shared/utils/schema-inheritance.ts +28 -0
  114. package/src/shared/utils/test/animal-schema.ts +46 -0
  115. package/src/shared/utils/test/animal-trait-schema.ts +45 -0
  116. package/src/shared/utils/test/elephant-schema.ts +58 -0
  117. package/src/shared/utils/test/elephant-trait-schema.ts +53 -0
  118. package/src/test/mock/models/mock-book-models.ts +78 -0
  119. package/src/test/mock/repositories/mock-memory-repository.assign.test.ts +215 -0
  120. package/src/test/mock/repositories/mock-memory-repository.basic.test.ts +129 -0
  121. package/src/test/mock/repositories/mock-memory-repository.bulk-upsert.test.ts +159 -0
  122. package/src/test/mock/repositories/mock-memory-repository.count.test.ts +98 -0
  123. package/src/test/mock/repositories/mock-memory-repository.search.test.ts +265 -0
  124. package/src/test/mock/repositories/mock-memory-repository.trash.test.ts +736 -0
  125. package/src/test/mock/repositories/mock-memory-repository.ts +401 -0
  126. package/src/test/mock/repositories/mock-memory-repository.upsert.test.ts +108 -0
  127. package/dist/databaseConnection.d.ts +0 -24
  128. package/dist/datastoreAbstract.d.ts +0 -37
  129. package/dist/declaro-data.cjs +0 -1
  130. package/dist/declaro-data.mjs +0 -250
  131. package/dist/hydrateEntity.d.ts +0 -8
  132. package/dist/index.d.ts +0 -4
  133. package/dist/serverConnection.d.ts +0 -15
  134. package/dist/trackedStatus.d.ts +0 -15
  135. package/src/databaseConnection.ts +0 -137
  136. package/src/datastoreAbstract.ts +0 -190
  137. package/src/hydrateEntity.ts +0 -36
  138. package/src/placeholder.test.ts +0 -7
  139. package/src/serverConnection.ts +0 -74
  140. package/src/trackedStatus.ts +0 -35
  141. package/tsconfig.json +0 -10
  142. package/vite.config.ts +0 -23
@@ -0,0 +1,704 @@
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
+ import type { InferDetail, InferSummary } from '../../shared/utils/schema-inference'
8
+ import { ModelMutationAction } from '../events/event-types'
9
+
10
+ describe('ModelService - Normalization', () => {
11
+ const namespace = 'books'
12
+ const mockSchema = MockBookSchema
13
+
14
+ let repository: MockMemoryRepository<typeof mockSchema>
15
+ let emitter: EventManager
16
+ let service: ModelService<typeof mockSchema>
17
+
18
+ beforeEach(() => {
19
+ repository = new MockMemoryRepository({ schema: mockSchema })
20
+ emitter = new EventManager()
21
+ service = new ModelService({ repository, emitter, schema: mockSchema, namespace })
22
+ })
23
+
24
+ const beforeCreateSpy = mock((event) => {})
25
+ const afterCreateSpy = mock((event) => {})
26
+
27
+ beforeEach(() => {
28
+ emitter.on('books::book.beforeCreate', beforeCreateSpy)
29
+ emitter.on('books::book.afterCreate', afterCreateSpy)
30
+
31
+ beforeCreateSpy.mockClear()
32
+ afterCreateSpy.mockClear()
33
+ })
34
+
35
+ describe('normalizeInput functionality', () => {
36
+ class TestModelService extends ModelService<typeof mockSchema> {
37
+ public testNormalizeInput(input: any) {
38
+ return this.normalizeInput(input)
39
+ }
40
+
41
+ protected async normalizeInput(input: MockBookInput): Promise<MockBookInput> {
42
+ return {
43
+ ...input,
44
+ title: input.title?.trim(),
45
+ author: input.author?.trim(),
46
+ publishedDate: new Date('2023-01-01'),
47
+ }
48
+ }
49
+ }
50
+
51
+ let testService: TestModelService
52
+
53
+ beforeEach(() => {
54
+ testService = new TestModelService({ repository, emitter, schema: mockSchema, namespace })
55
+ })
56
+
57
+ it('should use default normalizeInput method (no changes) when not overridden', async () => {
58
+ const input = { title: ' Test Book ', author: ' Author Name ', publishedDate: new Date() }
59
+ const normalized = await service['normalizeInput'](input, {
60
+ descriptor: service['getDescriptor'](ModelMutationAction.Create),
61
+ })
62
+
63
+ expect(normalized).toEqual(input)
64
+ expect(normalized).toBe(input) // Should be the exact same reference
65
+ })
66
+
67
+ it('should use custom normalizeInput method for create operation', async () => {
68
+ const input = { title: ' Test Book ', author: ' Author Name ', publishedDate: new Date() }
69
+ const createdItem = await testService.create(input)
70
+
71
+ expect(createdItem.title).toBe('Test Book')
72
+ expect(createdItem.author).toBe('Author Name')
73
+ expect(createdItem.publishedDate).toEqual(new Date('2023-01-01'))
74
+ })
75
+
76
+ it('should use custom normalizeInput method for update operation', async () => {
77
+ const input = { id: 42, title: 'Original Book', author: 'Original Author', publishedDate: new Date() }
78
+ const createdItem = await testService.create(input)
79
+
80
+ const updateInput = { title: ' Updated Book ', author: ' Updated Author ', publishedDate: new Date() }
81
+ const updatedItem = await testService.update({ id: createdItem.id }, updateInput)
82
+
83
+ expect(updatedItem.title).toBe('Updated Book')
84
+ expect(updatedItem.author).toBe('Updated Author')
85
+ expect(updatedItem.publishedDate).toEqual(new Date('2023-01-01'))
86
+ })
87
+
88
+ it('should use custom normalizeInput method for upsert operation', async () => {
89
+ const input = { id: 42, title: ' Test Book ', author: ' Author Name ', publishedDate: new Date() }
90
+ const upsertedItem = await testService.upsert(input)
91
+
92
+ expect(upsertedItem.title).toBe('Test Book')
93
+ expect(upsertedItem.author).toBe('Author Name')
94
+ expect(upsertedItem.publishedDate).toEqual(new Date('2023-01-01'))
95
+
96
+ // Upsert again with different data
97
+ const updateInput = {
98
+ id: 42,
99
+ title: ' Updated Book ',
100
+ author: ' Updated Author ',
101
+ publishedDate: new Date(),
102
+ }
103
+ const updatedItem = await testService.upsert(updateInput)
104
+
105
+ expect(updatedItem.title).toBe('Updated Book')
106
+ expect(updatedItem.author).toBe('Updated Author')
107
+ expect(updatedItem.publishedDate).toEqual(new Date('2023-01-01'))
108
+ })
109
+
110
+ it('should use custom normalizeInput method for bulkUpsert operation', async () => {
111
+ const inputs = [
112
+ { id: 1, title: ' Book One ', author: ' Author One ', publishedDate: new Date() },
113
+ { id: 2, title: ' Book Two ', author: ' Author Two ', publishedDate: new Date() },
114
+ { title: ' Book Three ', author: ' Author Three ', publishedDate: new Date() }, // No ID - will be created
115
+ ]
116
+
117
+ const results = await testService.bulkUpsert(inputs)
118
+
119
+ expect(results).toHaveLength(3)
120
+ expect(results[0].title).toBe('Book One')
121
+ expect(results[0].author).toBe('Author One')
122
+ expect(results[0].publishedDate).toEqual(new Date('2023-01-01'))
123
+
124
+ expect(results[1].title).toBe('Book Two')
125
+ expect(results[1].author).toBe('Author Two')
126
+ expect(results[1].publishedDate).toEqual(new Date('2023-01-01'))
127
+
128
+ expect(results[2].title).toBe('Book Three')
129
+ expect(results[2].author).toBe('Author Three')
130
+ expect(results[2].publishedDate).toEqual(new Date('2023-01-01'))
131
+ })
132
+
133
+ it('should preserve events order with normalized input in create operation', async () => {
134
+ const input = { title: ' Test Book ', author: ' Author Name ', publishedDate: new Date() }
135
+ await testService.create(input)
136
+
137
+ // Debug: Let's see what was actually called
138
+ const beforeCreateCall = beforeCreateSpy.mock.calls[0][0]
139
+ const afterCreateCall = afterCreateSpy.mock.calls[0][0]
140
+
141
+ expect(beforeCreateCall.input.title).toBe('Test Book')
142
+ expect(beforeCreateCall.input.author).toBe('Author Name')
143
+ expect(beforeCreateCall.input.publishedDate).toEqual(new Date('2023-01-01'))
144
+
145
+ expect(afterCreateCall.input.title).toBe('Test Book')
146
+ expect(afterCreateCall.input.author).toBe('Author Name')
147
+ expect(afterCreateCall.input.publishedDate).toEqual(new Date('2023-01-01'))
148
+ })
149
+
150
+ it('should call normalizeInput method exactly once per input during bulkUpsert with Promise.all', async () => {
151
+ const normalizeInputSpy = mock(async (input: any) => ({ ...input, normalized: true }))
152
+
153
+ class SpyService extends ModelService<typeof mockSchema> {
154
+ protected async normalizeInput(input: any) {
155
+ return normalizeInputSpy(input)
156
+ }
157
+ }
158
+
159
+ const spyService = new SpyService({ repository, emitter, schema: mockSchema, namespace })
160
+
161
+ const inputs = [
162
+ { id: 1, title: 'Book One', author: 'Author One', publishedDate: new Date() },
163
+ { id: 2, title: 'Book Two', author: 'Author Two', publishedDate: new Date() },
164
+ ]
165
+
166
+ await spyService.bulkUpsert(inputs)
167
+
168
+ expect(normalizeInputSpy).toHaveBeenCalledTimes(2)
169
+ expect(normalizeInputSpy).toHaveBeenNthCalledWith(1, inputs[0])
170
+ expect(normalizeInputSpy).toHaveBeenNthCalledWith(2, inputs[1])
171
+ })
172
+
173
+ it('should handle async normalization errors gracefully', async () => {
174
+ class ErrorNormalizationService extends ModelService<typeof mockSchema> {
175
+ protected async normalizeInput(input: any) {
176
+ if (input.title === 'ERROR') {
177
+ throw new Error('Normalization failed')
178
+ }
179
+ return input
180
+ }
181
+ }
182
+
183
+ const errorService = new ErrorNormalizationService({
184
+ repository,
185
+ emitter,
186
+ schema: mockSchema,
187
+ namespace,
188
+ })
189
+
190
+ const input = { title: 'ERROR', author: 'Author Name', publishedDate: new Date() }
191
+
192
+ await expect(errorService.create(input)).rejects.toThrow('Normalization failed')
193
+ })
194
+
195
+ it('should process bulk normalization in parallel for performance', async () => {
196
+ const processingTimes: number[] = []
197
+
198
+ class TimingNormalizationService extends ModelService<typeof mockSchema> {
199
+ protected async normalizeInput(input: any) {
200
+ const start = Date.now()
201
+ // Simulate some async work
202
+ await new Promise((resolve) => setTimeout(resolve, 50))
203
+ processingTimes.push(Date.now() - start)
204
+ return input
205
+ }
206
+ }
207
+
208
+ const timingService = new TimingNormalizationService({
209
+ repository,
210
+ emitter,
211
+ schema: mockSchema,
212
+ namespace,
213
+ })
214
+
215
+ const inputs = [
216
+ { id: 1, title: 'Book One', author: 'Author One', publishedDate: new Date() },
217
+ { id: 2, title: 'Book Two', author: 'Author Two', publishedDate: new Date() },
218
+ { id: 3, title: 'Book Three', author: 'Author Three', publishedDate: new Date() },
219
+ ]
220
+
221
+ const start = Date.now()
222
+ await timingService.bulkUpsert(inputs)
223
+ const totalTime = Date.now() - start
224
+
225
+ // With Promise.all, total time should be closer to single operation time rather than sum of all
226
+ // Allow some variance for test stability
227
+ expect(totalTime).toBeLessThan(150) // Much less than 3 * 50ms = 150ms
228
+ expect(processingTimes).toHaveLength(3)
229
+ })
230
+
231
+ describe('normalizeInput parameters verification', () => {
232
+ let normalizeInputCalls: Array<{
233
+ input: any
234
+ existing?: any
235
+ action: string
236
+ scope: string
237
+ }> = []
238
+
239
+ class ParameterVerificationService extends ModelService<typeof mockSchema> {
240
+ protected async normalizeInput(input: any, args: any) {
241
+ normalizeInputCalls.push({
242
+ input,
243
+ existing: args.existing,
244
+ action: args.descriptor.action,
245
+ scope: args.descriptor.scope,
246
+ })
247
+ return input
248
+ }
249
+ }
250
+
251
+ let verificationService: ParameterVerificationService
252
+
253
+ beforeEach(() => {
254
+ normalizeInputCalls = []
255
+ verificationService = new ParameterVerificationService({
256
+ repository,
257
+ emitter,
258
+ schema: mockSchema,
259
+ namespace,
260
+ })
261
+ })
262
+
263
+ it('should pass correct descriptor and no existing entity for create operation', async () => {
264
+ const input = { title: 'New Book', author: 'New Author', publishedDate: new Date() }
265
+
266
+ await verificationService.create(input)
267
+
268
+ expect(normalizeInputCalls).toHaveLength(1)
269
+ expect(normalizeInputCalls[0].input).toEqual(input)
270
+ expect(normalizeInputCalls[0].existing).toBeUndefined()
271
+ expect(normalizeInputCalls[0].action).toBe('create')
272
+ expect(normalizeInputCalls[0].scope).toBeUndefined()
273
+ })
274
+
275
+ it('should pass correct descriptor and existing entity for update operation', async () => {
276
+ // First create a book
277
+ const originalInput = {
278
+ id: 42,
279
+ title: 'Original Book',
280
+ author: 'Original Author',
281
+ publishedDate: new Date(),
282
+ }
283
+ await repository.create(originalInput)
284
+
285
+ // Clear calls from create operation
286
+ normalizeInputCalls = []
287
+
288
+ // Now update it
289
+ const updateInput = { title: 'Updated Book', author: 'Updated Author', publishedDate: new Date() }
290
+ await verificationService.update({ id: 42 }, updateInput)
291
+
292
+ expect(normalizeInputCalls).toHaveLength(1)
293
+ expect(normalizeInputCalls[0].input).toEqual(updateInput)
294
+ expect(normalizeInputCalls[0].existing).toEqual(originalInput)
295
+ expect(normalizeInputCalls[0].action).toBe('update')
296
+ expect(normalizeInputCalls[0].scope).toBeUndefined()
297
+ })
298
+
299
+ it('should pass correct descriptor and existing entity for upsert operation with existing record', async () => {
300
+ // First create a book
301
+ const originalInput = {
302
+ id: 42,
303
+ title: 'Original Book',
304
+ author: 'Original Author',
305
+ publishedDate: new Date(),
306
+ }
307
+ await repository.create(originalInput)
308
+
309
+ // Clear calls from create operation
310
+ normalizeInputCalls = []
311
+
312
+ // Now upsert with same ID (update scenario)
313
+ const upsertInput = {
314
+ id: 42,
315
+ title: 'Upserted Book',
316
+ author: 'Upserted Author',
317
+ publishedDate: new Date(),
318
+ }
319
+ await verificationService.upsert(upsertInput)
320
+
321
+ expect(normalizeInputCalls).toHaveLength(1)
322
+ expect(normalizeInputCalls[0].input).toEqual(upsertInput)
323
+ expect(normalizeInputCalls[0].existing).toEqual(originalInput)
324
+ expect(normalizeInputCalls[0].action).toBe('update')
325
+ expect(normalizeInputCalls[0].scope).toBeUndefined()
326
+ })
327
+
328
+ it('should pass correct descriptor and no existing entity for upsert operation without existing record', async () => {
329
+ const upsertInput = {
330
+ id: 42,
331
+ title: 'New Book via Upsert',
332
+ author: 'New Author',
333
+ publishedDate: new Date(),
334
+ }
335
+
336
+ await verificationService.upsert(upsertInput)
337
+
338
+ expect(normalizeInputCalls).toHaveLength(1)
339
+ expect(normalizeInputCalls[0].input).toEqual(upsertInput)
340
+ // Note: repository.load returns null for non-existent records, not undefined
341
+ expect(normalizeInputCalls[0].existing).toBeNull()
342
+ expect(normalizeInputCalls[0].action).toBe('create')
343
+ expect(normalizeInputCalls[0].scope).toBeUndefined()
344
+ })
345
+
346
+ it('should pass correct descriptor and no existing entity for upsert operation without primary key', async () => {
347
+ const upsertInput = { title: 'Auto-ID Book', author: 'Auto Author', publishedDate: new Date() }
348
+
349
+ await verificationService.upsert(upsertInput)
350
+
351
+ expect(normalizeInputCalls).toHaveLength(1)
352
+ expect(normalizeInputCalls[0].input).toEqual(upsertInput)
353
+ expect(normalizeInputCalls[0].existing).toBeUndefined()
354
+ expect(normalizeInputCalls[0].action).toBe('create')
355
+ expect(normalizeInputCalls[0].scope).toBeUndefined()
356
+ })
357
+
358
+ it('should pass correct descriptors and existing entities for bulkUpsert operation with mixed scenarios', async () => {
359
+ // Pre-create some books
360
+ const existing1 = {
361
+ id: 1,
362
+ title: 'Existing Book 1',
363
+ author: 'Existing Author 1',
364
+ publishedDate: new Date('2023-01-01'),
365
+ }
366
+ const existing2 = {
367
+ id: 2,
368
+ title: 'Existing Book 2',
369
+ author: 'Existing Author 2',
370
+ publishedDate: new Date('2023-02-01'),
371
+ }
372
+ await repository.create(existing1)
373
+ await repository.create(existing2)
374
+
375
+ // Clear calls from create operations
376
+ normalizeInputCalls = []
377
+
378
+ // Now bulk upsert with mixed scenarios
379
+ const bulkInputs = [
380
+ { id: 1, title: 'Updated Book 1', author: 'Updated Author 1', publishedDate: new Date() }, // Update existing
381
+ { id: 3, title: 'New Book 3', author: 'New Author 3', publishedDate: new Date() }, // Create with ID
382
+ { title: 'Auto Book', author: 'Auto Author', publishedDate: new Date() }, // Create without ID
383
+ { id: 2, title: 'Updated Book 2', author: 'Updated Author 2', publishedDate: new Date() }, // Update existing
384
+ ]
385
+
386
+ await verificationService.bulkUpsert(bulkInputs)
387
+
388
+ expect(normalizeInputCalls).toHaveLength(4)
389
+
390
+ // First input: update existing book with ID 1
391
+ expect(normalizeInputCalls[0].input).toEqual(bulkInputs[0])
392
+ expect(normalizeInputCalls[0].existing).toEqual(existing1)
393
+ expect(normalizeInputCalls[0].action).toBe('update')
394
+ expect(normalizeInputCalls[0].scope).toBeUndefined()
395
+
396
+ // Second input: create new book with ID 3
397
+ expect(normalizeInputCalls[1].input).toEqual(bulkInputs[1])
398
+ expect(normalizeInputCalls[1].existing).toBeUndefined()
399
+ expect(normalizeInputCalls[1].action).toBe('create')
400
+ expect(normalizeInputCalls[1].scope).toBeUndefined()
401
+
402
+ // Third input: create new book without ID
403
+ expect(normalizeInputCalls[2].input).toEqual(bulkInputs[2])
404
+ expect(normalizeInputCalls[2].existing).toBeUndefined()
405
+ expect(normalizeInputCalls[2].action).toBe('create')
406
+ expect(normalizeInputCalls[2].scope).toBeUndefined()
407
+
408
+ // Fourth input: update existing book with ID 2
409
+ expect(normalizeInputCalls[3].input).toEqual(bulkInputs[3])
410
+ expect(normalizeInputCalls[3].existing).toEqual(existing2)
411
+ expect(normalizeInputCalls[3].action).toBe('update')
412
+ expect(normalizeInputCalls[3].scope).toBeUndefined()
413
+ })
414
+
415
+ it('should pass correct descriptors for bulkUpsert with duplicate primary keys', async () => {
416
+ // Pre-create a book
417
+ const existing = { id: 1, title: 'Existing Book', author: 'Existing Author', publishedDate: new Date() }
418
+ await repository.create(existing)
419
+
420
+ // Clear calls from create operation
421
+ normalizeInputCalls = []
422
+
423
+ // Bulk upsert with duplicate primary keys
424
+ const bulkInputs = [
425
+ { id: 1, title: 'First Update', author: 'First Author', publishedDate: new Date() },
426
+ { id: 2, title: 'New Book', author: 'New Author', publishedDate: new Date() },
427
+ { id: 1, title: 'Second Update', author: 'Second Author', publishedDate: new Date() }, // Duplicate ID
428
+ ]
429
+
430
+ await verificationService.bulkUpsert(bulkInputs)
431
+
432
+ expect(normalizeInputCalls).toHaveLength(3)
433
+
434
+ // First input: update existing book with ID 1
435
+ expect(normalizeInputCalls[0].input).toEqual(bulkInputs[0])
436
+ expect(normalizeInputCalls[0].existing).toEqual(existing)
437
+ expect(normalizeInputCalls[0].action).toBe('update')
438
+
439
+ // Second input: create new book with ID 2
440
+ expect(normalizeInputCalls[1].input).toEqual(bulkInputs[1])
441
+ expect(normalizeInputCalls[1].existing).toBeUndefined()
442
+ expect(normalizeInputCalls[1].action).toBe('create')
443
+
444
+ // Third input: also update existing book with ID 1 (duplicate)
445
+ expect(normalizeInputCalls[2].input).toEqual(bulkInputs[2])
446
+ expect(normalizeInputCalls[2].existing).toEqual(existing)
447
+ expect(normalizeInputCalls[2].action).toBe('update')
448
+ })
449
+
450
+ it('should handle bulkUpsert with only records without primary keys', async () => {
451
+ const bulkInputs = [
452
+ { title: 'Auto Book 1', author: 'Auto Author 1', publishedDate: new Date() },
453
+ { title: 'Auto Book 2', author: 'Auto Author 2', publishedDate: new Date() },
454
+ ]
455
+
456
+ await verificationService.bulkUpsert(bulkInputs)
457
+
458
+ expect(normalizeInputCalls).toHaveLength(2)
459
+
460
+ // Both should be create operations with no existing entities
461
+ expect(normalizeInputCalls[0].input).toEqual(bulkInputs[0])
462
+ expect(normalizeInputCalls[0].existing).toBeUndefined()
463
+ expect(normalizeInputCalls[0].action).toBe('create')
464
+
465
+ expect(normalizeInputCalls[1].input).toEqual(bulkInputs[1])
466
+ expect(normalizeInputCalls[1].existing).toBeUndefined()
467
+ expect(normalizeInputCalls[1].action).toBe('create')
468
+ })
469
+ })
470
+ })
471
+
472
+ describe('Response Normalization', () => {
473
+ class TestRepository extends MockMemoryRepository<typeof mockSchema> {
474
+ constructor() {
475
+ super({
476
+ schema: mockSchema,
477
+ })
478
+ }
479
+
480
+ async create(input: MockBookInput): Promise<any> {
481
+ const record = await super.create(input)
482
+ // Return with publishedDate as string to test normalization
483
+ record.publishedDate = '2024-01-01' as any
484
+ return record
485
+ }
486
+
487
+ async update(lookup: any, input: MockBookInput): Promise<any> {
488
+ const record = await super.update(lookup, input)
489
+ // Return with publishedDate as string to test normalization
490
+ record.publishedDate = '2024-01-01' as any
491
+ return record
492
+ }
493
+
494
+ async upsert(input: MockBookInput): Promise<any> {
495
+ const record = await super.upsert(input)
496
+ // Return with publishedDate as string to test normalization
497
+ record.publishedDate = '2024-01-01' as any
498
+ return record
499
+ }
500
+
501
+ async bulkUpsert(inputs: MockBookInput[]): Promise<any[]> {
502
+ const records = await super.bulkUpsert(inputs)
503
+ // Return with publishedDate as string to test normalization
504
+ for (const record of records) {
505
+ record.publishedDate = '2024-01-01' as any
506
+ }
507
+ return records
508
+ }
509
+
510
+ async remove(lookup: any): Promise<any> {
511
+ const record = await super.remove(lookup)
512
+ // Return with publishedDate as string to test normalization
513
+ if (record) {
514
+ record.publishedDate = '2024-01-01' as any
515
+ }
516
+ return record
517
+ }
518
+
519
+ async restore(lookup: any): Promise<any> {
520
+ const record = await super.restore(lookup)
521
+ // Return with publishedDate as string to test normalization
522
+ if (record) {
523
+ record.publishedDate = '2024-01-01' as any
524
+ }
525
+ return record
526
+ }
527
+ }
528
+
529
+ class TestServiceWithNormalization extends ModelService<typeof mockSchema> {
530
+ async normalizeDetail(detail: InferDetail<typeof mockSchema>): Promise<InferDetail<typeof mockSchema>> {
531
+ // Handle null case (e.g., when load returns null)
532
+ if (!detail) return detail
533
+
534
+ // Convert string dates back to Date objects
535
+ if (typeof detail.publishedDate === 'string') {
536
+ detail.publishedDate = new Date(detail.publishedDate) as any
537
+ }
538
+ return detail
539
+ }
540
+
541
+ async normalizeSummary(summary: InferSummary<typeof mockSchema>): Promise<InferSummary<typeof mockSchema>> {
542
+ // Handle null case (e.g., when load returns null)
543
+ if (!summary) return summary
544
+
545
+ // Convert string dates back to Date objects
546
+ if (typeof summary.publishedDate === 'string') {
547
+ summary.publishedDate = new Date(summary.publishedDate) as any
548
+ }
549
+ return summary
550
+ }
551
+ }
552
+
553
+ let testRepository: TestRepository
554
+ let testService: TestServiceWithNormalization
555
+
556
+ beforeEach(() => {
557
+ testRepository = new TestRepository()
558
+ emitter = new EventManager()
559
+
560
+ testService = new TestServiceWithNormalization({
561
+ repository: testRepository,
562
+ emitter,
563
+ schema: mockSchema,
564
+ namespace,
565
+ })
566
+ })
567
+
568
+ it('should allow custom normalization of details in the create response when overridden', async () => {
569
+ const input = { id: 200, title: 'Create Test', author: 'Creator', publishedDate: new Date() }
570
+ const record = await testService.create(input)
571
+
572
+ const expectedDate = new Date('2024-01-01')
573
+ const actualDate = record.publishedDate
574
+
575
+ expect(actualDate).toEqual(expectedDate)
576
+ expect(actualDate).toBeInstanceOf(Date)
577
+ })
578
+
579
+ it('should allow custom normalization of details in the update response when overridden', async () => {
580
+ const input = { id: 201, title: 'Update Test', author: 'Updater', publishedDate: new Date() }
581
+ await testRepository.create(input)
582
+
583
+ const updatedInput = { title: 'Updated Test', author: 'Updated Author', publishedDate: new Date() }
584
+ const record = await testService.update({ id: 201 }, updatedInput)
585
+
586
+ const expectedDate = new Date('2024-01-01')
587
+ const actualDate = record.publishedDate
588
+
589
+ expect(actualDate).toEqual(expectedDate)
590
+ expect(actualDate).toBeInstanceOf(Date)
591
+ })
592
+
593
+ it('should allow custom normalization of details in the upsert response when creating and overridden', async () => {
594
+ const input = { id: 202, title: 'Upsert Create Test', author: 'Upserter', publishedDate: new Date() }
595
+ const record = await testService.upsert(input)
596
+
597
+ const expectedDate = new Date('2024-01-01')
598
+ const actualDate = record.publishedDate
599
+
600
+ expect(actualDate).toEqual(expectedDate)
601
+ expect(actualDate).toBeInstanceOf(Date)
602
+ })
603
+
604
+ it('should allow custom normalization of details in the upsert response when updating and overridden', async () => {
605
+ const input = { id: 203, title: 'Upsert Update Test', author: 'Upserter', publishedDate: new Date() }
606
+ await testRepository.create(input)
607
+
608
+ const updatedInput = {
609
+ id: 203,
610
+ title: 'Updated Upsert Test',
611
+ author: 'Updated Upserter',
612
+ publishedDate: new Date(),
613
+ }
614
+ const record = await testService.upsert(updatedInput)
615
+
616
+ const expectedDate = new Date('2024-01-01')
617
+ const actualDate = record.publishedDate
618
+
619
+ expect(actualDate).toEqual(expectedDate)
620
+ expect(actualDate).toBeInstanceOf(Date)
621
+ })
622
+
623
+ it('should allow custom normalization of details in the bulkUpsert response when overridden', async () => {
624
+ const input1 = { id: 204, title: 'Bulk Test 1', author: 'Bulk Author 1', publishedDate: new Date() }
625
+ const input2 = { id: 205, title: 'Bulk Test 2', author: 'Bulk Author 2', publishedDate: new Date() }
626
+
627
+ const records = await testService.bulkUpsert([input1, input2])
628
+
629
+ const expectedDate = new Date('2024-01-01')
630
+
631
+ for (const record of records) {
632
+ const actualDate = record.publishedDate
633
+ expect(actualDate).toEqual(expectedDate)
634
+ expect(actualDate).toBeInstanceOf(Date)
635
+ }
636
+ })
637
+
638
+ it('should allow custom normalization of details in the bulkUpsert response with mixed create and update when overridden', async () => {
639
+ const existingInput = { id: 206, title: 'Existing', author: 'Existing Author', publishedDate: new Date() }
640
+ await testRepository.create(existingInput)
641
+
642
+ const updateInput = {
643
+ id: 206,
644
+ title: 'Updated Existing',
645
+ author: 'Updated Author',
646
+ publishedDate: new Date(),
647
+ }
648
+ const createInput = { id: 207, title: 'New Record', author: 'New Author', publishedDate: new Date() }
649
+
650
+ const records = await testService.bulkUpsert([updateInput, createInput])
651
+
652
+ const expectedDate = new Date('2024-01-01')
653
+
654
+ for (const record of records) {
655
+ const actualDate = record.publishedDate
656
+ expect(actualDate).toEqual(expectedDate)
657
+ expect(actualDate).toBeInstanceOf(Date)
658
+ }
659
+ })
660
+
661
+ it('should allow custom normalization of summaries in the remove response when overridden', async () => {
662
+ const input = { id: 208, title: 'Remove Test', author: 'Remover', publishedDate: new Date() }
663
+ await testRepository.create(input)
664
+
665
+ const record = await testService.remove({ id: 208 })
666
+
667
+ const expectedDate = new Date('2024-01-01')
668
+ const actualDate = record.publishedDate
669
+
670
+ expect(actualDate).toEqual(expectedDate)
671
+ expect(actualDate).toBeInstanceOf(Date)
672
+ })
673
+
674
+ it('should allow custom normalization of summaries in the restore response when overridden', async () => {
675
+ const input = { id: 209, title: 'Restore Test', author: 'Restorer', publishedDate: new Date() }
676
+ await testRepository.create(input)
677
+ await testRepository.remove({ id: 209 })
678
+
679
+ const record = await testService.restore({ id: 209 })
680
+
681
+ const expectedDate = new Date('2024-01-01')
682
+ const actualDate = record.publishedDate
683
+
684
+ expect(actualDate).toEqual(expectedDate)
685
+ expect(actualDate).toBeInstanceOf(Date)
686
+ })
687
+
688
+ it('should not normalize data by default when normalization methods are not overridden', async () => {
689
+ const defaultService = new ModelService({
690
+ repository: testRepository,
691
+ emitter,
692
+ schema: mockSchema,
693
+ namespace,
694
+ })
695
+
696
+ const input = { id: 210, title: 'Default Test', author: 'Default Author', publishedDate: new Date() }
697
+ const record = await defaultService.create(input)
698
+
699
+ // Should return the raw string from repository since no normalization is applied
700
+ expect(record.publishedDate as any).toBe('2024-01-01')
701
+ expect(typeof record.publishedDate).toBe('string')
702
+ })
703
+ })
704
+ })