@flusys/nestjs-shared 6.4.0 → 7.0.0-beta.1

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 (111) hide show
  1. package/README.md +437 -0
  2. package/cjs/classes/api-service.class.js +71 -8
  3. package/cjs/classes/api-service.class.spec.js +220 -0
  4. package/cjs/classes/disabled-event-bus.class.js +15 -0
  5. package/cjs/classes/disabled-event-bus.class.spec.js +40 -0
  6. package/cjs/classes/domain-event.publisher.js +76 -0
  7. package/cjs/classes/domain-event.publisher.spec.js +234 -0
  8. package/cjs/classes/event-bus.registry.js +69 -0
  9. package/cjs/classes/event-bus.registry.spec.js +116 -0
  10. package/cjs/classes/event-transports/event-transport.registry.js +41 -0
  11. package/cjs/classes/event-transports/event-transport.registry.spec.js +31 -0
  12. package/cjs/classes/event-transports/index.js +23 -0
  13. package/cjs/classes/event-transports/kafka-transport.optional.js +151 -0
  14. package/cjs/classes/event-transports/kafka-transport.optional.spec.js +199 -0
  15. package/cjs/classes/event-transports/memory-transport.js +54 -0
  16. package/cjs/classes/event-transports/memory-transport.spec.js +58 -0
  17. package/cjs/classes/event-transports/rabbitmq-transport.optional.js +184 -0
  18. package/cjs/classes/event-transports/rabbitmq-transport.optional.spec.js +256 -0
  19. package/cjs/classes/hybrid-event-bus.class.js +261 -0
  20. package/cjs/classes/hybrid-event-bus.class.spec.js +189 -0
  21. package/cjs/classes/index.js +5 -0
  22. package/cjs/constants/index.js +24 -0
  23. package/cjs/decorators/index.js +1 -0
  24. package/cjs/decorators/on-domain-event.decorator.js +13 -0
  25. package/cjs/decorators/on-domain-event.decorator.spec.js +50 -0
  26. package/cjs/entities/user-root.js +2 -2
  27. package/cjs/enums/event-action.enum.js +18 -0
  28. package/cjs/enums/index.js +1 -0
  29. package/cjs/interfaces/event-bus.interface.js +4 -0
  30. package/cjs/interfaces/index.js +1 -0
  31. package/cjs/modules/event-bus/event-bus.explorer.js +124 -0
  32. package/cjs/modules/event-bus/event-bus.explorer.spec.js +189 -0
  33. package/cjs/modules/event-bus/event-bus.integration.spec.js +254 -0
  34. package/cjs/modules/event-bus/event-bus.module.js +94 -0
  35. package/cjs/modules/event-bus/event-bus.module.spec.js +149 -0
  36. package/cjs/modules/event-bus/module-events.provider.js +39 -0
  37. package/cjs/modules/event-bus/module-events.provider.spec.js +56 -0
  38. package/cjs/modules/index.js +3 -0
  39. package/cjs/utils/event-pattern.util.js +98 -0
  40. package/cjs/utils/event-pattern.util.spec.js +234 -0
  41. package/cjs/utils/index.js +2 -0
  42. package/cjs/utils/optional-module.util.js +35 -0
  43. package/cjs/utils/optional-module.util.spec.js +14 -0
  44. package/classes/api-service.class.d.ts +11 -1
  45. package/classes/disabled-event-bus.class.d.ts +6 -0
  46. package/classes/domain-event.publisher.d.ts +14 -0
  47. package/classes/event-bus.registry.d.ts +14 -0
  48. package/classes/event-transports/event-transport.registry.d.ts +10 -0
  49. package/classes/event-transports/index.d.ts +2 -0
  50. package/classes/event-transports/kafka-transport.optional.d.ts +21 -0
  51. package/classes/event-transports/memory-transport.d.ts +13 -0
  52. package/classes/event-transports/rabbitmq-transport.optional.d.ts +27 -0
  53. package/classes/hybrid-event-bus.class.d.ts +32 -0
  54. package/classes/index.d.ts +5 -0
  55. package/constants/index.d.ts +6 -0
  56. package/decorators/index.d.ts +1 -0
  57. package/decorators/on-domain-event.decorator.d.ts +1 -0
  58. package/enums/event-action.enum.d.ts +7 -0
  59. package/enums/index.d.ts +1 -0
  60. package/fesm/classes/api-service.class.js +71 -8
  61. package/fesm/classes/api-service.class.spec.js +220 -0
  62. package/fesm/classes/disabled-event-bus.class.js +11 -0
  63. package/fesm/classes/disabled-event-bus.class.spec.js +36 -0
  64. package/fesm/classes/domain-event.publisher.js +63 -0
  65. package/fesm/classes/domain-event.publisher.spec.js +230 -0
  66. package/fesm/classes/event-bus.registry.js +65 -0
  67. package/fesm/classes/event-bus.registry.spec.js +112 -0
  68. package/fesm/classes/event-transports/event-transport.registry.js +35 -0
  69. package/fesm/classes/event-transports/event-transport.registry.spec.js +27 -0
  70. package/fesm/classes/event-transports/index.js +6 -0
  71. package/fesm/classes/event-transports/kafka-transport.optional.js +144 -0
  72. package/fesm/classes/event-transports/kafka-transport.optional.spec.js +195 -0
  73. package/fesm/classes/event-transports/memory-transport.js +46 -0
  74. package/fesm/classes/event-transports/memory-transport.spec.js +54 -0
  75. package/fesm/classes/event-transports/rabbitmq-transport.optional.js +174 -0
  76. package/fesm/classes/event-transports/rabbitmq-transport.optional.spec.js +252 -0
  77. package/fesm/classes/hybrid-event-bus.class.js +217 -0
  78. package/fesm/classes/hybrid-event-bus.class.spec.js +185 -0
  79. package/fesm/classes/index.js +5 -0
  80. package/fesm/constants/index.js +7 -0
  81. package/fesm/decorators/index.js +1 -0
  82. package/fesm/decorators/on-domain-event.decorator.js +10 -0
  83. package/fesm/decorators/on-domain-event.decorator.spec.js +46 -0
  84. package/fesm/entities/user-root.js +1 -1
  85. package/fesm/enums/event-action.enum.js +8 -0
  86. package/fesm/enums/index.js +1 -0
  87. package/fesm/interfaces/event-bus.interface.js +1 -0
  88. package/fesm/interfaces/index.js +1 -0
  89. package/fesm/modules/event-bus/event-bus.explorer.js +114 -0
  90. package/fesm/modules/event-bus/event-bus.explorer.spec.js +185 -0
  91. package/fesm/modules/event-bus/event-bus.integration.spec.js +250 -0
  92. package/fesm/modules/event-bus/event-bus.module.js +84 -0
  93. package/fesm/modules/event-bus/event-bus.module.spec.js +145 -0
  94. package/fesm/modules/event-bus/module-events.provider.js +26 -0
  95. package/fesm/modules/event-bus/module-events.provider.spec.js +52 -0
  96. package/fesm/modules/index.js +3 -0
  97. package/fesm/utils/event-pattern.util.js +86 -0
  98. package/fesm/utils/event-pattern.util.spec.js +230 -0
  99. package/fesm/utils/index.js +2 -0
  100. package/fesm/utils/optional-module.util.js +25 -0
  101. package/fesm/utils/optional-module.util.spec.js +10 -0
  102. package/interfaces/event-bus.interface.d.ts +74 -0
  103. package/interfaces/index.d.ts +1 -0
  104. package/modules/event-bus/event-bus.explorer.d.ts +14 -0
  105. package/modules/event-bus/event-bus.module.d.ts +8 -0
  106. package/modules/event-bus/module-events.provider.d.ts +3 -0
  107. package/modules/index.d.ts +3 -0
  108. package/package.json +2 -2
  109. package/utils/event-pattern.util.d.ts +3 -0
  110. package/utils/index.d.ts +2 -0
  111. package/utils/optional-module.util.d.ts +1 -0
