@declaro/data 2.0.0-beta.126 → 2.0.0-beta.128

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 (39) hide show
  1. package/dist/browser/index.js +11 -11
  2. package/dist/browser/index.js.map +6 -6
  3. package/dist/node/index.cjs +217 -92
  4. package/dist/node/index.cjs.map +6 -6
  5. package/dist/node/index.js +209 -84
  6. package/dist/node/index.js.map +6 -6
  7. package/dist/ts/application/model-controller.d.ts +15 -5
  8. package/dist/ts/application/model-controller.d.ts.map +1 -1
  9. package/dist/ts/application/read-only-model-controller.d.ts +5 -1
  10. package/dist/ts/application/read-only-model-controller.d.ts.map +1 -1
  11. package/dist/ts/domain/services/model-service.d.ts +27 -0
  12. package/dist/ts/domain/services/model-service.d.ts.map +1 -1
  13. package/dist/ts/domain/services/read-only-model-service.d.ts +8 -0
  14. package/dist/ts/domain/services/read-only-model-service.d.ts.map +1 -1
  15. package/dist/ts/shared/utils/schema-inheritance.test.d.ts +2 -0
  16. package/dist/ts/shared/utils/schema-inheritance.test.d.ts.map +1 -0
  17. package/dist/ts/shared/utils/test/animal-schema.d.ts +57 -0
  18. package/dist/ts/shared/utils/test/animal-schema.d.ts.map +1 -0
  19. package/dist/ts/shared/utils/test/animal-trait-schema.d.ts +55 -0
  20. package/dist/ts/shared/utils/test/animal-trait-schema.d.ts.map +1 -0
  21. package/dist/ts/shared/utils/test/elephant-schema.d.ts +30 -0
  22. package/dist/ts/shared/utils/test/elephant-schema.d.ts.map +1 -0
  23. package/dist/ts/shared/utils/test/elephant-trait-schema.d.ts +26 -0
  24. package/dist/ts/shared/utils/test/elephant-trait-schema.d.ts.map +1 -0
  25. package/package.json +5 -5
  26. package/src/application/model-controller.ts +110 -59
  27. package/src/application/read-only-model-controller.ts +43 -25
  28. package/src/domain/services/model-service.test.ts +460 -0
  29. package/src/domain/services/model-service.ts +165 -67
  30. package/src/domain/services/read-only-model-service.test.ts +230 -0
  31. package/src/domain/services/read-only-model-service.ts +65 -40
  32. package/src/shared/utils/schema-inheritance.test.ts +295 -0
  33. package/src/shared/utils/test/animal-schema.ts +46 -0
  34. package/src/shared/utils/test/animal-trait-schema.ts +45 -0
  35. package/src/shared/utils/test/elephant-schema.ts +58 -0
  36. package/src/shared/utils/test/elephant-trait-schema.ts +53 -0
  37. package/dist/ts/test/mock/repositories/mock-memory-repository.custom-lookup.test.d.ts +0 -1
  38. package/dist/ts/test/mock/repositories/mock-memory-repository.custom-lookup.test.d.ts.map +0 -1
  39. package/src/test/mock/repositories/mock-memory-repository.custom-lookup.test.ts +0 -0
