@apso/cli 0.17.0 → 0.19.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.
- package/dist/commands/generate.js +28 -8
- package/dist/commands/mcp/serve.js +21 -6
- package/dist/lib/apsorc-parser.d.ts +7 -0
- package/dist/lib/apsorc-parser.js +4 -0
- package/dist/lib/events.d.ts +5 -4
- package/dist/lib/events.js +5 -4
- package/dist/lib/generators/base.d.ts +3 -2
- package/dist/lib/generators/base.js +3 -2
- package/dist/lib/generators/typescript.d.ts +9 -4
- package/dist/lib/generators/typescript.js +17 -41
- package/dist/lib/templates/events/event-emitting.entities.eta +23 -0
- package/dist/lib/templates/graphql/index-module-graphql.eta +0 -6
- package/dist/lib/templates/rest/index-module-rest.eta +0 -6
- package/dist/lib/templates/rest/module-rest.eta +4 -5
- package/dist/lib/types/entity.d.ts +21 -0
- package/dist/lib/types/generator.d.ts +12 -2
- package/npm-shrinkwrap.json +2 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/lib/templates/events/domain-event.entity.eta +0 -51
- package/dist/lib/templates/events/domain-event.mapper.eta +0 -62
- package/dist/lib/templates/events/domain-event.relay.eta +0 -86
- package/dist/lib/templates/events/domain-event.subscriber.eta +0 -118
- package/dist/lib/templates/events/domain-events.module.eta +0 -40
- package/dist/lib/templates/events/index.eta +0 -7
|
@@ -10,10 +10,11 @@ const perf_hooks_1 = require("perf_hooks");
|
|
|
10
10
|
const file_system_1 = require("../lib/utils/file-system");
|
|
11
11
|
class Generate extends base_command_1.default {
|
|
12
12
|
async run() {
|
|
13
|
+
var _a, _b;
|
|
13
14
|
const { flags } = await this.parse(Generate);
|
|
14
15
|
const skipFormat = flags["skip-format"];
|
|
15
16
|
const totalBuildStart = perf_hooks_1.performance.now();
|
|
16
|
-
const { rootFolder, entities, relationshipMap, apiType, auth, emitEvents, language: configLanguage } = (0, lib_1.parseApsorc)();
|
|
17
|
+
const { rootFolder, entities, relationshipMap, apiType, auth, emitEvents, http, language: configLanguage } = (0, lib_1.parseApsorc)();
|
|
17
18
|
// Resolve language: flag > .apsorc > prompt
|
|
18
19
|
let language;
|
|
19
20
|
if (flags.language) {
|
|
@@ -64,6 +65,7 @@ class Generate extends base_command_1.default {
|
|
|
64
65
|
relationshipMap,
|
|
65
66
|
auth,
|
|
66
67
|
emitEvents,
|
|
68
|
+
http,
|
|
67
69
|
};
|
|
68
70
|
const generator = (0, lib_1.createGenerator)(generatorConfig);
|
|
69
71
|
const validationResult = generator.validateConfig(generatorConfig);
|
|
@@ -94,8 +96,21 @@ class Generate extends base_command_1.default {
|
|
|
94
96
|
console.log(`[apso] Building... ${entity.name}`);
|
|
95
97
|
const entityBuildStart = perf_hooks_1.performance.now();
|
|
96
98
|
const entityRelationships = relationshipMap[entity.name] || [];
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
+
// Effective HTTP-controller flag (TypeScript REST only for now): entity
|
|
100
|
+
// overrides the top-level default, which defaults to true. When false, the
|
|
101
|
+
// controller is neither generated nor wired into the module (a hand-written
|
|
102
|
+
// controller owns the route). GraphQL uses resolvers, so the flag doesn't
|
|
103
|
+
// apply there. Python (FastAPI) and Go (Gin) wire routers differently and
|
|
104
|
+
// don't yet honor this flag — keep it a no-op for them (controllers always
|
|
105
|
+
// generated) so we never emit broken wiring (a skipped router still
|
|
106
|
+
// referenced by the index module). Tracked separately for Python/Go.
|
|
107
|
+
const includeController = language === "typescript" && lowerCaseApiType === "rest"
|
|
108
|
+
? ((_b = (_a = entity.http) !== null && _a !== void 0 ? _a : http) !== null && _b !== void 0 ? _b : true)
|
|
109
|
+
: true;
|
|
110
|
+
if (!includeController) {
|
|
111
|
+
console.log(`[apso] ${entity.name}: http=false — skipping generated controller`);
|
|
112
|
+
}
|
|
113
|
+
const generationTasks = [
|
|
99
114
|
generator.generateEntity({
|
|
100
115
|
entity,
|
|
101
116
|
relationships: entityRelationships,
|
|
@@ -115,20 +130,25 @@ class Generate extends base_command_1.default {
|
|
|
115
130
|
apiType: lowerCaseApiType,
|
|
116
131
|
relationshipMap,
|
|
117
132
|
}),
|
|
118
|
-
generator.
|
|
133
|
+
generator.generateModule({
|
|
119
134
|
entity,
|
|
120
135
|
relationships: entityRelationships,
|
|
121
136
|
allEntities: entities,
|
|
122
137
|
apiType: lowerCaseApiType,
|
|
123
|
-
|
|
138
|
+
includeController,
|
|
124
139
|
}),
|
|
125
|
-
|
|
140
|
+
];
|
|
141
|
+
if (includeController) {
|
|
142
|
+
generationTasks.push(generator.generateController({
|
|
126
143
|
entity,
|
|
127
144
|
relationships: entityRelationships,
|
|
128
145
|
allEntities: entities,
|
|
129
146
|
apiType: lowerCaseApiType,
|
|
130
|
-
|
|
131
|
-
|
|
147
|
+
relationshipMap,
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
// eslint-disable-next-line no-await-in-loop
|
|
151
|
+
const allFiles = await Promise.all(generationTasks);
|
|
132
152
|
// eslint-disable-next-line no-await-in-loop
|
|
133
153
|
for (const files of allFiles) {
|
|
134
154
|
for (const file of files) {
|
|
@@ -924,6 +924,7 @@ const INLINE_SCHEMA_REFERENCE = `# Schema Quick Reference
|
|
|
924
924
|
"primaryKeyType": "serial",
|
|
925
925
|
"scopeBy": "organizationId",
|
|
926
926
|
"emitEvents": true,
|
|
927
|
+
"http": false,
|
|
927
928
|
"fields": [
|
|
928
929
|
{ "name": "fieldName", "type": "text", "length": 100 }
|
|
929
930
|
],
|
|
@@ -939,12 +940,26 @@ state changes. A top-level \`emitEvents: true\` enables it for every entity; an
|
|
|
939
940
|
individual entity can opt out with \`emitEvents: false\` (effective value is
|
|
940
941
|
\`entity.emitEvents ?? <top-level emitEvents> ?? false\`).
|
|
941
942
|
|
|
942
|
-
When at least one entity opts in, the generator emits a
|
|
943
|
-
|
|
944
|
-
\`
|
|
945
|
-
|
|
946
|
-
\`
|
|
947
|
-
|
|
943
|
+
When at least one entity opts in, the generator emits a single schema-derived
|
|
944
|
+
manifest at \`autogen/events/event-emitting.entities.ts\` exporting
|
|
945
|
+
\`EVENT_EMITTING_ENTITIES\` (the opted-in entity classes) and
|
|
946
|
+
\`EVENT_EMITTING_ENTITY_NAMES\`. The domain-event engine itself ships in the
|
|
947
|
+
\`@apso/domain-events\` library and is wired by the \`domain-events\` skill; the
|
|
948
|
+
CLI no longer generates the engine code.
|
|
949
|
+
|
|
950
|
+
## HTTP controllers (http)
|
|
951
|
+
Controllers are generated by default. Set \`http: false\` on an entity (or a
|
|
952
|
+
top-level \`http: false\` default) to NOT generate/mount its HTTP controller while
|
|
953
|
+
still generating the entity, service, DTOs, and module (\`TypeOrmModule.forFeature\`
|
|
954
|
+
+ the service). Use this when a hand-written controller owns the route for that
|
|
955
|
+
entity (e.g. a custom/Stripe-shaped surface). Effective value is
|
|
956
|
+
\`entity.http ?? <top-level http> ?? true\`.
|
|
957
|
+
|
|
958
|
+
Per framework: NestJS and Gin (Go) actually suppress the controller — those
|
|
959
|
+
frameworks can't cleanly override a generated route. For FastAPI it's a no-op:
|
|
960
|
+
override by registering your router ahead of the generated \`autogen_router\`
|
|
961
|
+
(FastAPI matches in registration order). Currently implemented for NestJS; Go
|
|
962
|
+
is tracked separately.
|
|
948
963
|
|
|
949
964
|
## Relationship Types
|
|
950
965
|
- OneToMany: parent has many children
|
|
@@ -20,6 +20,11 @@ export type ApsorcType = {
|
|
|
20
20
|
* its own `emitEvents: false`.
|
|
21
21
|
*/
|
|
22
22
|
emitEvents?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Top-level default for HTTP controller generation. When false, no entity
|
|
25
|
+
* gets a generated controller unless it opts back in with `http: true`.
|
|
26
|
+
*/
|
|
27
|
+
http?: boolean;
|
|
23
28
|
};
|
|
24
29
|
type ParsedApsorcData = {
|
|
25
30
|
entities: Entity[];
|
|
@@ -34,6 +39,8 @@ type ParsedApsorc = {
|
|
|
34
39
|
language?: TargetLanguage;
|
|
35
40
|
/** Top-level default for the DomainEvent ("emitEvents") feature. */
|
|
36
41
|
emitEvents?: boolean;
|
|
42
|
+
/** Top-level default for HTTP controller generation. */
|
|
43
|
+
http?: boolean;
|
|
37
44
|
};
|
|
38
45
|
export declare const parseApsorcV1: (apsorc: ApsorcType) => ParsedApsorcData;
|
|
39
46
|
export declare const parseApsorcV2: (apsorc: ApsorcType) => ParsedApsorcData;
|
|
@@ -37,6 +37,7 @@ const parseRc = () => {
|
|
|
37
37
|
const auth = apsoConfig.auth;
|
|
38
38
|
const language = apsoConfig.language;
|
|
39
39
|
const emitEvents = apsoConfig.emitEvents;
|
|
40
|
+
const http = apsoConfig.http;
|
|
40
41
|
return {
|
|
41
42
|
rootFolder,
|
|
42
43
|
apiType,
|
|
@@ -46,6 +47,7 @@ const parseRc = () => {
|
|
|
46
47
|
auth,
|
|
47
48
|
language,
|
|
48
49
|
emitEvents,
|
|
50
|
+
http,
|
|
49
51
|
};
|
|
50
52
|
};
|
|
51
53
|
const parseApsorc = () => {
|
|
@@ -66,6 +68,7 @@ const parseApsorc = () => {
|
|
|
66
68
|
auth: apsoConfig.auth,
|
|
67
69
|
language: apsoConfig.language,
|
|
68
70
|
emitEvents: apsoConfig.emitEvents,
|
|
71
|
+
http: apsoConfig.http,
|
|
69
72
|
...(0, exports.parseApsorcV1)(apsoConfig),
|
|
70
73
|
};
|
|
71
74
|
if (debug) {
|
|
@@ -83,6 +86,7 @@ const parseApsorc = () => {
|
|
|
83
86
|
auth: apsoConfig.auth,
|
|
84
87
|
language: apsoConfig.language,
|
|
85
88
|
emitEvents: apsoConfig.emitEvents,
|
|
89
|
+
http: apsoConfig.http,
|
|
86
90
|
...(() => {
|
|
87
91
|
const relStart = perf_hooks_1.performance.now();
|
|
88
92
|
const parsed = (0, exports.parseApsorcV2)(apsoConfig);
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -2,9 +2,10 @@ import { Entity } from "./types";
|
|
|
2
2
|
/**
|
|
3
3
|
* Helpers for the opt-in DomainEvent ("emitEvents") feature.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
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;
|
package/dist/lib/events.js
CHANGED
|
@@ -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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
69
|
-
*
|
|
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
|
-
*
|
|
32
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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.
|
|
@@ -249,7 +249,7 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
249
249
|
];
|
|
250
250
|
}
|
|
251
251
|
async generateModule(options) {
|
|
252
|
-
const { entity, apiType } = options;
|
|
252
|
+
const { entity, apiType, includeController = true } = options;
|
|
253
253
|
const { name: entityName } = entity;
|
|
254
254
|
const moduleName = `${entityName}Module`;
|
|
255
255
|
const svcName = `${entityName}Service`;
|
|
@@ -261,6 +261,7 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
261
261
|
ctrlName,
|
|
262
262
|
resolverName,
|
|
263
263
|
entityName,
|
|
264
|
+
includeController,
|
|
264
265
|
};
|
|
265
266
|
const templatePath = apiType === "graphql"
|
|
266
267
|
? "./graphql/gql-module-graphql"
|
|
@@ -303,9 +304,8 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
303
304
|
},
|
|
304
305
|
];
|
|
305
306
|
}
|
|
306
|
-
async generateIndexModule(entities, apiType,
|
|
307
|
-
const
|
|
308
|
-
const content = await this.renderTemplate(`./${apiType}/index-module-${apiType}`, { entities, emitDomainEvents });
|
|
307
|
+
async generateIndexModule(entities, apiType, _opts) {
|
|
308
|
+
const content = await this.renderTemplate(`./${apiType}/index-module-${apiType}`, { entities });
|
|
309
309
|
return [
|
|
310
310
|
{
|
|
311
311
|
path: "index.ts",
|
|
@@ -358,9 +358,14 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
358
358
|
return files;
|
|
359
359
|
}
|
|
360
360
|
/**
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
361
|
+
* Emits a schema-derived manifest of the entities that have opted in to
|
|
362
|
+
* domain-event emission (`.apsorc` `emitEvents`).
|
|
363
|
+
*
|
|
364
|
+
* Under the Apso Distribution Model the domain-event engine no longer ships
|
|
365
|
+
* as generated code; it lives in the `@apso/domain-events` library and is
|
|
366
|
+
* wired by the `domain-events` skill. The CLI's only responsibility is to
|
|
367
|
+
* declare WHICH entities participate, via a single generated file:
|
|
368
|
+
* `events/event-emitting.entities.ts`.
|
|
364
369
|
*
|
|
365
370
|
* Returns an empty array when no entity is opted in, so callers can skip
|
|
366
371
|
* wiring entirely.
|
|
@@ -374,46 +379,17 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
374
379
|
if (emittingEntities.length === 0) {
|
|
375
380
|
return [];
|
|
376
381
|
}
|
|
377
|
-
const
|
|
382
|
+
const content = await this.renderTemplate("./events/event-emitting.entities.eta", {
|
|
378
383
|
emittingEntities: emittingEntities.map((entity) => ({
|
|
379
384
|
name: entity.name,
|
|
380
385
|
})),
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
},
|
|
386
|
+
});
|
|
387
|
+
return [
|
|
406
388
|
{
|
|
407
|
-
|
|
408
|
-
|
|
389
|
+
path: "events/event-emitting.entities.ts",
|
|
390
|
+
content,
|
|
409
391
|
},
|
|
410
392
|
];
|
|
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
393
|
}
|
|
418
394
|
/**
|
|
419
395
|
* 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
|
]
|
|
@@ -4,13 +4,12 @@ import { Module } from "@nestjs/common";
|
|
|
4
4
|
import {<%= it.entityName %>} from './<%= it.entityName%>.entity'
|
|
5
5
|
import { TypeOrmModule } from "@nestjs/typeorm";
|
|
6
6
|
import {<%= it.svcName %>} from "./<%= it.entityName %>.service";
|
|
7
|
-
import {<%= it.ctrlName %>} from "./<%= it.entityName %>.controller";
|
|
8
|
-
|
|
7
|
+
<% if (it.includeController) { %>import {<%= it.ctrlName %>} from "./<%= it.entityName %>.controller";
|
|
8
|
+
<% } %>
|
|
9
9
|
@Module({
|
|
10
10
|
imports: [TypeOrmModule.forFeature([<%= it.entityName %>])],
|
|
11
11
|
providers: [<%= it.svcName %>],
|
|
12
12
|
exports: [<%= it.svcName %>],
|
|
13
|
-
controllers: [<%= it.ctrlName %>],
|
|
14
|
-
})
|
|
13
|
+
<% if (it.includeController) { %> controllers: [<%= it.ctrlName %>],
|
|
14
|
+
<% } %>})
|
|
15
15
|
export class <%= it.moduleName %> {}
|
|
16
|
-
|
|
@@ -55,5 +55,26 @@ export interface Entity {
|
|
|
55
55
|
* an individual entity can opt out with `emitEvents: false`.
|
|
56
56
|
*/
|
|
57
57
|
emitEvents?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Whether to generate the HTTP controller for this entity. Defaults to true.
|
|
60
|
+
* When false, the generator still emits the entity, service, DTOs, and a module
|
|
61
|
+
* (with `TypeOrmModule.forFeature` + the service), but omits the controller —
|
|
62
|
+
* leaving the HTTP surface to a hand-written controller without a route
|
|
63
|
+
* collision with the generated CRUD.
|
|
64
|
+
*
|
|
65
|
+
* Per-framework behavior (the flag is generic, but only matters where a
|
|
66
|
+
* generated route can't be cleanly overridden):
|
|
67
|
+
* - **NestJS** and **Gin (Go)**: actually suppresses the controller/route —
|
|
68
|
+
* these frameworks can't override a generated route (Nest controllers aren't
|
|
69
|
+
* DI-swappable; Gin panics on duplicate paths). Implemented for NestJS;
|
|
70
|
+
* Go is tracked separately.
|
|
71
|
+
* - **FastAPI**: effectively a **no-op** — FastAPI matches routes in
|
|
72
|
+
* registration order, so an app overrides by registering its router ahead of
|
|
73
|
+
* the generated `autogen_router` (no suppression needed).
|
|
74
|
+
*
|
|
75
|
+
* Resolution: the effective value is `entity.http ?? <global http> ?? true`, so
|
|
76
|
+
* controllers are on by default and opted out per entity (or globally).
|
|
77
|
+
*/
|
|
78
|
+
http?: boolean;
|
|
58
79
|
associations?: Association[];
|
|
59
80
|
}
|
|
@@ -109,6 +109,11 @@ export interface GeneratorConfig {
|
|
|
109
109
|
* Effective per-entity value is `entity.emitEvents ?? emitEvents ?? false`.
|
|
110
110
|
*/
|
|
111
111
|
emitEvents?: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Top-level default for HTTP controller generation.
|
|
114
|
+
* Effective per-entity value is `entity.http ?? http ?? true`.
|
|
115
|
+
*/
|
|
116
|
+
http?: boolean;
|
|
112
117
|
/**
|
|
113
118
|
* Language-specific configuration options
|
|
114
119
|
*/
|
|
@@ -134,6 +139,11 @@ export interface EntityGenerationOptions {
|
|
|
134
139
|
* API type being generated
|
|
135
140
|
*/
|
|
136
141
|
apiType: string;
|
|
142
|
+
/**
|
|
143
|
+
* Whether the entity's HTTP controller is generated/wired. Defaults to true.
|
|
144
|
+
* When false, the module omits the controller (see Entity.http).
|
|
145
|
+
*/
|
|
146
|
+
includeController?: boolean;
|
|
137
147
|
}
|
|
138
148
|
/**
|
|
139
149
|
* Options for generating controllers/routers
|
|
@@ -246,8 +256,8 @@ export interface LanguageGenerator {
|
|
|
246
256
|
*/
|
|
247
257
|
generateQueryUtils(entities: Entity[], apiType: string): Promise<GeneratedFile[]>;
|
|
248
258
|
/**
|
|
249
|
-
*
|
|
250
|
-
* `
|
|
259
|
+
* Emit a schema-derived manifest of entities that opt in to `emitEvents`
|
|
260
|
+
* (engine lives in `@apso/domain-events`). Returns [] when none are opted in.
|
|
251
261
|
*/
|
|
252
262
|
generateDomainEvents(entities: Entity[], apiType?: string, opts?: {
|
|
253
263
|
emitEvents?: boolean;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apso/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@apso/cli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.19.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@electric-sql/pglite": "^0.2.17",
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -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 {}
|