@declaro/data 2.0.0-beta.9 → 2.0.0-beta.90
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.
- package/{LICENSE → LICENSE.md} +1 -1
- package/README.md +0 -0
- package/dist/browser/index.js +32 -0
- package/dist/browser/index.js.map +86 -0
- package/dist/node/index.cjs +11547 -0
- package/dist/node/index.cjs.map +86 -0
- package/dist/node/index.js +11526 -0
- package/dist/node/index.js.map +86 -0
- package/dist/ts/application/model-controller.d.ts +29 -0
- package/dist/ts/application/model-controller.d.ts.map +1 -0
- package/dist/ts/application/model-controller.test.d.ts +2 -0
- package/dist/ts/application/model-controller.test.d.ts.map +1 -0
- package/dist/ts/application/read-only-model-controller.d.ts +20 -0
- package/dist/ts/application/read-only-model-controller.d.ts.map +1 -0
- package/dist/ts/application/read-only-model-controller.test.d.ts +2 -0
- package/dist/ts/application/read-only-model-controller.test.d.ts.map +1 -0
- package/dist/ts/domain/events/domain-event.d.ts +41 -0
- package/dist/ts/domain/events/domain-event.d.ts.map +1 -0
- package/dist/ts/domain/events/domain-event.test.d.ts +2 -0
- package/dist/ts/domain/events/domain-event.test.d.ts.map +1 -0
- package/dist/ts/domain/events/event-types.d.ts +21 -0
- package/dist/ts/domain/events/event-types.d.ts.map +1 -0
- package/dist/ts/domain/events/mutation-event.d.ts +6 -0
- package/dist/ts/domain/events/mutation-event.d.ts.map +1 -0
- package/dist/ts/domain/events/mutation-event.test.d.ts +2 -0
- package/dist/ts/domain/events/mutation-event.test.d.ts.map +1 -0
- package/dist/ts/domain/events/query-event.d.ts +6 -0
- package/dist/ts/domain/events/query-event.d.ts.map +1 -0
- package/dist/ts/domain/events/query-event.test.d.ts +2 -0
- package/dist/ts/domain/events/query-event.test.d.ts.map +1 -0
- package/dist/ts/domain/events/request-event.d.ts +11 -0
- package/dist/ts/domain/events/request-event.d.ts.map +1 -0
- package/dist/ts/domain/events/request-event.test.d.ts +2 -0
- package/dist/ts/domain/events/request-event.test.d.ts.map +1 -0
- package/dist/ts/domain/interfaces/repository.d.ts +84 -0
- package/dist/ts/domain/interfaces/repository.d.ts.map +1 -0
- package/dist/ts/domain/models/pagination.d.ts +28 -0
- package/dist/ts/domain/models/pagination.d.ts.map +1 -0
- package/dist/ts/domain/services/base-model-service.d.ts +22 -0
- package/dist/ts/domain/services/base-model-service.d.ts.map +1 -0
- package/dist/ts/domain/services/model-service-args.d.ts +9 -0
- package/dist/ts/domain/services/model-service-args.d.ts.map +1 -0
- package/dist/ts/domain/services/model-service.d.ts +42 -0
- package/dist/ts/domain/services/model-service.d.ts.map +1 -0
- package/dist/ts/domain/services/model-service.test.d.ts +2 -0
- package/dist/ts/domain/services/model-service.test.d.ts.map +1 -0
- package/dist/ts/domain/services/read-only-model-service.d.ts +40 -0
- package/dist/ts/domain/services/read-only-model-service.d.ts.map +1 -0
- package/dist/ts/domain/services/read-only-model-service.test.d.ts +2 -0
- package/dist/ts/domain/services/read-only-model-service.test.d.ts.map +1 -0
- package/dist/ts/index.d.ts +18 -0
- package/dist/ts/index.d.ts.map +1 -0
- package/dist/ts/shared/utils/schema-inference.d.ts +23 -0
- package/dist/ts/shared/utils/schema-inference.d.ts.map +1 -0
- package/dist/ts/shared/utils/schema-inheritance.d.ts +24 -0
- package/dist/ts/shared/utils/schema-inheritance.d.ts.map +1 -0
- package/dist/ts/test/domain/services/model-service.test.d.ts +1 -0
- package/dist/ts/test/domain/services/model-service.test.d.ts.map +1 -0
- package/dist/ts/test/mock/models/mock-book-models.d.ts +42 -0
- package/dist/ts/test/mock/models/mock-book-models.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts +36 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.test.d.ts +2 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.test.d.ts.map +1 -0
- package/package.json +45 -42
- package/src/application/model-controller.test.ts +488 -0
- package/src/application/model-controller.ts +92 -0
- package/src/application/read-only-model-controller.test.ts +327 -0
- package/src/application/read-only-model-controller.ts +61 -0
- package/src/domain/events/domain-event.test.ts +82 -0
- package/src/domain/events/domain-event.ts +69 -0
- package/src/domain/events/event-types.ts +21 -0
- package/src/domain/events/mutation-event.test.ts +38 -0
- package/src/domain/events/mutation-event.ts +8 -0
- package/src/domain/events/query-event.test.ts +28 -0
- package/src/domain/events/query-event.ts +8 -0
- package/src/domain/events/request-event.test.ts +38 -0
- package/src/domain/events/request-event.ts +32 -0
- package/src/domain/interfaces/repository.ts +107 -0
- package/src/domain/models/pagination.ts +28 -0
- package/src/domain/services/base-model-service.ts +50 -0
- package/src/domain/services/model-service-args.ts +9 -0
- package/src/domain/services/model-service.test.ts +631 -0
- package/src/domain/services/model-service.ts +322 -0
- package/src/domain/services/read-only-model-service.test.ts +296 -0
- package/src/domain/services/read-only-model-service.ts +133 -0
- package/src/index.ts +17 -4
- package/src/shared/utils/schema-inference.ts +26 -0
- package/src/shared/utils/schema-inheritance.ts +28 -0
- package/src/test/domain/services/model-service.test.ts +0 -0
- package/src/test/mock/models/mock-book-models.ts +78 -0
- package/src/test/mock/repositories/mock-memory-repository.test.ts +715 -0
- package/src/test/mock/repositories/mock-memory-repository.ts +235 -0
- package/dist/databaseConnection.d.ts +0 -24
- package/dist/datastoreAbstract.d.ts +0 -37
- package/dist/declaro-data.cjs +0 -1
- package/dist/declaro-data.mjs +0 -250
- package/dist/hydrateEntity.d.ts +0 -8
- package/dist/index.d.ts +0 -4
- package/dist/serverConnection.d.ts +0 -15
- package/dist/trackedStatus.d.ts +0 -15
- package/src/databaseConnection.ts +0 -137
- package/src/datastoreAbstract.ts +0 -190
- package/src/hydrateEntity.ts +0 -36
- package/src/placeholder.test.ts +0 -7
- package/src/serverConnection.ts +0 -74
- package/src/trackedStatus.ts +0 -35
- package/tsconfig.json +0 -10
- package/vite.config.ts +0 -23
|
@@ -0,0 +1,631 @@
|
|
|
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 } 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
|
+
})
|