@flusys/nestjs-shared 7.0.0 → 7.0.2

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 (40) hide show
  1. package/README.md +132 -84
  2. package/cjs/classes/api-service.class.js +37 -47
  3. package/cjs/classes/api-service.class.spec.js +120 -12
  4. package/cjs/classes/event-bus.registry.js +2 -3
  5. package/cjs/classes/event-bus.registry.spec.js +49 -24
  6. package/cjs/classes/event-transports/kafka-transport.optional.spec.js +1 -2
  7. package/cjs/classes/event-transports/memory-transport.js +3 -4
  8. package/cjs/classes/event-transports/memory-transport.spec.js +1 -2
  9. package/cjs/classes/event-transports/rabbitmq-transport.optional.spec.js +1 -2
  10. package/cjs/classes/hybrid-event-bus.class.js +23 -3
  11. package/cjs/classes/hybrid-event-bus.class.spec.js +31 -2
  12. package/cjs/enums/index.js +0 -1
  13. package/cjs/modules/event-bus/event-bus.integration.spec.js +1 -2
  14. package/cjs/utils/event-pattern.util.js +6 -1
  15. package/cjs/utils/event-pattern.util.spec.js +20 -3
  16. package/classes/api-service.class.d.ts +8 -12
  17. package/classes/domain-event.publisher.d.ts +2 -2
  18. package/classes/event-bus.registry.d.ts +2 -2
  19. package/classes/event-transports/memory-transport.d.ts +1 -1
  20. package/classes/hybrid-event-bus.class.d.ts +2 -0
  21. package/enums/index.d.ts +0 -1
  22. package/fesm/classes/api-service.class.js +37 -47
  23. package/fesm/classes/api-service.class.spec.js +120 -12
  24. package/fesm/classes/event-bus.registry.js +2 -3
  25. package/fesm/classes/event-bus.registry.spec.js +49 -24
  26. package/fesm/classes/event-transports/kafka-transport.optional.spec.js +1 -2
  27. package/fesm/classes/event-transports/memory-transport.js +7 -4
  28. package/fesm/classes/event-transports/memory-transport.spec.js +1 -2
  29. package/fesm/classes/event-transports/rabbitmq-transport.optional.spec.js +1 -2
  30. package/fesm/classes/hybrid-event-bus.class.js +23 -3
  31. package/fesm/classes/hybrid-event-bus.class.spec.js +31 -2
  32. package/fesm/enums/index.js +0 -1
  33. package/fesm/modules/event-bus/event-bus.integration.spec.js +1 -2
  34. package/fesm/utils/event-pattern.util.js +6 -1
  35. package/fesm/utils/event-pattern.util.spec.js +20 -3
  36. package/interfaces/event-bus.interface.d.ts +3 -6
  37. package/package.json +2 -2
  38. package/cjs/enums/event-action.enum.js +0 -18
  39. package/enums/event-action.enum.d.ts +0 -7
  40. package/fesm/enums/event-action.enum.js +0 -8
@@ -1,13 +1,12 @@
1
- import { DataSource, EntityTarget, QueryRunner, Repository, SelectQueryBuilder } from 'typeorm';
1
+ import { EntityTarget, QueryRunner, Repository, SelectQueryBuilder } from 'typeorm';
2
2
  import { DeleteDto, FilterAndPaginationDto } from '../dtos';
3
3
  import { Identity, RawEntity } from '../entities';
4
- import { EventAction } from '../enums/event-action.enum';
5
- import { DomainEventActionName, IDataSourceProvider, ILoggedUserInfo, IService } from '../interfaces';
4
+ import { CrudEventActionName, IDataSourceProvider, ILoggedUserInfo, IService } from '../interfaces';
6
5
  import { UtilsService } from '../modules/utils/utils.service';
7
6
  import { IPublishDomainEventInput } from './domain-event.publisher';
8
7
  import { HybridCache } from './hybrid-cache.class';
9
8
  interface IEventGroup<T> {
10
- action: EventAction;
9
+ action: CrudEventActionName;
11
10
  entities: T[];
12
11
  idsOnly?: boolean;
13
12
  }