@@ -0,0 +1,6 @@
1
+ import { DomainEventHandler, IDomainEvent, IEventBus } from '../interfaces/event-bus.interface';
2
+ export declare class DisabledEventBus implements IEventBus {
3
+ publish<T>(_event: IDomainEvent<T>): Promise<void>;
4
+ subscribe<T>(_pattern: string, _handler: DomainEventHandler<T>): Promise<void>;
5
+ close(): Promise<void>;
6
+ }
@@ -0,0 +1,14 @@
1
+ import { DomainEventActionName, IDomainEvent, IDomainEventPayload, IEventBus, ILoggedUserInfo } from '../interfaces';
2
+ export declare const DOMAIN_EVENT_VERSION = 1;
3
+ export interface IPublishDomainEventInput<T = Record<string, unknown>> {
4
+ module: string;
5
+ entity: string;
6
+ action: DomainEventActionName;
7
+ ids?: string[];
8
+ data?: T | T[];
9
+ metadata?: Record<string, unknown>;
10
+ user?: ILoggedUserInfo | null;
11
+ eventBus?: IEventBus;
12
+ }
13
+ export declare function buildDomainEvent<T>(input: IPublishDomainEventInput<T>): IDomainEvent<IDomainEventPayload<T>>;
14
+ export declare function publishDomainEvent<T>(input: IPublishDomainEventInput<T>): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import { DomainEventActionName, IEventBus, IModuleEventsConfig } from '../interfaces/event-bus.interface';
2
+ export declare class EventBusRegistry {
3
+ private static bus?;
4
+ private static moduleConfigs;
5
+ private static defaultConfig;
6
+ static setBus(bus: IEventBus): void;
7
+ static getBus(): IEventBus | undefined;
8
+ static setDefaultModuleEvents(config: IModuleEventsConfig): void;
9
+ static configureModule(moduleName: string, config?: IModuleEventsConfig): void;
10
+ static getModuleConfig(moduleName?: string): IModuleEventsConfig;
11
+ static isEnabled(moduleName: string | undefined, entity: string, action: DomainEventActionName): boolean;
12
+ static shouldIncludePayload(moduleName?: string): boolean;
13
+ static reset(): void;
14
+ }
@@ -0,0 +1,10 @@
1
+ import { IEventTransport } from '../../interfaces/event-bus.interface';
2
+ export type EventTransportFactory = (config: Record<string, unknown>) => IEventTransport;
3
+ export declare class EventTransportRegistry {
4
+ private static transports;
5
+ static register(name: string, factory: EventTransportFactory): void;
6
+ static get(name: string): EventTransportFactory | undefined;
7
+ static has(name: string): boolean;
8
+ static getAll(): string[];
9
+ static clear(): void;
10
+ }
@@ -0,0 +1,2 @@
1
+ export * from './event-transport.registry';
2
+ export * from './memory-transport';
@@ -0,0 +1,21 @@
1
+ import { DomainEventHandler, IDomainEvent, IEventTransport, IKafkaTransportConfig } from '../../interfaces/event-bus.interface';
2
+ export declare class KafkaTransport implements IEventTransport {
3
+ private readonly config;
4
+ readonly name = "kafka";
5
+ private readonly logger;
6
+ private readonly handlers;
7
+ private readonly boundPatterns;
8
+ private client;
9
+ private producer;
10
+ private consumer;
11
+ private running;
12
+ constructor(config?: IKafkaTransportConfig);
13
+ connect(): Promise<void>;
14
+ publish(event: IDomainEvent): Promise<void>;
15
+ bind(pattern: string): Promise<void>;
16
+ onMessage(handler: DomainEventHandler): void;
17
+ close(): Promise<void>;
18
+ isReady(): boolean;
19
+ private get topic();
20
+ private startConsuming;
21
+ }
@@ -0,0 +1,13 @@
1
+ import { DomainEventHandler, IDomainEvent, IEventTransport } from '../../interfaces/event-bus.interface';
2
+ export declare class MemoryTransport implements IEventTransport {
3
+ readonly name = "memory";
4
+ private readonly emitter;
5
+ private ready;
6
+ constructor(maxListeners?: number);
7
+ connect(): Promise<void>;
8
+ publish(event: IDomainEvent): Promise<void>;
9
+ bind(_pattern: string): Promise<void>;
10
+ onMessage(handler: DomainEventHandler): void;
11
+ close(): Promise<void>;
12
+ isReady(): boolean;
13
+ }
@@ -0,0 +1,27 @@
1
+ import { DomainEventHandler, IDomainEvent, IEventTransport, IRabbitMqTransportConfig } from '../../interfaces/event-bus.interface';
2
+ export declare class RabbitMqTransport implements IEventTransport {
3
+ private readonly config;
4
+ readonly name = "rabbitmq";
5
+ private readonly logger;
6
+ private readonly handlers;
7
+ private readonly boundPatterns;
8
+ private connection;
9
+ private channel;
10
+ private consuming;
11
+ private closed;
12
+ private reconnectTimer?;
13
+ constructor(config?: IRabbitMqTransportConfig);
14
+ connect(): Promise<void>;
15
+ publish(event: IDomainEvent): Promise<void>;
16
+ bind(pattern: string): Promise<void>;
17
+ onMessage(handler: DomainEventHandler): void;
18
+ close(): Promise<void>;
19
+ isReady(): boolean;
20
+ private get exchange();
21
+ private get queue();
22
+ private get durable();
23
+ private toRoutingKey;
24
+ private startConsuming;
25
+ private scheduleReconnect;
26
+ private teardown;
27
+ }
@@ -0,0 +1,32 @@
1
+ import { DomainEventHandler, EventTransportLabel, IDomainEvent, IEventBus, IEventBusModuleOptions } from '../interfaces/event-bus.interface';
2
+ export declare class HybridEventBus implements IEventBus {
3
+ private readonly options;
4
+ private readonly logger;
5
+ private readonly subscriptions;
6
+ private readonly transports;
7
+ private readonly seenIds;
8
+ private readonly seenOrder;
9
+ private readonly label;
10
+ private readonly dedupeSize;
11
+ private memory?;
12
+ private connecting?;
13
+ private connected;
14
+ constructor(options?: IEventBusModuleOptions);
15
+ getTransportLabel(): EventTransportLabel;
16
+ getActiveTransports(): string[];
17
+ connect(): Promise<void>;
18
+ publish<T>(event: IDomainEvent<T>): Promise<void>;
19
+ subscribe<T>(pattern: string, handler: DomainEventHandler<T>): Promise<void>;
20
+ close(): Promise<void>;
21
+ private ensureConnected;
22
+ private resolveLabelFromEnv;
23
+ private buildTransports;
24
+ private createBrokerTransport;
25
+ private resolveHybridBrokerName;
26
+ private loadOptionalTransport;
27
+ private getTransportConfig;
28
+ private parseBrokers;
29
+ private registerTransport;
30
+ private dispatch;
31
+ private isDuplicate;
32
+ }
@@ -1,6 +1,11 @@
1
1
  export * from './api-controller.class';
