@apso/cli 0.17.0 → 0.18.0

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.
@@ -939,12 +939,12 @@ state changes. A top-level \`emitEvents: true\` enables it for every entity; an
939
939
  individual entity can opt out with \`emitEvents: false\` (effective value is
940
940
  \`entity.emitEvents ?? <top-level emitEvents> ?? false\`).
941
941
 
942
- When at least one entity opts in, the generator emits a \`DomainEvent\` spine
943
- under \`autogen/events/\`: a \`DomainEvent\` entity (table \`events\`), a TypeORM
944
- \`DomainEventSubscriber\` that writes events in the SAME DB transaction as the
945
- change, an app-overridable \`DomainEventMapper\` (event-type + payload), and a
946
- \`DomainEventRelay\` whose \`publish()\` you override to deliver events. The
947
- \`DomainEventsModule\` is wired into the generated module list automatically.
942
+ When at least one entity opts in, the generator emits a single schema-derived
943
+ manifest at \`autogen/events/event-emitting.entities.ts\` exporting
944
+ \`EVENT_EMITTING_ENTITIES\` (the opted-in entity classes) and
945
+ \`EVENT_EMITTING_ENTITY_NAMES\`. The domain-event engine itself ships in the
946
+ \`@apso/domain-events\` library and is wired by the \`domain-events\` skill; the
947
+ CLI no longer generates the engine code.
948
948
 
949
949
  ## Relationship Types
950
950
  - OneToMany: parent has many children
@@ -2,9 +2,10 @@ import { Entity } from "./types";
2
2
  /**
3
3
  * Helpers for the opt-in DomainEvent ("emitEvents") feature.
4
4
  *
5
- * This implements the standard transactional-outbox pattern for durability, but
6
- * is surfaced with generic "domain event" naming (the user found "outbox"
7
- * unintuitive). No public artifact is named "outbox".
5
+ * Under the Apso Distribution Model the domain-event engine ships in the
6
+ * `@apso/domain-events` library (wired by the `domain-events` skill). The CLI
7
+ * only uses these helpers to compute which entities opted in, and emits a
8
+ * schema-derived manifest (`events/event-emitting.entities.ts`).
8
9
  *
9
10
  * Flag resolution: the effective per-entity value is
10
11
  * `entity.emitEvents ?? globalEmitEvents ?? false`
@@ -25,7 +26,7 @@ export declare function isEmitEventsEnabled(entity: Entity, globalEmitEvents?: b
25
26
  export declare function getEventEmittingEntities(entities: Entity[], globalEmitEvents?: boolean): Entity[];
26
27
  /**
27
28
  * Returns true when at least one entity has domain events enabled, meaning the
28
- * DomainEvent spine (entity, subscriber, mapper, relay, module) should be
29
+ * event-emitting manifest (`events/event-emitting.entities.ts`) should be
29
30
  * generated.
30
31
  */
31
32
  export declare function hasEventEmittingEntities(entities: Entity[], globalEmitEvents?: boolean): boolean;
@@ -4,9 +4,10 @@ exports.hasEventEmittingEntities = exports.getEventEmittingEntities = exports.is
4
4
  /**
5
5
  * Helpers for the opt-in DomainEvent ("emitEvents") feature.
6
6
  *
7
- * This implements the standard transactional-outbox pattern for durability, but
8
- * is surfaced with generic "domain event" naming (the user found "outbox"
9
- * unintuitive). No public artifact is named "outbox".
7
+ * Under the Apso Distribution Model the domain-event engine ships in the
8
+ * `@apso/domain-events` library (wired by the `domain-events` skill). The CLI
9
+ * only uses these helpers to compute which entities opted in, and emits a
10
+ * schema-derived manifest (`events/event-emitting.entities.ts`).
10
11
  *
11
12
  * Flag resolution: the effective per-entity value is
12
13
  * `entity.emitEvents ?? globalEmitEvents ?? false`
@@ -34,7 +35,7 @@ function getEventEmittingEntities(entities, globalEmitEvents) {
34
35
  exports.getEventEmittingEntities = getEventEmittingEntities;
35
36
  /**
36
37
  * Returns true when at least one entity has domain events enabled, meaning the
37
- * DomainEvent spine (entity, subscriber, mapper, relay, module) should be
38
+ * event-emitting manifest (`events/event-emitting.entities.ts`) should be
38
39
  * generated.
39
40
  */
40
41
  function hasEventEmittingEntities(entities, globalEmitEvents) {
@@ -65,8 +65,9 @@ export declare abstract class BaseGenerator implements LanguageGenerator {
65
65
  */
66
66
  generateQueryUtils(_entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
67
67
  /**
68
- * Generate the durable DomainEvent spine for entities that opt in to
69
- * `emitEvents` (transactional-outbox pattern, generic domain-event naming).
68
+ * Emit a schema-derived manifest of entities that opt in to `emitEvents`.
69
+ * The domain-event engine ships in the `@apso/domain-events` library (wired
70
+ * by the `domain-events` skill); the generator only declares participants.
70
71
  * Default no-op. Override in language generators that support it.
71
72
  */
72
73
  generateDomainEvents(_entities: Entity[], _apiType?: string, _opts?: {
@@ -28,8 +28,9 @@ class BaseGenerator {
28
28
  return [];
29
29
  }
30
30
  /**
31
- * Generate the durable DomainEvent spine for entities that opt in to
32
- * `emitEvents` (transactional-outbox pattern, generic domain-event naming).
31
+ * Emit a schema-derived manifest of entities that opt in to `emitEvents`.
32
+ * The domain-event engine ships in the `@apso/domain-events` library (wired
33
+ * by the `domain-events` skill); the generator only declares participants.
33
34
  * Default no-op. Override in language generators that support it.
34
35
  */
35
36
  async generateDomainEvents(_entities, _apiType, _opts) {
@@ -16,14 +16,19 @@ export declare class TypeScriptGenerator extends BaseGenerator {
16
16
  generateModule(options: EntityGenerationOptions): Promise<GeneratedFile[]>;
17
17
  generateMigration(_options: MigrationGenerationOptions): Promise<GeneratedFile[]>;
18
18
  generateEnums(entities: Entity[], apiType: string): Promise<GeneratedFile[]>;
19
- generateIndexModule(entities: Entity[], apiType: string, opts?: {
19
+ generateIndexModule(entities: Entity[], apiType: string, _opts?: {
20
20
  emitEvents?: boolean;
21
21
  }): Promise<GeneratedFile[]>;
22
22
  generateGuards(entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
23
23
  /**
24
- * Generates the durable DomainEvent spine (transactional-outbox pattern,
25
- * surfaced with generic domain-event naming) when at least one entity has
26
- * opted in to `emitEvents`.
24
+ * Emits a schema-derived manifest of the entities that have opted in to
25
+ * domain-event emission (`.apsorc` `emitEvents`).
26
+ *
27
+ * Under the Apso Distribution Model the domain-event engine no longer ships
28
+ * as generated code; it lives in the `@apso/domain-events` library and is
29
+ * wired by the `domain-events` skill. The CLI's only responsibility is to
30
+ * declare WHICH entities participate, via a single generated file:
31
+ * `events/event-emitting.entities.ts`.
27
32
  *
28
33
  * Returns an empty array when no entity is opted in, so callers can skip
29
34
  * wiring entirely.
@@ -303,9 +303,8 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
303
303
  },
304
304
  ];
305
305
  }
306
- async generateIndexModule(entities, apiType, opts) {
307
- const emitDomainEvents = (0, events_1.getEventEmittingEntities)(entities, opts === null || opts === void 0 ? void 0 : opts.emitEvents).length > 0;
308
- const content = await this.renderTemplate(`./${apiType}/index-module-${apiType}`, { entities, emitDomainEvents });
306
+ async generateIndexModule(entities, apiType, _opts) {
307
+ const content = await this.renderTemplate(`./${apiType}/index-module-${apiType}`, { entities });
309
308
  return [
310
309
  {
311
310
  path: "index.ts",
@@ -358,9 +357,14 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
358
357
  return files;
359
358
  }
360
359
  /**
361
- * Generates the durable DomainEvent spine (transactional-outbox pattern,
362
- * surfaced with generic domain-event naming) when at least one entity has
363
- * opted in to `emitEvents`.
360
+ * Emits a schema-derived manifest of the entities that have opted in to
361
+ * domain-event emission (`.apsorc` `emitEvents`).
362
+ *
363
+ * Under the Apso Distribution Model the domain-event engine no longer ships
364
+ * as generated code; it lives in the `@apso/domain-events` library and is
365
+ * wired by the `domain-events` skill. The CLI's only responsibility is to
366
+ * declare WHICH entities participate, via a single generated file:
367
+ * `events/event-emitting.entities.ts`.
364
368
  *
365
369
  * Returns an empty array when no entity is opted in, so callers can skip
366
370
  * wiring entirely.
@@ -374,46 +378,17 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
374
378
  if (emittingEntities.length === 0) {
375
379
  return [];
376
380
  }
377
- const templateData = {
381
+ const content = await this.renderTemplate("./events/event-emitting.entities.eta", {
378
382
  emittingEntities: emittingEntities.map((entity) => ({
379
383
  name: entity.name,
380
384
  })),
381
- generatedAt: new Date().toISOString(),
382
- generatedBy: "Apso CLI",
383
- };
384
- const files = [];
385
- const targets = [
386
- {
387
- template: "./events/domain-event.entity.eta",
388
- path: "events/domain-event.entity.ts",
389
- },
390
- {
391
- template: "./events/domain-event.mapper.eta",
392
- path: "events/domain-event.mapper.ts",
393
- },
394
- {
395
- template: "./events/domain-event.subscriber.eta",
396
- path: "events/domain-event.subscriber.ts",
397
- },
398
- {
399
- template: "./events/domain-event.relay.eta",
400
- path: "events/domain-event.relay.ts",
401
- },
402
- {
403
- template: "./events/domain-events.module.eta",
404
- path: "events/domain-events.module.ts",
405
- },
385
+ });
386
+ return [
406
387
  {
407
- template: "./events/index.eta",
408
- path: "events/index.ts",
388
+ path: "events/event-emitting.entities.ts",
389
+ content,
409
390
  },
410
391
  ];
411
- for (const target of targets) {
412
- // eslint-disable-next-line no-await-in-loop
413
- const content = await this.renderTemplate(target.template, templateData);
414
- files.push({ path: target.path, content });
415
- }
416
- return files;
417
392
  }
418
393
  /**
419
394
  * Normalizes auth config by applying defaults for DB session providers
@@ -0,0 +1,23 @@
1
+ <%~ includeFile('../header.eta') %>
2
+
3
+ <% it.emittingEntities.forEach((entity) => { %>
4
+ import { <%= entity.name %> } from '../<%= entity.name %>/<%= entity.name %>.entity';
5
+ <%}) %>
6
+
7
+ /**
8
+ * Schema-derived manifest of entities that opted in to domain-event emission
9
+ * (`.apsorc` `emitEvents`). The domain-event engine itself lives in the
10
+ * `@apso/domain-events` library and is wired by the `domain-events` skill;
11
+ * this file only declares WHICH entities participate.
12
+ */
13
+ export const EVENT_EMITTING_ENTITIES = [
14
+ <% it.emittingEntities.forEach((entity) => { %>
15
+ <%= entity.name %>,
16
+ <%}) %>
17
+ ];
18
+
19
+ export const EVENT_EMITTING_ENTITY_NAMES = [
20
+ <% it.emittingEntities.forEach((entity) => { %>
21
+ '<%= entity.name %>',
22
+ <%}) %>
23
+ ];
@@ -3,9 +3,6 @@
3
3
  <% it.entities.forEach((entity) => { %>
4
4
  import {<%= entity.name %>Module} from './<%= entity.name %>/<%= entity.name %>.module'
5
5
  <%}) %>
6
- <% if (it.emitDomainEvents) { %>
7
- import { DomainEventsModule } from './events'
8
- <% } %>
9
6
  import { JSONResolver } from 'graphql-scalars';
10
7
 
11
8
  <% const scalars = [] %>
@@ -26,7 +23,4 @@ export default [
26
23
  <% it.entities.forEach((entity) => { %>
27
24
  <%= entity.name %>Module,
28
25
  <%}) %>
29
- <% if (it.emitDomainEvents) { %>
30
- DomainEventsModule,
31
- <% } %>
32
26
  ]
@@ -3,15 +3,9 @@
3
3
  <% it.entities.forEach((entity) => { %>
4
4
  import {<%= entity.name %>Module} from './<%= entity.name %>/<%= entity.name %>.module'
5
5
  <%}) %>
6
- <% if (it.emitDomainEvents) { %>
7
- import { DomainEventsModule } from './events'
8
- <% } %>
9
6
 
10
7
  export default [
11
8
  <% it.entities.forEach((entity) => { %>
12
9
  <%= entity.name %>Module,
13
10
  <%}) %>
14
- <% if (it.emitDomainEvents) { %>
15
- DomainEventsModule,
16
- <% } %>
17
11
  ]
@@ -246,8 +246,8 @@ export interface LanguageGenerator {
246
246
  */
247
247
  generateQueryUtils(entities: Entity[], apiType: string): Promise<GeneratedFile[]>;
248
248
  /**
249
- * Generate the durable DomainEvent spine for entities that opt in to
250
- * `emitEvents`. Returns [] when no entity is opted in.
249
+ * Emit a schema-derived manifest of entities that opt in to `emitEvents`
250
+ * (engine lives in `@apso/domain-events`). Returns [] when none are opted in.
251
251
  */
252
252
  generateDomainEvents(entities: Entity[], apiType?: string, opts?: {
253
253
  emitEvents?: boolean;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@apso/cli",
9
- "version": "0.17.0",
9
+ "version": "0.18.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@electric-sql/pglite": "^0.2.17",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.17.0",
2
+ "version": "0.18.0",
3
3
  "commands": {
4
4
  "config": {
5
5
  "id": "config",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "mcpName": "io.github.apsoai/apso",
5
5
  "description": "Apso CLI",
6
6
  "author": "Apso by Mavric - @mavric",
@@ -1,51 +0,0 @@
1
- <%~ includeFile('../header.eta') %>
2
-
3
- import {
4
- Entity,
5
- PrimaryGeneratedColumn,
6
- Column,
7
- CreateDateColumn,
8
- Index,
9
- } from 'typeorm';
10
-
11
- /**
12
- * DomainEvent is a durable, append-only log of domain state changes.
13
- *
14
- * Rows are written in the SAME database transaction as the state change that
15
- * produced them (see DomainEventSubscriber), which is what makes delivery
16
- * durable. This implements the standard "transactional outbox" pattern, but is
17
- * surfaced with generic domain-event naming.
18
- *
19
- * A separate DomainEventRelay polls pending rows and delivers them. Because the
20
- * stable `id` is preserved across delivery attempts, consumers can dedupe on it
21
- * for at-least-once delivery semantics.
22
- */
23
- @Index(['status', 'created_at'])
24
- @Entity('events')
25
- export class DomainEvent {
26
- @PrimaryGeneratedColumn('uuid')
27
- id: string;
28
-
29
- /** Event type, e.g. "product.created". */
30
- @Column({ type: 'varchar' })
31
- type: string;
32
-
33
- /** Serialized event payload. */
34
- @Column({ type: 'jsonb' })
35
- payload: Record<string, unknown>;
36
-
37
- /** Delivery lifecycle: pending -> published, or failed after max attempts. */
38
- @Column({ type: 'varchar', default: 'pending' })
39
- status: 'pending' | 'published' | 'failed';
40
-
41
- /** Number of delivery attempts made by the relay. */
42
- @Column({ type: 'int', default: 0 })
43
- attempts: number;
44
-
45
- @CreateDateColumn({ type: 'timestamptz' })
46
- created_at: Date;
47
-
48
- /** When the event was successfully published; null until then. */
49
- @Column({ type: 'timestamptz', nullable: true })
50
- publishedAt: Date | null;
51
- }
@@ -1,62 +0,0 @@
1
- <%~ includeFile('../header.eta') %>
2
-
3
- import { Injectable } from '@nestjs/common';
4
-
5
- /**
6
- * Action that produced a domain event.
7
- */
8
- export type DomainEventAction = 'created' | 'updated' | 'removed';
9
-
10
- /**
11
- * DI token for the DomainEventMapper.
12
- *
13
- * The generated DomainEventsModule binds this token to DefaultDomainEventMapper.
14
- * Your application can override it by providing its own implementation:
15
- *
16
- * { provide: DOMAIN_EVENT_MAPPER, useClass: MyDomainEventMapper }
17
- */
18
- export const DOMAIN_EVENT_MAPPER = 'DOMAIN_EVENT_MAPPER';
19
-
20
- /**
21
- * DomainEventMapper is the extension point that keeps application semantics
22
- * (event-type taxonomy, payload shape) OUT of the generated code. Provide your
23
- * own implementation under DOMAIN_EVENT_MAPPER to customize.
24
- */
25
- export interface DomainEventMapper {
26
- /**
27
- * Maps an entity name + action to an event type string, e.g.
28
- * ("Product", "created") -> "product.created".
29
- */
30
- eventType(entityName: string, action: DomainEventAction): string;
31
-
32
- /**
33
- * Builds the serializable payload for an event from the changed entity.
34
- */
35
- toPayload(
36
- entity: unknown,
37
- action: DomainEventAction
38
- ): Record<string, unknown>;
39
- }
40
-
41
- /**
42
- * Default mapper. Uses an `entity.action` type taxonomy (entity name
43
- * lower-camel-cased) and returns the entity as-is (shallow) as the payload.
44
- *
45
- * Override by providing your own DomainEventMapper under DOMAIN_EVENT_MAPPER.
46
- */
47
- @Injectable()
48
- export class DefaultDomainEventMapper implements DomainEventMapper {
49
- eventType(entityName: string, action: DomainEventAction): string {
50
- const normalized = entityName
51
- ? entityName.charAt(0).toLowerCase() + entityName.slice(1)
52
- : entityName;
53
- return `${normalized}.${action}`;
54
- }
55
-
56
- toPayload(
57
- entity: unknown,
58
- _action: DomainEventAction
59
- ): Record<string, unknown> {
60
- return { ...(entity as Record<string, unknown>) };
61
- }
62
- }
@@ -1,86 +0,0 @@
1
- <%~ includeFile('../header.eta') %>
2
-
3
- import { Injectable, Logger } from '@nestjs/common';
4
- import { InjectRepository } from '@nestjs/typeorm';
5
- import { Repository } from 'typeorm';
6
- import { DomainEvent } from './domain-event.entity';
7
-
8
- /**
9
- * Maximum number of delivery attempts before an event is marked 'failed'.
10
- */
11
- const MAX_ATTEMPTS = 5;
12
-
13
- /**
14
- * DomainEventRelay drains the durable `events` table and delivers pending
15
- * events. It is the consumer side of the transactional-outbox pattern: the
16
- * subscriber writes events transactionally, the relay publishes them
17
- * asynchronously with at-least-once semantics.
18
- *
19
- * DELIVERY is the extension point: override `publish()` (see below) to send
20
- * events anywhere — webhooks, Kafka, SNS, an internal event bus, etc. The
21
- * generated code intentionally does NOT implement any specific transport.
22
- */
23
- @Injectable()
24
- export class DomainEventRelay {
25
- private readonly logger = new Logger(DomainEventRelay.name);
26
-
27
- constructor(
28
- @InjectRepository(DomainEvent)
29
- private readonly events: Repository<DomainEvent>
30
- ) {}
31
-
32
- /**
33
- * Schedule this from your app, e.g. with @nestjs/schedule:
34
- *
35
- * // import { Interval } from '@nestjs/schedule';
36
- * // @Interval(5000)
37
- * // async tick() { await this.relay.processPending(); }
38
- *
39
- * (@nestjs/schedule is intentionally NOT imported here so no dependency is
40
- * forced on your project.)
41
- */
42
- async processPending(limit = 50): Promise<void> {
43
- const pending = await this.events.find({
44
- where: { status: 'pending' },
45
- order: { created_at: 'ASC' },
46
- take: limit,
47
- });
48
-
49
- for (const event of pending) {
50
- try {
51
- // eslint-disable-next-line no-await-in-loop
52
- await this.publish(event);
53
- event.status = 'published';
54
- event.publishedAt = new Date();
55
- // eslint-disable-next-line no-await-in-loop
56
- await this.events.save(event);
57
- } catch (error) {
58
- event.attempts += 1;
59
- if (event.attempts >= MAX_ATTEMPTS) {
60
- event.status = 'failed';
61
- this.logger.error(
62
- `DomainEvent ${event.id} (${event.type}) failed after ${event.attempts} attempts`,
63
- error instanceof Error ? error.stack : undefined
64
- );
65
- }
66
- // eslint-disable-next-line no-await-in-loop
67
- await this.events.save(event);
68
- }
69
- }
70
- }
71
-
72
- /**
73
- * EXTENSION POINT — deliver a single domain event.
74
- *
75
- * Override (subclass or re-provide) this method to actually publish events to
76
- * your transport of choice (webhooks, Kafka, SNS, internal bus, ...). On
77
- * success the relay marks the event published; on a thrown error it retries
78
- * up to MAX_ATTEMPTS and then marks it failed.
79
- */
80
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
81
- async publish(event: DomainEvent): Promise<void> {
82
- throw new Error(
83
- 'DomainEventRelay.publish() not implemented — override to deliver events'
84
- );
85
- }
86
- }
@@ -1,118 +0,0 @@
1
- <%~ includeFile('../header.eta') %>
2
-
3
- import { Inject, Injectable, Optional } from '@nestjs/common';
4
- import {
5
- DataSource,
6
- EntitySubscriberInterface,
7
- EventSubscriber,
8
- InsertEvent,
9
- UpdateEvent,
10
- RemoveEvent,
11
- } from 'typeorm';
12
- import { DomainEvent } from './domain-event.entity';
13
- import {
14
- DOMAIN_EVENT_MAPPER,
15
- DomainEventAction,
16
- DomainEventMapper,
17
- } from './domain-event.mapper';
18
- <% it.emittingEntities.forEach((entity) => { %>
19
- import { <%= entity.name %> } from '../<%= entity.name %>/<%= entity.name %>.entity';
20
- <% }) %>
21
-
22
- /**
23
- * Entity classes that have opted in to domain-event emission. Only state
24
- * changes to these entities produce DomainEvent rows; everything else is
25
- * skipped.
26
- */
27
- const EMITTING_ENTITIES: Function[] = [
28
- <% it.emittingEntities.forEach((entity) => { %>
29
- <%= entity.name %>,
30
- <% }) %>
31
- ];
32
-
33
- /**
34
- * DomainEventSubscriber writes a DomainEvent row for every insert/update/remove
35
- * of an opted-in entity.
36
- *
37
- * DURABILITY: each row is written via `event.manager` (the EntityManager of the
38
- * in-flight transaction), so the event is committed atomically WITH the state
39
- * change. This is the durability lever behind the transactional-outbox pattern.
40
- *
41
- * RECURSION GUARD: we never emit events for DomainEvent itself, otherwise each
42
- * emitted row would itself trigger another emission. Any other bookkeeping
43
- * tables you do not want logged should likewise be excluded from
44
- * EMITTING_ENTITIES (this scoping handles that — only opted-in entities emit).
45
- */
46
- @Injectable()
47
- @EventSubscriber()
48
- export class DomainEventSubscriber implements EntitySubscriberInterface {
49
- constructor(
50
- @Inject(DOMAIN_EVENT_MAPPER) private readonly mapper: DomainEventMapper,
51
- // MULTI-DATASOURCE: Nest auto-registers @EventSubscriber() providers on the
52
- // DEFAULT DataSource. We ALSO push ourselves onto any injected DataSource
53
- // here so registration is robust for named/non-default DataSources too. For
54
- // multiple DataSources, register DomainEventsModule per DataSource.
55
- @Optional() dataSource?: DataSource
56
- ) {
57
- dataSource?.subscribers?.push(this);
58
- }
59
-
60
- private isEmitting(target: Function | string): boolean {
61
- // RECURSION GUARD: never emit for DomainEvent itself.
62
- if (target === DomainEvent || target === 'DomainEvent') {
63
- return false;
64
- }
65
- return EMITTING_ENTITIES.some(
66
- (cls) => target === cls || target === cls.name
67
- );
68
- }
69
-
70
- private async emit(
71
- manager: InsertEvent<unknown>['manager'],
72
- entityClass: Function,
73
- entity: unknown,
74
- action: DomainEventAction
75
- ): Promise<void> {
76
- const entityName = entityClass.name;
77
- const repo = manager.getRepository(DomainEvent);
78
- // event.manager keeps us inside the active transaction.
79
- await repo.save(
80
- repo.create({
81
- type: this.mapper.eventType(entityName, action),
82
- payload: this.mapper.toPayload(entity, action),
83
- status: 'pending',
84
- attempts: 0,
85
- })
86
- );
87
- }
88
-
89
- async afterInsert(event: InsertEvent<unknown>): Promise<void> {
90
- if (!this.isEmitting(event.metadata.target)) return;
91
- await this.emit(
92
- event.manager,
93
- event.metadata.target as Function,
94
- event.entity,
95
- 'created'
96
- );
97
- }
98
-
99
- async afterUpdate(event: UpdateEvent<unknown>): Promise<void> {
100
- if (!this.isEmitting(event.metadata.target)) return;
101
- await this.emit(
102
- event.manager,
103
- event.metadata.target as Function,
104
- event.entity ?? event.databaseEntity,
105
- 'updated'
106
- );
107
- }
108
-
109
- async afterRemove(event: RemoveEvent<unknown>): Promise<void> {
110
- if (!this.isEmitting(event.metadata.target)) return;
111
- await this.emit(
112
- event.manager,
113
- event.metadata.target as Function,
114
- event.entity ?? event.databaseEntity,
115
- 'removed'
116
- );
117
- }
118
- }
@@ -1,40 +0,0 @@
1
- <%~ includeFile('../header.eta') %>
2
-
3
- import { Global, Module } from '@nestjs/common';
4
- import { TypeOrmModule } from '@nestjs/typeorm';
5
- import { DomainEvent } from './domain-event.entity';
6
- import { DomainEventSubscriber } from './domain-event.subscriber';
7
- import { DomainEventRelay } from './domain-event.relay';
8
- import {
9
- DOMAIN_EVENT_MAPPER,
10
- DefaultDomainEventMapper,
11
- } from './domain-event.mapper';
12
-
13
- /**
14
- * DomainEventsModule wires the durable domain-event spine (transactional-outbox
15
- * pattern, surfaced as generic domain events).
16
- *
17
- * - DomainEventSubscriber writes events in-transaction with state changes.
18
- * - DomainEventRelay drains and delivers pending events (override
19
- * DomainEventRelay.publish()).
20
- * - DOMAIN_EVENT_MAPPER controls event-type/payload semantics; override by
21
- * re-providing the token with your own DomainEventMapper.
22
- *
23
- * MULTI-DATASOURCE: @EventSubscriber() auto-registers on the default DataSource;
24
- * for additional named DataSources, register this module (or the subscriber)
25
- * per DataSource.
26
- */
27
- @Global()
28
- @Module({
29
- imports: [TypeOrmModule.forFeature([DomainEvent])],
30
- providers: [
31
- DomainEventSubscriber,
32
- DomainEventRelay,
33
- {
34
- provide: DOMAIN_EVENT_MAPPER,
35
- useClass: DefaultDomainEventMapper,
36
- },
37
- ],
38
- exports: [DomainEventRelay, DOMAIN_EVENT_MAPPER],
39
- })
40
- export class DomainEventsModule {}
@@ -1,7 +0,0 @@
1
- <%~ includeFile('../header.eta') %>
2
-
3
- export * from './domain-event.entity';
4
- export * from './domain-event.subscriber';
5
- export * from './domain-event.mapper';
6
- export * from './domain-event.relay';
7
- export * from './domain-events.module';