@@ -19,17 +18,15 @@ export declare abstract class ApiService<CreateDtoT extends object, UpdateDtoT e
19
18
  protected utilsService: UtilsService;
20
19
  protected _loggerName: string;
21
20
  protected isCacheable: boolean;
22
- protected moduleName?: string | undefined;
21
+ protected moduleName: string;
23
22
  protected repository: Repository<EntityT>;
24
23
  private repositoryInitialized;
25
- private _entity?;
26
- private _dataSourceProvider?;
27
- constructor(entityName: string, cacheManager: HybridCache, utilsService: UtilsService, _loggerName: string, isCacheable?: boolean, moduleName?: string | undefined, entity?: EntityTarget<EntityT>, dataSourceProvider?: IDataSourceProvider);
28
- protected resolveEntity(): EntityTarget<EntityT>;
24
+ private readonly _entity;
25
+ private readonly _dataSourceProvider;
26
+ constructor(entityName: string, cacheManager: HybridCache, utilsService: UtilsService, _loggerName: string, isCacheable: boolean | undefined, moduleName: string, dataSourceProvider: IDataSourceProvider, entity: EntityTarget<EntityT>);
29
27
  protected getDataSourceProvider(): IDataSourceProvider;
30
28
  protected ensureDataSourceRepository(): Promise<void>;
31
29
  protected resolveRepositories(entities: EntityTarget<any>[]): Promise<Repository<any>[]>;
32
- protected getDataSourceForService(): Promise<DataSource>;
33
30
  insert(dto: CreateDtoT, user: ILoggedUserInfo | null): Promise<InterfaceT>;
34
31
  insertMany(dtos: Array<CreateDtoT>, user: ILoggedUserInfo | null): Promise<InterfaceT[]>;
35
32
  update(dto: UpdateDtoT, user: ILoggedUserInfo | null): Promise<InterfaceT>;
@@ -43,7 +40,7 @@ export declare abstract class ApiService<CreateDtoT extends object, UpdateDtoT e
43
40
  }>;
44
41
  delete(option: DeleteDto, user: ILoggedUserInfo | null): Promise<null>;
45
42
  protected publishLifecycleEvents(groups: IEventGroup<EntityT>[], user: ILoggedUserInfo | null): Promise<void>;
46
- protected publishDomainAction(action: DomainEventActionName, input?: Omit<IPublishDomainEventInput, 'module' | 'entity' | 'action'>): Promise<void>;
43
+ protected publishDomainAction(action: string, input?: Omit<IPublishDomainEventInput, 'module' | 'entity' | 'action'>): Promise<void>;
47
44
  clearCacheForAll(): Promise<void>;
48
45
  clearCacheForId(entities: EntityT[]): Promise<void>;
49
46
  private handleError;
@@ -82,6 +79,5 @@ export declare abstract class ApiService<CreateDtoT extends object, UpdateDtoT e
82
79
  protected convertArrayDtoToEntities(dtos: Array<CreateDtoT | UpdateDtoT>, user: ILoggedUserInfo | null): Promise<Array<EntityT>>;
83
80
  protected convertEntityToResponseDto(entity: EntityT | RawEntity, _isRaw: boolean): InterfaceT;
84
81
  protected convertEntityListToResponseListDto(entities: EntityT[] | RawEntity[], _isRaw: boolean): InterfaceT[];
85
- protected getEntityClass(): Promise<EntityT>;
86
82
  }
87
83
  export {};
@@ -1,9 +1,9 @@
1
- import { DomainEventActionName, IDomainEvent, IDomainEventPayload, IEventBus, ILoggedUserInfo } from '../interfaces';
1
+ import { IDomainEvent, IDomainEventPayload, IEventBus, ILoggedUserInfo } from '../interfaces';
2
2
  export declare const DOMAIN_EVENT_VERSION = 1;