2
2
  export * from './api-service.class';
3
+ export * from './disabled-event-bus.class';
4
+ export * from './domain-event.publisher';
5
+ export * from './event-bus.registry';
6
+ export * from './event-transports';
3
7
  export * from './handle-custome-db';
4
8
  export * from './hybrid-cache.class';
9
+ export * from './hybrid-event-bus.class';
5
10
  export * from './winston.logger.class';
6
11
  export * from '../constants/permissions';
@@ -1,11 +1,17 @@
1
1
  export declare const IS_PUBLIC_KEY = "isPublic";
2
2
  export declare const PERMISSIONS_KEY = "permissions";
3
+ export declare const DOMAIN_EVENT_HANDLER_KEY = "domainEventHandler";
3
4
  export declare const CACHE_INSTANCE = "CACHE_INSTANCE";
4
5
  export declare const PERMISSION_GUARD_CONFIG = "PERMISSION_GUARD_CONFIG";
6
+ export declare const EVENT_BUS_INSTANCE = "EVENT_BUS_INSTANCE";
7
+ export declare const EVENT_BUS_MODULE_OPTIONS = "EVENT_BUS_MODULE_OPTIONS";
5
8
  export declare const IDEMPOTENCY_KEY_HEADER = "x-idempotency-key";
6
9
  export declare const REQUEST_ID_HEADER = "x-request-id";
