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

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 +189 -92
  4. package/dist/node/index.cjs.map +6 -6
  5. package/dist/node/index.js +181 -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 +8 -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 +280 -0
  29. package/src/domain/services/model-service.ts +98 -66
  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
@@ -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>
@@ -101,21 +111,25 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
101
111
  })
102
112
 
103
113
  // 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)
114
+ if (!options?.doNotDispatchEvents) {
115
+ const beforeCreateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
116
+ this.getDescriptor(ModelMutationAction.BeforeCreate),
117
+ normalizedInput,
118
+ )
119
+ await this.emitter.emitAsync(beforeCreateEvent)
120
+ }
109
121
 
110
122
  // Perform the creation
111
123
  const result = await this.repository.create(normalizedInput, options)
112
124
 
113
125
  // 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)
126
+ if (!options?.doNotDispatchEvents) {
127
+ const afterCreateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
128
+ this.getDescriptor(ModelMutationAction.AfterCreate),
129
+ normalizedInput,
130
+ ).setResult(result)
131
+ await this.emitter.emitAsync(afterCreateEvent)
132
+ }
119
133
 
120
134
  // Return the results of the creation
121
135
  return await this.normalizeDetail(result)
@@ -126,7 +140,7 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
126
140
  input: InferInput<TSchema>,
127
141
  options?: IUpdateOptions,
128
142
  ): Promise<InferDetail<TSchema>> {
129
- const existing = await this.repository.load(lookup, options)
143
+ const existing = await this.repository.load(lookup, { ...options, doNotDispatchEvents: true })
130
144
  // Normalize the input data
131
145
  const normalizedInput = await this.normalizeInput(input, {
132
146
  existing,
@@ -134,21 +148,25 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
134
148
  })
135
149
 
136
150
  // 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)
151
+ if (!options?.doNotDispatchEvents) {
152
+ const beforeUpdateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
153
+ this.getDescriptor(ModelMutationAction.BeforeUpdate),
154
+ normalizedInput,
155
+ )
156
+ await this.emitter.emitAsync(beforeUpdateEvent)
157
+ }
142
158
 
143
159
  // Perform the update
144
160
  const result = await this.repository.update(lookup, normalizedInput, options)
145
161
 
146
162
  // 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)
163
+ if (!options?.doNotDispatchEvents) {
164
+ const afterUpdateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
165
+ this.getDescriptor(ModelMutationAction.AfterUpdate),
166
+ normalizedInput,
167
+ ).setResult(result)
168
+ await this.emitter.emitAsync(afterUpdateEvent)
169
+ }
152
170
 
153
171
  // Return the results of the update
154
172
  return await this.normalizeDetail(result)
@@ -177,7 +195,10 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
177
195
  {
178
196
  [this.entityMetadata.primaryKey]: primaryKeyValue,
179
197
  } as InferLookup<TSchema>,
180
- options,
198
+ {
199
+ ...options,
200
+ doNotDispatchEvents: true,
201
+ },
181
202
  )
182
203
 
183
204
  if (existingItem) {
@@ -198,21 +219,25 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
198
219
  })
199
220
 
200
221
  // 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)
222
+ if (!options?.doNotDispatchEvents) {
223
+ const beforeUpsertEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
224
+ this.getDescriptor(beforeOperation),
225
+ normalizedInput,
226
+ )
227
+ await this.emitter.emitAsync(beforeUpsertEvent)
228
+ }
206
229
 
207
230
  // Perform the upsert operation
208
231
  const result = await this.repository.upsert(normalizedInput, options)
209
232
 
210
233
  // 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)
234
+ if (!options?.doNotDispatchEvents) {
235
+ const afterUpsertEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
236
+ this.getDescriptor(afterOperation),
237
+ normalizedInput,
238
+ ).setResult(result)
239
+ await this.emitter.emitAsync(afterUpsertEvent)
240
+ }
216
241
 
217
242
  // Return the results of the upsert operation