3
3
  export interface IPublishDomainEventInput<T = Record<string, unknown>> {
4
4
  module: string;
5
5
  entity: string;
6
- action: DomainEventActionName;
6
+ action: string;
7
7
  ids?: string[];
8
8
  data?: T | T[];
9
9
  metadata?: Record<string, unknown>;
@@ -1,4 +1,4 @@
1
- import { DomainEventActionName, IEventBus, IModuleEventsConfig } from '../interfaces/event-bus.interface';
1
+ import { IEventBus, IModuleEventsConfig } from '../interfaces/event-bus.interface';
2
2
  export declare class EventBusRegistry {
3
3
  private static bus?;
4
4
  private static moduleConfigs;
@@ -8,7 +8,7 @@ export declare class EventBusRegistry {
8
8
  static setDefaultModuleEvents(config: IModuleEventsConfig): void;
9
9
  static configureModule(moduleName: string, config?: IModuleEventsConfig): void;
10
10
  static getModuleConfig(moduleName?: string): IModuleEventsConfig;
11
- static isEnabled(moduleName: string | undefined, entity: string, action: DomainEventActionName): boolean;
11
+ static isEnabled(moduleName: string | undefined, entity: string, action: string): boolean;
12
12
  static shouldIncludePayload(moduleName?: string): boolean;
13
13
  static reset(): void;
14
14
  }
@@ -3,7 +3,7 @@ export declare class MemoryTransport implements IEventTransport {
3
3
  readonly name = "memory";
4
4
  private readonly emitter;
5
5
  private ready;
6
- constructor(maxListeners?: number);
6
+ constructor();
7
7
  connect(): Promise<void>;
8
8
  publish(event: IDomainEvent): Promise<void>;
9
9
  bind(_pattern: string): Promise<void>;
@@ -4,6 +4,7 @@ export declare class HybridEventBus implements IEventBus {
4
4
  private readonly logger;
5
5
  private readonly subscriptions;
6
6
  private readonly transports;
7
+ private readonly boundPatterns;
7
8
  private readonly seenIds;
8
9
  private readonly seenOrder;
9
10
  private readonly label;
@@ -21,6 +22,7 @@ export declare class HybridEventBus implements IEventBus {
21
22
  private ensureConnected;
22
23
  private resolveLabelFromEnv;
23
24
  private buildTransports;
25
+ private bindSubscriptions;
24
26
  private createBrokerTransport;
25
27
  private resolveHybridBrokerName;
26
28
  private loadOptionalTransport;
package/enums/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- export * from './event-action.enum';
2
1
  export * from './notification-type.enum';
3
2
  export * from './participant-status.enum';
4
3
  export * from './recurrence-type.enum';
@@ -25,35 +25,23 @@ import { EntityTarget, In } from 'typeorm';
25
25
  import { ERROR_MESSAGES, SYSTEM_MESSAGES } from '../constants';
26
26
  import { LogAction } from '../decorators/log-action.decorator';
27
27
  import { DeleteDto } from '../dtos';
28
- import { EventAction } from '../enums/event-action.enum';
29
28
  import { IDataSourceProvider } from '../interfaces';
30
29
  import { UtilsService } from '../modules/utils/utils.service';
31
30
  import { publishDomainEvent } from './domain-event.publisher';
32
31
  import { HybridCache } from './hybrid-cache.class';
33
- const DEFAULT_EVENT_MODULE = 'app';
34
- const OPERATION_EVENT_ACTIONS = {
35
- insert: EventAction.CREATED,
36
- insertMany: EventAction.CREATED,
37
- update: EventAction.UPDATED,
38
- updateMany: EventAction.UPDATED,
39
- bulkUpsert: EventAction.UPDATED
40
- };
41
32
  const DELETE_EVENT_ACTIONS = {
42
- delete: EventAction.DELETED,
43
- restore: EventAction.RESTORED
33
+ delete: 'deleted',
34
+ restore: 'restored',
35
+ permanent: 'purged'
44
36
  };
45
37
  export class ApiService {
46
- resolveEntity() {
47
- return this._entity;
48
- }
49
38
  getDataSourceProvider() {
50
39
  return this._dataSourceProvider;
51
40
  }
52
41
  async ensureDataSourceRepository() {
53
42
  if (!this.repositoryInitialized) {
54
- const entity = this.resolveEntity();
55
43
  const provider = this.getDataSourceProvider();
56
- this.repository = await provider.getRepository(entity);
44
+ this.repository = await provider.getRepository(this._entity);
57
45
  this.repositoryInitialized = true;
58
46
  }
59
47
  }
@@ -65,11 +53,8 @@ export class ApiService {
65
53
  }
66
54
  return repositories;
67
55
  }
68
- async getDataSourceForService() {
69
- return this.getDataSourceProvider().getDataSource();
70
- }
71
56
  async insert(dto, user) {
72
- return this.executeInTransaction('insert', async (qr)=>{
57
+ return this.executeInTransaction('created', async (qr)=>{
73
58
  await this.beforeInsertOperation(dto, user, qr);
74
59
  const entities = await this.convertRequestDtoToEntity(dto, user);
75
60
  const saved = await qr.manager.save(this.repository.target, entities);
@@ -81,7 +66,7 @@ export class ApiService {
81
66
  }, user);
82
67
  }
83
68
  async insertMany(dtos, user) {
84
- return this.executeInTransaction('insertMany', async (qr)=>{
69
+ return this.executeInTransaction('created', async (qr)=>{
85
70
  await this.beforeInsertOperation(dtos, user, qr);
86
71
  const entities = await this.convertRequestDtoToEntity(dtos, user);
87
72
  const saved = await qr.manager.save(this.repository.target, entities);
@@ -93,7 +78,7 @@ export class ApiService {
93
78
  }, user);
94
79
  }
95
80
  async update(dto, user) {
96
- return this.executeInTransaction('update', async (qr)=>{
81
+ return this.executeInTransaction('updated', async (qr)=>{
97
82
  await this.beforeUpdateOperation(dto, user, qr);
98
83
  const entities = await this.convertRequestDtoToEntity(dto, user);
99
84
  const saved = await qr.manager.save(this.repository.target, entities);
@@ -105,7 +90,7 @@ export class ApiService {
105
90
  }, user);
106
91
  }
107
92
  async updateMany(dtos, user) {
108
- return this.executeInTransaction('updateMany', async (qr)=>{
93
+ return this.executeInTransaction('updated', async (qr)=>{
109
94
  await this.beforeUpdateOperation(dtos, user, qr);
110
95
  const entities = await this.convertRequestDtoToEntity(dtos, user);
111
96
  const saved = await qr.manager.save(this.repository.target, entities);
@@ -117,7 +102,7 @@ export class ApiService {
117
102
  }, user);
118
103
  }
119
104
  async bulkUpsert(toInsert, toUpdate, user) {
120
- return this.executeInTransaction('bulkUpsert', async (qr)=>{
105
+ return this.executeInTransaction(undefined, async (qr)=>{
121
106
  const allResults = [];
122
107
  const eventGroups = [];
123
108
  if (toInsert.length > 0) {
@@ -127,7 +112,7 @@ export class ApiService {
127
112
  await this.afterInsertOperation(this.ensureArray(insertedSaved), toInsert, user, qr);
128
113
  allResults.push(...this.ensureArray(insertedSaved));
129
114
  eventGroups.push({
130
- action: EventAction.CREATED,
115
+ action: 'created',
131
116
  entities: this.ensureArray(insertedSaved)
132
117
  });
133
118
  }
@@ -138,7 +123,7 @@ export class ApiService {
138
123
  await this.afterUpdateOperation(this.ensureArray(updatedSaved), toUpdate, user, qr);
139
124
  allResults.push(...this.ensureArray(updatedSaved));
140
125
  eventGroups.push({
141
- action: EventAction.UPDATED,
126
+ action: 'updated',
142
127
  entities: this.ensureArray(updatedSaved)
143
128
  });
144
129
  }
@@ -174,7 +159,7 @@ export class ApiService {
174
159
  if (error instanceof NotFoundException) {
175
160
  throw error;
176
161
  }
177
- this.handleError(error, 'findByIds');
162
+ this.handleError(error);
178
163
  }
179
164
  }
180
165
  async findById(id, user, select) {
@@ -214,7 +199,7 @@ export class ApiService {
214
199
  if (error instanceof NotFoundException) {
215
200
  throw error;
216
201
  }
217
- this.handleError(error, 'findById');
202
+ this.handleError(error);
218
203
  }
219
204
  }
220
205
  async getAll(search, filterAndPaginationDto, user) {
@@ -313,7 +298,7 @@ export class ApiService {
313
298
  }
314
299
  return finalResponse;
315
300
  } catch (error) {
316
- this.handleError(error, 'getAll');
301
+ this.handleError(error);
317
302
  }
318
303
  }
319
304
  async delete(option, user) {
@@ -351,7 +336,7 @@ export class ApiService {
351
336
  ]);
352
337
  await this.publishLifecycleEvents([
353
338
  {
354
- action: DELETE_EVENT_ACTIONS[option.type] ?? EventAction.PURGED,
339
+ action: DELETE_EVENT_ACTIONS[option.type],
355
340
  entities: ids,
356
341
  idsOnly: true
357
342
  }
@@ -359,7 +344,7 @@ export class ApiService {
359
344
  return null;
360
345
  } catch (error) {
361
346
  await queryRunner.rollbackTransaction();
362
- this.handleError(error, 'delete');
347
+ this.handleError(error);
363
348
  } finally{
364
349
  await queryRunner.release();
365
350
  }
@@ -368,7 +353,7 @@ export class ApiService {
368
353
  for (const group of groups){
369
354
  if (!group.entities.length) continue;
370
355
  await publishDomainEvent({
371
- module: this.moduleName ?? DEFAULT_EVENT_MODULE,
356
+ module: this.moduleName,
372
357
  entity: this.entityName,
373
358
  action: group.action,
374
359
  ids: group.entities.map((entity)=>entity.id),
@@ -385,7 +370,7 @@ export class ApiService {
385
370
  */ async publishDomainAction(action, input = {}) {
386
371
  await publishDomainEvent({
387
372
  ...input,
388
- module: this.moduleName ?? DEFAULT_EVENT_MODULE,
373
+ module: this.moduleName,
389
374
  entity: this.entityName,
390
375
  action
391
376
  });
@@ -396,7 +381,7 @@ export class ApiService {
396
381
  async clearCacheForId(entities) {
397
382
  await Promise.all(entities.map((item)=>this.utilsService.clearCache(this.entityName, this.cacheManager, item.id)));
398
383
  }
399
- handleError(error, _operation) {
384
+ handleError(error) {
400
385
  if (error instanceof Error) {
401
386
  throw error;
402
387
  }
@@ -410,7 +395,11 @@ export class ApiService {
410
395
  value
411
396
  ];
412
397
  }
413
- async executeInTransaction(operation, fn, user) {
398
+ /**
399
+ * `action` is the lifecycle action to publish for the whole batch; an
400
+ * operation that saves more than one kind of row - a bulk upsert - passes
401
+ * `undefined` and returns its own `eventGroups` instead.
402
+ */ async executeInTransaction(action, fn, user) {
414
403
  await this.ensureDataSourceRepository();
415
404
  const qr = this.repository.manager.connection.createQueryRunner();
416
405
  await qr.connect();
@@ -425,19 +414,19 @@ export class ApiService {
425
414
  this.clearCacheForId(savedArray)
426
415
  ]);
427
416
  }
428
- await this.publishLifecycleEvents(eventGroups ?? [
417
+ await this.publishLifecycleEvents(eventGroups ?? (action ? [
429
418
  {
430
- action: OPERATION_EVENT_ACTIONS[operation],
419
+ action,
431
420
  entities: savedArray
432
421
  }
433
- ], user ?? null);
422
+ ] : []), user ?? null);
434
423
  if (returnFirst) {
435
424
  return this.convertEntityToResponseDto(savedArray[0], false);
436
425
  }
437
426
  return savedArray.map((e)=>this.convertEntityToResponseDto(e, false));
438
427
  } catch (error) {
439
428
  await qr.rollbackTransaction();
440
- this.handleError(error, operation);
429
+ this.handleError(error);
441
430
  } finally{
442
431
  await qr.release();
443
432
  }
@@ -510,7 +499,7 @@ export class ApiService {
510
499
  return Object.assign(existing, d);
511
500
  }
512
501
  delete d.id;
513
- return Object.assign(await this.getEntityClass(), d);
502
+ return Object.assign(this.repository.create(), d);
514
503
  }
515
504
  async convertArrayDtoToEntities(dtos, user) {
516
505
  return await Promise.all(dtos.map((dto)=>this.convertSingleDtoToEntity(dto, user)));
@@ -521,10 +510,11 @@ export class ApiService {
521
510
  convertEntityListToResponseListDto(entities, _isRaw) {
522
511
  return entities.map((entity)=>this.convertEntityToResponseDto(entity, _isRaw));
523
512
  }
524
- async getEntityClass() {
525
- return this.repository.create();
526
- }
527
- constructor(entityName, cacheManager, utilsService, _loggerName, isCacheable = false, moduleName, entity, dataSourceProvider){
513
+ /**
514
+ * `entity` is the table this service reads and writes. A service whose table
515
+ * depends on runtime config - the company feature, say - passes the ternary
516
+ * here, reading the injected config through the constructor parameter.
517
+ */ constructor(entityName, cacheManager, utilsService, _loggerName, isCacheable = false, moduleName, dataSourceProvider, entity){
528
518
  _define_property(this, "entityName", void 0);
529
519
  _define_property(this, "cacheManager", void 0);
530
520
  _define_property(this, "utilsService", void 0);
@@ -542,8 +532,8 @@ export class ApiService {
542
532
  this.isCacheable = isCacheable;
543
533
  this.moduleName = moduleName;
544
534
  this.repositoryInitialized = false;
545
- this._entity = entity;
546
535
  this._dataSourceProvider = dataSourceProvider;
536
+ this._entity = entity;
547
537
  }
548
538
  }
549
539
  _ts_decorate([
@@ -625,7 +615,7 @@ ApiService = _ts_decorate([
625
615
  String,
626
616
  Boolean,
627
617
  String,
628
- typeof EntityTarget === "undefined" ? Object : EntityTarget,
629
- typeof IDataSourceProvider === "undefined" ? Object : IDataSourceProvider
618
+ typeof IDataSourceProvider === "undefined" ? Object : IDataSourceProvider,
619
+ typeof EntityTarget === "undefined" ? Object : EntityTarget
630
620
  ])
631
621
  ], ApiService);
@@ -16,7 +16,6 @@ import { createMockRepository, createMockDataSourceProvider } from '@test-utils/
16
16
  import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
17
17
  import { ApiService } from './api-service.class';
18
18
  import { EventBusRegistry } from './event-bus.registry';
19
- import { EventAction } from '../enums/event-action.enum';
20
19
  import { UtilsService } from '../modules/utils/utils.service';
21
20
  import { Identity } from '../entities';
22
21
  let TestEntity = class TestEntity extends Identity {
@@ -25,6 +24,14 @@ let TestEntity = class TestEntity extends Identity {
25
24
  }
26
25
  };
27
26
  let TestApiService = class TestApiService extends ApiService {
27
+ /** `publishDomainAction` is protected - feature services reach it as subclasses do */ publishAction(action, metadata) {
28
+ return this.publishDomainAction(action, {
29
+ ids: [
30
+ '1'
31
+ ],
32
+ metadata
33
+ });
34
+ }
28
35
  };
29
36
  function createMockQueryRunner() {
30
37
  return {
@@ -52,7 +59,7 @@ describe('ApiService', ()=>{
52
59
  let errorLogSpy;
53
60
  function makeService(isCacheable = false) {
54
61
  const dataSourceProvider = createMockDataSourceProvider(mockRepo);
55
- return new TestApiService('test-entity', mockCache, utilsService, 'TestApiService', isCacheable, undefined, TestEntity, dataSourceProvider);
62
+ return new TestApiService('test-entity', mockCache, utilsService, 'TestApiService', isCacheable, 'app', dataSourceProvider, TestEntity);
56
63
  }
57
64
  beforeEach(()=>{
58
65
  mockRepo = createMockRepository();
@@ -641,7 +648,7 @@ describe('ApiService', ()=>{
641
648
  function makeRawService(dbType) {
642
649
  mockRepo.manager.connection.options.type = dbType;
643
650
  const dataSourceProvider = createMockDataSourceProvider(mockRepo);
644
- return new RawTestApiService('test-entity', mockCache, utilsService, 'RawTestApiService', false, undefined, TestEntity, dataSourceProvider);
651
+ return new RawTestApiService('test-entity', mockCache, utilsService, 'RawTestApiService', false, 'app', dataSourceProvider, TestEntity);
645
652
  }
646
653
  function primeRawQueryBuilder(qb) {
647
654
  qb.expressionMap = {
@@ -808,7 +815,7 @@ describe('ApiService', ()=>{
808
815
  let publish;
809
816
  function makeEventService(moduleName = 'test-module') {
810
817
  const dataSourceProvider = createMockDataSourceProvider(mockRepo);
811
- return new TestApiService('test-entity', mockCache, utilsService, 'TestApiService', false, moduleName, TestEntity, dataSourceProvider);
818
+ return new TestApiService('test-entity', mockCache, utilsService, 'TestApiService', false, moduleName, dataSourceProvider, TestEntity);
812
819
  }
813
820
  beforeEach(()=>{
814
821
  publish = jest.fn().mockResolvedValue(undefined);
@@ -839,7 +846,7 @@ describe('ApiService', ()=>{
839
846
  name: 'test-module.test-entity.created',
840
847
  module: 'test-module',
841
848
  entity: 'test-entity',
842
- action: EventAction.CREATED,
849
+ action: 'created',
843
850
  payload: expect.objectContaining({
844
851
  ids: [
845
852
  'new-1'
@@ -860,6 +867,74 @@ describe('ApiService', ()=>{
860
867
  userId: mockUser.id
861
868
  }));
862
869
  });
870
+ it('should publish one created event carrying every id for insertMany', async ()=>{
871
+ const service = makeEventService();
872
+ mockQueryRunner.manager.save.mockResolvedValue([
873
+ {
874
+ id: 'new-1',
875
+ name: 'A'
876
+ },
877
+ {
878
+ id: 'new-2',
879
+ name: 'B'
880
+ }
881
+ ]);
882
+ await service.insertMany([
883
+ {
884
+ name: 'A'
885
+ },
886
+ {
887
+ name: 'B'
888
+ }
889
+ ], mockUser);
890
+ expect(publish).toHaveBeenCalledTimes(1);
891
+ expect(publish.mock.calls[0][0]).toEqual(expect.objectContaining({
892
+ action: 'created',
893
+ payload: expect.objectContaining({
894
+ ids: [
895
+ 'new-1',
896
+ 'new-2'
897
+ ]
898
+ })
899
+ }));
900
+ });
901
+ it('should publish one updated event carrying every id for updateMany', async ()=>{
902
+ const service = makeEventService();
903
+ mockRepo.findOne.mockResolvedValue({
904
+ id: '1',
905
+ name: 'Old'
906
+ });
907
+ mockQueryRunner.manager.save.mockResolvedValue([
908
+ {
909
+ id: '1',
910
+ name: 'A'
911
+ },
912
+ {
913
+ id: '2',
914
+ name: 'B'
915
+ }
916
+ ]);
917
+ await service.updateMany([
918
+ {
919
+ id: '1',
920
+ name: 'A'
921
+ },
922
+ {
923
+ id: '2',
924
+ name: 'B'
925
+ }
926
+ ], mockUser);
927
+ expect(publish).toHaveBeenCalledTimes(1);
928
+ expect(publish.mock.calls[0][0]).toEqual(expect.objectContaining({
929
+ action: 'updated',
930
+ payload: expect.objectContaining({
931
+ ids: [
932
+ '1',
933
+ '2'
934
+ ]
935
+ })
936
+ }));
937
+ });
863
938
  it('should publish an updated event after update commits', async ()=>{
864
939
  const service = makeEventService();
865
940
  mockRepo.findOne.mockResolvedValue({
@@ -875,7 +950,7 @@ describe('ApiService', ()=>{
875
950
  name: 'New'
876
951
  }, mockUser);
877
952
  expect(publish.mock.calls[0][0]).toEqual(expect.objectContaining({
878
- action: EventAction.UPDATED
953
+ action: 'updated'
879
954
  }));
880
955
  });
881
956
  it('should publish created and updated separately for a bulk upsert', async ()=>{
@@ -906,22 +981,22 @@ describe('ApiService', ()=>{
906
981
  }
907
982
  ], mockUser);
908
983
  expect(publish.mock.calls.map(([event])=>event.action)).toEqual([
909
- EventAction.CREATED,
910
- EventAction.UPDATED
984
+ 'created',
985
+ 'updated'
911
986
  ]);
912
987
  });
913
988
  it.each([
914
989
  [
915
990
  'delete',
916
- EventAction.DELETED
991
+ 'deleted'
917
992
  ],
918
993
  [
919
994
  'restore',
920
- EventAction.RESTORED
995
+ 'restored'
921
996
  ],
922
997
  [
923
998
  'permanent',
924
- EventAction.PURGED
999
+ 'purged'
925
1000
  ]
926
1001
  ])('should publish %s as the %s action', async (type, action)=>{
927
1002
  const service = makeEventService();
@@ -945,6 +1020,39 @@ describe('ApiService', ()=>{
945
1020
  ]
946
1021
  });
947
1022
  });
1023
+ it('should publish a domain action under this service module and entity', async ()=>{
1024
+ const service = makeEventService();
1025
+ await service.publishAction('submitted', {
1026
+ formId: 'form-1'
1027
+ });
1028
+ expect(publish.mock.calls[0][0]).toEqual(expect.objectContaining({
1029
+ name: 'test-module.test-entity.submitted',
1030
+ module: 'test-module',
1031
+ entity: 'test-entity',
1032
+ action: 'submitted',
1033
+ payload: {
1034
+ ids: [
1035
+ '1'
1036
+ ],
1037
+ metadata: {
1038
+ formId: 'form-1'
1039
+ }
1040
+ }
1041
+ }));
1042
+ });
1043
+ it('should hold a domain action to the same allowlist as the CRUD actions', async ()=>{
1044
+ EventBusRegistry.configureModule('test-module', {
1045
+ enabled: true,
1046
+ actions: [
1047
+ 'test-entity.submitted'
1048
+ ]
1049
+ });
1050
+ const service = makeEventService();
1051
+ await service.publishAction('submitted');
1052
+ await service.publishAction('draft-saved');
1053
+ expect(publish).toHaveBeenCalledTimes(1);
1054
+ expect(publish.mock.calls[0][0].action).toBe('submitted');
1055
+ });
948
1056
  it('should not publish when the module has events disabled', async ()=>{
949
1057
  EventBusRegistry.configureModule('test-module', {
950
1058
  enabled: false
@@ -963,7 +1071,7 @@ describe('ApiService', ()=>{
963
1071
  EventBusRegistry.configureModule('test-module', {
964
1072
  enabled: true,
965
1073
  actions: [
966
- EventAction.DELETED
1074
+ 'deleted'
967
1075
  ]
968
1076
  });
969
1077
  const service = makeEventService();
@@ -11,6 +11,7 @@ function _define_property(obj, key, value) {
11
11
  }
12
12
  return obj;
13
13
  }
14
+ import { matchEventPattern } from '../utils/event-pattern.util';
14
15
  const DISABLED = {
15
16
  enabled: false
16
17
  };
@@ -44,12 +45,10 @@ const DISABLED = {
44
45
  */ static isEnabled(moduleName, entity, action) {
45
46
  const config = this.getModuleConfig(moduleName);
46
47
  if (!config.enabled) return false;
47
- if (config.excludeEntities?.includes(entity)) return false;
48
- if (config.entities?.length && !config.entities.includes(entity)) return false;
49
48
  // Without an explicit allowlist, CRUD lifecycle actions and every domain
50
49
  // action a package defines are all published.
51
50
  if (!config.actions?.length) return true;
52
- return config.actions.includes(action);
51
+ return config.actions.some((entry)=>entry.includes('.') ? matchEventPattern(entry, `${entity}.${action}`) : matchEventPattern(entry, action));
53
52
  }
54
53
  static shouldIncludePayload(moduleName) {
55
54
  return this.getModuleConfig(moduleName).includePayload ?? true;