7
10
  export declare const CLIENT_TYPE_HEADER = "x-client-type";
8
11
  export declare const PERMISSIONS_CACHE_PREFIX = "my-permissions";
9
12
  export declare const IDEMPOTENCY_CACHE_PREFIX = "idempotency";
13
+ export declare const DEFAULT_EVENT_EXCHANGE = "flusys.events";
14
+ export declare const DEFAULT_EVENT_TOPIC = "flusys.domain-events";
15
+ export declare const DEFAULT_EVENT_SERVICE_NAME = "flusys";
10
16
  export * from './permissions';
11
17
  export * from './message-keys';
@@ -1,5 +1,6 @@
1
1
  export * from './api-response.decorator';
2
2
  export * from './current-user.decorator';
3
3
  export * from './log-action.decorator';
4
+ export * from './on-domain-event.decorator';
4
5
  export * from './public.decorator';
5
6
  export * from './require-permission.decorator';
@@ -0,0 +1 @@
1
+ export declare const OnDomainEvent: (...patterns: string[]) => MethodDecorator;
@@ -0,0 +1,7 @@
1
+ export declare enum EventAction {
2
+ CREATED = "created",
3
+ UPDATED = "updated",
4
+ DELETED = "deleted",
5
+ RESTORED = "restored",
6
+ PURGED = "purged"
7
+ }
package/enums/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './event-action.enum';
1
2
  export * from './notification-type.enum';