@@ -1,4 +1,4 @@
1
- import type { ActionDescriptor, AnyModelSchema, IActionDescriptor } from '@declaro/core'
1
+ import type { ActionDescriptor, AnyModelSchema, IActionDescriptor, IAnyModel } from '@declaro/core'
2
2
  import type {
3
3
  InferDetail,
4
4
  InferFilters,
@@ -12,8 +12,18 @@ import type { IModelServiceArgs } from './model-service-args'
12
12
  import { ReadOnlyModelService, type ILoadOptions } from './read-only-model-service'
13
13
  import type { IActionOptions } from './base-model-service'
14
14
 
15
- export interface ICreateOptions extends IActionOptions {}
16
- export interface IUpdateOptions extends IActionOptions {}
15
+ export interface ICreateOptions extends IActionOptions {
16
+ /**
17
+ * If true, skips dispatching events for this action.
18
+ */
19
+ doNotDispatchEvents?: boolean
20
+ }
21
+ export interface IUpdateOptions extends IActionOptions {
22
+ /**
23
+ * If true, skips dispatching events for this action.
24
+ */
25
+ doNotDispatchEvents?: boolean
26
+ }
17
27
 
18
28
  export interface INormalizeInputArgs<TSchema extends AnyModelSchema> {
19
29
  existing?: InferDetail<TSchema>
@@ -39,6 +49,72 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
39
49
  return input
40
50
  }
41
51
 
52
+ /**
53
+ * Converts a detail object to a valid input for this service's schema.
54
+ * Picks only fields that exist in the input model and validates/coerces
55
+ * them through the input schema. Useful for duplicating entities or
56
+ * converting a detail from one entity type into an input for another.
57
+ * @param detail The detail object to convert (can be from any schema).
58
+ * @returns A validated input object with coerced values.
59
+ */
60
+ async detailsToInput(detail: Record<string, unknown>): Promise<InferInput<TSchema>> {
61
+ const inputModel: IAnyModel = this.schema.definition.input
62
+ const inputJsonSchema = inputModel.toJSONSchema()
63
+ const inputFields = Object.keys(inputJsonSchema.properties ?? {})
64
+
65
+ // Pick only fields that exist in the input model
66
+ const picked: Record<string, unknown> = {}
67
+ for (const field of inputFields) {
68
+ if (field in detail) {
69
+ picked[field] = detail[field]
70
+ }
71
+ }
72
+
73
+ // Validate through the input model to coerce values
74
+ const result = await inputModel.validate(picked as any)
75
+
76
+ if ('value' in result) {
77
+ return result.value as InferInput<TSchema>
78
+ }
79
+
80
+ return picked as InferInput<TSchema>
81
+ }
82
+
83
+ /**
84
+ * Duplicates an existing entity by loading it, converting to input, removing the
85
+ * primary key, and creating a new record. Accepts an optional partial input to
86
+ * merge on top of the converted copy before creation.
87
+ * @param lookup The lookup criteria to find the record to duplicate.
88
+ * @param overrides Optional partial input to merge on top of the duplicated data.
89
+ * @param options Optional create options.
90
+ * @returns The newly created duplicate record.
91
+ */
92
+ async duplicate(
93
+ lookup: InferLookup<TSchema>,
94
+ overrides?: Partial<InferInput<TSchema>>,
95
+ options?: ICreateOptions,
96
+ ): Promise<InferDetail<TSchema>> {
97
+ // Load the existing entity
98
+ const existing = await this.load(lookup)
99
+ if (!existing) {
100
+ throw new Error('Item not found')
101
+ }
102
+
103
+ // Convert the detail to an input
104
+ const input = await this.detailsToInput(existing as Record<string, unknown>)
105
+
106
+ // Remove the primary key to ensure a new record gets created
107
+ if (this.entityMetadata?.primaryKey) {
108
+ delete (input as Record<string, unknown>)[this.entityMetadata.primaryKey]
109
+ }
110
+
111
+ // Merge optional overrides
112
+ const finalInput = overrides ? Object.assign({}, input, overrides) : input
113
+
114
+ // Create the new record
115
+ return this.create(finalInput as InferInput<TSchema>, options)
116
+ }
117
+
42
118
  /**
43
119
  * Removes a record by its lookup criteria.
44
120
  * @param lookup The lookup criteria to find the record.
@@ -101,21 +177,25 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
101
177
  })
102
178
 
103
179
  // Emit the before create event
104
- const beforeCreateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
105
- this.getDescriptor(ModelMutationAction.BeforeCreate),
106
- normalizedInput,
107
- )
108
- await this.emitter.emitAsync(beforeCreateEvent)
180
+ if (!options?.doNotDispatchEvents) {
181
+ const beforeCreateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
182
+ this.getDescriptor(ModelMutationAction.BeforeCreate),
183
+ normalizedInput,
184
+ )
185
+ await this.emitter.emitAsync(beforeCreateEvent)
186
+ }
109
187
 
110
188
  // Perform the creation
111
189
  const result = await this.repository.create(normalizedInput, options)
112
190
 
113
191
  // Emit the after create event
114
- const afterCreateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
115
- this.getDescriptor(ModelMutationAction.AfterCreate),
116
- normalizedInput,
117
- ).setResult(result)
118
- await this.emitter.emitAsync(afterCreateEvent)
192
+ if (!options?.doNotDispatchEvents) {
193
+ const afterCreateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
194
+ this.getDescriptor(ModelMutationAction.AfterCreate),
195
+ normalizedInput,
196
+ ).setResult(result)
197
+ await this.emitter.emitAsync(afterCreateEvent)
198
+ }
119
199
 
120
200
  // Return the results of the creation
121
201
  return await this.normalizeDetail(result)
@@ -126,7 +206,7 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
126
206
  input: InferInput<TSchema>,
127
207
  options?: IUpdateOptions,
128
208
  ): Promise<InferDetail<TSchema>> {
129
- const existing = await this.repository.load(lookup, options)
209
+ const existing = await this.repository.load(lookup, { ...options, doNotDispatchEvents: true })
130
210
  // Normalize the input data
131
211
  const normalizedInput = await this.normalizeInput(input, {
132
212
  existing,
@@ -134,21 +214,25 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
134
214
  })
135
215
 
136
216
  // Emit the before update event
137
- const beforeUpdateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
138
- this.getDescriptor(ModelMutationAction.BeforeUpdate),
139
- normalizedInput,
140
- )
141
- await this.emitter.emitAsync(beforeUpdateEvent)
217
+ if (!options?.doNotDispatchEvents) {
218
+ const beforeUpdateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
219
+ this.getDescriptor(ModelMutationAction.BeforeUpdate),
220
+ normalizedInput,
221
+ )
222
+ await this.emitter.emitAsync(beforeUpdateEvent)
223
+ }
142
224
 
143
225
  // Perform the update
144
226
  const result = await this.repository.update(lookup, normalizedInput, options)
145
227
 
146
228
  // Emit the after update event
147
- const afterUpdateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
148
- this.getDescriptor(ModelMutationAction.AfterUpdate),
149
- normalizedInput,
150
- ).setResult(result)
151
- await this.emitter.emitAsync(afterUpdateEvent)
229
+ if (!options?.doNotDispatchEvents) {
230
+ const afterUpdateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
231
+ this.getDescriptor(ModelMutationAction.AfterUpdate),
232
+ normalizedInput,
233
+ ).setResult(result)
234
+ await this.emitter.emitAsync(afterUpdateEvent)
235
+ }
152
236
 
153
237
  // Return the results of the update
154
238
  return await this.normalizeDetail(result)
@@ -177,7 +261,10 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
177
261
  {
178
262
  [this.entityMetadata.primaryKey]: primaryKeyValue,
179
263
  } as InferLookup<TSchema>,
180
- options,
264
+ {
265
+ ...options,
266
+ doNotDispatchEvents: true,
267
+ },
181
268
  )
182
269
 
183
270
  if (existingItem) {
@@ -198,21 +285,25 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
198
285
  })
199
286
 
200
287
  // Emit the before upsert event
201
- const beforeUpsertEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
202
- this.getDescriptor(beforeOperation),
203
- normalizedInput,
204
- )
205
- await this.emitter.emitAsync(beforeUpsertEvent)
288
+ if (!options?.doNotDispatchEvents) {
289
+ const beforeUpsertEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
290
+ this.getDescriptor(beforeOperation),
291
+ normalizedInput,
292
+ )
293
+ await this.emitter.emitAsync(beforeUpsertEvent)
294
+ }
206
295
 
207
296
  // Perform the upsert operation
208
297
  const result = await this.repository.upsert(normalizedInput, options)
209
298
 
210
299
  // Emit the after upsert event
211
- const afterUpsertEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
212
- this.getDescriptor(afterOperation),
213
- normalizedInput,
214
- ).setResult(result)
215
- await this.emitter.emitAsync(afterUpsertEvent)
300
+ if (!options?.doNotDispatchEvents) {
301
+ const afterUpsertEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
302
+ this.getDescriptor(afterOperation),
303
+ normalizedInput,
304
+ ).setResult(result)
305
+ await this.emitter.emitAsync(afterUpsertEvent)
306
+ }
216
307
 
217
308
  // Return the results of the upsert operation
218
309
  return await this.normalizeDetail(result)
@@ -268,7 +359,10 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
268
359
  const existingEntitiesMap = new Map<string | number, InferDetail<TSchema>>()
269
360
  if (uniqueLookups.size > 0) {
270
361
  const lookups = Array.from(uniqueLookups.values())
271
- const existingEntities = await this.loadMany(lookups, options)
362
+ const existingEntities = await this.loadMany(lookups, {
363
+ ...options,
364
+ doNotDispatchEvents: true,
365
+ })
272
366
  existingEntities.forEach((entity) => {
273
367
  if (entity) {
274
368
  const pkValue = this.getPrimaryKeyValue(entity)
@@ -306,44 +400,48 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
306
400
  const normalizedInputs = await Promise.all(normalizationPromises)
307
401
 
308
402
  // Create before events
309
- const beforeEvents: MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>[] = []
310
- for (const inputInfo of inputInfos) {
311
- beforeEvents.push(
312
- new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
313
- this.getDescriptor(inputInfo.operation!),
314
- inputInfo.input,
315
- ),
316
- )
317
- }
403
+ if (!options?.doNotDispatchEvents) {
404
+ const beforeEvents: MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>[] = []
405
+ for (const inputInfo of inputInfos) {
406
+ beforeEvents.push(
407
+ new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
408
+ this.getDescriptor(inputInfo.operation!),
409
+ inputInfo.input,
410
+ ),
411
+ )
412
+ }
318
413
 
319
- // Emit all before events
320
- await Promise.all(beforeEvents.map((event) => this.emitter.emitAsync(event)))
414
+ // Emit all before events
415
+ await Promise.all(beforeEvents.map((event) => this.emitter.emitAsync(event)))
416
+ }
321
417
 
322
418
  // Perform the bulk upsert operation with all normalized inputs
323
419
  const results = await this.repository.bulkUpsert(normalizedInputs, options)
324
420
 
325
421
  // Create after events and return results
326
- const afterEvents: MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>[] = []
327
-
328
- for (let i = 0; i < inputInfos.length; i++) {
329
- const inputInfo = inputInfos[i]
330
- const result = results[i]
331
-
332
- const afterOperation =
333
- inputInfo.operation === ModelMutationAction.BeforeCreate
334
- ? ModelMutationAction.AfterCreate
335
- : ModelMutationAction.AfterUpdate
336
-
337
- afterEvents.push(
338
- new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
339
- this.getDescriptor(afterOperation),
340
- inputInfo.input,
341
- ).setResult(result),
342
- )
343
- }
422
+ if (!options?.doNotDispatchEvents) {
423
+ const afterEvents: MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>[] = []
424
+
425
+ for (let i = 0; i < inputInfos.length; i++) {
426
+ const inputInfo = inputInfos[i]
427
+ const result = results[i]
428
+
429
+ const afterOperation =
430
+ inputInfo.operation === ModelMutationAction.BeforeCreate
431
+ ? ModelMutationAction.AfterCreate
432
+ : ModelMutationAction.AfterUpdate
433
+
434
+ afterEvents.push(
435
+ new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
436
+ this.getDescriptor(afterOperation),
437
+ inputInfo.input,
438
+ ).setResult(result),
439
+ )
440
+ }
344
441
 
345
- // Emit all after events
346
- await Promise.all(afterEvents.map((event) => this.emitter.emitAsync(event)))
442
+ // Emit all after events
443
+ await Promise.all(afterEvents.map((event) => this.emitter.emitAsync(event)))
444
+ }
347
445
 
348
446
  // Return normalized results
349
447
  return await Promise.all(results.map((result) => this.normalizeDetail(result)))
@@ -825,4 +825,234 @@ describe('ReadOnlyModelService', () => {
825
825
  })
826
826
  })
827
827
  })
828
+
829
+ describe('doNotDispatchEvents Option', () => {
830
+ const beforeLoadSpy = mock(
831
+ (event: QueryEvent<InferDetail<typeof mockSchema>, InferLookup<typeof mockSchema>>) => {},
832
+ )
833
+ const afterLoadSpy = mock(
834
+ (event: QueryEvent<InferDetail<typeof mockSchema>, InferLookup<typeof mockSchema>>) => {},
835
+ )
836
+
837
+ const beforeLoadManySpy = mock(
838
+ (event: QueryEvent<InferDetail<typeof mockSchema>[], InferLookup<typeof mockSchema>[]>) => {},
839
+ )
840
+ const afterLoadManySpy = mock(
841
+ (event: QueryEvent<InferDetail<typeof mockSchema>[], InferLookup<typeof mockSchema>[]>) => {},
842
+ )
843
+
844
+ const beforeSearchSpy = mock(
845
+ (event: QueryEvent<InferSearchResults<typeof mockSchema>[], InferFilters<typeof mockSchema>[]>) => {},
846
+ )
847
+ const afterSearchSpy = mock(
848
+ (event: QueryEvent<InferSearchResults<typeof mockSchema>[], InferFilters<typeof mockSchema>[]>) => {},
849
+ )
850
+
851
+ const beforeCountSpy = mock((event: QueryEvent<number, InferFilters<typeof mockSchema>>) => {})
852
+ const afterCountSpy = mock((event: QueryEvent<number, InferFilters<typeof mockSchema>>) => {})
853
+
854
+ beforeEach(() => {
855
+ repository = new MockMemoryRepository({ schema: mockSchema })
856
+ emitter = new EventManager()
857
+
858
+ beforeLoadSpy.mockClear()
859
+ afterLoadSpy.mockClear()
860
+ beforeLoadManySpy.mockClear()
861
+ afterLoadManySpy.mockClear()
862
+ beforeSearchSpy.mockClear()
863
+ afterSearchSpy.mockClear()
864
+ beforeCountSpy.mockClear()
865
+ afterCountSpy.mockClear()
866
+
867
+ emitter.on<QueryEvent<InferDetail<typeof mockSchema>, InferLookup<typeof mockSchema>>>(
868
+ 'books::book.beforeLoad',
869
+ beforeLoadSpy,
870
+ )
871
+ emitter.on<QueryEvent<InferDetail<typeof mockSchema>, InferLookup<typeof mockSchema>>>(
872
+ 'books::book.afterLoad',
873
+ afterLoadSpy,
874
+ )
875
+
876
+ emitter.on<QueryEvent<InferDetail<typeof mockSchema>[], InferLookup<typeof mockSchema>[]>>(
877
+ 'books::book.beforeLoadMany',
878
+ beforeLoadManySpy,
879
+ )
880
+ emitter.on<QueryEvent<InferDetail<typeof mockSchema>[], InferLookup<typeof mockSchema>[]>>(
881
+ 'books::book.afterLoadMany',
882
+ afterLoadManySpy,
883
+ )
884
+
885
+ emitter.on<QueryEvent<InferSearchResults<typeof mockSchema>[], InferFilters<typeof mockSchema>[]>>(
886
+ 'books::book.beforeSearch',
887
+ beforeSearchSpy,
888
+ )
889
+ emitter.on<QueryEvent<InferSearchResults<typeof mockSchema>[], InferFilters<typeof mockSchema>[]>>(
890
+ 'books::book.afterSearch',
891
+ afterSearchSpy,
892
+ )
893
+
894
+ emitter.on<QueryEvent<number, InferFilters<typeof mockSchema>>>('books::book.beforeCount', beforeCountSpy)
895
+ emitter.on<QueryEvent<number, InferFilters<typeof mockSchema>>>('books::book.afterCount', afterCountSpy)
896
+
897
+ service = new ReadOnlyModelService({ repository, emitter, schema: mockSchema, namespace })
898
+ })
899
+
900
+ describe('load', () => {
901
+ it('should not dispatch events when doNotDispatchEvents is true', async () => {
902
+ const input = { id: 1, title: 'Test Book', author: 'Author', publishedDate: new Date() }
903
+ await repository.create(input)
904
+
905
+ const result = await service.load({ id: 1 }, { doNotDispatchEvents: true })
906
+
907
+ expect(result).toEqual(input)
908
+ expect(beforeLoadSpy).not.toHaveBeenCalled()
909
+ expect(afterLoadSpy).not.toHaveBeenCalled()
910
+ })
911
+
912
+ it('should dispatch events when doNotDispatchEvents is false', async () => {
913
+ const input = { id: 2, title: 'Test Book 2', author: 'Author', publishedDate: new Date() }
914
+ await repository.create(input)
915
+
916
+ const result = await service.load({ id: 2 }, { doNotDispatchEvents: false })
917
+
918
+ expect(result).toEqual(input)
919
+ expect(beforeLoadSpy).toHaveBeenCalledTimes(1)
920
+ expect(afterLoadSpy).toHaveBeenCalledTimes(1)
921
+ })
922
+
923
+ it('should dispatch events when doNotDispatchEvents is not specified', async () => {
924
+ const input = { id: 3, title: 'Test Book 3', author: 'Author', publishedDate: new Date() }
925
+ await repository.create(input)
926
+
927
+ const result = await service.load({ id: 3 })
928
+
929
+ expect(result).toEqual(input)
930
+ expect(beforeLoadSpy).toHaveBeenCalledTimes(1)
931
+ expect(afterLoadSpy).toHaveBeenCalledTimes(1)
932
+ })
933
+ })
934
+
935
+ describe('loadMany', () => {
936
+ it('should not dispatch events when doNotDispatchEvents is true', async () => {
937
+ const input1 = { id: 1, title: 'Test Book 1', author: 'Author 1', publishedDate: new Date() }
938
+ const input2 = { id: 2, title: 'Test Book 2', author: 'Author 2', publishedDate: new Date() }
939
+ await repository.create(input1)
940
+ await repository.create(input2)
941
+
942
+ const result = await service.loadMany([{ id: 1 }, { id: 2 }], { doNotDispatchEvents: true })
943
+
944
+ expect(result).toEqual([input1, input2])
945
+ expect(beforeLoadManySpy).not.toHaveBeenCalled()
946
+ expect(afterLoadManySpy).not.toHaveBeenCalled()
947
+ })
948
+
949
+ it('should dispatch events when doNotDispatchEvents is false', async () => {
950
+ const input1 = { id: 3, title: 'Test Book 3', author: 'Author 3', publishedDate: new Date() }
951
+ const input2 = { id: 4, title: 'Test Book 4', author: 'Author 4', publishedDate: new Date() }
952
+ await repository.create(input1)
953
+ await repository.create(input2)
954
+
955
+ const result = await service.loadMany([{ id: 3 }, { id: 4 }], { doNotDispatchEvents: false })
956
+
957
+ expect(result).toEqual([input1, input2])
958
+ expect(beforeLoadManySpy).toHaveBeenCalledTimes(1)
959
+ expect(afterLoadManySpy).toHaveBeenCalledTimes(1)
960
+ })
961
+
962
+ it('should dispatch events when doNotDispatchEvents is not specified', async () => {
963
+ const input1 = { id: 5, title: 'Test Book 5', author: 'Author 5', publishedDate: new Date() }
964
+ const input2 = { id: 6, title: 'Test Book 6', author: 'Author 6', publishedDate: new Date() }
965
+ await repository.create(input1)
966
+ await repository.create(input2)
967
+
968
+ const result = await service.loadMany([{ id: 5 }, { id: 6 }])
969
+
970
+ expect(result).toEqual([input1, input2])
971
+ expect(beforeLoadManySpy).toHaveBeenCalledTimes(1)
972
+ expect(afterLoadManySpy).toHaveBeenCalledTimes(1)
973
+ })
974
+ })
975
+
976
+ describe('search', () => {
977
+ it('should not dispatch events when doNotDispatchEvents is true', async () => {
978
+ const input1 = { id: 1, title: 'Test Book 1', author: 'Author 1', publishedDate: new Date() }
979
+ const input2 = { id: 2, title: 'Test Book 2', author: 'Author 2', publishedDate: new Date() }
980
+ await repository.create(input1)
981
+ await repository.create(input2)
982
+
983
+ const result = await service.search({}, { doNotDispatchEvents: true })
984
+
985
+ expect(result.results).toEqual([input1, input2])
986
+ expect(beforeSearchSpy).not.toHaveBeenCalled()
987
+ expect(afterSearchSpy).not.toHaveBeenCalled()
988
+ })
989
+
990
+ it('should dispatch events when doNotDispatchEvents is false', async () => {
991
+ const input1 = { id: 3, title: 'Test Book 3', author: 'Author 3', publishedDate: new Date() }
992
+ const input2 = { id: 4, title: 'Test Book 4', author: 'Author 4', publishedDate: new Date() }
993
+ await repository.create(input1)
994
+ await repository.create(input2)
995
+
996
+ const result = await service.search({}, { doNotDispatchEvents: false })
997
+
998
+ expect(result.results).toEqual([input1, input2])
999
+ expect(beforeSearchSpy).toHaveBeenCalledTimes(1)
1000
+ expect(afterSearchSpy).toHaveBeenCalledTimes(1)
1001
+ })
1002
+
1003
+ it('should dispatch events when doNotDispatchEvents is not specified', async () => {
1004
+ const input1 = { id: 5, title: 'Test Book 5', author: 'Author 5', publishedDate: new Date() }
1005
+ const input2 = { id: 6, title: 'Test Book 6', author: 'Author 6', publishedDate: new Date() }
1006
+ await repository.create(input1)
1007
+ await repository.create(input2)
1008
+
1009
+ const result = await service.search({})
1010
+
1011
+ expect(result.results).toEqual([input1, input2])
1012
+ expect(beforeSearchSpy).toHaveBeenCalledTimes(1)
1013
+ expect(afterSearchSpy).toHaveBeenCalledTimes(1)
1014
+ })
1015
+ })
1016
+
1017
+ describe('count', () => {
1018
+ it('should not dispatch events when doNotDispatchEvents is true', async () => {
1019
+ const input1 = { id: 1, title: 'Test Book 1', author: 'Author 1', publishedDate: new Date() }
1020
+ const input2 = { id: 2, title: 'Test Book 2', author: 'Author 2', publishedDate: new Date() }
1021
+ await repository.create(input1)
1022
+ await repository.create(input2)
1023
+
1024
+ const result = await service.count({}, { doNotDispatchEvents: true })
1025
+
1026
+ expect(result).toBe(2)
1027
+ expect(beforeCountSpy).not.toHaveBeenCalled()
1028
+ expect(afterCountSpy).not.toHaveBeenCalled()
1029
+ })
1030
+
1031
+ it('should dispatch events when doNotDispatchEvents is false', async () => {
1032
+ const input1 = { id: 3, title: 'Test Book 3', author: 'Author 3', publishedDate: new Date() }
1033
+ const input2 = { id: 4, title: 'Test Book 4', author: 'Author 4', publishedDate: new Date() }
1034
+ await repository.create(input1)
1035
+ await repository.create(input2)
1036
+
1037
+ const result = await service.count({}, { doNotDispatchEvents: false })
1038
+
1039
+ expect(result).toBe(2)
1040
+ expect(beforeCountSpy).toHaveBeenCalledTimes(1)
1041
+ expect(afterCountSpy).toHaveBeenCalledTimes(1)
1042
+ })
1043
+
1044
+ it('should dispatch events when doNotDispatchEvents is not specified', async () => {
1045
+ const input1 = { id: 5, title: 'Test Book 5', author: 'Author 5', publishedDate: new Date() }
1046
+ const input2 = { id: 6, title: 'Test Book 6', author: 'Author 6', publishedDate: new Date() }
1047
+ await repository.create(input1)
1048
+ await repository.create(input2)
1049
+
1050
+ const result = await service.count({})
1051
+
1052
+ expect(result).toBe(2)
1053
+ expect(beforeCountSpy).toHaveBeenCalledTimes(1)
1054
+ expect(afterCountSpy).toHaveBeenCalledTimes(1)
1055
+ })
1056
+ })
1057
+ })
828
1058
  })