218
243
  return await this.normalizeDetail(result)
@@ -268,7 +293,10 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
268
293
  const existingEntitiesMap = new Map<string | number, InferDetail<TSchema>>()
269
294
  if (uniqueLookups.size > 0) {
270
295
  const lookups = Array.from(uniqueLookups.values())
271
- const existingEntities = await this.loadMany(lookups, options)
296
+ const existingEntities = await this.loadMany(lookups, {
297
+ ...options,
298
+ doNotDispatchEvents: true,
299
+ })
272
300
  existingEntities.forEach((entity) => {
273
301
  if (entity) {
274
302
  const pkValue = this.getPrimaryKeyValue(entity)
@@ -306,44 +334,48 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
306
334
  const normalizedInputs = await Promise.all(normalizationPromises)
307
335
 
308
336
  // 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
- }
337
+ if (!options?.doNotDispatchEvents) {
338
+ const beforeEvents: MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>[] = []
339
+ for (const inputInfo of inputInfos) {
340
+ beforeEvents.push(
341
+ new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
342
+ this.getDescriptor(inputInfo.operation!),
343
+ inputInfo.input,
344
+ ),
345
+ )
346
+ }
318
347
 
319
- // Emit all before events
320
- await Promise.all(beforeEvents.map((event) => this.emitter.emitAsync(event)))
348
+ // Emit all before events
349
+ await Promise.all(beforeEvents.map((event) => this.emitter.emitAsync(event)))
350
+ }
321
351
 
322
352
  // Perform the bulk upsert operation with all normalized inputs
323
353
  const results = await this.repository.bulkUpsert(normalizedInputs, options)
324
354
 
325
355
  // 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
- }
356
+ if (!options?.doNotDispatchEvents) {
357
+ const afterEvents: MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>[] = []
358
+
359
+ for (let i = 0; i < inputInfos.length; i++) {
360
+ const inputInfo = inputInfos[i]
361
+ const result = results[i]
362
+
363
+ const afterOperation =
364
+ inputInfo.operation === ModelMutationAction.BeforeCreate
365
+ ? ModelMutationAction.AfterCreate
366
+ : ModelMutationAction.AfterUpdate
367
+
368
+ afterEvents.push(
369
+ new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
370
+ this.getDescriptor(afterOperation),
371
+ inputInfo.input,
372
+ ).setResult(result),
373
+ )
374
+ }
344
375
 
345
- // Emit all after events
346
- await Promise.all(afterEvents.map((event) => this.emitter.emitAsync(event)))
376
+ // Emit all after events
377
+ await Promise.all(afterEvents.map((event) => this.emitter.emitAsync(event)))
378
+ }
347
379
 
348
380
  // Return normalized results
349
381
  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
  })
@@ -23,6 +23,11 @@ export interface ILoadOptions extends IActionOptions {
23
23
  * If true, both removed and non-removed records will be returned.
24
24
  */
25
25
  includeRemoved?: boolean
26
+
27
+ /**
28
+ * If true, skips dispatching events for this action.
29
+ */
30
+ doNotDispatchEvents?: boolean
26
31
  }
27
32
  export interface ISearchOptions<TSchema extends AnyModelSchema> extends IActionOptions {
28
33
  pagination?: IPaginationInput
@@ -35,6 +40,10 @@ export interface ISearchOptions<TSchema extends AnyModelSchema> extends IActionO
35
40
  * If true, both removed and non-removed records will be returned.
36
41
  */
37
42
  includeRemoved?: boolean
43
+ /**
44
+ * If true, skips dispatching events for this action.
45
+ */
46
+ doNotDispatchEvents?: boolean
38
47
  }
39
48
 