2
3
  export * from './participant-status.enum';
3
4
  export * from './recurrence-type.enum';
@@ -25,9 +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';
28
29
  import { IDataSourceProvider } from '../interfaces';
29
30
  import { UtilsService } from '../modules/utils/utils.service';
31
+ import { publishDomainEvent } from './domain-event.publisher';
30
32
  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
+ const DELETE_EVENT_ACTIONS = {
42
+ delete: EventAction.DELETED,
43
+ restore: EventAction.RESTORED
44
+ };
31
45
  export class ApiService {
32
46
  resolveEntity() {
33
47
  return this._entity;
@@ -64,7 +78,7 @@ export class ApiService {
64
78
  saved,
65
79
  returnFirst: true
66
80
  };
67
- });
81
+ }, user);
68
82
  }
69
83
  async insertMany(dtos, user) {
70
84
  return this.executeInTransaction('insertMany', async (qr)=>{
@@ -76,7 +90,7 @@ export class ApiService {
76
90
  saved,
77
91
  returnFirst: false
78
92
  };
79
- });
93
+ }, user);
80
94
  }
81
95
  async update(dto, user) {
82
96
  return this.executeInTransaction('update', async (qr)=>{
@@ -88,7 +102,7 @@ export class ApiService {
88
102
  saved,
89
103
  returnFirst: true
90
104
  };
91
- });
105
+ }, user);
92
106
  }
93
107
  async updateMany(dtos, user) {
94
108
  return this.executeInTransaction('updateMany', async (qr)=>{
@@ -100,17 +114,22 @@ export class ApiService {
100
114
  saved,
101
115
  returnFirst: false
102
116
  };
103
- });
117
+ }, user);
104
118
  }
105
119
  async bulkUpsert(toInsert, toUpdate, user) {
106
120
  return this.executeInTransaction('bulkUpsert', async (qr)=>{
107
121
  const allResults = [];
122
+ const eventGroups = [];
108
123
  if (toInsert.length > 0) {
109
124
  await this.beforeInsertOperation(toInsert, user, qr);
110
125
  const insertedEntities = await this.convertRequestDtoToEntity(toInsert, user);
111
126
  const insertedSaved = await qr.manager.save(this.repository.target, insertedEntities);
112
127
  await this.afterInsertOperation(this.ensureArray(insertedSaved), toInsert, user, qr);
113
128
  allResults.push(...this.ensureArray(insertedSaved));
129
+ eventGroups.push({
130
+ action: EventAction.CREATED,
131
+ entities: this.ensureArray(insertedSaved)
132
+ });
114
133
  }
115
134
  if (toUpdate.length > 0) {
116
135
  await this.beforeUpdateOperation(toUpdate, user, qr);
@@ -118,12 +137,17 @@ export class ApiService {
118
137
  const updatedSaved = await qr.manager.save(this.repository.target, updatedEntities);
119
138
  await this.afterUpdateOperation(this.ensureArray(updatedSaved), toUpdate, user, qr);
120
139
  allResults.push(...this.ensureArray(updatedSaved));
140
+ eventGroups.push({
141
+ action: EventAction.UPDATED,
142
+ entities: this.ensureArray(updatedSaved)
143
+ });
121
144
  }
122
145
  return {
123
146
  saved: allResults,
124
- returnFirst: false
147
+ returnFirst: false,
148
+ eventGroups
125
149
  };
126
- });
150
+ }, user);
127
151
  }
128
152
  async findByIds(ids, user, select) {
129
153
  await this.ensureDataSourceRepository();
@@ -325,6 +349,13 @@ export class ApiService {
325
349
  this.clearCacheForAll(),
326
350
  this.clearCacheForId(ids)
327
351
  ]);
352
+ await this.publishLifecycleEvents([
353
+ {
354
+ action: DELETE_EVENT_ACTIONS[option.type] ?? EventAction.PURGED,
355
+ entities: ids,
356
+ idsOnly: true
357
+ }
358
+ ], user);
328
359
  return null;
