@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
package/README.md
CHANGED
|
@@ -232,6 +232,443 @@ export class MyService {
|
|
|
232
232
|
|
|
233
233
|
`CacheModule.forRoot(true)` connects to Redis automatically when `REDIS_URL` is set; otherwise uses in-memory only.
|
|
234
234
|
|
|
235
|
+
## 8. Hybrid Event Bus
|
|
236
|
+
|
|
237
|
+
The event counterpart of the hybrid cache: one wrapper, transport chosen by configuration.
|
|
238
|
+
|
|
239
|
+
| `USE_EVENT_LABEL` | Behaviour | Extra install |
|
|
240
|
+
| ----------------- | ------------------------------------------------------ | ---------------- |
|
|
241
|
+
| `memory` | In-process only (default) | none |
|
|
242
|
+
| `rabbitmq` | Topic exchange, cross-service | `npm i amqplib` |
|
|
243
|
+
| `kafka` | Single topic, cross-service | `npm i kafkajs` |
|
|
244
|
+
| `hybrid` | Both layers, duplicate delivery suppressed by event id | broker of choice |
|
|
245
|
+
|
|
246
|
+
### Registering
|
|
247
|
+
|
|
248
|
+
`EventBusModule` is optional - register it only in an app that actually publishes or
|
|
249
|
+
consumes events.
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
import { EventBusModule } from '@flusys/nestjs-shared/modules';
|
|
253
|
+
|
|
254
|
+
@Module({
|
|
255
|
+
imports: [EventBusModule.forRoot({ serviceName: 'flusysnest' })],
|
|
256
|
+
})
|
|
257
|
+
export class AppModule {}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Leave it out and nothing breaks: no package injects the bus as a hard dependency, and
|
|
261
|
+
`publishDomainEvent` - including the calls `ApiService` makes for you - finds no bus and
|
|
262
|
+
returns without doing anything. There is no placeholder registration to add just to keep
|
|
263
|
+
DI happy. `forRoot({ enabled: false })` is for the other case: the app supports events but
|
|
264
|
+
this deployment has them switched off, and you want that stated at startup.
|
|
265
|
+
|
|
266
|
+
The one thing an unregistered app gives up is consumption - `@OnDomainEvent` methods are
|
|
267
|
+
bound at bootstrap by an explorer that ships inside the module, so without it handlers
|
|
268
|
+
never run and nothing warns about it.
|
|
269
|
+
|
|
270
|
+
### Running against a real broker
|
|
271
|
+
|
|
272
|
+
Both brokers need their driver installed - `npm i amqplib` or `npm i kafkajs`. The
|
|
273
|
+
transports load it at runtime, so the package builds and boots without either.
|
|
274
|
+
|
|
275
|
+
- **One delivery per service, not per replica.** RabbitMQ binds a single queue named
|
|
276
|
+
`<serviceName>.events` and Kafka joins one consumer group named `<serviceName>-events`,
|
|
277
|
+
so two replicas of the same service compete for each event and only one of them handles
|
|
278
|
+
it. Give a service that must see every event on every replica its own `queue` / `groupId`.
|
|
279
|
+
- **A publisher consumes its own events.** In `rabbitmq` and `kafka` mode the local
|
|
280
|
+
handlers run off the broker round trip, which is what makes a single code path work
|
|
281
|
+
across services. In `hybrid` mode the in-process copy arrives first and the broker copy
|
|
282
|
+
is dropped by event id, so a handler still runs exactly once.
|
|
283
|
+
- **RabbitMQ reconnects on its own.** A dropped connection *and* a channel killed by a
|
|
284
|
+
protocol error both schedule one reconnect (`reconnectDelay`, 5s by default), which
|
|
285
|
+
re-binds every pattern onto the fresh channel. While it is down, publishes are dropped
|
|
286
|
+
rather than queued - events must never fail the operation that produced them.
|
|
287
|
+
- **Kafka's consumer is built on the first bind**, not at connect. A missing topic or an
|
|
288
|
+
unavailable group coordinator therefore fails consumption alone and leaves the producer
|
|
289
|
+
working, instead of taking the whole transport down to in-process delivery.
|
|
290
|
+
|
|
291
|
+
### Publishing
|
|
292
|
+
|
|
293
|
+
Every service extending `ApiService` publishes automatically once its module enables
|
|
294
|
+
events — no per-service code. Turn it on in the module's own config:
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
StorageModule.forRoot({
|
|
298
|
+
config: {
|
|
299
|
+
...dbConfig,
|
|
300
|
+
events: { enabled: true, includePayload: true },
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
#### Every `events` option
|
|
306
|
+
|
|
307
|
+
| Option | Type | Default | Effect |
|
|
308
|
+
| ----------------- | --------------------------- | ------- | ---------------------------------------------------------------------- |
|
|
309
|
+
| `enabled` | `boolean` | `false` | Master switch for the module. Everything else is ignored when off. |
|
|
310
|
+
| `actions` | `(EventAction \| string)[]` | all | Allowlist of action names. Omit to publish every action. |
|
|
311
|
+
| `entities` | `string[]` | all | Allowlist of entity names. Omit to publish every entity. |
|
|
312
|
+
| `excludeEntities` | `string[]` | `[]` | Denylist, applied before `entities`. |
|
|
313
|
+
| `includePayload` | `boolean` | `true` | Attach the saved rows as `payload.data`. Ids and metadata always ride. |
|
|
314
|
+
|
|
315
|
+
Three rules that decide what actually reaches a consumer:
|
|
316
|
+
|
|
317
|
+
1. `actions` is **one allowlist covering CRUD and domain actions together**. Listing only
|
|
318
|
+
`['logged-in']` silences that module's `created`/`updated`/`deleted` too - name
|
|
319
|
+
everything you want kept, or omit `actions` entirely.
|
|
320
|
+
2. `entities` matches the entity **in the event name**, which for a domain action is not
|
|
321
|
+
always the table you expect: `auth.session.logged-in` is filtered by `session`, not
|
|
322
|
+
`appuser`.
|
|
323
|
+
3. Entity names are the ones the services register, so some are `snake_case`
|
|
324
|
+
(`form_result`, `file_manager`) and some are `camelCase` (`storageConfig`,
|
|
325
|
+
`emailConfig`, `translationKey`). The per-module tables below list them verbatim.
|
|
326
|
+
|
|
327
|
+
Event names are always `<module>.<entity>.<action>`. CRUD lifecycle actions are
|
|
328
|
+
`created`, `updated`, `deleted`, `restored`, `purged`. Events are published after the
|
|
329
|
+
transaction commits and never fail the write that produced them.
|
|
330
|
+
|
|
331
|
+
The module's own `events` block is the single authority on whether it publishes: passing
|
|
332
|
+
an `eventBus` explicitly chooses _which_ bus to publish on, it never overrides the switch,
|
|
333
|
+
so `enabled: false` silences the module everywhere. `delete` and `restore` carry ids only -
|
|
334
|
+
there is no row left to attach.
|
|
335
|
+
|
|
336
|
+
Payloads are walked before they leave the process: credential-like fields become
|
|
337
|
+
`[REDACTED]`, a relation that loops back becomes `[CIRCULAR]`, anything past 8 levels deep
|
|
338
|
+
becomes `[TRUNCATED]`, and a Buffer becomes `[BINARY]`. Saved TypeORM rows with
|
|
339
|
+
bidirectional relations are therefore safe to publish.
|
|
340
|
+
|
|
341
|
+
### Registering each module
|
|
342
|
+
|
|
343
|
+
Every feature module takes the same `events` block in its own config, and every one of
|
|
344
|
+
them also has a `forRootAsync` variant that returns the identical `config`. The blocks
|
|
345
|
+
below spell out all options with that module's real entity and action names, so each is
|
|
346
|
+
copy-pasteable. `EventAction` comes from `@flusys/nestjs-shared/enums`; the action and
|
|
347
|
+
entity constants come from each package's own `config` entry point.
|
|
348
|
+
|
|
349
|
+
In an application, keep the per-module blocks in one map rather than scattering them
|
|
350
|
+
across registrations - `flusysnest/src/config/modules.config.ts` is the reference:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
const domainEventsEnabled = envConfig.getBoolean('ENABLE_DOMAIN_EVENTS', false);
|
|
354
|
+
|
|
355
|
+
export const moduleEventsConfig: Record<FeatureModuleName, IModuleEventsConfig> = {
|
|
356
|
+
storage: {
|
|
357
|
+
enabled: domainEventsEnabled,
|
|
358
|
+
actions: [...CRUD_EVENT_ACTIONS, STORAGE_EVENT_ACTIONS.UPLOADED, STORAGE_EVENT_ACTIONS.REMOVED],
|
|
359
|
+
entities: [
|
|
360
|
+
STORAGE_EVENT_ENTITIES.FILE,
|
|
361
|
+
STORAGE_EVENT_ENTITIES.FOLDER,
|
|
362
|
+
STORAGE_EVENT_ENTITIES.STORAGE_CONFIG,
|
|
363
|
+
],
|
|
364
|
+
excludeEntities: [STORAGE_EVENT_ENTITIES.STORAGE_CONFIG],
|
|
365
|
+
includePayload: true,
|
|
366
|
+
},
|
|
367
|
+
// ...one entry per feature module
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
export function getBaseModuleConfig(moduleName: FeatureModuleName) {
|
|
371
|
+
return { ...getDbConfig(), events: moduleEventsConfig[moduleName] };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
AuthModule.forRoot({ config: { ...authConfig, ...getBaseModuleConfig('auth') } });
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Spelling `actions` and `entities` out in full is the same as omitting them, and makes the
|
|
378
|
+
file a readable inventory of what each module can publish - delete a line to stop
|
|
379
|
+
publishing it. The trade-off is that a package gaining a new entity or action stays silent
|
|
380
|
+
until it is added here, so keep a test asserting these lists cover each package's own
|
|
381
|
+
`*_EVENT_ACTIONS` / `*_EVENT_ENTITIES` constants. One env flag still arms every module, but
|
|
382
|
+
each keeps its own block, so narrowing one module never touches the others. Note the two `excludeEntities` entries that ship by
|
|
383
|
+
default: `storageConfig` and `emailConfig` rows hold S3, Azure, SFTP and SMTP credentials
|
|
384
|
+
in a `config` blob, so they stay off the bus entirely.
|
|
385
|
+
|
|
386
|
+
#### auth
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
import { AUTH_EVENT_ACTIONS, AUTH_EVENT_ENTITIES } from '@flusys/nestjs-auth/config';
|
|
390
|
+
|
|
391
|
+
AuthModule.forRoot({
|
|
392
|
+
config: {
|
|
393
|
+
...dbConfig,
|
|
394
|
+
events: {
|
|
395
|
+
enabled: true,
|
|
396
|
+
actions: [EventAction.CREATED, AUTH_EVENT_ACTIONS.LOGGED_IN],
|
|
397
|
+
entities: [AUTH_EVENT_ENTITIES.USER, AUTH_EVENT_ENTITIES.SESSION],
|
|
398
|
+
excludeEntities: ['company_branch'],
|
|
399
|
+
includePayload: false,
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
| Entity | Actions it can publish |
|
|
406
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
407
|
+
| `appuser` | CRUD + `registered`, `password-changed`, `password-reset-requested`, `password-reset`, `email-verified` |
|
|
408
|
+
| `company` | CRUD |
|
|
409
|
+
| `company_branch` | CRUD |
|
|
410
|
+
| `session` | `logged-in`, `logged-out`, `token-refreshed`, `company-switched` |
|
|
411
|
+
| `user_company_permission` | `permission-granted`, `permission-revoked` |
|
|
412
|
+
|
|
413
|
+
#### iam
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
import { IAM_EVENT_ACTIONS, IAM_EVENT_ENTITIES } from '@flusys/nestjs-iam/config';
|
|
417
|
+
|
|
418
|
+
IAMModule.forRoot({
|
|
419
|
+
config: {
|
|
420
|
+
...dbConfig,
|
|
421
|
+
events: {
|
|
422
|
+
enabled: true,
|
|
423
|
+
actions: [EventAction.CREATED, EventAction.DELETED, IAM_EVENT_ACTIONS.PERMISSIONS_ASSIGNED],
|
|
424
|
+
entities: ['role', IAM_EVENT_ENTITIES.USER_ROLE],
|
|
425
|
+
excludeEntities: ['action'],
|
|
426
|
+
includePayload: true,
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
| Entity | Actions it can publish |
|
|
433
|
+
| ----------------------------------------------------------- | ---------------------- |
|
|
434
|
+
| `action`, `role` | CRUD |
|
|
435
|
+
| `user_action`, `role_action`, `company_action`, `user_role` | `permissions-assigned` |
|
|
436
|
+
|
|
437
|
+
#### storage
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
import { STORAGE_EVENT_ACTIONS, STORAGE_EVENT_ENTITIES } from '@flusys/nestjs-storage/config';
|
|
441
|
+
|
|
442
|
+
StorageModule.forRoot({
|
|
443
|
+
config: {
|
|
444
|
+
...dbConfig,
|
|
445
|
+
events: {
|
|
446
|
+
enabled: true,
|
|
447
|
+
actions: [EventAction.CREATED, EventAction.DELETED, STORAGE_EVENT_ACTIONS.UPLOADED],
|
|
448
|
+
entities: [STORAGE_EVENT_ENTITIES.FILE, 'folder'],
|
|
449
|
+
excludeEntities: ['storageConfig'],
|
|
450
|
+
includePayload: true,
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
| Entity | Actions it can publish |
|
|
457
|
+
| --------------- | ---------------------------- |
|
|
458
|
+
| `file_manager` | CRUD + `uploaded`, `removed` |
|
|
459
|
+
| `folder` | CRUD |
|
|
460
|
+
| `storageConfig` | CRUD |
|
|
461
|
+
|
|
462
|
+
Uploads and removals report provider keys in `payload.ids`, with `location`,
|
|
463
|
+
`storageConfigId`, `totalSize` and `count` in `payload.metadata`.
|
|
464
|
+
|
|
465
|
+
#### form-builder
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
import { FORM_BUILDER_EVENT_ACTIONS } from '@flusys/nestjs-form-builder/config';
|
|
469
|
+
|
|
470
|
+
FormBuilderModule.forRoot({
|
|
471
|
+
config: {
|
|
472
|
+
...dbConfig,
|
|
473
|
+
events: {
|
|
474
|
+
enabled: true,
|
|
475
|
+
actions: [FORM_BUILDER_EVENT_ACTIONS.SUBMITTED],
|
|
476
|
+
entities: ['form_result'],
|
|
477
|
+
excludeEntities: [],
|
|
478
|
+
includePayload: false,
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
| Entity | Actions it can publish |
|
|
485
|
+
| ------------- | --------------------------------- |
|
|
486
|
+
| `form` | CRUD |
|
|
487
|
+
| `form_result` | CRUD + `submitted`, `draft-saved` |
|
|
488
|
+
|
|
489
|
+
Submissions carry `formId`, `isDraft` and `schemaVersion` in `payload.metadata`, so an
|
|
490
|
+
audit consumer can work with `includePayload: false` and never see answer data.
|
|
491
|
+
|
|
492
|
+
#### email
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
import { EMAIL_EVENT_ACTIONS, EMAIL_EVENT_ENTITIES } from '@flusys/nestjs-email/config';
|
|
496
|
+
|
|
497
|
+
EmailModule.forRoot({
|
|
498
|
+
config: {
|
|
499
|
+
...dbConfig,
|
|
500
|
+
events: {
|
|
501
|
+
enabled: true,
|
|
502
|
+
actions: [EMAIL_EVENT_ACTIONS.SENT, EMAIL_EVENT_ACTIONS.FAILED],
|
|
503
|
+
entities: [EMAIL_EVENT_ENTITIES.MESSAGE],
|
|
504
|
+
excludeEntities: ['emailConfig'],
|
|
505
|
+
includePayload: false,
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
| Entity | Actions it can publish |
|
|
512
|
+
| --------------- | ---------------------- |
|
|
513
|
+
| `emailConfig` | CRUD |
|
|
514
|
+
| `emailTemplate` | CRUD |
|
|
515
|
+
| `email_message` | `sent`, `failed` |
|
|
516
|
+
|
|
517
|
+
#### event-manager
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
import { EVENT_MANAGER_EVENT_ACTIONS } from '@flusys/nestjs-event-manager/config';
|
|
521
|
+
|
|
522
|
+
EventManagerModule.forRoot({
|
|
523
|
+
config: {
|
|
524
|
+
...dbConfig,
|
|
525
|
+
events: {
|
|
526
|
+
enabled: true,
|
|
527
|
+
actions: [EventAction.CREATED, EVENT_MANAGER_EVENT_ACTIONS.PARTICIPANT_STATUS_CHANGED],
|
|
528
|
+
entities: ['event'],
|
|
529
|
+
excludeEntities: [],
|
|
530
|
+
includePayload: true,
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
| Entity | Actions it can publish |
|
|
537
|
+
| ------- | ----------------------------------- |
|
|
538
|
+
| `event` | CRUD + `participant-status-changed` |
|
|
539
|
+
|
|
540
|
+
The participant event is keyed by the **event** id, with `participantId`,
|
|
541
|
+
`participantUserId` and `status` in `payload.metadata`.
|
|
542
|
+
|
|
543
|
+
#### notification
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
import {
|
|
547
|
+
NOTIFICATION_EVENT_ACTIONS,
|
|
548
|
+
NOTIFICATION_EVENT_ENTITIES,
|
|
549
|
+
} from '@flusys/nestjs-notification/config';
|
|
550
|
+
|
|
551
|
+
NotificationModule.forRoot({
|
|
552
|
+
config: {
|
|
553
|
+
...dbConfig,
|
|
554
|
+
enableRealtime: true,
|
|
555
|
+
events: {
|
|
556
|
+
enabled: true,
|
|
557
|
+
actions: [NOTIFICATION_EVENT_ACTIONS.SENT, NOTIFICATION_EVENT_ACTIONS.READ],
|
|
558
|
+
entities: [NOTIFICATION_EVENT_ENTITIES.NOTIFICATION],
|
|
559
|
+
excludeEntities: [],
|
|
560
|
+
includePayload: false,
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
| Entity | Actions it can publish |
|
|
567
|
+
| -------------- | --------------------------------- |
|
|
568
|
+
| `notification` | CRUD + `sent`, `read`, `all-read` |
|
|
569
|
+
|
|
570
|
+
Subscribing to `notification.**` while the notification module itself reacts to other
|
|
571
|
+
modules' events is the usual way to fan a domain event out to users.
|
|
572
|
+
|
|
573
|
+
#### localization
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
import { LOCALIZATION_EVENT_ACTIONS } from '@flusys/nestjs-localization/config';
|
|
577
|
+
|
|
578
|
+
LocalizationModule.forRoot({
|
|
579
|
+
config: {
|
|
580
|
+
...dbConfig,
|
|
581
|
+
defaultLanguageCode: 'en',
|
|
582
|
+
events: {
|
|
583
|
+
enabled: true,
|
|
584
|
+
actions: [LOCALIZATION_EVENT_ACTIONS.BULK_UPDATED],
|
|
585
|
+
entities: ['translation'],
|
|
586
|
+
excludeEntities: ['translationKey'],
|
|
587
|
+
includePayload: false,
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
| Entity | Actions it can publish |
|
|
594
|
+
| ---------------- | ---------------------- |
|
|
595
|
+
| `language` | CRUD |
|
|
596
|
+
| `translationKey` | CRUD |
|
|
597
|
+
| `translation` | CRUD + `bulk-updated` |
|
|
598
|
+
|
|
599
|
+
`bulk-updated` carries every written id plus `count` in `payload.metadata`, and is not
|
|
600
|
+
published when there was nothing to write - useful for cache busting a translation CDN.
|
|
601
|
+
|
|
602
|
+
#### task-manager
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
import { TASK_MANAGER_EVENT_ACTIONS } from '@flusys/nestjs-task-manager/config';
|
|
606
|
+
|
|
607
|
+
TaskManagerModule.forRoot({
|
|
608
|
+
config: {
|
|
609
|
+
...dbConfig,
|
|
610
|
+
events: {
|
|
611
|
+
enabled: true,
|
|
612
|
+
actions: [TASK_MANAGER_EVENT_ACTIONS.MOVED, TASK_MANAGER_EVENT_ACTIONS.ASSIGNED],
|
|
613
|
+
entities: ['task', 'task_list'],
|
|
614
|
+
excludeEntities: ['task_comment'],
|
|
615
|
+
includePayload: true,
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
| Entity | Actions it can publish |
|
|
622
|
+
| ------------------------------------------ | -------------------------- |
|
|
623
|
+
| `task` | CRUD + `moved`, `assigned` |
|
|
624
|
+
| `task_list` | CRUD + `reordered` |
|
|
625
|
+
| `task_board`, `task_label`, `task_comment` | CRUD |
|
|
626
|
+
|
|
627
|
+
`moved` carries `fromListId`/`toListId`; `assigned` carries the assignee ids. Domain
|
|
628
|
+
events carry a `payload.metadata` descriptor that is attached even when `includePayload`
|
|
629
|
+
is off.
|
|
630
|
+
|
|
631
|
+
### Publishing your own
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
import { publishDomainEvent } from '@flusys/nestjs-shared/classes';
|
|
635
|
+
|
|
636
|
+
await publishDomainEvent({
|
|
637
|
+
module: 'billing',
|
|
638
|
+
entity: 'invoice',
|
|
639
|
+
action: 'settled',
|
|
640
|
+
ids: [invoice.id],
|
|
641
|
+
metadata: { amount: invoice.total },
|
|
642
|
+
user,
|
|
643
|
+
});
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
Inside a service that already extends `ApiService`, call
|
|
647
|
+
`this.publishDomainAction(action, { ... })` - it fills in the module and entity for you.
|
|
648
|
+
|
|
649
|
+
### Consuming
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
import { OnDomainEvent } from '@flusys/nestjs-shared/decorators';
|
|
653
|
+
import { DomainEvent } from '@flusys/nestjs-shared/interfaces';
|
|
654
|
+
|
|
655
|
+
@Injectable()
|
|
656
|
+
export class AuditConsumer {
|
|
657
|
+
@OnDomainEvent('storage.*.created', 'auth.appuser.deleted')
|
|
658
|
+
async handle(event: DomainEvent) {
|
|
659
|
+
// event.payload.ids / .data / .metadata, event.actor, event.action
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
`DomainEvent` is the standard envelope shape - it covers both CRUD lifecycle and domain
|
|
665
|
+
actions, so one type fits every handler. `*` matches exactly one segment; `**` (or `#`)
|
|
666
|
+
matches the rest. Handlers are discovered at bootstrap on any singleton provider or
|
|
667
|
+
controller registered with Nest - a REQUEST-scoped provider has no instance at that point,
|
|
668
|
+
so put consumers on singletons. Injecting `EVENT_BUS_INSTANCE` gives direct
|
|
669
|
+
`publish` / `subscribe` access - mark it `@Optional()`, so the service still resolves in an
|
|
670
|
+
app that never registers `EventBusModule`.
|
|
671
|
+
|
|
235
672
|
## License
|
|
236
673
|
|
|
237
674
|
MIT © FLUSYS
|
|
@@ -13,8 +13,10 @@ const _typeorm = require("typeorm");
|
|
|
13
13
|
const _constants = require("../constants");
|
|
14
14
|
const _logactiondecorator = require("../decorators/log-action.decorator");
|
|
15
15
|
const _dtos = require("../dtos");
|
|
16
|
+
const _eventactionenum = require("../enums/event-action.enum");
|
|
16
17
|
const _interfaces = require("../interfaces");
|
|
17
18
|
const _utilsservice = require("../modules/utils/utils.service");
|
|
19
|
+
const _domaineventpublisher = require("./domain-event.publisher");
|
|
18
20
|
const _hybridcacheclass = require("./hybrid-cache.class");
|
|
19
21
|
function _define_property(obj, key, value) {
|
|
20
22
|
if (key in obj) {
|
|
@@ -38,6 +40,18 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
38
40
|
function _ts_metadata(k, v) {
|
|
39
41
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
40
42
|
}
|
|
43
|
+
const DEFAULT_EVENT_MODULE = 'app';
|
|
44
|
+
const OPERATION_EVENT_ACTIONS = {
|
|
45
|
+
insert: _eventactionenum.EventAction.CREATED,
|
|
46
|
+
insertMany: _eventactionenum.EventAction.CREATED,
|
|
47
|
+
update: _eventactionenum.EventAction.UPDATED,
|
|
48
|
+
updateMany: _eventactionenum.EventAction.UPDATED,
|
|
49
|
+
bulkUpsert: _eventactionenum.EventAction.UPDATED
|
|
50
|
+
};
|
|
51
|
+
const DELETE_EVENT_ACTIONS = {
|
|
52
|
+
delete: _eventactionenum.EventAction.DELETED,
|
|
53
|
+
restore: _eventactionenum.EventAction.RESTORED
|
|
54
|
+
};
|
|
41
55
|
let ApiService = class ApiService {
|
|
42
56
|
resolveEntity() {
|
|
43
57
|
return this._entity;
|
|
@@ -74,7 +88,7 @@ let ApiService = class ApiService {
|
|
|
74
88
|
saved,
|
|
75
89
|
returnFirst: true
|
|
76
90
|
};
|
|
77
|
-
});
|
|
91
|
+
}, user);
|
|
78
92
|
}
|
|
79
93
|
async insertMany(dtos, user) {
|
|
80
94
|
return this.executeInTransaction('insertMany', async (qr)=>{
|
|
@@ -86,7 +100,7 @@ let ApiService = class ApiService {
|
|
|
86
100
|
saved,
|
|
87
101
|
returnFirst: false
|
|
88
102
|
};
|
|
89
|
-
});
|
|
103
|
+
}, user);
|
|
90
104
|
}
|
|
91
105
|
async update(dto, user) {
|
|
92
106
|
return this.executeInTransaction('update', async (qr)=>{
|
|
@@ -98,7 +112,7 @@ let ApiService = class ApiService {
|
|
|
98
112
|
saved,
|
|
99
113
|
returnFirst: true
|
|
100
114
|
};
|
|
101
|
-
});
|
|
115
|
+
}, user);
|
|
102
116
|
}
|
|
103
117
|
async updateMany(dtos, user) {
|
|
104
118
|
return this.executeInTransaction('updateMany', async (qr)=>{
|
|
@@ -110,17 +124,22 @@ let ApiService = class ApiService {
|
|
|
110
124
|
saved,
|
|
111
125
|
returnFirst: false
|
|
112
126
|
};
|
|
113
|
-
});
|
|
127
|
+
}, user);
|
|
114
128
|
}
|
|
115
129
|
async bulkUpsert(toInsert, toUpdate, user) {
|
|
116
130
|
return this.executeInTransaction('bulkUpsert', async (qr)=>{
|
|
117
131
|
const allResults = [];
|
|
132
|
+
const eventGroups = [];
|
|
118
133
|
if (toInsert.length > 0) {
|
|
119
134
|
await this.beforeInsertOperation(toInsert, user, qr);
|
|
120
135
|
const insertedEntities = await this.convertRequestDtoToEntity(toInsert, user);
|
|
121
136
|
const insertedSaved = await qr.manager.save(this.repository.target, insertedEntities);
|
|
122
137
|
await this.afterInsertOperation(this.ensureArray(insertedSaved), toInsert, user, qr);
|
|
123
138
|
allResults.push(...this.ensureArray(insertedSaved));
|
|
139
|
+
eventGroups.push({
|
|
140
|
+
action: _eventactionenum.EventAction.CREATED,
|
|
141
|
+
entities: this.ensureArray(insertedSaved)
|
|
142
|
+
});
|
|
124
143
|
}
|
|
125
144
|
if (toUpdate.length > 0) {
|
|
126
145
|
await this.beforeUpdateOperation(toUpdate, user, qr);
|
|
@@ -128,12 +147,17 @@ let ApiService = class ApiService {
|
|
|
128
147
|
const updatedSaved = await qr.manager.save(this.repository.target, updatedEntities);
|
|
129
148
|
await this.afterUpdateOperation(this.ensureArray(updatedSaved), toUpdate, user, qr);
|
|
130
149
|
allResults.push(...this.ensureArray(updatedSaved));
|
|
150
|
+
eventGroups.push({
|
|
151
|
+
action: _eventactionenum.EventAction.UPDATED,
|
|
152
|
+
entities: this.ensureArray(updatedSaved)
|
|
153
|
+
});
|
|
131
154
|
}
|
|
132
155
|
return {
|
|
133
156
|
saved: allResults,
|
|
134
|
-
returnFirst: false
|
|
157
|
+
returnFirst: false,
|
|
158
|
+
eventGroups
|
|
135
159
|
};
|
|
136
|
-
});
|
|
160
|
+
}, user);
|
|
137
161
|
}
|
|
138
162
|
async findByIds(ids, user, select) {
|
|
139
163
|
await this.ensureDataSourceRepository();
|
|
@@ -335,6 +359,13 @@ let ApiService = class ApiService {
|
|
|
335
359
|
this.clearCacheForAll(),
|
|
336
360
|
this.clearCacheForId(ids)
|
|
337
361
|
]);
|
|
362
|
+
await this.publishLifecycleEvents([
|
|
363
|
+
{
|
|
364
|
+
action: DELETE_EVENT_ACTIONS[option.type] ?? _eventactionenum.EventAction.PURGED,
|
|
365
|
+
entities: ids,
|
|
366
|
+
idsOnly: true
|
|
367
|
+
}
|
|
368
|
+
], user);
|
|
338
369
|
return null;
|
|
339
370
|
} catch (error) {
|
|
340
371
|
await queryRunner.rollbackTransaction();
|
|
@@ -343,6 +374,32 @@ let ApiService = class ApiService {
|
|
|
343
374
|
await queryRunner.release();
|
|
344
375
|
}
|
|
345
376
|
}
|
|
377
|
+
async publishLifecycleEvents(groups, user) {
|
|
378
|
+
for (const group of groups){
|
|
379
|
+
if (!group.entities.length) continue;
|
|
380
|
+
await (0, _domaineventpublisher.publishDomainEvent)({
|
|
381
|
+
module: this.moduleName ?? DEFAULT_EVENT_MODULE,
|
|
382
|
+
entity: this.entityName,
|
|
383
|
+
action: group.action,
|
|
384
|
+
ids: group.entities.map((entity)=>entity.id),
|
|
385
|
+
...group.idsOnly ? {} : {
|
|
386
|
+
data: group.entities
|
|
387
|
+
},
|
|
388
|
+
user
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Publishes a domain action for this service's entity - anything past plain
|
|
394
|
+
* CRUD, such as a submission or a state transition.
|
|
395
|
+
*/ async publishDomainAction(action, input = {}) {
|
|
396
|
+
await (0, _domaineventpublisher.publishDomainEvent)({
|
|
397
|
+
...input,
|
|
398
|
+
module: this.moduleName ?? DEFAULT_EVENT_MODULE,
|
|
399
|
+
entity: this.entityName,
|
|
400
|
+
action
|
|
401
|
+
});
|
|
402
|
+
}
|
|
346
403
|
async clearCacheForAll() {
|
|
347
404
|
await this.utilsService.clearCache(this.entityName, this.cacheManager);
|
|
348
405
|
}
|
|
@@ -363,13 +420,13 @@ let ApiService = class ApiService {
|
|
|
363
420
|
value
|
|
364
421
|
];
|
|
365
422
|
}
|
|
366
|
-
async executeInTransaction(operation, fn) {
|
|
423
|
+
async executeInTransaction(operation, fn, user) {
|
|
367
424
|
await this.ensureDataSourceRepository();
|
|
368
425
|
const qr = this.repository.manager.connection.createQueryRunner();
|
|
369
426
|
await qr.connect();
|
|
370
427
|
await qr.startTransaction();
|
|
371
428
|
try {
|
|
372
|
-
const { saved, returnFirst } = await fn(qr);
|
|
429
|
+
const { saved, returnFirst, eventGroups } = await fn(qr);
|
|
373
430
|
await qr.commitTransaction();
|
|
374
431
|
const savedArray = this.ensureArray(saved);
|
|
375
432
|
if (this.isCacheable) {
|
|
@@ -378,6 +435,12 @@ let ApiService = class ApiService {
|
|
|
378
435
|
this.clearCacheForId(savedArray)
|
|
379
436
|
]);
|
|
380
437
|
}
|
|
438
|
+
await this.publishLifecycleEvents(eventGroups ?? [
|
|
439
|
+
{
|
|
440
|
+
action: OPERATION_EVENT_ACTIONS[operation],
|
|
441
|
+
entities: savedArray
|
|
442
|
+
}
|
|
443
|
+
], user ?? null);
|
|
381
444
|
if (returnFirst) {
|
|
382
445
|
return this.convertEntityToResponseDto(savedArray[0], false);
|
|
383
446
|
}
|