40
49
  export class ReadOnlyModelService<TSchema extends AnyModelSchema> extends BaseModelService<TSchema> {
@@ -69,21 +78,25 @@ export class ReadOnlyModelService<TSchema extends AnyModelSchema> extends BaseMo
69
78
  */
70
79
  async load(lookup: InferLookup<TSchema>, options?: ILoadOptions): Promise<InferDetail<TSchema>> {
71
80
  // Emit the before load event
72
- const beforeLoadEvent = new QueryEvent<InferDetail<TSchema>, InferLookup<TSchema>>(
73
- this.getDescriptor(ModelQueryEvent.BeforeLoad, options?.scope),
74
- lookup,
75
- )
76
- await this.emitter.emitAsync(beforeLoadEvent)
81
+ if (!options?.doNotDispatchEvents) {
82
+ const beforeLoadEvent = new QueryEvent<InferDetail<TSchema>, InferLookup<TSchema>>(
83
+ this.getDescriptor(ModelQueryEvent.BeforeLoad, options?.scope),
84
+ lookup,
85
+ )
86
+ await this.emitter.emitAsync(beforeLoadEvent)
87
+ }
77
88
 
78
89
  // Load the details from the repository
79
90
  const details = await this.repository.load(lookup, options)
80
91
 
81
92
  // Emit the after load event
82
- const afterLoadEvent = new QueryEvent<InferDetail<TSchema>, InferLookup<TSchema>>(
83
- this.getDescriptor(ModelQueryEvent.AfterLoad, options?.scope),
84
- lookup,
85
- ).setResult(details)
86
- await this.emitter.emitAsync(afterLoadEvent)
93
+ if (!options?.doNotDispatchEvents) {
94
+ const afterLoadEvent = new QueryEvent<InferDetail<TSchema>, InferLookup<TSchema>>(
95
+ this.getDescriptor(ModelQueryEvent.AfterLoad, options?.scope),
96
+ lookup,
97
+ ).setResult(details)
98
+ await this.emitter.emitAsync(afterLoadEvent)
99
+ }
87
100
 
88
101
  return await this.normalizeDetail(details)
89
102
  }
@@ -96,21 +109,25 @@ export class ReadOnlyModelService<TSchema extends AnyModelSchema> extends BaseMo
96
109
  */
97
110
  async loadMany(lookups: InferLookup<TSchema>[], options?: ILoadOptions): Promise<InferDetail<TSchema>[]> {
98
111
  // Emit the before load many event
99
- const beforeLoadManyEvent = new QueryEvent<InferDetail<TSchema>[], InferLookup<TSchema>[]>(
100
- this.getDescriptor(ModelQueryEvent.BeforeLoadMany, options?.scope),
101
- lookups,
102
- )
103
- await this.emitter.emitAsync(beforeLoadManyEvent)
112
+ if (!options?.doNotDispatchEvents) {
113
+ const beforeLoadManyEvent = new QueryEvent<InferDetail<TSchema>[], InferLookup<TSchema>[]>(
114
+ this.getDescriptor(ModelQueryEvent.BeforeLoadMany, options?.scope),
115
+ lookups,
116
+ )
117
+ await this.emitter.emitAsync(beforeLoadManyEvent)
118
+ }
104
119
 
105
120
  // Load the details from the repository
106
121
  const details = await this.repository.loadMany(lookups, options)
107
122
 
108
123
  // Emit the after load many event
109
- const afterLoadManyEvent = new QueryEvent<InferDetail<TSchema>[], InferLookup<TSchema>[]>(
110
- this.getDescriptor(ModelQueryEvent.AfterLoadMany, options?.scope),
111
- lookups,
112
- ).setResult(details)
113
- await this.emitter.emitAsync(afterLoadManyEvent)
124
+ if (!options?.doNotDispatchEvents) {
125
+ const afterLoadManyEvent = new QueryEvent<InferDetail<TSchema>[], InferLookup<TSchema>[]>(
126
+ this.getDescriptor(ModelQueryEvent.AfterLoadMany, options?.scope),
127
+ lookups,
128
+ ).setResult(details)
129
+ await this.emitter.emitAsync(afterLoadManyEvent)
130
+ }
114
131
 
115
132
  return await Promise.all(details.map((detail) => this.normalizeDetail(detail)))
116
133
  }
@@ -126,21 +143,25 @@ export class ReadOnlyModelService<TSchema extends AnyModelSchema> extends BaseMo
126
143
  options?: ISearchOptions<TSchema>,
127
144
  ): Promise<InferSearchResults<TSchema>> {
128
145
  // Emit the before search event
129
- const beforeSearchEvent = new QueryEvent<InferSearchResults<TSchema>, InferFilters<TSchema>>(
130
- this.getDescriptor(ModelQueryEvent.BeforeSearch, options?.scope),
131
- filters,
132
- )
133
- await this.emitter.emitAsync(beforeSearchEvent)
146
+ if (!options?.doNotDispatchEvents) {
147
+ const beforeSearchEvent = new QueryEvent<InferSearchResults<TSchema>, InferFilters<TSchema>>(
148
+ this.getDescriptor(ModelQueryEvent.BeforeSearch, options?.scope),
149
+ filters,
150
+ )
151
+ await this.emitter.emitAsync(beforeSearchEvent)
152
+ }
134
153
 
135
154
  // Search the repository with the provided filters
136
155
  const results = await this.repository.search(filters, options)
137
156
 
138
157
  // Emit the after search event
139
- const afterSearchEvent = new QueryEvent<InferSearchResults<TSchema>, InferFilters<TSchema>>(
140
- this.getDescriptor(ModelQueryEvent.AfterSearch, options?.scope),
141
- filters,
142
- ).setResult(results)
143
- await this.emitter.emitAsync(afterSearchEvent)
158
+ if (!options?.doNotDispatchEvents) {
159
+ const afterSearchEvent = new QueryEvent<InferSearchResults<TSchema>, InferFilters<TSchema>>(
160
+ this.getDescriptor(ModelQueryEvent.AfterSearch, options?.scope),
161
+ filters,
162
+ ).setResult(results)
163
+ await this.emitter.emitAsync(afterSearchEvent)
164
+ }
144
165
 
145
166
  // Return the search results
146
167
  return {
@@ -156,21 +177,25 @@ export class ReadOnlyModelService<TSchema extends AnyModelSchema> extends BaseMo
156
177
  */
157
178
  async count(filters: InferFilters<TSchema>, options?: ISearchOptions<TSchema>): Promise<number> {
158
179
  // Emit the before count event
159
- const beforeCountEvent = new QueryEvent<number, InferFilters<TSchema>>(
160
- this.getDescriptor(ModelQueryEvent.BeforeCount, options?.scope),
161
- filters,
162
- )
163
- await this.emitter.emitAsync(beforeCountEvent)
180
+ if (!options?.doNotDispatchEvents) {
181
+ const beforeCountEvent = new QueryEvent<number, InferFilters<TSchema>>(
182
+ this.getDescriptor(ModelQueryEvent.BeforeCount, options?.scope),
183
+ filters,
184
+ )
185
+ await this.emitter.emitAsync(beforeCountEvent)
186
+ }
164
187
 
165
188
  // Count the records in the repository
166
189
  const count = await this.repository.count(filters, options)
167
190
 
168
191
  // Emit the after count event
169
- const afterCountEvent = new QueryEvent<number, InferFilters<TSchema>>(
170
- this.getDescriptor(ModelQueryEvent.AfterCount, options?.scope),
171
- filters,
172
- ).setResult(count)
173
- await this.emitter.emitAsync(afterCountEvent)
192
+ if (!options?.doNotDispatchEvents) {
193
+ const afterCountEvent = new QueryEvent<number, InferFilters<TSchema>>(
194
+ this.getDescriptor(ModelQueryEvent.AfterCount, options?.scope),
195
+ filters,
196
+ ).setResult(count)
197
+ await this.emitter.emitAsync(afterCountEvent)
198
+ }
174
199
 
175
200
  // Return the count
176
201
  return count