329
360
  } catch (error) {
330
361
  await queryRunner.rollbackTransaction();
@@ -333,6 +364,32 @@ export class ApiService {
333
364
  await queryRunner.release();
334
365
  }
335
366
  }
367
+ async publishLifecycleEvents(groups, user) {
368
+ for (const group of groups){
369
+ if (!group.entities.length) continue;
370
+ await publishDomainEvent({
371
+ module: this.moduleName ?? DEFAULT_EVENT_MODULE,
372
+ entity: this.entityName,
373
+ action: group.action,
374
+ ids: group.entities.map((entity)=>entity.id),
375
+ ...group.idsOnly ? {} : {
376
+ data: group.entities
377
+ },
378
+ user
379
+ });
380
+ }
381
+ }
382
+ /**
383
+ * Publishes a domain action for this service's entity - anything past plain
384
+ * CRUD, such as a submission or a state transition.
385
+ */ async publishDomainAction(action, input = {}) {
386
+ await publishDomainEvent({
387
+ ...input,
388
+ module: this.moduleName ?? DEFAULT_EVENT_MODULE,
389
+ entity: this.entityName,
390
+ action
391
+ });
392
+ }
336
393
  async clearCacheForAll() {
337
394
  await this.utilsService.clearCache(this.entityName, this.cacheManager);
338
395
  }
@@ -353,13 +410,13 @@ export class ApiService {
353
410
  value
354
411
  ];
355
412
  }
