@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,1130 @@
1
+ import { describe, it, expect, beforeEach, spyOn, mock, beforeAll } from 'bun:test'
2
+ import { ReadOnlyModelService, type ILoadOptions } from './read-only-model-service'
3
+ import { MockMemoryRepository } from '../../test/mock/repositories/mock-memory-repository'
4
+ import { MockBookSchema, type MockBookDetail, type MockBookLookup } from '../../test/mock/models/mock-book-models'
5
+ import { EventManager } from '@declaro/core'
6
+ import type { QueryEvent } from '../events/query-event'
7
+ import type {
8
+ InferDetail,
9
+ InferFilters,
10
+ InferLookup,
11
+ InferSearchResults,
12
+ InferSummary,
13
+ } from '../../shared/utils/schema-inference'
14
+
15
+ describe('ReadOnlyModelService', () => {
16
+ const namespace = 'books'
17
+ const mockSchema = MockBookSchema
18
+
19
+ let repository: MockMemoryRepository<typeof mockSchema>
20
+ let emitter: EventManager
21
+ let service: ReadOnlyModelService<typeof mockSchema>
22
+
23
+ const beforeLoadSpy = mock(
24
+ (event: QueryEvent<InferDetail<typeof mockSchema>, InferLookup<typeof mockSchema>>) => {},
25
+ )
26
+ const afterLoadSpy = mock((event: QueryEvent<InferDetail<typeof mockSchema>, InferLookup<typeof mockSchema>>) => {})
27
+
28
+ const beforeLoadManySpy = mock(
29
+ (event: QueryEvent<InferDetail<typeof mockSchema>[], InferLookup<typeof mockSchema>[]>) => {},
30
+ )
31
+ const afterLoadManySpy = mock(
32
+ (event: QueryEvent<InferDetail<typeof mockSchema>[], InferLookup<typeof mockSchema>[]>) => {},
33
+ )
34
+
35
+ const beforeSearchSpy = mock(
36
+ (event: QueryEvent<InferSearchResults<typeof mockSchema>[], InferFilters<typeof mockSchema>[]>) => {},
37
+ )
38
+ const afterSearchSpy = mock(
39
+ (event: QueryEvent<InferSearchResults<typeof mockSchema>[], InferFilters<typeof mockSchema>[]>) => {},
40
+ )
41
+
42
+ beforeEach(() => {
43
+ repository = new MockMemoryRepository({ schema: mockSchema })
44
+ emitter = new EventManager()
45
+
46
+ beforeLoadSpy.mockClear()
47
+ afterLoadSpy.mockClear()
48
+ beforeLoadManySpy.mockClear()
49
+ afterLoadManySpy.mockClear()
50
+ beforeSearchSpy.mockClear()
51
+ afterSearchSpy.mockClear()
52
+
53
+ emitter.on<QueryEvent<InferDetail<typeof mockSchema>, InferLookup<typeof mockSchema>>>(
54
+ 'books::book.beforeLoad',
55
+ beforeLoadSpy,
56
+ )
57
+ emitter.on<QueryEvent<InferDetail<typeof mockSchema>, InferLookup<typeof mockSchema>>>(
58
+ 'books::book.afterLoad',
59
+ afterLoadSpy,
60
+ )
61
+
62
+ emitter.on<QueryEvent<InferDetail<typeof mockSchema>[], InferLookup<typeof mockSchema>[]>>(
63
+ 'books::book.beforeLoadMany',
64
+ beforeLoadManySpy,
65
+ )
66
+ emitter.on<QueryEvent<InferDetail<typeof mockSchema>[], InferLookup<typeof mockSchema>[]>>(
67
+ 'books::book.afterLoadMany',
68
+ afterLoadManySpy,
69
+ )
70
+
71
+ emitter.on<QueryEvent<InferSearchResults<typeof mockSchema>[], InferFilters<typeof mockSchema>[]>>(
72
+ 'books::book.beforeSearch',
73
+ beforeSearchSpy,
74
+ )
75
+ emitter.on<QueryEvent<InferSearchResults<typeof mockSchema>[], InferFilters<typeof mockSchema>[]>>(
76
+ 'books::book.afterSearch',
77
+ afterSearchSpy,
78
+ )
79
+
80
+ service = new ReadOnlyModelService({ repository, emitter, schema: mockSchema, namespace })
81
+ })
82
+
83
+ it('should load a single record', async () => {
84
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
85
+ await repository.create(input)
86
+
87
+ const record = await service.load({ id: 42 })
88
+
89
+ expect(record).toEqual(input)
90
+ })
91
+
92
+ it('should return null when loading a non-existent record', async () => {
93
+ const record = await service.load({ id: 999 })
94
+
95
+ expect(record).toBeNull()
96
+ })
97
+
98
+ it('should load multiple records', async () => {
99
+ const input1 = { id: 42, title: 'Test Book 1', author: 'Author Name 1', publishedDate: new Date() }
100
+ const input2 = { id: 43, title: 'Test Book 2', author: 'Author Name 2', publishedDate: new Date() }
101
+ await repository.create(input1)
102
+ await repository.create(input2)
103
+
104
+ const records = await service.loadMany([{ id: 42 }, { id: 43 }])
105
+
106
+ expect(records).toEqual([input1, input2])
107
+ })
108
+
109
+ it('should search for records', async () => {
110
+ const input1 = { id: 42, title: 'Test Book 1', author: 'Author Name 1', publishedDate: new Date() }
111
+ const input2 = { id: 43, title: 'Test Book 2', author: 'Author Name 2', publishedDate: new Date() }
112
+ await repository.create(input1)
113
+ await repository.create(input2)
114
+
115
+ const results = await service.search(
116
+ { text: 'Test' },
117
+ {
118
+ sort: [
119
+ {
120
+ title: 'asc',
121
+ },
122
+ {
123
+ author: 'desc',
124
+ },
125
+ ],
126
+ },
127
+ )
128
+
129
+ expect(results.results).toEqual([input1, input2])
130
+ expect(results.pagination.total).toBe(2)
131
+ })
132
+
133
+ it('should return empty results when searching for non-existent records', async () => {
134
+ const results = await service.search({ text: 'Non-existent' })
135
+
136
+ expect(results.results).toEqual([])
137
+ expect(results.pagination.total).toBe(0)
138
+ })
139
+
140
+ it('should handle pagination options correctly', async () => {
141
+ // Create 5 items
142
+ for (let i = 1; i <= 5; i++) {
143
+ await repository.create({
144
+ id: i,
145
+ title: `Test Book ${i}`,
146
+ author: `Author ${i}`,
147
+ publishedDate: new Date(),
148
+ })
149
+ }
150
+
151
+ // Test first page with pageSize 2
152
+ const page1 = await service.search(
153
+ {},
154
+ {
155
+ pagination: { page: 1, pageSize: 2 },
156
+ },
157
+ )
158
+ expect(page1.results).toHaveLength(2)
159
+ expect(page1.pagination.page).toBe(1)
160
+ expect(page1.pagination.pageSize).toBe(2)
161
+ expect(page1.pagination.total).toBe(5)
162
+ expect(page1.pagination.totalPages).toBe(3)
163
+
164
+ // Test second page
165
+ const page2 = await service.search(
166
+ {},
167
+ {
168
+ pagination: { page: 2, pageSize: 2 },
169
+ },
170
+ )
171
+ expect(page2.results).toHaveLength(2)
172
+ expect(page2.pagination.page).toBe(2)
173
+
174
+ // Test last page
175
+ const page3 = await service.search(
176
+ {},
177
+ {
178
+ pagination: { page: 3, pageSize: 2 },
179
+ },
180
+ )
181
+ expect(page3.results).toHaveLength(1)
182
+ expect(page3.pagination.page).toBe(3)
183
+ })
184
+
185
+ it('should handle sort options correctly', async () => {
186
+ const input1 = { id: 1, title: 'Z Book', author: 'Author A', publishedDate: new Date('2023-01-01') }
187
+ const input2 = { id: 2, title: 'A Book', author: 'Author B', publishedDate: new Date('2023-02-01') }
188
+ const input3 = { id: 3, title: 'M Book', author: 'Author C', publishedDate: new Date('2023-03-01') }
189
+
190
+ await repository.create(input1)
191
+ await repository.create(input2)
192
+ await repository.create(input3)
193
+
194
+ // Sort by title ascending
195
+ const titleAscResults = await service.search(
196
+ {},
197
+ {
198
+ sort: [{ title: 'asc' }],
199
+ },
200
+ )
201
+ expect(titleAscResults.results.map((r) => r.title)).toEqual(['A Book', 'M Book', 'Z Book'])
202
+
203
+ // Sort by title descending
204
+ const titleDescResults = await service.search(
205
+ {},
206
+ {
207
+ sort: [{ title: 'desc' }],
208
+ },
209
+ )
210
+ expect(titleDescResults.results.map((r) => r.title)).toEqual(['Z Book', 'M Book', 'A Book'])
211
+
212
+ // Sort by author ascending
213
+ const authorAscResults = await service.search(
214
+ {},
215
+ {
216
+ sort: [{ author: 'asc' }],
217
+ },
218
+ )
219
+ expect(authorAscResults.results.map((r) => r.author)).toEqual(['Author A', 'Author B', 'Author C'])
220
+ })
221
+
222
+ it('should handle combined filtering, sorting, and pagination', async () => {
223
+ const repositoryWithFilter = new MockMemoryRepository({
224
+ schema: mockSchema,
225
+ filter: (data, filters) => {
226
+ if (filters.text) {
227
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
228
+ }
229
+ return true
230
+ },
231
+ })
232
+
233
+ const serviceWithFilter = new ReadOnlyModelService({
234
+ repository: repositoryWithFilter,
235
+ emitter,
236
+ namespace,
237
+ schema: mockSchema,
238
+ })
239
+
240
+ await repositoryWithFilter.create({ title: 'Test Z Book', author: 'Author 1', publishedDate: new Date() })
241
+ await repositoryWithFilter.create({ title: 'Test A Book', author: 'Author 2', publishedDate: new Date() })
242
+ await repositoryWithFilter.create({ title: 'Other Book', author: 'Author 3', publishedDate: new Date() })
243
+ await repositoryWithFilter.create({ title: 'Test M Book', author: 'Author 4', publishedDate: new Date() })
244
+
245
+ const results = await serviceWithFilter.search(
246
+ { text: 'Test' },
247
+ {
248
+ sort: [{ title: 'asc' }],
249
+ pagination: { page: 1, pageSize: 2 },
250
+ },
251
+ )
252
+
253
+ expect(results.results).toHaveLength(2)
254
+ expect(results.results.map((r) => r.title)).toEqual(['Test A Book', 'Test M Book'])
255
+ expect(results.pagination.total).toBe(3) // 3 "Test" books total
256
+ expect(results.pagination.totalPages).toBe(2)
257
+ })
258
+
259
+ it('should trigger before and after events for load', async () => {
260
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
261
+ await repository.create(input)
262
+
263
+ const record = await service.load({ id: 42 })
264
+
265
+ expect(record).toEqual(input)
266
+ expect(beforeLoadSpy).toHaveBeenCalledTimes(1)
267
+ expect(beforeLoadSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.beforeLoad' }))
268
+ expect(afterLoadSpy).toHaveBeenCalledTimes(1)
269
+ expect(afterLoadSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.afterLoad' }))
270
+ })
271
+
272
+ it('should trigger before and after events for loadMany', async () => {
273
+ const input1 = { id: 42, title: 'Test Book 1', author: 'Author Name 1', publishedDate: new Date() }
274
+ const input2 = { id: 43, title: 'Test Book 2', author: 'Author Name 2', publishedDate: new Date() }
275
+ await repository.create(input1)
276
+ await repository.create(input2)
277
+
278
+ const records = await service.loadMany([{ id: 42 }, { id: 43 }])
279
+
280
+ expect(records).toEqual([input1, input2])
281
+ expect(beforeLoadManySpy).toHaveBeenCalledTimes(1)
282
+ expect(beforeLoadManySpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.beforeLoadMany' }))
283
+ expect(afterLoadManySpy).toHaveBeenCalledTimes(1)
284
+ expect(afterLoadManySpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.afterLoadMany' }))
285
+ })
286
+
287
+ it('should trigger before and after events for search', async () => {
288
+ const input1 = { id: 42, title: 'Test Book 1', author: 'Author Name 1', publishedDate: new Date() }
289
+ const input2 = { id: 43, title: 'Test Book 2', author: 'Author Name 2', publishedDate: new Date() }
290
+ await repository.create(input1)
291
+ await repository.create(input2)
292
+
293
+ const results = await service.search({ text: 'Test' })
294
+
295
+ expect(results.results).toEqual([input1, input2])
296
+ expect(results.pagination.total).toBe(2)
297
+ expect(beforeSearchSpy).toHaveBeenCalledTimes(1)
298
+ expect(beforeSearchSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.beforeSearch' }))
299
+ expect(afterSearchSpy).toHaveBeenCalledTimes(1)
300
+ expect(afterSearchSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.afterSearch' }))
301
+ })
302
+
303
+ describe('Response Normalization', () => {
304
+ class TestRepository extends MockMemoryRepository<typeof mockSchema> {
305
+ constructor() {
306
+ super({
307
+ schema: mockSchema,
308
+ })
309
+ }
310
+
311
+ async load(input: MockBookLookup): Promise<MockBookDetail | null> {
312
+ const record = await super.load(input)
313
+
314
+ if (record) {
315
+ record.publishedDate = '2024-01-01' as any
316
+ }
317
+
318
+ return record
319
+ }
320
+
321
+ async loadMany(inputs: MockBookLookup[]): Promise<MockBookDetail[]> {
322
+ const records = await super.loadMany(inputs)
323
+
324
+ for (const record of records) {
325
+ record.publishedDate = '2024-01-01' as any
326
+ }
327
+
328
+ return records
329
+ }
330
+
331
+ async search(
332
+ filters: InferFilters<typeof mockSchema>,
333
+ options?: ILoadOptions,
334
+ ): Promise<InferSearchResults<typeof mockSchema>> {
335
+ const results = await super.search(filters, options)
336
+
337
+ for (const record of results.results) {
338
+ record.publishedDate = '2024-01-01' as any
339
+ }
340
+
341
+ return results
342
+ }
343
+ }
344
+
345
+ class TestServiceWithNormalization extends ReadOnlyModelService<typeof mockSchema> {
346
+ async normalizeDetail(detail: InferDetail<typeof mockSchema>): Promise<InferDetail<typeof mockSchema>> {
347
+ // Handle null case (e.g., when load returns null)
348
+ if (!detail) return detail
349
+
350
+ // Convert string dates back to Date objects
351
+ if (typeof detail.publishedDate === 'string') {
352
+ detail.publishedDate = new Date(detail.publishedDate) as any
353
+ }
354
+ return detail
355
+ }
356
+
357
+ async normalizeSummary(summary: InferSummary<typeof mockSchema>): Promise<InferSummary<typeof mockSchema>> {
358
+ // Handle null case (e.g., when load returns null)
359
+ if (!summary) return summary
360
+
361
+ // Convert string dates back to Date objects
362
+ if (typeof summary.publishedDate === 'string') {
363
+ summary.publishedDate = new Date(summary.publishedDate) as any
364
+ }
365
+ return summary
366
+ }
367
+ }
368
+
369
+ let testService: TestServiceWithNormalization
370
+
371
+ beforeEach(() => {
372
+ repository = new TestRepository()
373
+ emitter = new EventManager()
374
+
375
+ testService = new TestServiceWithNormalization({ repository, emitter, schema: mockSchema, namespace })
376
+ })
377
+
378
+ it('should allow custom normalization of details in the load response when overridden', async () => {
379
+ const input = { id: 100, title: 'Normalization Test', author: 'Normalizer', publishedDate: new Date() }
380
+ await repository.create(input)
381
+
382
+ const record = await testService.load({ id: 100 })
383
+
384
+ const expectedDate = new Date('2024-01-01')
385
+ const actualDate = record.publishedDate
386
+
387
+ expect(actualDate).toEqual(expectedDate)
388
+ expect(actualDate).toBeInstanceOf(Date)
389
+ })
390
+
391
+ it('should allow custom normalization of details in the loadMany response when overridden', async () => {
392
+ const input1 = { id: 101, title: 'Normalization Test 1', author: 'Normalizer 1', publishedDate: new Date() }
393
+ const input2 = { id: 102, title: 'Normalization Test 2', author: 'Normalizer 2', publishedDate: new Date() }
394
+ await repository.create(input1)
395
+ await repository.create(input2)
396
+
397
+ const records = await testService.loadMany([{ id: 101 }, { id: 102 }])
398
+
399
+ const expectedDate = new Date('2024-01-01')
400
+
401
+ for (const record of records) {
402
+ const actualDate = record.publishedDate
403
+ expect(actualDate).toEqual(expectedDate)
404
+ expect(actualDate).toBeInstanceOf(Date)
405
+ }
406
+ })
407
+
408
+ it('should allow custom normalization of summaries in the search response when overridden', async () => {
409
+ const input1 = { id: 103, title: 'Normalization Test 3', author: 'Normalizer 3', publishedDate: new Date() }
410
+ const input2 = { id: 104, title: 'Normalization Test 4', author: 'Normalizer 4', publishedDate: new Date() }
411
+ await repository.create(input1)
412
+ await repository.create(input2)
413
+
414
+ const results = await testService.search({ text: 'Normalization' })
415
+
416
+ const expectedDate = new Date('2024-01-01')
417
+
418
+ for (const record of results.results) {
419
+ const actualDate = record.publishedDate
420
+ expect(actualDate).toEqual(expectedDate)
421
+ expect(actualDate).toBeInstanceOf(Date)
422
+ }
423
+ })
424
+
425
+ it('should not normalize data by default when normalization methods are not overridden', async () => {
426
+ const defaultService = new ReadOnlyModelService({ repository, emitter, schema: mockSchema, namespace })
427
+
428
+ const input = { id: 105, title: 'Default Test', author: 'Default Author', publishedDate: new Date() }
429
+ await repository.create(input)
430
+
431
+ const record = await defaultService.load({ id: 105 })
432
+
433
+ // Should return the raw string from repository since no normalization is applied
434
+ expect(record.publishedDate as any).toBe('2024-01-01')
435
+ expect(typeof record.publishedDate).toBe('string')
436
+ })
437
+ })
438
+
439
+ describe('Trash Functionality', () => {
440
+ beforeEach(async () => {
441
+ repository = new MockMemoryRepository({ schema: mockSchema })
442
+ emitter = new EventManager()
443
+ service = new ReadOnlyModelService({ repository, emitter, schema: mockSchema, namespace })
444
+ })
445
+
446
+ describe('load with trash options', () => {
447
+ it('should not load removed items by default', async () => {
448
+ const input = { id: 1, title: 'Book to Remove', author: 'Author', publishedDate: new Date() }
449
+ await repository.create(input)
450
+ await repository.remove({ id: 1 })
451
+
452
+ const record = await service.load({ id: 1 })
453
+ expect(record).toBeNull()
454
+ })
455
+
456
+ it('should load removed items with removedOnly option', async () => {
457
+ const input = { id: 2, title: 'Removed Book', author: 'Author', publishedDate: new Date() }
458
+ await repository.create(input)
459
+ await repository.remove({ id: 2 })
460
+
461
+ const record = await service.load({ id: 2 }, { removedOnly: true })
462
+ expect(record).not.toBeNull()
463
+ expect(record?.title).toBe('Removed Book')
464
+ })
465
+
466
+ it('should not load active items with removedOnly option', async () => {
467
+ const input = { id: 3, title: 'Active Book', author: 'Author', publishedDate: new Date() }
468
+ await repository.create(input)
469
+
470
+ const record = await service.load({ id: 3 }, { removedOnly: true })
471
+ expect(record).toBeNull()
472
+ })
473
+
474
+ it('should load removed items with includeRemoved option', async () => {
475
+ const input = { id: 4, title: 'Removed Book', author: 'Author', publishedDate: new Date() }
476
+ await repository.create(input)
477
+ await repository.remove({ id: 4 })
478
+
479
+ const record = await service.load({ id: 4 }, { includeRemoved: true })
480
+ expect(record).not.toBeNull()
481
+ expect(record?.title).toBe('Removed Book')
482
+ })
483
+
484
+ it('should load active items with includeRemoved option', async () => {
485
+ const input = { id: 5, title: 'Active Book', author: 'Author', publishedDate: new Date() }
486
+ await repository.create(input)
487
+
488
+ const record = await service.load({ id: 5 }, { includeRemoved: true })
489
+ expect(record).not.toBeNull()
490
+ expect(record?.title).toBe('Active Book')
491
+ })
492
+ })
493
+
494
+ describe('search with trash options', () => {
495
+ it('should not return removed items by default', async () => {
496
+ await repository.create({ id: 1, title: 'Active Book', author: 'Author 1', publishedDate: new Date() })
497
+ await repository.create({
498
+ id: 2,
499
+ title: 'Removed Book',
500
+ author: 'Author 2',
501
+ publishedDate: new Date(),
502
+ })
503
+ await repository.remove({ id: 2 })
504
+
505
+ const results = await service.search({})
506
+ expect(results.results).toHaveLength(1)
507
+ expect(results.results[0].title).toBe('Active Book')
508
+ })
509
+
510
+ it('should return only removed items with removedOnly option', async () => {
511
+ await repository.create({ id: 1, title: 'Active Book', author: 'Author 1', publishedDate: new Date() })
512
+ await repository.create({
513
+ id: 2,
514
+ title: 'Removed Book 1',
515
+ author: 'Author 2',
516
+ publishedDate: new Date(),
517
+ })
518
+ await repository.create({
519
+ id: 3,
520
+ title: 'Removed Book 2',
521
+ author: 'Author 3',
522
+ publishedDate: new Date(),
523
+ })
524
+
525
+ await repository.remove({ id: 2 })
526
+ await repository.remove({ id: 3 })
527
+
528
+ const results = await service.search({}, { removedOnly: true })
529
+ expect(results.results).toHaveLength(2)
530
+ expect(results.results.every((book) => book.title.startsWith('Removed'))).toBe(true)
531
+ })
532
+
533
+ it('should return both active and removed items with includeRemoved option', async () => {
534
+ await repository.create({
535
+ id: 1,
536
+ title: 'Active Book 1',
537
+ author: 'Author 1',
538
+ publishedDate: new Date(),
539
+ })
540
+ await repository.create({
541
+ id: 2,
542
+ title: 'Active Book 2',
543
+ author: 'Author 2',
544
+ publishedDate: new Date(),
545
+ })
546
+ await repository.create({
547
+ id: 3,
548
+ title: 'Removed Book',
549
+ author: 'Author 3',
550
+ publishedDate: new Date(),
551
+ })
552
+
553
+ await repository.remove({ id: 3 })
554
+
555
+ const results = await service.search({}, { includeRemoved: true })
556
+ expect(results.results).toHaveLength(3)
557
+ })
558
+
559
+ it('should filter removed items with removedOnly option', async () => {
560
+ const repositoryWithFilter = new MockMemoryRepository({
561
+ schema: mockSchema,
562
+ filter: (data, filters) => {
563
+ if (filters.text) {
564
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
565
+ }
566
+ return true
567
+ },
568
+ })
569
+
570
+ const serviceWithFilter = new ReadOnlyModelService({
571
+ repository: repositoryWithFilter,
572
+ emitter,
573
+ namespace,
574
+ schema: mockSchema,
575
+ })
576
+
577
+ const removed1 = await repositoryWithFilter.create({
578
+ title: 'Test Removed Book',
579
+ author: 'Author 1',
580
+ publishedDate: new Date(),
581
+ })
582
+ const removed2 = await repositoryWithFilter.create({
583
+ title: 'Other Removed Book',
584
+ author: 'Author 2',
585
+ publishedDate: new Date(),
586
+ })
587
+
588
+ await repositoryWithFilter.remove({ id: removed1.id })
589
+ await repositoryWithFilter.remove({ id: removed2.id })
590
+
591
+ const results = await serviceWithFilter.search({ text: 'Test' }, { removedOnly: true })
592
+ expect(results.results).toHaveLength(1)
593
+ expect(results.results[0].title).toBe('Test Removed Book')
594
+ })
595
+
596
+ it('should filter across active and removed items with includeRemoved option', async () => {
597
+ const repositoryWithFilter = new MockMemoryRepository({
598
+ schema: mockSchema,
599
+ filter: (data, filters) => {
600
+ if (filters.text) {
601
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
602
+ }
603
+ return true
604
+ },
605
+ })
606
+
607
+ const serviceWithFilter = new ReadOnlyModelService({
608
+ repository: repositoryWithFilter,
609
+ emitter,
610
+ namespace,
611
+ schema: mockSchema,
612
+ })
613
+
614
+ await repositoryWithFilter.create({
615
+ title: 'Test Active Book',
616
+ author: 'Author 1',
617
+ publishedDate: new Date(),
618
+ })
619
+ const removed = await repositoryWithFilter.create({
620
+ title: 'Test Removed Book',
621
+ author: 'Author 2',
622
+ publishedDate: new Date(),
623
+ })
624
+ await repositoryWithFilter.create({
625
+ title: 'Other Book',
626
+ author: 'Author 3',
627
+ publishedDate: new Date(),
628
+ })
629
+
630
+ await repositoryWithFilter.remove({ id: removed.id })
631
+
632
+ const results = await serviceWithFilter.search({ text: 'Test' }, { includeRemoved: true })
633
+ expect(results.results).toHaveLength(2)
634
+ expect(results.results.some((book) => book.title === 'Test Active Book')).toBe(true)
635
+ expect(results.results.some((book) => book.title === 'Test Removed Book')).toBe(true)
636
+ })
637
+ })
638
+
639
+ describe('count with trash options', () => {
640
+ it('should count only active items by default', async () => {
641
+ await repository.create({ id: 1, title: 'Active Book', author: 'Author 1', publishedDate: new Date() })
642
+ await repository.create({
643
+ id: 2,
644
+ title: 'Removed Book',
645
+ author: 'Author 2',
646
+ publishedDate: new Date(),
647
+ })
648
+ await repository.remove({ id: 2 })
649
+
650
+ const count = await service.count({})
651
+ expect(count).toBe(1)
652
+ })
653
+
654
+ it('should count only removed items with removedOnly option', async () => {
655
+ await repository.create({ id: 1, title: 'Active Book', author: 'Author 1', publishedDate: new Date() })
656
+ await repository.create({
657
+ id: 2,
658
+ title: 'Removed Book 1',
659
+ author: 'Author 2',
660
+ publishedDate: new Date(),
661
+ })
662
+ await repository.create({
663
+ id: 3,
664
+ title: 'Removed Book 2',
665
+ author: 'Author 3',
666
+ publishedDate: new Date(),
667
+ })
668
+
669
+ await repository.remove({ id: 2 })
670
+ await repository.remove({ id: 3 })
671
+
672
+ const count = await service.count({}, { removedOnly: true })
673
+ expect(count).toBe(2)
674
+ })
675
+
676
+ it('should count both active and removed items with includeRemoved option', async () => {
677
+ await repository.create({ id: 1, title: 'Active Book', author: 'Author 1', publishedDate: new Date() })
678
+ await repository.create({
679
+ id: 2,
680
+ title: 'Removed Book',
681
+ author: 'Author 2',
682
+ publishedDate: new Date(),
683
+ })
684
+ await repository.remove({ id: 2 })
685
+
686
+ const count = await service.count({}, { includeRemoved: true })
687
+ expect(count).toBe(2)
688
+ })
689
+
690
+ it('should count filtered active items by default', async () => {
691
+ const repositoryWithFilter = new MockMemoryRepository({
692
+ schema: mockSchema,
693
+ filter: (data, filters) => {
694
+ if (filters.text) {
695
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
696
+ }
697
+ return true
698
+ },
699
+ })
700
+
701
+ const serviceWithFilter = new ReadOnlyModelService({
702
+ repository: repositoryWithFilter,
703
+ emitter,
704
+ namespace,
705
+ schema: mockSchema,
706
+ })
707
+
708
+ await repositoryWithFilter.create({
709
+ title: 'Test Book 1',
710
+ author: 'Author 1',
711
+ publishedDate: new Date(),
712
+ })
713
+ await repositoryWithFilter.create({
714
+ title: 'Test Book 2',
715
+ author: 'Author 2',
716
+ publishedDate: new Date(),
717
+ })
718
+ await repositoryWithFilter.create({
719
+ title: 'Other Book',
720
+ author: 'Author 3',
721
+ publishedDate: new Date(),
722
+ })
723
+ const removed = await repositoryWithFilter.create({
724
+ title: 'Test Book 3',
725
+ author: 'Author 4',
726
+ publishedDate: new Date(),
727
+ })
728
+
729
+ await repositoryWithFilter.remove({ id: removed.id })
730
+
731
+ const count = await serviceWithFilter.count({ text: 'Test' })
732
+ expect(count).toBe(2)
733
+ })
734
+
735
+ it('should count filtered removed items with removedOnly option', async () => {
736
+ const repositoryWithFilter = new MockMemoryRepository({
737
+ schema: mockSchema,
738
+ filter: (data, filters) => {
739
+ if (filters.text) {
740
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
741
+ }
742
+ return true
743
+ },
744
+ })
745
+
746
+ const serviceWithFilter = new ReadOnlyModelService({
747
+ repository: repositoryWithFilter,
748
+ emitter,
749
+ namespace,
750
+ schema: mockSchema,
751
+ })
752
+
753
+ const removed1 = await repositoryWithFilter.create({
754
+ title: 'Test Removed Book 1',
755
+ author: 'Author 1',
756
+ publishedDate: new Date(),
757
+ })
758
+ const removed2 = await repositoryWithFilter.create({
759
+ title: 'Test Removed Book 2',
760
+ author: 'Author 2',
761
+ publishedDate: new Date(),
762
+ })
763
+ const removed3 = await repositoryWithFilter.create({
764
+ title: 'Other Removed Book',
765
+ author: 'Author 3',
766
+ publishedDate: new Date(),
767
+ })
768
+ await repositoryWithFilter.create({
769
+ title: 'Test Active Book',
770
+ author: 'Author 4',
771
+ publishedDate: new Date(),
772
+ })
773
+
774
+ await repositoryWithFilter.remove({ id: removed1.id })
775
+ await repositoryWithFilter.remove({ id: removed2.id })
776
+ await repositoryWithFilter.remove({ id: removed3.id })
777
+
778
+ const count = await serviceWithFilter.count({ text: 'Test' }, { removedOnly: true })
779
+ expect(count).toBe(2)
780
+ })
781
+
782
+ it('should count filtered items across active and removed with includeRemoved option', async () => {
783
+ const repositoryWithFilter = new MockMemoryRepository({
784
+ schema: mockSchema,
785
+ filter: (data, filters) => {
786
+ if (filters.text) {
787
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
788
+ }
789
+ return true
790
+ },
791
+ })
792
+
793
+ const serviceWithFilter = new ReadOnlyModelService({
794
+ repository: repositoryWithFilter,
795
+ emitter,
796
+ namespace,
797
+ schema: mockSchema,
798
+ })
799
+
800
+ await repositoryWithFilter.create({
801
+ title: 'Test Active Book 1',
802
+ author: 'Author 1',
803
+ publishedDate: new Date(),
804
+ })
805
+ await repositoryWithFilter.create({
806
+ title: 'Test Active Book 2',
807
+ author: 'Author 2',
808
+ publishedDate: new Date(),
809
+ })
810
+ const removed1 = await repositoryWithFilter.create({
811
+ title: 'Test Removed Book 1',
812
+ author: 'Author 3',
813
+ publishedDate: new Date(),
814
+ })
815
+ const removed2 = await repositoryWithFilter.create({
816
+ title: 'Test Removed Book 2',
817
+ author: 'Author 4',
818
+ publishedDate: new Date(),
819
+ })
820
+ await repositoryWithFilter.create({
821
+ title: 'Other Active Book',
822
+ author: 'Author 5',
823
+ publishedDate: new Date(),
824
+ })
825
+
826
+ await repositoryWithFilter.remove({ id: removed1.id })
827
+ await repositoryWithFilter.remove({ id: removed2.id })
828
+
829
+ const count = await serviceWithFilter.count({ text: 'Test' }, { includeRemoved: true })
830
+ expect(count).toBe(4)
831
+ })
832
+ })
833
+ })
834
+
835
+ describe('doNotDispatchEvents Option', () => {
836
+ const beforeLoadSpy = mock(
837
+ (event: QueryEvent<InferDetail<typeof mockSchema>, InferLookup<typeof mockSchema>>) => {},
838
+ )
839
+ const afterLoadSpy = mock(
840
+ (event: QueryEvent<InferDetail<typeof mockSchema>, InferLookup<typeof mockSchema>>) => {},
841
+ )
842
+
843
+ const beforeLoadManySpy = mock(
844
+ (event: QueryEvent<InferDetail<typeof mockSchema>[], InferLookup<typeof mockSchema>[]>) => {},
845
+ )
846
+ const afterLoadManySpy = mock(
847
+ (event: QueryEvent<InferDetail<typeof mockSchema>[], InferLookup<typeof mockSchema>[]>) => {},
848
+ )
849
+
850
+ const beforeSearchSpy = mock(
851
+ (event: QueryEvent<InferSearchResults<typeof mockSchema>[], InferFilters<typeof mockSchema>[]>) => {},
852
+ )
853
+ const afterSearchSpy = mock(
854
+ (event: QueryEvent<InferSearchResults<typeof mockSchema>[], InferFilters<typeof mockSchema>[]>) => {},
855
+ )
856
+
857
+ const beforeCountSpy = mock((event: QueryEvent<number, InferFilters<typeof mockSchema>>) => {})
858
+ const afterCountSpy = mock((event: QueryEvent<number, InferFilters<typeof mockSchema>>) => {})
859
+
860
+ beforeEach(() => {
861
+ repository = new MockMemoryRepository({ schema: mockSchema })
862
+ emitter = new EventManager()
863
+
864
+ beforeLoadSpy.mockClear()
865
+ afterLoadSpy.mockClear()
866
+ beforeLoadManySpy.mockClear()
867
+ afterLoadManySpy.mockClear()
868
+ beforeSearchSpy.mockClear()
869
+ afterSearchSpy.mockClear()
870
+ beforeCountSpy.mockClear()
871
+ afterCountSpy.mockClear()
872
+
873
+ emitter.on<QueryEvent<InferDetail<typeof mockSchema>, InferLookup<typeof mockSchema>>>(
874
+ 'books::book.beforeLoad',
875
+ beforeLoadSpy,
876
+ )
877
+ emitter.on<QueryEvent<InferDetail<typeof mockSchema>, InferLookup<typeof mockSchema>>>(
878
+ 'books::book.afterLoad',
879
+ afterLoadSpy,
880
+ )
881
+
882
+ emitter.on<QueryEvent<InferDetail<typeof mockSchema>[], InferLookup<typeof mockSchema>[]>>(
883
+ 'books::book.beforeLoadMany',
884
+ beforeLoadManySpy,
885
+ )
886
+ emitter.on<QueryEvent<InferDetail<typeof mockSchema>[], InferLookup<typeof mockSchema>[]>>(
887
+ 'books::book.afterLoadMany',
888
+ afterLoadManySpy,
889
+ )
890
+
891
+ emitter.on<QueryEvent<InferSearchResults<typeof mockSchema>[], InferFilters<typeof mockSchema>[]>>(
892
+ 'books::book.beforeSearch',
893
+ beforeSearchSpy,
894
+ )
895
+ emitter.on<QueryEvent<InferSearchResults<typeof mockSchema>[], InferFilters<typeof mockSchema>[]>>(
896
+ 'books::book.afterSearch',
897
+ afterSearchSpy,
898
+ )
899
+
900
+ emitter.on<QueryEvent<number, InferFilters<typeof mockSchema>>>('books::book.beforeCount', beforeCountSpy)
901
+ emitter.on<QueryEvent<number, InferFilters<typeof mockSchema>>>('books::book.afterCount', afterCountSpy)
902
+
903
+ service = new ReadOnlyModelService({ repository, emitter, schema: mockSchema, namespace })
904
+ })
905
+
906
+ describe('load', () => {
907
+ it('should not dispatch events when doNotDispatchEvents is true', async () => {
908
+ const input = { id: 1, title: 'Test Book', author: 'Author', publishedDate: new Date() }
909
+ await repository.create(input)
910
+
911
+ const result = await service.load({ id: 1 }, { doNotDispatchEvents: true })
912
+
913
+ expect(result).toEqual(input)
914
+ expect(beforeLoadSpy).not.toHaveBeenCalled()
915
+ expect(afterLoadSpy).not.toHaveBeenCalled()
916
+ })
917
+
918
+ it('should dispatch events when doNotDispatchEvents is false', async () => {
919
+ const input = { id: 2, title: 'Test Book 2', author: 'Author', publishedDate: new Date() }
920
+ await repository.create(input)
921
+
922
+ const result = await service.load({ id: 2 }, { doNotDispatchEvents: false })
923
+
924
+ expect(result).toEqual(input)
925
+ expect(beforeLoadSpy).toHaveBeenCalledTimes(1)
926
+ expect(afterLoadSpy).toHaveBeenCalledTimes(1)
927
+ })
928
+
929
+ it('should dispatch events when doNotDispatchEvents is not specified', async () => {
930
+ const input = { id: 3, title: 'Test Book 3', author: 'Author', publishedDate: new Date() }
931
+ await repository.create(input)
932
+
933
+ const result = await service.load({ id: 3 })
934
+
935
+ expect(result).toEqual(input)
936
+ expect(beforeLoadSpy).toHaveBeenCalledTimes(1)
937
+ expect(afterLoadSpy).toHaveBeenCalledTimes(1)
938
+ })
939
+ })
940
+
941
+ describe('loadMany', () => {
942
+ it('should not dispatch events when doNotDispatchEvents is true', async () => {
943
+ const input1 = { id: 1, title: 'Test Book 1', author: 'Author 1', publishedDate: new Date() }
944
+ const input2 = { id: 2, title: 'Test Book 2', author: 'Author 2', publishedDate: new Date() }
945
+ await repository.create(input1)
946
+ await repository.create(input2)
947
+
948
+ const result = await service.loadMany([{ id: 1 }, { id: 2 }], { doNotDispatchEvents: true })
949
+
950
+ expect(result).toEqual([input1, input2])
951
+ expect(beforeLoadManySpy).not.toHaveBeenCalled()
952
+ expect(afterLoadManySpy).not.toHaveBeenCalled()
953
+ })
954
+
955
+ it('should dispatch events when doNotDispatchEvents is false', async () => {
956
+ const input1 = { id: 3, title: 'Test Book 3', author: 'Author 3', publishedDate: new Date() }
957
+ const input2 = { id: 4, title: 'Test Book 4', author: 'Author 4', publishedDate: new Date() }
958
+ await repository.create(input1)
959
+ await repository.create(input2)
960
+
961
+ const result = await service.loadMany([{ id: 3 }, { id: 4 }], { doNotDispatchEvents: false })
962
+
963
+ expect(result).toEqual([input1, input2])
964
+ expect(beforeLoadManySpy).toHaveBeenCalledTimes(1)
965
+ expect(afterLoadManySpy).toHaveBeenCalledTimes(1)
966
+ })
967
+
968
+ it('should dispatch events when doNotDispatchEvents is not specified', async () => {
969
+ const input1 = { id: 5, title: 'Test Book 5', author: 'Author 5', publishedDate: new Date() }
970
+ const input2 = { id: 6, title: 'Test Book 6', author: 'Author 6', publishedDate: new Date() }
971
+ await repository.create(input1)
972
+ await repository.create(input2)
973
+
974
+ const result = await service.loadMany([{ id: 5 }, { id: 6 }])
975
+
976
+ expect(result).toEqual([input1, input2])
977
+ expect(beforeLoadManySpy).toHaveBeenCalledTimes(1)
978
+ expect(afterLoadManySpy).toHaveBeenCalledTimes(1)
979
+ })
980
+ })
981
+
982
+ describe('search', () => {
983
+ it('should not dispatch events when doNotDispatchEvents is true', async () => {
984
+ const input1 = { id: 1, title: 'Test Book 1', author: 'Author 1', publishedDate: new Date() }
985
+ const input2 = { id: 2, title: 'Test Book 2', author: 'Author 2', publishedDate: new Date() }
986
+ await repository.create(input1)
987
+ await repository.create(input2)
988
+
989
+ const result = await service.search({}, { doNotDispatchEvents: true })
990
+
991
+ expect(result.results).toEqual([input1, input2])
992
+ expect(beforeSearchSpy).not.toHaveBeenCalled()
993
+ expect(afterSearchSpy).not.toHaveBeenCalled()
994
+ })
995
+
996
+ it('should dispatch events when doNotDispatchEvents is false', async () => {
997
+ const input1 = { id: 3, title: 'Test Book 3', author: 'Author 3', publishedDate: new Date() }
998
+ const input2 = { id: 4, title: 'Test Book 4', author: 'Author 4', publishedDate: new Date() }
999
+ await repository.create(input1)
1000
+ await repository.create(input2)
1001
+
1002
+ const result = await service.search({}, { doNotDispatchEvents: false })
1003
+
1004
+ expect(result.results).toEqual([input1, input2])
1005
+ expect(beforeSearchSpy).toHaveBeenCalledTimes(1)
1006
+ expect(afterSearchSpy).toHaveBeenCalledTimes(1)
1007
+ })
1008
+
1009
+ it('should dispatch events when doNotDispatchEvents is not specified', async () => {
1010
+ const input1 = { id: 5, title: 'Test Book 5', author: 'Author 5', publishedDate: new Date() }
1011
+ const input2 = { id: 6, title: 'Test Book 6', author: 'Author 6', publishedDate: new Date() }
1012
+ await repository.create(input1)
1013
+ await repository.create(input2)
1014
+
1015
+ const result = await service.search({})
1016
+
1017
+ expect(result.results).toEqual([input1, input2])
1018
+ expect(beforeSearchSpy).toHaveBeenCalledTimes(1)
1019
+ expect(afterSearchSpy).toHaveBeenCalledTimes(1)
1020
+ })
1021
+ })
1022
+
1023
+ describe('count', () => {
1024
+ it('should not dispatch events when doNotDispatchEvents is true', async () => {
1025
+ const input1 = { id: 1, title: 'Test Book 1', author: 'Author 1', publishedDate: new Date() }
1026
+ const input2 = { id: 2, title: 'Test Book 2', author: 'Author 2', publishedDate: new Date() }
1027
+ await repository.create(input1)
1028
+ await repository.create(input2)
1029
+
1030
+ const result = await service.count({}, { doNotDispatchEvents: true })
1031
+
1032
+ expect(result).toBe(2)
1033
+ expect(beforeCountSpy).not.toHaveBeenCalled()
1034
+ expect(afterCountSpy).not.toHaveBeenCalled()
1035
+ })
1036
+
1037
+ it('should dispatch events when doNotDispatchEvents is false', async () => {
1038
+ const input1 = { id: 3, title: 'Test Book 3', author: 'Author 3', publishedDate: new Date() }
1039
+ const input2 = { id: 4, title: 'Test Book 4', author: 'Author 4', publishedDate: new Date() }
1040
+ await repository.create(input1)
1041
+ await repository.create(input2)
1042
+
1043
+ const result = await service.count({}, { doNotDispatchEvents: false })
1044
+
1045
+ expect(result).toBe(2)
1046
+ expect(beforeCountSpy).toHaveBeenCalledTimes(1)
1047
+ expect(afterCountSpy).toHaveBeenCalledTimes(1)
1048
+ })
1049
+
1050
+ it('should dispatch events when doNotDispatchEvents is not specified', async () => {
1051
+ const input1 = { id: 5, title: 'Test Book 5', author: 'Author 5', publishedDate: new Date() }
1052
+ const input2 = { id: 6, title: 'Test Book 6', author: 'Author 6', publishedDate: new Date() }
1053
+ await repository.create(input1)
1054
+ await repository.create(input2)
1055
+
1056
+ const result = await service.count({})
1057
+
1058
+ expect(result).toBe(2)
1059
+ expect(beforeCountSpy).toHaveBeenCalledTimes(1)
1060
+ expect(afterCountSpy).toHaveBeenCalledTimes(1)
1061
+ })
1062
+ })
1063
+ })
1064
+
1065
+ describe('noCache Option', () => {
1066
+ beforeEach(() => {
1067
+ repository = new MockMemoryRepository({ schema: mockSchema })
1068
+ emitter = new EventManager()
1069
+ service = new ReadOnlyModelService({ repository, emitter, schema: mockSchema, namespace })
1070
+ })
1071
+
1072
+ describe('load', () => {
1073
+ it('should return the correct result when noCache is true', async () => {
1074
+ const input = { id: 1, title: 'Test Book', author: 'Author', publishedDate: new Date() }
1075
+ await repository.create(input)
1076
+
1077
+ const result = await service.load({ id: 1 }, { noCache: true })
1078
+
1079
+ expect(result).toEqual(input)
1080
+ })
1081
+
1082
+ it('should return the correct result when noCache is false', async () => {
1083
+ const input = { id: 2, title: 'Test Book 2', author: 'Author', publishedDate: new Date() }
1084
+ await repository.create(input)
1085
+
1086
+ const result = await service.load({ id: 2 }, { noCache: false })
1087
+
1088
+ expect(result).toEqual(input)
1089
+ })
1090
+
1091
+ it('should forward noCache: true to the repository', async () => {
1092
+ const input = { id: 3, title: 'Test Book 3', author: 'Author', publishedDate: new Date() }
1093
+ await repository.create(input)
1094
+
1095
+ const loadSpy = spyOn(repository, 'load')
1096
+ await service.load({ id: 3 }, { noCache: true })
1097
+
1098
+ expect(loadSpy).toHaveBeenCalledWith({ id: 3 }, expect.objectContaining({ noCache: true }))
1099
+ })
1100
+ })
1101
+
1102
+ describe('loadMany', () => {
1103
+ it('should return the correct results when noCache is true', async () => {
1104
+ const input1 = { id: 1, title: 'Test Book 1', author: 'Author 1', publishedDate: new Date() }
1105
+ const input2 = { id: 2, title: 'Test Book 2', author: 'Author 2', publishedDate: new Date() }
1106
+ await repository.create(input1)
1107
+ await repository.create(input2)
1108
+
1109
+ const result = await service.loadMany([{ id: 1 }, { id: 2 }], { noCache: true })
1110
+
1111
+ expect(result).toEqual([input1, input2])
1112
+ })
1113
+
1114
+ it('should forward noCache: true to the repository', async () => {
1115
+ const input1 = { id: 3, title: 'Test Book 3', author: 'Author 3', publishedDate: new Date() }
1116
+ const input2 = { id: 4, title: 'Test Book 4', author: 'Author 4', publishedDate: new Date() }
1117
+ await repository.create(input1)
1118
+ await repository.create(input2)
1119
+
1120
+ const loadManySpy = spyOn(repository, 'loadMany')
1121
+ await service.loadMany([{ id: 3 }, { id: 4 }], { noCache: true })
1122
+
1123
+ expect(loadManySpy).toHaveBeenCalledWith(
1124
+ [{ id: 3 }, { id: 4 }],
1125
+ expect.objectContaining({ noCache: true }),
1126
+ )
1127
+ })
1128
+ })
1129
+ })
1130
+ })