@flusys/nestjs-shared 6.4.1 → 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.
- package/README.md +437 -0
- package/cjs/classes/api-service.class.js +71 -8
- package/cjs/classes/api-service.class.spec.js +220 -0
- package/cjs/classes/disabled-event-bus.class.js +15 -0
- package/cjs/classes/disabled-event-bus.class.spec.js +40 -0
- package/cjs/classes/domain-event.publisher.js +76 -0
- package/cjs/classes/domain-event.publisher.spec.js +234 -0
- package/cjs/classes/event-bus.registry.js +69 -0
- package/cjs/classes/event-bus.registry.spec.js +116 -0
- package/cjs/classes/event-transports/event-transport.registry.js +41 -0
- package/cjs/classes/event-transports/event-transport.registry.spec.js +31 -0
- package/cjs/classes/event-transports/index.js +23 -0
- package/cjs/classes/event-transports/kafka-transport.optional.js +151 -0
- package/cjs/classes/event-transports/kafka-transport.optional.spec.js +199 -0
- package/cjs/classes/event-transports/memory-transport.js +54 -0
- package/cjs/classes/event-transports/memory-transport.spec.js +58 -0
- package/cjs/classes/event-transports/rabbitmq-transport.optional.js +184 -0
- package/cjs/classes/event-transports/rabbitmq-transport.optional.spec.js +256 -0
- package/cjs/classes/hybrid-event-bus.class.js +261 -0
- package/cjs/classes/hybrid-event-bus.class.spec.js +189 -0
- package/cjs/classes/index.js +5 -0
- package/cjs/constants/index.js +24 -0
- package/cjs/decorators/index.js +1 -0
- package/cjs/decorators/on-domain-event.decorator.js +13 -0
- package/cjs/decorators/on-domain-event.decorator.spec.js +50 -0
- package/cjs/entities/user-root.js +2 -2
- package/cjs/enums/event-action.enum.js +18 -0
- package/cjs/enums/index.js +1 -0
- package/cjs/interfaces/event-bus.interface.js +4 -0
- package/cjs/interfaces/index.js +1 -0
- package/cjs/modules/event-bus/event-bus.explorer.js +124 -0
- package/cjs/modules/event-bus/event-bus.explorer.spec.js +189 -0
- package/cjs/modules/event-bus/event-bus.integration.spec.js +254 -0
- package/cjs/modules/event-bus/event-bus.module.js +94 -0
- package/cjs/modules/event-bus/event-bus.module.spec.js +149 -0
- package/cjs/modules/event-bus/module-events.provider.js +39 -0
- package/cjs/modules/event-bus/module-events.provider.spec.js +56 -0
- package/cjs/modules/index.js +3 -0
- package/cjs/utils/event-pattern.util.js +98 -0
- package/cjs/utils/event-pattern.util.spec.js +234 -0
- package/cjs/utils/index.js +2 -0
- package/cjs/utils/optional-module.util.js +35 -0
- package/cjs/utils/optional-module.util.spec.js +14 -0
- package/classes/api-service.class.d.ts +11 -1
- package/classes/disabled-event-bus.class.d.ts +6 -0
- package/classes/domain-event.publisher.d.ts +14 -0
- package/classes/event-bus.registry.d.ts +14 -0
- package/classes/event-transports/event-transport.registry.d.ts +10 -0
- package/classes/event-transports/index.d.ts +2 -0
- package/classes/event-transports/kafka-transport.optional.d.ts +21 -0
- package/classes/event-transports/memory-transport.d.ts +13 -0
- package/classes/event-transports/rabbitmq-transport.optional.d.ts +27 -0
- package/classes/hybrid-event-bus.class.d.ts +32 -0
- package/classes/index.d.ts +5 -0
- package/constants/index.d.ts +6 -0
- package/decorators/index.d.ts +1 -0
- package/decorators/on-domain-event.decorator.d.ts +1 -0
- package/enums/event-action.enum.d.ts +7 -0
- package/enums/index.d.ts +1 -0
- package/fesm/classes/api-service.class.js +71 -8
- package/fesm/classes/api-service.class.spec.js +220 -0
- package/fesm/classes/disabled-event-bus.class.js +11 -0
- package/fesm/classes/disabled-event-bus.class.spec.js +36 -0
- package/fesm/classes/domain-event.publisher.js +63 -0
- package/fesm/classes/domain-event.publisher.spec.js +230 -0
- package/fesm/classes/event-bus.registry.js +65 -0
- package/fesm/classes/event-bus.registry.spec.js +112 -0
- package/fesm/classes/event-transports/event-transport.registry.js +35 -0
- package/fesm/classes/event-transports/event-transport.registry.spec.js +27 -0
- package/fesm/classes/event-transports/index.js +6 -0
- package/fesm/classes/event-transports/kafka-transport.optional.js +144 -0
- package/fesm/classes/event-transports/kafka-transport.optional.spec.js +195 -0
- package/fesm/classes/event-transports/memory-transport.js +46 -0
- package/fesm/classes/event-transports/memory-transport.spec.js +54 -0
- package/fesm/classes/event-transports/rabbitmq-transport.optional.js +174 -0
- package/fesm/classes/event-transports/rabbitmq-transport.optional.spec.js +252 -0
- package/fesm/classes/hybrid-event-bus.class.js +217 -0
- package/fesm/classes/hybrid-event-bus.class.spec.js +185 -0
- package/fesm/classes/index.js +5 -0
- package/fesm/constants/index.js +7 -0
- package/fesm/decorators/index.js +1 -0
- package/fesm/decorators/on-domain-event.decorator.js +10 -0
- package/fesm/decorators/on-domain-event.decorator.spec.js +46 -0
- package/fesm/entities/user-root.js +1 -1
- package/fesm/enums/event-action.enum.js +8 -0
- package/fesm/enums/index.js +1 -0
- package/fesm/interfaces/event-bus.interface.js +1 -0
- package/fesm/interfaces/index.js +1 -0
- package/fesm/modules/event-bus/event-bus.explorer.js +114 -0
- package/fesm/modules/event-bus/event-bus.explorer.spec.js +185 -0
- package/fesm/modules/event-bus/event-bus.integration.spec.js +250 -0
- package/fesm/modules/event-bus/event-bus.module.js +84 -0
- package/fesm/modules/event-bus/event-bus.module.spec.js +145 -0
- package/fesm/modules/event-bus/module-events.provider.js +26 -0
- package/fesm/modules/event-bus/module-events.provider.spec.js +52 -0
- package/fesm/modules/index.js +3 -0
- package/fesm/utils/event-pattern.util.js +86 -0
- package/fesm/utils/event-pattern.util.spec.js +230 -0
- package/fesm/utils/index.js +2 -0
- package/fesm/utils/optional-module.util.js +25 -0
- package/fesm/utils/optional-module.util.spec.js +10 -0
- package/interfaces/event-bus.interface.d.ts +74 -0
- package/interfaces/index.d.ts +1 -0
- package/modules/event-bus/event-bus.explorer.d.ts +14 -0
- package/modules/event-bus/event-bus.module.d.ts +8 -0
- package/modules/event-bus/module-events.provider.d.ts +3 -0
- package/modules/index.d.ts +3 -0
- package/package.json +2 -2
- package/utils/event-pattern.util.d.ts +3 -0
- package/utils/index.d.ts +2 -0
- 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,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
|
+
}
|
package/classes/index.d.ts
CHANGED
|
@@ -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';
|
package/constants/index.d.ts
CHANGED
|
@@ -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';
|
package/decorators/index.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const OnDomainEvent: (...patterns: string[]) => MethodDecorator;
|
package/enums/index.d.ts
CHANGED
|
@@ -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
|
+
});
|