356
- async executeInTransaction(operation, fn) {
413
+ async executeInTransaction(operation, fn, user) {
357
414
  await this.ensureDataSourceRepository();
358
415
  const qr = this.repository.manager.connection.createQueryRunner();
359
416
  await qr.connect();
360
417
  await qr.startTransaction();
361
418
  try {
362
- const { saved, returnFirst } = await fn(qr);
419
+ const { saved, returnFirst, eventGroups } = await fn(qr);
363
420
  await qr.commitTransaction();
364
421
  const savedArray = this.ensureArray(saved);
365
422
  if (this.isCacheable) {
@@ -368,6 +425,12 @@ export class ApiService {
368
425
  this.clearCacheForId(savedArray)
369
426
  ]);
370
427
  }
428
+ await this.publishLifecycleEvents(eventGroups ?? [
429
+ {
430
+ action: OPERATION_EVENT_ACTIONS[operation],
431
+ entities: savedArray
432
+ }
433
+ ], user ?? null);
371
434
  if (returnFirst) {
372
435
  return this.convertEntityToResponseDto(savedArray[0], false);
373
436
  }
@@ -15,6 +15,8 @@ import { Logger, InternalServerErrorException, NotFoundException } from '@nestjs
15
15
  import { createMockRepository, createMockDataSourceProvider } from '@test-utils/mocks/repository.mock';
16
16
  import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
17
17
  import { ApiService } from './api-service.class';
18
+ import { EventBusRegistry } from './event-bus.registry';
19
+ import { EventAction } from '../enums/event-action.enum';
18
20
  import { UtilsService } from '../modules/utils/utils.service';
19
21
  import { Identity } from '../entities';
20
22
  let TestEntity = class TestEntity extends Identity {
@@ -802,4 +804,222 @@ describe('ApiService', ()=>{
802
804
  expect(clearSpy).toHaveBeenCalledWith('test-entity', mockCache, 'b');
803
805
  });
804
806
  });
807
+ describe('domain events', ()=>{
808
+ let publish;
809
+ function makeEventService(moduleName = 'test-module') {
810
+ const dataSourceProvider = createMockDataSourceProvider(mockRepo);
811
+ return new TestApiService('test-entity', mockCache, utilsService, 'TestApiService', false, moduleName, TestEntity, dataSourceProvider);
812
+ }
813
+ beforeEach(()=>{
814
+ publish = jest.fn().mockResolvedValue(undefined);
815
+ EventBusRegistry.reset();
816
+ EventBusRegistry.setBus({
817
+ publish,
818
+ subscribe: jest.fn(),
819
+ close: jest.fn()
820
+ });
821
+ EventBusRegistry.configureModule('test-module', {
822
+ enabled: true
823
+ });
824
+ });
825
+ afterEach(()=>{
826
+ EventBusRegistry.reset();
827
+ });
828
+ it('should publish a created event after insert commits', async ()=>{
829
+ const service = makeEventService();
830
+ mockQueryRunner.manager.save.mockResolvedValue({
831
+ id: 'new-1',
832
+ name: 'Widget'
833
+ });
834
+ await service.insert({
835
+ name: 'Widget'
836
+ }, mockUser);
837
+ expect(publish).toHaveBeenCalledTimes(1);
838
+ expect(publish).toHaveBeenCalledWith(expect.objectContaining({
839
+ name: 'test-module.test-entity.created',
840
+ module: 'test-module',
841
+ entity: 'test-entity',
842
+ action: EventAction.CREATED,
843
+ payload: expect.objectContaining({
844
+ ids: [
845
+ 'new-1'
846
+ ]
847
+ })
848
+ }));
849
+ });
850
+ it('should carry the acting user on the envelope', async ()=>{
851
+ const service = makeEventService();
852
+ mockQueryRunner.manager.save.mockResolvedValue({
853
+ id: 'new-1',
854
+ name: 'Widget'
855
+ });
856
+ await service.insert({
857
+ name: 'Widget'
858
+ }, mockUser);
859
+ expect(publish.mock.calls[0][0].actor).toEqual(expect.objectContaining({
860
+ userId: mockUser.id
861
+ }));
862
+ });
863
+ it('should publish an updated event after update commits', async ()=>{
864
+ const service = makeEventService();
865
+ mockRepo.findOne.mockResolvedValue({
866
+ id: '1',
867
+ name: 'Old'
868
+ });
869
+ mockQueryRunner.manager.save.mockResolvedValue({
870
+ id: '1',
871
+ name: 'New'
872
+ });
873
+ await service.update({
874
+ id: '1',
875
+ name: 'New'
876
+ }, mockUser);
877
+ expect(publish.mock.calls[0][0]).toEqual(expect.objectContaining({
878
+ action: EventAction.UPDATED
879
+ }));
880
+ });
881
+ it('should publish created and updated separately for a bulk upsert', async ()=>{
882
+ const service = makeEventService();
883
+ mockRepo.findOne.mockResolvedValue({
884
+ id: '1',
885
+ name: 'Old'
886
+ });
887
+ mockQueryRunner.manager.save.mockResolvedValueOnce([
888
+ {
889
+ id: 'new-1',
890
+ name: 'A'
891
+ }
892
+ ]).mockResolvedValueOnce([
893
+ {
894
+ id: '1',
895
+ name: 'B'
896
+ }
897
+ ]);
898
+ await service.bulkUpsert([
899
+ {
900
+ name: 'A'
901
+ }
902
+ ], [
903
+ {
904
+ id: '1',
905
+ name: 'B'
906
+ }
907
+ ], mockUser);
908
+ expect(publish.mock.calls.map(([event])=>event.action)).toEqual([
909
+ EventAction.CREATED,
910
+ EventAction.UPDATED
911
+ ]);
912
+ });
913
+ it.each([
914
+ [
915
+ 'delete',
916
+ EventAction.DELETED
917
+ ],
918
+ [
919
+ 'restore',
920
+ EventAction.RESTORED
921
+ ],
922
+ [
923
+ 'permanent',
924
+ EventAction.PURGED
925
+ ]
926
+ ])('should publish %s as the %s action', async (type, action)=>{
927
+ const service = makeEventService();
928
+ await service.delete({
929
+ id: '1',
930
+ type
931
+ }, mockUser);
932
+ expect(publish.mock.calls[0][0]).toEqual(expect.objectContaining({
933
+ action
934
+ }));
935
+ });
936
+ it('should carry only the ids for a delete, with no row data to duplicate them', async ()=>{
937
+ const service = makeEventService();
938
+ await service.delete({
939
+ id: '1',
940
+ type: 'delete'
941
+ }, mockUser);
942
+ expect(publish.mock.calls[0][0].payload).toEqual({
943
+ ids: [
944
+ '1'
945
+ ]
946
+ });
947
+ });
948
+ it('should not publish when the module has events disabled', async ()=>{
949
+ EventBusRegistry.configureModule('test-module', {
950
+ enabled: false
951
+ });
952
+ const service = makeEventService();
953
+ mockQueryRunner.manager.save.mockResolvedValue({
954
+ id: 'new-1',
955
+ name: 'Widget'
956
+ });
957
+ await service.insert({
958
+ name: 'Widget'
959
+ }, mockUser);
960
+ expect(publish).not.toHaveBeenCalled();
961
+ });
962
+ it('should not publish for an action the module excluded', async ()=>{
963
+ EventBusRegistry.configureModule('test-module', {
964
+ enabled: true,
965
+ actions: [
966
+ EventAction.DELETED
967
+ ]
968
+ });
969
+ const service = makeEventService();
970
+ mockQueryRunner.manager.save.mockResolvedValue({
971
+ id: 'new-1',
972
+ name: 'Widget'
973
+ });
974
+ await service.insert({
975
+ name: 'Widget'
976
+ }, mockUser);
977
+ expect(publish).not.toHaveBeenCalled();
978
+ });
979
+ it('should omit the row data when the module opts out of payloads', async ()=>{
980
+ EventBusRegistry.configureModule('test-module', {
981
+ enabled: true,
982
+ includePayload: false
983
+ });
984
+ const service = makeEventService();
985
+ mockQueryRunner.manager.save.mockResolvedValue({
986
+ id: 'new-1',
987
+ name: 'Widget'
988
+ });
989
+ await service.insert({
990
+ name: 'Widget'
991
+ }, mockUser);
992
+ expect(publish.mock.calls[0][0].payload).toEqual({
993
+ ids: [
994
+ 'new-1'
995
+ ]
996
+ });
997
+ });
998
+ it('should not fail the write when publishing throws', async ()=>{
999
+ publish.mockRejectedValue(new Error('broker down'));
1000
+ const service = makeEventService();
1001
+ mockQueryRunner.manager.save.mockResolvedValue({
1002
+ id: 'new-1',
1003
+ name: 'Widget'
1004
+ });
1005
+ await expect(service.insert({
1006
+ name: 'Widget'
1007
+ }, mockUser)).resolves.toEqual({
1008
+ id: 'new-1',
1009
+ name: 'Widget'
1010
+ });
1011
+ });
1012
+ it('should do nothing when no bus is registered', async ()=>{
1013
+ EventBusRegistry.reset();
1014
+ const service = makeEventService();
1015
+ mockQueryRunner.manager.save.mockResolvedValue({
1016
+ id: 'new-1',
1017
+ name: 'Widget'
1018
+ });
1019
+ await expect(service.insert({
1020
+ name: 'Widget'
1021
+ }, mockUser)).resolves.toBeDefined();
1022
+ expect(publish).not.toHaveBeenCalled();
1023
+ });
1024
+ });
805
1025
  });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Stands in for HybridEventBus when the subsystem is switched off.
3
+ *
4
+ * Injecting `EVENT_BUS_INSTANCE` keeps working, so turning events off cannot
5
+ * break a provider's dependencies, but nothing here can ever open a broker
6
+ * connection or run a handler.
7
+ */ export class DisabledEventBus {
8
+ async publish(_event) {}
9
+ async subscribe(_pattern, _handler) {}
10
+ async close() {}
11
+ }
@@ -0,0 +1,36 @@
1
+ import { DisabledEventBus } from './disabled-event-bus.class';
2
+ describe('DisabledEventBus', ()=>{
3
+ const event = {
4
+ id: 'e1',
5
+ name: 'auth.session.logged-in',
6
+ module: 'auth',
7
+ entity: 'session',
8
+ action: 'logged-in',
9
+ payload: {
10
+ ids: [
11
+ 'u1'
12
+ ]
13
+ },
14
+ actor: {
15
+ userId: 'u1',
16
+ companyId: null,
17
+ tenantId: null,
18
+ requestId: null
19
+ },
20
+ occurredAt: new Date().toISOString(),
21
+ version: 1
22
+ };
23
+ it('should never deliver an event to a subscriber', async ()=>{
24
+ const bus = new DisabledEventBus();
25
+ const handler = jest.fn();
26
+ await bus.subscribe('**', handler);
27
+ await bus.publish(event);
28
+ expect(handler).not.toHaveBeenCalled();
29
+ });
30
+ it('should resolve without throwing so callers need no special case', async ()=>{
31
+ const bus = new DisabledEventBus();
32
+ await expect(bus.publish(event)).resolves.toBeUndefined();
33
+ await expect(bus.subscribe('**', jest.fn())).resolves.toBeUndefined();
34
+ await expect(bus.close()).resolves.toBeUndefined();
35
+ });
36
+ });