nestjs-ddd-cli 2.2.1 → 3.2.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 +247 -408
- package/ddd.schema.json +111 -0
- package/dist/commands/aggregate-validator.d.ts +9 -0
- package/dist/commands/aggregate-validator.js +953 -0
- package/dist/commands/aggregate-validator.js.map +1 -0
- package/dist/commands/ai-assist.d.ts +8 -0
- package/dist/commands/ai-assist.js +337 -0
- package/dist/commands/ai-assist.js.map +1 -0
- package/dist/commands/api-contracts.d.ts +9 -0
- package/dist/commands/api-contracts.js +1368 -0
- package/dist/commands/api-contracts.js.map +1 -0
- package/dist/commands/api-docs.d.ts +8 -0
- package/dist/commands/api-docs.js +408 -0
- package/dist/commands/api-docs.js.map +1 -0
- package/dist/commands/api-versioning.d.ts +11 -0
- package/dist/commands/api-versioning.js +643 -0
- package/dist/commands/api-versioning.js.map +1 -0
- package/dist/commands/audit-logging.d.ts +9 -0
- package/dist/commands/audit-logging.js +1129 -0
- package/dist/commands/audit-logging.js.map +1 -0
- package/dist/commands/batch-generate.d.ts +10 -0
- package/dist/commands/batch-generate.js +405 -0
- package/dist/commands/batch-generate.js.map +1 -0
- package/dist/commands/caching-strategies.d.ts +9 -0
- package/dist/commands/caching-strategies.js +874 -0
- package/dist/commands/caching-strategies.js.map +1 -0
- package/dist/commands/code-analyzer.d.ts +42 -0
- package/dist/commands/code-analyzer.js +474 -0
- package/dist/commands/code-analyzer.js.map +1 -0
- package/dist/commands/database-seeding.d.ts +6 -0
- package/dist/commands/database-seeding.js +621 -0
- package/dist/commands/database-seeding.js.map +1 -0
- package/dist/commands/db-optimization.d.ts +7 -0
- package/dist/commands/db-optimization.js +687 -0
- package/dist/commands/db-optimization.js.map +1 -0
- package/dist/commands/dependency-graph.d.ts +6 -0
- package/dist/commands/dependency-graph.js +329 -0
- package/dist/commands/dependency-graph.js.map +1 -0
- package/dist/commands/doctor-enhanced.d.ts +22 -0
- package/dist/commands/doctor-enhanced.js +543 -0
- package/dist/commands/doctor-enhanced.js.map +1 -0
- package/dist/commands/doctor.d.ts +4 -0
- package/dist/commands/doctor.js +151 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/env-manager.d.ts +6 -0
- package/dist/commands/env-manager.js +419 -0
- package/dist/commands/env-manager.js.map +1 -0
- package/dist/commands/event-sourcing-full.d.ts +10 -0
- package/dist/commands/event-sourcing-full.js +1107 -0
- package/dist/commands/event-sourcing-full.js.map +1 -0
- package/dist/commands/feature-flags.d.ts +9 -0
- package/dist/commands/feature-flags.js +824 -0
- package/dist/commands/feature-flags.js.map +1 -0
- package/dist/commands/filter-dsl.d.ts +10 -0
- package/dist/commands/filter-dsl.js +1407 -0
- package/dist/commands/filter-dsl.js.map +1 -0
- package/dist/commands/generate-all.js +485 -32
- package/dist/commands/generate-all.js.map +1 -1
- package/dist/commands/generate-deployment.d.ts +8 -0
- package/dist/commands/generate-deployment.js +746 -0
- package/dist/commands/generate-deployment.js.map +1 -0
- package/dist/commands/generate-domain-service.d.ts +14 -0
- package/dist/commands/generate-domain-service.js +796 -0
- package/dist/commands/generate-domain-service.js.map +1 -0
- package/dist/commands/generate-entity.js +82 -24
- package/dist/commands/generate-entity.js.map +1 -1
- package/dist/commands/generate-from-schema.d.ts +56 -0
- package/dist/commands/generate-from-schema.js +222 -0
- package/dist/commands/generate-from-schema.js.map +1 -0
- package/dist/commands/generate-orchestrator.d.ts +14 -0
- package/dist/commands/generate-orchestrator.js +887 -0
- package/dist/commands/generate-orchestrator.js.map +1 -0
- package/dist/commands/generate-repository.d.ts +14 -0
- package/dist/commands/generate-repository.js +1019 -0
- package/dist/commands/generate-repository.js.map +1 -0
- package/dist/commands/generate-shared.d.ts +4 -0
- package/dist/commands/generate-shared.js +388 -0
- package/dist/commands/generate-shared.js.map +1 -0
- package/dist/commands/generate-value-object.d.ts +32 -0
- package/dist/commands/generate-value-object.js +700 -0
- package/dist/commands/generate-value-object.js.map +1 -0
- package/dist/commands/graphql-subscriptions.d.ts +6 -0
- package/dist/commands/graphql-subscriptions.js +607 -0
- package/dist/commands/graphql-subscriptions.js.map +1 -0
- package/dist/commands/graphql-types.d.ts +5 -0
- package/dist/commands/graphql-types.js +423 -0
- package/dist/commands/graphql-types.js.map +1 -0
- package/dist/commands/health-probes-advanced.d.ts +6 -0
- package/dist/commands/health-probes-advanced.js +655 -0
- package/dist/commands/health-probes-advanced.js.map +1 -0
- package/dist/commands/i18n-setup.d.ts +10 -0
- package/dist/commands/i18n-setup.js +677 -0
- package/dist/commands/i18n-setup.js.map +1 -0
- package/dist/commands/init-config.d.ts +6 -0
- package/dist/commands/init-config.js +370 -0
- package/dist/commands/init-config.js.map +1 -0
- package/dist/commands/init-project.js +56 -6
- package/dist/commands/init-project.js.map +1 -1
- package/dist/commands/interactive-scaffold.d.ts +5 -0
- package/dist/commands/interactive-scaffold.js +271 -0
- package/dist/commands/interactive-scaffold.js.map +1 -0
- package/dist/commands/metrics-prometheus.d.ts +6 -0
- package/dist/commands/metrics-prometheus.js +681 -0
- package/dist/commands/metrics-prometheus.js.map +1 -0
- package/dist/commands/migration-engine.d.ts +6 -0
- package/dist/commands/migration-engine.js +446 -0
- package/dist/commands/migration-engine.js.map +1 -0
- package/dist/commands/migration.d.ts +12 -0
- package/dist/commands/migration.js +484 -0
- package/dist/commands/migration.js.map +1 -0
- package/dist/commands/monorepo.d.ts +8 -0
- package/dist/commands/monorepo.js +483 -0
- package/dist/commands/monorepo.js.map +1 -0
- package/dist/commands/multi-database.d.ts +5 -0
- package/dist/commands/multi-database.js +439 -0
- package/dist/commands/multi-database.js.map +1 -0
- package/dist/commands/observability-tracing.d.ts +10 -0
- package/dist/commands/observability-tracing.js +740 -0
- package/dist/commands/observability-tracing.js.map +1 -0
- package/dist/commands/openapi-export.d.ts +8 -0
- package/dist/commands/openapi-export.js +359 -0
- package/dist/commands/openapi-export.js.map +1 -0
- package/dist/commands/perf-analyzer.d.ts +8 -0
- package/dist/commands/perf-analyzer.js +423 -0
- package/dist/commands/perf-analyzer.js.map +1 -0
- package/dist/commands/rate-limiting.d.ts +10 -0
- package/dist/commands/rate-limiting.js +953 -0
- package/dist/commands/rate-limiting.js.map +1 -0
- package/dist/commands/recipe-plugin.d.ts +56 -0
- package/dist/commands/recipe-plugin.js +315 -0
- package/dist/commands/recipe-plugin.js.map +1 -0
- package/dist/commands/recipe.d.ts +6 -0
- package/dist/commands/recipe.js +3941 -0
- package/dist/commands/recipe.js.map +1 -0
- package/dist/commands/recipes/elasticsearch.recipe.d.ts +1 -0
- package/dist/commands/recipes/elasticsearch.recipe.js +761 -0
- package/dist/commands/recipes/elasticsearch.recipe.js.map +1 -0
- package/dist/commands/recipes/event-sourcing.recipe.d.ts +1 -0
- package/dist/commands/recipes/event-sourcing.recipe.js +889 -0
- package/dist/commands/recipes/event-sourcing.recipe.js.map +1 -0
- package/dist/commands/recipes/index.d.ts +7 -0
- package/dist/commands/recipes/index.js +24 -0
- package/dist/commands/recipes/index.js.map +1 -0
- package/dist/commands/recipes/message-queue.recipe.d.ts +1 -0
- package/dist/commands/recipes/message-queue.recipe.js +706 -0
- package/dist/commands/recipes/message-queue.recipe.js.map +1 -0
- package/dist/commands/recipes/middleware.recipe.d.ts +1 -0
- package/dist/commands/recipes/middleware.recipe.js +383 -0
- package/dist/commands/recipes/middleware.recipe.js.map +1 -0
- package/dist/commands/recipes/multi-tenancy.recipe.d.ts +1 -0
- package/dist/commands/recipes/multi-tenancy.recipe.js +520 -0
- package/dist/commands/recipes/multi-tenancy.recipe.js.map +1 -0
- package/dist/commands/recipes/oauth2.recipe.d.ts +1 -0
- package/dist/commands/recipes/oauth2.recipe.js +472 -0
- package/dist/commands/recipes/oauth2.recipe.js.map +1 -0
- package/dist/commands/recipes/websocket.recipe.d.ts +1 -0
- package/dist/commands/recipes/websocket.recipe.js +453 -0
- package/dist/commands/recipes/websocket.recipe.js.map +1 -0
- package/dist/commands/resilience-patterns.d.ts +13 -0
- package/dist/commands/resilience-patterns.js +1029 -0
- package/dist/commands/resilience-patterns.js.map +1 -0
- package/dist/commands/security-patterns.d.ts +11 -0
- package/dist/commands/security-patterns.js +2233 -0
- package/dist/commands/security-patterns.js.map +1 -0
- package/dist/commands/template-debug.d.ts +27 -0
- package/dist/commands/template-debug.js +388 -0
- package/dist/commands/template-debug.js.map +1 -0
- package/dist/commands/test-factory-full.d.ts +9 -0
- package/dist/commands/test-factory-full.js +1570 -0
- package/dist/commands/test-factory-full.js.map +1 -0
- package/dist/commands/test-scaffold.d.ts +7 -0
- package/dist/commands/test-scaffold.js +621 -0
- package/dist/commands/test-scaffold.js.map +1 -0
- package/dist/index.js +1088 -0
- package/dist/index.js.map +1 -1
- package/dist/templates/ai-context/CLAUDE.md.hbs +158 -0
- package/dist/templates/ai-context/conventions.md.hbs +154 -0
- package/dist/templates/command/create-command.hbs +6 -14
- package/dist/templates/command/delete-command.hbs +19 -0
- package/dist/templates/command/update-command.hbs +24 -0
- package/dist/templates/controller/controller.hbs +64 -17
- package/dist/templates/dto/create-dto.hbs +29 -5
- package/dist/templates/dto/filter-dto.hbs +52 -0
- package/dist/templates/dto/filter-query.dto.hbs +148 -0
- package/dist/templates/dto/paginated-response.dto.hbs +29 -0
- package/dist/templates/dto/pagination-query.dto.hbs +30 -0
- package/dist/templates/dto/response-dto.hbs +38 -0
- package/dist/templates/dto/update-dto.hbs +11 -0
- package/dist/templates/entity/entity.hbs +32 -1
- package/dist/templates/event/domain-event.hbs +33 -7
- package/dist/templates/event/event-handler.hbs +40 -0
- package/dist/templates/exception/base-exceptions.hbs +69 -0
- package/dist/templates/exception/entity-not-found.exception.hbs +7 -0
- package/dist/templates/mapper/mapper.hbs +49 -24
- package/dist/templates/module/module.hbs +34 -10
- package/dist/templates/orm-entity/orm-entity.hbs +63 -12
- package/dist/templates/prisma/prisma-mapper.hbs +71 -0
- package/dist/templates/prisma/prisma-repository.hbs +114 -0
- package/dist/templates/prisma/prisma-schema.hbs +20 -0
- package/dist/templates/prisma/prisma-service.hbs +51 -0
- package/dist/templates/query/get-all.query.hbs +50 -0
- package/dist/templates/query/get-by-id.query.hbs +31 -0
- package/dist/templates/repository/repository.hbs +55 -13
- package/dist/templates/resolver/graphql-input.hbs +54 -0
- package/dist/templates/resolver/graphql-type.hbs +58 -0
- package/dist/templates/resolver/pagination-args.hbs +33 -0
- package/dist/templates/resolver/resolver.hbs +62 -0
- package/dist/templates/shared/prisma-query-builder.util.hbs +189 -0
- package/dist/templates/shared/query-builder.util.hbs +218 -0
- package/dist/templates/test/controller.spec.hbs +124 -0
- package/dist/templates/test/repository.spec.hbs +158 -0
- package/dist/templates/test/usecase.spec.hbs +116 -0
- package/dist/templates/usecase/create-usecase.hbs +19 -7
- package/dist/templates/usecase/delete-usecase.hbs +17 -0
- package/dist/templates/usecase/update-usecase.hbs +31 -0
- package/dist/utils/config.utils.d.ts +45 -0
- package/dist/utils/config.utils.js +211 -0
- package/dist/utils/config.utils.js.map +1 -0
- package/dist/utils/error.utils.d.ts +145 -0
- package/dist/utils/error.utils.js +422 -0
- package/dist/utils/error.utils.js.map +1 -0
- package/dist/utils/field.utils.d.ts +54 -0
- package/dist/utils/field.utils.js +389 -0
- package/dist/utils/field.utils.js.map +1 -0
- package/dist/utils/file.utils.d.ts +19 -8
- package/dist/utils/file.utils.js +135 -4
- package/dist/utils/file.utils.js.map +1 -1
- package/dist/utils/idempotency.utils.d.ts +123 -0
- package/dist/utils/idempotency.utils.js +444 -0
- package/dist/utils/idempotency.utils.js.map +1 -0
- package/dist/utils/naming.utils.js +24 -5
- package/dist/utils/naming.utils.js.map +1 -1
- package/dist/utils/performance.utils.d.ts +37 -0
- package/dist/utils/performance.utils.js +158 -0
- package/dist/utils/performance.utils.js.map +1 -0
- package/dist/utils/relation.utils.d.ts +92 -0
- package/dist/utils/relation.utils.js +388 -0
- package/dist/utils/relation.utils.js.map +1 -0
- package/dist/utils/rollback.utils.d.ts +49 -0
- package/dist/utils/rollback.utils.js +306 -0
- package/dist/utils/rollback.utils.js.map +1 -0
- package/dist/utils/schema.utils.d.ts +123 -0
- package/dist/utils/schema.utils.js +419 -0
- package/dist/utils/schema.utils.js.map +1 -0
- package/dist/utils/security.utils.d.ts +57 -0
- package/dist/utils/security.utils.js +315 -0
- package/dist/utils/security.utils.js.map +1 -0
- package/dist/utils/template-engine.utils.d.ts +80 -0
- package/dist/utils/template-engine.utils.js +463 -0
- package/dist/utils/template-engine.utils.js.map +1 -0
- package/dist/utils/validation-registry.utils.d.ts +160 -0
- package/dist/utils/validation-registry.utils.js +526 -0
- package/dist/utils/validation-registry.utils.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,1107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Complete Event Sourcing Framework
|
|
4
|
+
* Full infrastructure for event sourcing architecture
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
40
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.setupEventSourcingFramework = setupEventSourcingFramework;
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
46
|
+
const file_utils_1 = require("../utils/file.utils");
|
|
47
|
+
async function setupEventSourcingFramework(basePath, options = {}) {
|
|
48
|
+
console.log(chalk_1.default.bold.blue('\n📦 Setting up Event Sourcing Framework\n'));
|
|
49
|
+
const esPath = path.join(basePath, 'src/shared/event-sourcing');
|
|
50
|
+
await (0, file_utils_1.ensureDir)(esPath);
|
|
51
|
+
await (0, file_utils_1.ensureDir)(path.join(esPath, 'decorators'));
|
|
52
|
+
await (0, file_utils_1.ensureDir)(path.join(esPath, 'stores'));
|
|
53
|
+
await (0, file_utils_1.ensureDir)(path.join(esPath, 'handlers'));
|
|
54
|
+
await (0, file_utils_1.ensureDir)(path.join(esPath, 'projections'));
|
|
55
|
+
// Generate core types
|
|
56
|
+
await generateEventTypes(esPath);
|
|
57
|
+
// Generate aggregate root
|
|
58
|
+
await generateAggregateRoot(esPath);
|
|
59
|
+
// Generate event store
|
|
60
|
+
await generateEventStore(esPath, options);
|
|
61
|
+
// Generate snapshot store
|
|
62
|
+
await generateSnapshotStore(esPath, options);
|
|
63
|
+
// Generate event bus
|
|
64
|
+
await generateEventBus(esPath);
|
|
65
|
+
// Generate projection manager
|
|
66
|
+
await generateProjectionManager(esPath);
|
|
67
|
+
// Generate decorators
|
|
68
|
+
await generateDecorators(esPath);
|
|
69
|
+
// Generate module
|
|
70
|
+
await generateEventSourcingModule(esPath);
|
|
71
|
+
// Generate index
|
|
72
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'index.ts'), `export * from './event.types';
|
|
73
|
+
export * from './aggregate-root';
|
|
74
|
+
export * from './stores/event.store';
|
|
75
|
+
export * from './stores/snapshot.store';
|
|
76
|
+
export * from './event-bus';
|
|
77
|
+
export * from './projections/projection.manager';
|
|
78
|
+
export * from './decorators';
|
|
79
|
+
export * from './event-sourcing.module';
|
|
80
|
+
`);
|
|
81
|
+
console.log(chalk_1.default.green('\n✅ Event Sourcing Framework set up!'));
|
|
82
|
+
}
|
|
83
|
+
async function generateEventTypes(esPath) {
|
|
84
|
+
const content = `/**
|
|
85
|
+
* Core Event Sourcing Types
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
export interface DomainEvent<T = any> {
|
|
89
|
+
eventId: string;
|
|
90
|
+
eventType: string;
|
|
91
|
+
aggregateId: string;
|
|
92
|
+
aggregateType: string;
|
|
93
|
+
version: number;
|
|
94
|
+
timestamp: Date;
|
|
95
|
+
payload: T;
|
|
96
|
+
metadata?: EventMetadata;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface EventMetadata {
|
|
100
|
+
correlationId?: string;
|
|
101
|
+
causationId?: string;
|
|
102
|
+
userId?: string;
|
|
103
|
+
tenantId?: string;
|
|
104
|
+
timestamp?: Date;
|
|
105
|
+
[key: string]: any;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface StoredEvent extends DomainEvent {
|
|
109
|
+
id: string;
|
|
110
|
+
createdAt: Date;
|
|
111
|
+
sequence: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface EventStream {
|
|
115
|
+
aggregateId: string;
|
|
116
|
+
aggregateType: string;
|
|
117
|
+
version: number;
|
|
118
|
+
events: StoredEvent[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface Snapshot<T = any> {
|
|
122
|
+
aggregateId: string;
|
|
123
|
+
aggregateType: string;
|
|
124
|
+
version: number;
|
|
125
|
+
state: T;
|
|
126
|
+
timestamp: Date;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface EventEnvelope<T = any> {
|
|
130
|
+
event: DomainEvent<T>;
|
|
131
|
+
position: number;
|
|
132
|
+
timestamp: Date;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type EventHandler<T = any> = (event: DomainEvent<T>) => Promise<void> | void;
|
|
136
|
+
|
|
137
|
+
export type EventApplier<TState, TEvent> = (state: TState, event: TEvent) => TState;
|
|
138
|
+
|
|
139
|
+
export interface EventStoreOptions {
|
|
140
|
+
batchSize?: number;
|
|
141
|
+
timeout?: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface AppendResult {
|
|
145
|
+
success: boolean;
|
|
146
|
+
nextVersion: number;
|
|
147
|
+
position: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ReadOptions {
|
|
151
|
+
fromVersion?: number;
|
|
152
|
+
toVersion?: number;
|
|
153
|
+
limit?: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Event versioning for schema evolution
|
|
158
|
+
*/
|
|
159
|
+
export interface EventVersion {
|
|
160
|
+
version: number;
|
|
161
|
+
upcast: (oldEvent: any) => any;
|
|
162
|
+
downcast?: (newEvent: any) => any;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface VersionedEvent {
|
|
166
|
+
schemaVersion: number;
|
|
167
|
+
[key: string]: any;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Create a domain event
|
|
172
|
+
*/
|
|
173
|
+
export function createEvent<T>(
|
|
174
|
+
eventType: string,
|
|
175
|
+
aggregateId: string,
|
|
176
|
+
aggregateType: string,
|
|
177
|
+
payload: T,
|
|
178
|
+
metadata?: EventMetadata
|
|
179
|
+
): DomainEvent<T> {
|
|
180
|
+
return {
|
|
181
|
+
eventId: generateEventId(),
|
|
182
|
+
eventType,
|
|
183
|
+
aggregateId,
|
|
184
|
+
aggregateType,
|
|
185
|
+
version: 0, // Will be set by aggregate
|
|
186
|
+
timestamp: new Date(),
|
|
187
|
+
payload,
|
|
188
|
+
metadata: {
|
|
189
|
+
timestamp: new Date(),
|
|
190
|
+
...metadata,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function generateEventId(): string {
|
|
196
|
+
return \`evt_\${Date.now()}_\${Math.random().toString(36).slice(2, 11)}\`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Event serialization helpers
|
|
201
|
+
*/
|
|
202
|
+
export function serializeEvent(event: DomainEvent): string {
|
|
203
|
+
return JSON.stringify({
|
|
204
|
+
...event,
|
|
205
|
+
timestamp: event.timestamp.toISOString(),
|
|
206
|
+
metadata: event.metadata ? {
|
|
207
|
+
...event.metadata,
|
|
208
|
+
timestamp: event.metadata.timestamp?.toISOString(),
|
|
209
|
+
} : undefined,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function deserializeEvent(json: string): DomainEvent {
|
|
214
|
+
const parsed = JSON.parse(json);
|
|
215
|
+
return {
|
|
216
|
+
...parsed,
|
|
217
|
+
timestamp: new Date(parsed.timestamp),
|
|
218
|
+
metadata: parsed.metadata ? {
|
|
219
|
+
...parsed.metadata,
|
|
220
|
+
timestamp: parsed.metadata.timestamp ? new Date(parsed.metadata.timestamp) : undefined,
|
|
221
|
+
} : undefined,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
`;
|
|
225
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'event.types.ts'), content);
|
|
226
|
+
console.log(chalk_1.default.green(' ✓ Event types'));
|
|
227
|
+
}
|
|
228
|
+
async function generateAggregateRoot(esPath) {
|
|
229
|
+
const content = `import { DomainEvent, createEvent, EventMetadata } from './event.types';
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Base class for Event Sourced Aggregates
|
|
233
|
+
*/
|
|
234
|
+
export abstract class AggregateRoot<TState = any> {
|
|
235
|
+
private _id: string;
|
|
236
|
+
private _version: number = 0;
|
|
237
|
+
private _uncommittedEvents: DomainEvent[] = [];
|
|
238
|
+
protected _state: TState;
|
|
239
|
+
|
|
240
|
+
constructor(id: string) {
|
|
241
|
+
this._id = id;
|
|
242
|
+
this._state = this.getInitialState();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
get id(): string {
|
|
246
|
+
return this._id;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
get version(): number {
|
|
250
|
+
return this._version;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
get state(): TState {
|
|
254
|
+
return this._state;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
get uncommittedEvents(): DomainEvent[] {
|
|
258
|
+
return [...this._uncommittedEvents];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get the aggregate type name
|
|
263
|
+
*/
|
|
264
|
+
abstract getAggregateType(): string;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get the initial state for the aggregate
|
|
268
|
+
*/
|
|
269
|
+
protected abstract getInitialState(): TState;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Apply an event to update state
|
|
273
|
+
*/
|
|
274
|
+
protected abstract applyEvent(event: DomainEvent): void;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Raise a new domain event
|
|
278
|
+
*/
|
|
279
|
+
protected raise<T>(eventType: string, payload: T, metadata?: EventMetadata): void {
|
|
280
|
+
const event = createEvent(
|
|
281
|
+
eventType,
|
|
282
|
+
this._id,
|
|
283
|
+
this.getAggregateType(),
|
|
284
|
+
payload,
|
|
285
|
+
metadata
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
event.version = this._version + this._uncommittedEvents.length + 1;
|
|
289
|
+
this._uncommittedEvents.push(event);
|
|
290
|
+
this.applyEvent(event);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Apply historical events to rebuild state
|
|
295
|
+
*/
|
|
296
|
+
loadFromHistory(events: DomainEvent[]): void {
|
|
297
|
+
for (const event of events) {
|
|
298
|
+
this.applyEvent(event);
|
|
299
|
+
this._version = event.version;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Apply events from a snapshot
|
|
305
|
+
*/
|
|
306
|
+
loadFromSnapshot(snapshot: { version: number; state: TState }): void {
|
|
307
|
+
this._version = snapshot.version;
|
|
308
|
+
this._state = snapshot.state;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Mark events as committed
|
|
313
|
+
*/
|
|
314
|
+
markEventsAsCommitted(): void {
|
|
315
|
+
this._version += this._uncommittedEvents.length;
|
|
316
|
+
this._uncommittedEvents = [];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Check if there are uncommitted events
|
|
321
|
+
*/
|
|
322
|
+
hasUncommittedEvents(): boolean {
|
|
323
|
+
return this._uncommittedEvents.length > 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get snapshot of current state
|
|
328
|
+
*/
|
|
329
|
+
getSnapshot(): { aggregateId: string; aggregateType: string; version: number; state: TState } {
|
|
330
|
+
return {
|
|
331
|
+
aggregateId: this._id,
|
|
332
|
+
aggregateType: this.getAggregateType(),
|
|
333
|
+
version: this._version,
|
|
334
|
+
state: this._state,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Decorator to mark a method as an event applier
|
|
341
|
+
*/
|
|
342
|
+
export function Apply(eventType: string): MethodDecorator {
|
|
343
|
+
return (target, propertyKey, descriptor) => {
|
|
344
|
+
const appliers = Reflect.getMetadata('event:appliers', target.constructor) || new Map();
|
|
345
|
+
appliers.set(eventType, propertyKey);
|
|
346
|
+
Reflect.defineMetadata('event:appliers', appliers, target.constructor);
|
|
347
|
+
return descriptor;
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Helper to create an aggregate class with automatic event application
|
|
353
|
+
*/
|
|
354
|
+
export function createAggregate<TState>(config: {
|
|
355
|
+
aggregateType: string;
|
|
356
|
+
initialState: () => TState;
|
|
357
|
+
appliers: Record<string, (state: TState, payload: any) => TState>;
|
|
358
|
+
}): new (id: string) => AggregateRoot<TState> {
|
|
359
|
+
return class extends AggregateRoot<TState> {
|
|
360
|
+
getAggregateType(): string {
|
|
361
|
+
return config.aggregateType;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
protected getInitialState(): TState {
|
|
365
|
+
return config.initialState();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
protected applyEvent(event: DomainEvent): void {
|
|
369
|
+
const applier = config.appliers[event.eventType];
|
|
370
|
+
if (applier) {
|
|
371
|
+
this._state = applier(this._state, event.payload);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
`;
|
|
377
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'aggregate-root.ts'), content);
|
|
378
|
+
console.log(chalk_1.default.green(' ✓ Aggregate root'));
|
|
379
|
+
}
|
|
380
|
+
async function generateEventStore(esPath, options) {
|
|
381
|
+
const content = `import { Injectable, Logger } from '@nestjs/common';
|
|
382
|
+
import {
|
|
383
|
+
DomainEvent,
|
|
384
|
+
StoredEvent,
|
|
385
|
+
EventStream,
|
|
386
|
+
AppendResult,
|
|
387
|
+
ReadOptions,
|
|
388
|
+
serializeEvent,
|
|
389
|
+
deserializeEvent,
|
|
390
|
+
} from '../event.types';
|
|
391
|
+
|
|
392
|
+
export interface IEventStore {
|
|
393
|
+
append(aggregateId: string, events: DomainEvent[], expectedVersion: number): Promise<AppendResult>;
|
|
394
|
+
getEvents(aggregateId: string, options?: ReadOptions): Promise<StoredEvent[]>;
|
|
395
|
+
getEventStream(aggregateId: string): Promise<EventStream | null>;
|
|
396
|
+
getAllEvents(options?: { fromPosition?: number; limit?: number }): Promise<StoredEvent[]>;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* In-memory event store (for development/testing)
|
|
401
|
+
*/
|
|
402
|
+
@Injectable()
|
|
403
|
+
export class InMemoryEventStore implements IEventStore {
|
|
404
|
+
private readonly logger = new Logger(InMemoryEventStore.name);
|
|
405
|
+
private events: Map<string, StoredEvent[]> = new Map();
|
|
406
|
+
private allEvents: StoredEvent[] = [];
|
|
407
|
+
private sequence: number = 0;
|
|
408
|
+
|
|
409
|
+
async append(
|
|
410
|
+
aggregateId: string,
|
|
411
|
+
events: DomainEvent[],
|
|
412
|
+
expectedVersion: number
|
|
413
|
+
): Promise<AppendResult> {
|
|
414
|
+
const existingEvents = this.events.get(aggregateId) || [];
|
|
415
|
+
const currentVersion = existingEvents.length > 0
|
|
416
|
+
? existingEvents[existingEvents.length - 1].version
|
|
417
|
+
: 0;
|
|
418
|
+
|
|
419
|
+
// Optimistic concurrency check
|
|
420
|
+
if (currentVersion !== expectedVersion) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
\`Concurrency conflict: expected version \${expectedVersion}, but found \${currentVersion}\`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const storedEvents: StoredEvent[] = events.map((event, index) => ({
|
|
427
|
+
...event,
|
|
428
|
+
id: \`\${aggregateId}_\${currentVersion + index + 1}\`,
|
|
429
|
+
version: currentVersion + index + 1,
|
|
430
|
+
sequence: ++this.sequence,
|
|
431
|
+
createdAt: new Date(),
|
|
432
|
+
}));
|
|
433
|
+
|
|
434
|
+
this.events.set(aggregateId, [...existingEvents, ...storedEvents]);
|
|
435
|
+
this.allEvents.push(...storedEvents);
|
|
436
|
+
|
|
437
|
+
this.logger.debug(\`Appended \${events.length} events to aggregate \${aggregateId}\`);
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
success: true,
|
|
441
|
+
nextVersion: currentVersion + events.length,
|
|
442
|
+
position: this.sequence,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async getEvents(aggregateId: string, options?: ReadOptions): Promise<StoredEvent[]> {
|
|
447
|
+
let events = this.events.get(aggregateId) || [];
|
|
448
|
+
|
|
449
|
+
if (options?.fromVersion !== undefined) {
|
|
450
|
+
events = events.filter(e => e.version >= options.fromVersion!);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (options?.toVersion !== undefined) {
|
|
454
|
+
events = events.filter(e => e.version <= options.toVersion!);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (options?.limit !== undefined) {
|
|
458
|
+
events = events.slice(0, options.limit);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return events;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async getEventStream(aggregateId: string): Promise<EventStream | null> {
|
|
465
|
+
const events = this.events.get(aggregateId);
|
|
466
|
+
if (!events || events.length === 0) return null;
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
aggregateId,
|
|
470
|
+
aggregateType: events[0].aggregateType,
|
|
471
|
+
version: events[events.length - 1].version,
|
|
472
|
+
events,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async getAllEvents(options?: { fromPosition?: number; limit?: number }): Promise<StoredEvent[]> {
|
|
477
|
+
let events = [...this.allEvents];
|
|
478
|
+
|
|
479
|
+
if (options?.fromPosition !== undefined) {
|
|
480
|
+
events = events.filter(e => e.sequence > options.fromPosition!);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (options?.limit !== undefined) {
|
|
484
|
+
events = events.slice(0, options.limit);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return events;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// For testing
|
|
491
|
+
clear(): void {
|
|
492
|
+
this.events.clear();
|
|
493
|
+
this.allEvents = [];
|
|
494
|
+
this.sequence = 0;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* PostgreSQL event store implementation
|
|
500
|
+
*/
|
|
501
|
+
@Injectable()
|
|
502
|
+
export class PostgresEventStore implements IEventStore {
|
|
503
|
+
private readonly logger = new Logger(PostgresEventStore.name);
|
|
504
|
+
|
|
505
|
+
constructor(
|
|
506
|
+
// Inject your database connection/repository here
|
|
507
|
+
// private readonly eventRepository: Repository<EventEntity>
|
|
508
|
+
) {}
|
|
509
|
+
|
|
510
|
+
async append(
|
|
511
|
+
aggregateId: string,
|
|
512
|
+
events: DomainEvent[],
|
|
513
|
+
expectedVersion: number
|
|
514
|
+
): Promise<AppendResult> {
|
|
515
|
+
// Implementation would use a database transaction with optimistic locking
|
|
516
|
+
// Example SQL:
|
|
517
|
+
// BEGIN;
|
|
518
|
+
// SELECT version FROM event_streams WHERE aggregate_id = $1 FOR UPDATE;
|
|
519
|
+
// -- Check version matches expectedVersion
|
|
520
|
+
// INSERT INTO events (aggregate_id, event_type, payload, version, ...)
|
|
521
|
+
// UPDATE event_streams SET version = $newVersion WHERE aggregate_id = $1;
|
|
522
|
+
// COMMIT;
|
|
523
|
+
|
|
524
|
+
throw new Error('PostgresEventStore not fully implemented - inject your database connection');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async getEvents(aggregateId: string, options?: ReadOptions): Promise<StoredEvent[]> {
|
|
528
|
+
throw new Error('PostgresEventStore not fully implemented');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async getEventStream(aggregateId: string): Promise<EventStream | null> {
|
|
532
|
+
throw new Error('PostgresEventStore not fully implemented');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async getAllEvents(options?: { fromPosition?: number; limit?: number }): Promise<StoredEvent[]> {
|
|
536
|
+
throw new Error('PostgresEventStore not fully implemented');
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Event store factory
|
|
542
|
+
*/
|
|
543
|
+
export const EVENT_STORE = Symbol('EVENT_STORE');
|
|
544
|
+
|
|
545
|
+
export function createEventStore(type: 'memory' | 'postgres' | 'mongodb' = 'memory'): IEventStore {
|
|
546
|
+
switch (type) {
|
|
547
|
+
case 'memory':
|
|
548
|
+
return new InMemoryEventStore();
|
|
549
|
+
case 'postgres':
|
|
550
|
+
return new PostgresEventStore();
|
|
551
|
+
default:
|
|
552
|
+
return new InMemoryEventStore();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
`;
|
|
556
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'stores/event.store.ts'), content);
|
|
557
|
+
console.log(chalk_1.default.green(' ✓ Event store'));
|
|
558
|
+
}
|
|
559
|
+
async function generateSnapshotStore(esPath, options) {
|
|
560
|
+
const content = `import { Injectable, Logger } from '@nestjs/common';
|
|
561
|
+
import { Snapshot } from '../event.types';
|
|
562
|
+
|
|
563
|
+
export interface ISnapshotStore {
|
|
564
|
+
save(snapshot: Snapshot): Promise<void>;
|
|
565
|
+
get(aggregateId: string, aggregateType: string): Promise<Snapshot | null>;
|
|
566
|
+
getLatest(aggregateId: string): Promise<Snapshot | null>;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* In-memory snapshot store
|
|
571
|
+
*/
|
|
572
|
+
@Injectable()
|
|
573
|
+
export class InMemorySnapshotStore implements ISnapshotStore {
|
|
574
|
+
private readonly logger = new Logger(InMemorySnapshotStore.name);
|
|
575
|
+
private snapshots: Map<string, Snapshot[]> = new Map();
|
|
576
|
+
|
|
577
|
+
async save(snapshot: Snapshot): Promise<void> {
|
|
578
|
+
const key = \`\${snapshot.aggregateType}:\${snapshot.aggregateId}\`;
|
|
579
|
+
const existing = this.snapshots.get(key) || [];
|
|
580
|
+
existing.push(snapshot);
|
|
581
|
+
this.snapshots.set(key, existing);
|
|
582
|
+
this.logger.debug(\`Saved snapshot for \${key} at version \${snapshot.version}\`);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async get(aggregateId: string, aggregateType: string): Promise<Snapshot | null> {
|
|
586
|
+
const key = \`\${aggregateType}:\${aggregateId}\`;
|
|
587
|
+
const snapshots = this.snapshots.get(key);
|
|
588
|
+
return snapshots?.[snapshots.length - 1] || null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async getLatest(aggregateId: string): Promise<Snapshot | null> {
|
|
592
|
+
for (const [key, snapshots] of this.snapshots) {
|
|
593
|
+
if (key.endsWith(\`:\${aggregateId}\`)) {
|
|
594
|
+
return snapshots[snapshots.length - 1];
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// For testing
|
|
601
|
+
clear(): void {
|
|
602
|
+
this.snapshots.clear();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Snapshot strategy - determines when to take snapshots
|
|
608
|
+
*/
|
|
609
|
+
export interface SnapshotStrategy {
|
|
610
|
+
shouldSnapshot(aggregateId: string, version: number, eventsSinceSnapshot: number): boolean;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Take snapshot every N events
|
|
615
|
+
*/
|
|
616
|
+
export class EventCountSnapshotStrategy implements SnapshotStrategy {
|
|
617
|
+
constructor(private readonly threshold: number = ${options.snapshotThreshold || 100}) {}
|
|
618
|
+
|
|
619
|
+
shouldSnapshot(aggregateId: string, version: number, eventsSinceSnapshot: number): boolean {
|
|
620
|
+
return eventsSinceSnapshot >= this.threshold;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Take snapshot at specific version intervals
|
|
626
|
+
*/
|
|
627
|
+
export class VersionIntervalSnapshotStrategy implements SnapshotStrategy {
|
|
628
|
+
constructor(private readonly interval: number = 100) {}
|
|
629
|
+
|
|
630
|
+
shouldSnapshot(aggregateId: string, version: number, eventsSinceSnapshot: number): boolean {
|
|
631
|
+
return version % this.interval === 0;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Snapshot store factory
|
|
637
|
+
*/
|
|
638
|
+
export const SNAPSHOT_STORE = Symbol('SNAPSHOT_STORE');
|
|
639
|
+
|
|
640
|
+
export function createSnapshotStore(type: 'memory' | 'postgres' | 'redis' = 'memory'): ISnapshotStore {
|
|
641
|
+
switch (type) {
|
|
642
|
+
case 'memory':
|
|
643
|
+
return new InMemorySnapshotStore();
|
|
644
|
+
default:
|
|
645
|
+
return new InMemorySnapshotStore();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
`;
|
|
649
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'stores/snapshot.store.ts'), content);
|
|
650
|
+
console.log(chalk_1.default.green(' ✓ Snapshot store'));
|
|
651
|
+
}
|
|
652
|
+
async function generateEventBus(esPath) {
|
|
653
|
+
const content = `import { Injectable, Logger, Type } from '@nestjs/common';
|
|
654
|
+
import { ModuleRef } from '@nestjs/core';
|
|
655
|
+
import { DomainEvent, EventHandler } from './event.types';
|
|
656
|
+
|
|
657
|
+
export interface IEventBus {
|
|
658
|
+
publish<T>(event: DomainEvent<T>): Promise<void>;
|
|
659
|
+
publishAll(events: DomainEvent[]): Promise<void>;
|
|
660
|
+
subscribe<T>(eventType: string, handler: EventHandler<T>): void;
|
|
661
|
+
unsubscribe(eventType: string, handler: EventHandler): void;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Simple in-process event bus
|
|
666
|
+
*/
|
|
667
|
+
@Injectable()
|
|
668
|
+
export class EventBus implements IEventBus {
|
|
669
|
+
private readonly logger = new Logger(EventBus.name);
|
|
670
|
+
private handlers: Map<string, Set<EventHandler>> = new Map();
|
|
671
|
+
|
|
672
|
+
constructor(private readonly moduleRef: ModuleRef) {}
|
|
673
|
+
|
|
674
|
+
async publish<T>(event: DomainEvent<T>): Promise<void> {
|
|
675
|
+
const handlers = this.handlers.get(event.eventType);
|
|
676
|
+
if (!handlers || handlers.size === 0) {
|
|
677
|
+
this.logger.debug(\`No handlers for event type: \${event.eventType}\`);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
this.logger.debug(\`Publishing event \${event.eventType} to \${handlers.size} handlers\`);
|
|
682
|
+
|
|
683
|
+
const promises = Array.from(handlers).map(async (handler) => {
|
|
684
|
+
try {
|
|
685
|
+
await handler(event);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
this.logger.error(
|
|
688
|
+
\`Error in event handler for \${event.eventType}: \${(error as Error).message}\`,
|
|
689
|
+
(error as Error).stack
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
await Promise.all(promises);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async publishAll(events: DomainEvent[]): Promise<void> {
|
|
698
|
+
for (const event of events) {
|
|
699
|
+
await this.publish(event);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
subscribe<T>(eventType: string, handler: EventHandler<T>): void {
|
|
704
|
+
if (!this.handlers.has(eventType)) {
|
|
705
|
+
this.handlers.set(eventType, new Set());
|
|
706
|
+
}
|
|
707
|
+
this.handlers.get(eventType)!.add(handler);
|
|
708
|
+
this.logger.debug(\`Subscribed handler to event type: \${eventType}\`);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
unsubscribe(eventType: string, handler: EventHandler): void {
|
|
712
|
+
const handlers = this.handlers.get(eventType);
|
|
713
|
+
if (handlers) {
|
|
714
|
+
handlers.delete(handler);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Register a handler class
|
|
720
|
+
*/
|
|
721
|
+
registerHandler(handler: Type<IEventHandler>): void {
|
|
722
|
+
const instance = this.moduleRef.get(handler, { strict: false });
|
|
723
|
+
const eventTypes = Reflect.getMetadata('event:handles', handler) || [];
|
|
724
|
+
|
|
725
|
+
for (const eventType of eventTypes) {
|
|
726
|
+
this.subscribe(eventType, instance.handle.bind(instance));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Event handler interface
|
|
733
|
+
*/
|
|
734
|
+
export interface IEventHandler<T = any> {
|
|
735
|
+
handle(event: DomainEvent<T>): Promise<void> | void;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Decorator to mark a class as an event handler
|
|
740
|
+
*/
|
|
741
|
+
export function EventsHandler(...eventTypes: string[]): ClassDecorator {
|
|
742
|
+
return (target) => {
|
|
743
|
+
Reflect.defineMetadata('event:handles', eventTypes, target);
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Event bus with retry support
|
|
749
|
+
*/
|
|
750
|
+
@Injectable()
|
|
751
|
+
export class RetryableEventBus extends EventBus {
|
|
752
|
+
private readonly maxRetries = 3;
|
|
753
|
+
private readonly retryDelay = 1000;
|
|
754
|
+
|
|
755
|
+
async publish<T>(event: DomainEvent<T>): Promise<void> {
|
|
756
|
+
const handlers = this['handlers'].get(event.eventType);
|
|
757
|
+
if (!handlers || handlers.size === 0) return;
|
|
758
|
+
|
|
759
|
+
const promises = Array.from(handlers).map(async (handler) => {
|
|
760
|
+
let lastError: Error | undefined;
|
|
761
|
+
|
|
762
|
+
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
763
|
+
try {
|
|
764
|
+
await handler(event);
|
|
765
|
+
return;
|
|
766
|
+
} catch (error) {
|
|
767
|
+
lastError = error as Error;
|
|
768
|
+
if (attempt < this.maxRetries - 1) {
|
|
769
|
+
await this.delay(this.retryDelay * Math.pow(2, attempt));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
this['logger'].error(
|
|
775
|
+
\`Failed to handle event \${event.eventType} after \${this.maxRetries} attempts: \${lastError?.message}\`
|
|
776
|
+
);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
await Promise.all(promises);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
private delay(ms: number): Promise<void> {
|
|
783
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
`;
|
|
787
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'event-bus.ts'), content);
|
|
788
|
+
console.log(chalk_1.default.green(' ✓ Event bus'));
|
|
789
|
+
}
|
|
790
|
+
async function generateProjectionManager(esPath) {
|
|
791
|
+
const content = `import { Injectable, Logger, Type, OnModuleInit } from '@nestjs/common';
|
|
792
|
+
import { ModuleRef } from '@nestjs/core';
|
|
793
|
+
import { DomainEvent, StoredEvent } from '../event.types';
|
|
794
|
+
import { IEventStore, EVENT_STORE } from '../stores/event.store';
|
|
795
|
+
import { Inject } from '@nestjs/common';
|
|
796
|
+
|
|
797
|
+
export interface IProjection {
|
|
798
|
+
name: string;
|
|
799
|
+
handle(event: DomainEvent): Promise<void> | void;
|
|
800
|
+
getPosition(): Promise<number>;
|
|
801
|
+
setPosition(position: number): Promise<void>;
|
|
802
|
+
rebuild?(): Promise<void>;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Base projection class
|
|
807
|
+
*/
|
|
808
|
+
export abstract class BaseProjection implements IProjection {
|
|
809
|
+
abstract name: string;
|
|
810
|
+
protected position: number = 0;
|
|
811
|
+
|
|
812
|
+
abstract handle(event: DomainEvent): Promise<void> | void;
|
|
813
|
+
|
|
814
|
+
async getPosition(): Promise<number> {
|
|
815
|
+
return this.position;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
async setPosition(position: number): Promise<void> {
|
|
819
|
+
this.position = position;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async rebuild(): Promise<void> {
|
|
823
|
+
this.position = 0;
|
|
824
|
+
// Override in subclass to clear projection state
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Manages projections and keeps them up to date
|
|
830
|
+
*/
|
|
831
|
+
@Injectable()
|
|
832
|
+
export class ProjectionManager implements OnModuleInit {
|
|
833
|
+
private readonly logger = new Logger(ProjectionManager.name);
|
|
834
|
+
private projections: Map<string, IProjection> = new Map();
|
|
835
|
+
private running: boolean = false;
|
|
836
|
+
private pollInterval: number = 1000;
|
|
837
|
+
|
|
838
|
+
constructor(
|
|
839
|
+
@Inject(EVENT_STORE) private readonly eventStore: IEventStore,
|
|
840
|
+
private readonly moduleRef: ModuleRef
|
|
841
|
+
) {}
|
|
842
|
+
|
|
843
|
+
async onModuleInit() {
|
|
844
|
+
// Auto-discover and register projections
|
|
845
|
+
// In practice, you'd scan for classes decorated with @Projection
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Register a projection
|
|
850
|
+
*/
|
|
851
|
+
register(projection: IProjection): void {
|
|
852
|
+
this.projections.set(projection.name, projection);
|
|
853
|
+
this.logger.log(\`Registered projection: \${projection.name}\`);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Start processing events for all projections
|
|
858
|
+
*/
|
|
859
|
+
async start(): Promise<void> {
|
|
860
|
+
if (this.running) return;
|
|
861
|
+
this.running = true;
|
|
862
|
+
this.logger.log('Starting projection manager');
|
|
863
|
+
this.poll();
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Stop processing events
|
|
868
|
+
*/
|
|
869
|
+
stop(): void {
|
|
870
|
+
this.running = false;
|
|
871
|
+
this.logger.log('Stopped projection manager');
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Rebuild a specific projection
|
|
876
|
+
*/
|
|
877
|
+
async rebuild(projectionName: string): Promise<void> {
|
|
878
|
+
const projection = this.projections.get(projectionName);
|
|
879
|
+
if (!projection) {
|
|
880
|
+
throw new Error(\`Projection not found: \${projectionName}\`);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
this.logger.log(\`Rebuilding projection: \${projectionName}\`);
|
|
884
|
+
|
|
885
|
+
await projection.rebuild?.();
|
|
886
|
+
await this.catchUp(projection);
|
|
887
|
+
|
|
888
|
+
this.logger.log(\`Rebuilt projection: \${projectionName}\`);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Rebuild all projections
|
|
893
|
+
*/
|
|
894
|
+
async rebuildAll(): Promise<void> {
|
|
895
|
+
for (const [name, projection] of this.projections) {
|
|
896
|
+
await this.rebuild(name);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Poll for new events and process them
|
|
902
|
+
*/
|
|
903
|
+
private async poll(): Promise<void> {
|
|
904
|
+
while (this.running) {
|
|
905
|
+
try {
|
|
906
|
+
for (const [name, projection] of this.projections) {
|
|
907
|
+
await this.catchUp(projection);
|
|
908
|
+
}
|
|
909
|
+
} catch (error) {
|
|
910
|
+
this.logger.error(\`Error polling events: \${(error as Error).message}\`);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
await this.delay(this.pollInterval);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Catch up a projection to current position
|
|
919
|
+
*/
|
|
920
|
+
private async catchUp(projection: IProjection): Promise<void> {
|
|
921
|
+
const currentPosition = await projection.getPosition();
|
|
922
|
+
const events = await this.eventStore.getAllEvents({
|
|
923
|
+
fromPosition: currentPosition,
|
|
924
|
+
limit: 100,
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
for (const event of events) {
|
|
928
|
+
try {
|
|
929
|
+
await projection.handle(event);
|
|
930
|
+
await projection.setPosition(event.sequence);
|
|
931
|
+
} catch (error) {
|
|
932
|
+
this.logger.error(
|
|
933
|
+
\`Error processing event \${event.eventId} in projection \${projection.name}: \${(error as Error).message}\`
|
|
934
|
+
);
|
|
935
|
+
throw error;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
private delay(ms: number): Promise<void> {
|
|
941
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Decorator to mark a class as a projection
|
|
947
|
+
*/
|
|
948
|
+
export function Projection(name: string): ClassDecorator {
|
|
949
|
+
return (target) => {
|
|
950
|
+
Reflect.defineMetadata('projection:name', name, target);
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Decorator to mark a method as handling specific event types
|
|
956
|
+
*/
|
|
957
|
+
export function Handles(...eventTypes: string[]): MethodDecorator {
|
|
958
|
+
return (target, propertyKey, descriptor) => {
|
|
959
|
+
const existing = Reflect.getMetadata('projection:handles', target.constructor) || new Map();
|
|
960
|
+
for (const eventType of eventTypes) {
|
|
961
|
+
existing.set(eventType, propertyKey);
|
|
962
|
+
}
|
|
963
|
+
Reflect.defineMetadata('projection:handles', existing, target.constructor);
|
|
964
|
+
return descriptor;
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
`;
|
|
968
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'projections/projection.manager.ts'), content);
|
|
969
|
+
console.log(chalk_1.default.green(' ✓ Projection manager'));
|
|
970
|
+
}
|
|
971
|
+
async function generateDecorators(esPath) {
|
|
972
|
+
const content = `/**
|
|
973
|
+
* Event Sourcing Decorators
|
|
974
|
+
*/
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Mark a class as an Event Sourced Aggregate
|
|
978
|
+
*/
|
|
979
|
+
export function Aggregate(name: string): ClassDecorator {
|
|
980
|
+
return (target) => {
|
|
981
|
+
Reflect.defineMetadata('aggregate:name', name, target);
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Mark a method as a command handler
|
|
987
|
+
*/
|
|
988
|
+
export function CommandHandler(commandType: string): MethodDecorator {
|
|
989
|
+
return (target, propertyKey, descriptor) => {
|
|
990
|
+
const handlers = Reflect.getMetadata('aggregate:commands', target.constructor) || new Map();
|
|
991
|
+
handlers.set(commandType, propertyKey);
|
|
992
|
+
Reflect.defineMetadata('aggregate:commands', handlers, target.constructor);
|
|
993
|
+
return descriptor;
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Mark a method as an event applier
|
|
999
|
+
*/
|
|
1000
|
+
export function ApplyEvent(eventType: string): MethodDecorator {
|
|
1001
|
+
return (target, propertyKey, descriptor) => {
|
|
1002
|
+
const appliers = Reflect.getMetadata('aggregate:appliers', target.constructor) || new Map();
|
|
1003
|
+
appliers.set(eventType, propertyKey);
|
|
1004
|
+
Reflect.defineMetadata('aggregate:appliers', appliers, target.constructor);
|
|
1005
|
+
return descriptor;
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Mark a class as an event handler (saga/process manager)
|
|
1011
|
+
*/
|
|
1012
|
+
export function ProcessManager(name: string): ClassDecorator {
|
|
1013
|
+
return (target) => {
|
|
1014
|
+
Reflect.defineMetadata('process-manager:name', name, target);
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Subscribe to events in a process manager
|
|
1020
|
+
*/
|
|
1021
|
+
export function OnEvent(eventType: string): MethodDecorator {
|
|
1022
|
+
return (target, propertyKey, descriptor) => {
|
|
1023
|
+
const handlers = Reflect.getMetadata('process-manager:handlers', target.constructor) || new Map();
|
|
1024
|
+
handlers.set(eventType, propertyKey);
|
|
1025
|
+
Reflect.defineMetadata('process-manager:handlers', handlers, target.constructor);
|
|
1026
|
+
return descriptor;
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Mark a property as the aggregate state
|
|
1032
|
+
*/
|
|
1033
|
+
export function State(): PropertyDecorator {
|
|
1034
|
+
return (target, propertyKey) => {
|
|
1035
|
+
Reflect.defineMetadata('aggregate:state', propertyKey, target.constructor);
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
export * from './projections/projection.manager';
|
|
1040
|
+
export { EventsHandler } from './event-bus';
|
|
1041
|
+
`;
|
|
1042
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'decorators/index.ts'), content);
|
|
1043
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'decorators.ts'), `export * from './decorators/index';`);
|
|
1044
|
+
console.log(chalk_1.default.green(' ✓ Decorators'));
|
|
1045
|
+
}
|
|
1046
|
+
async function generateEventSourcingModule(esPath) {
|
|
1047
|
+
const content = `import { Module, Global, DynamicModule } from '@nestjs/common';
|
|
1048
|
+
import { InMemoryEventStore, EVENT_STORE, IEventStore } from './stores/event.store';
|
|
1049
|
+
import { InMemorySnapshotStore, SNAPSHOT_STORE, ISnapshotStore } from './stores/snapshot.store';
|
|
1050
|
+
import { EventBus } from './event-bus';
|
|
1051
|
+
import { ProjectionManager } from './projections/projection.manager';
|
|
1052
|
+
|
|
1053
|
+
export interface EventSourcingModuleOptions {
|
|
1054
|
+
eventStore?: 'memory' | 'postgres' | 'mongodb';
|
|
1055
|
+
snapshotStore?: 'memory' | 'postgres' | 'redis';
|
|
1056
|
+
snapshotThreshold?: number;
|
|
1057
|
+
autoStartProjections?: boolean;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
@Global()
|
|
1061
|
+
@Module({})
|
|
1062
|
+
export class EventSourcingModule {
|
|
1063
|
+
static forRoot(options: EventSourcingModuleOptions = {}): DynamicModule {
|
|
1064
|
+
const eventStoreProvider = {
|
|
1065
|
+
provide: EVENT_STORE,
|
|
1066
|
+
useClass: InMemoryEventStore, // Replace based on options
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
const snapshotStoreProvider = {
|
|
1070
|
+
provide: SNAPSHOT_STORE,
|
|
1071
|
+
useClass: InMemorySnapshotStore, // Replace based on options
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
return {
|
|
1075
|
+
module: EventSourcingModule,
|
|
1076
|
+
providers: [
|
|
1077
|
+
eventStoreProvider,
|
|
1078
|
+
snapshotStoreProvider,
|
|
1079
|
+
EventBus,
|
|
1080
|
+
ProjectionManager,
|
|
1081
|
+
{
|
|
1082
|
+
provide: 'EVENT_SOURCING_OPTIONS',
|
|
1083
|
+
useValue: options,
|
|
1084
|
+
},
|
|
1085
|
+
],
|
|
1086
|
+
exports: [
|
|
1087
|
+
EVENT_STORE,
|
|
1088
|
+
SNAPSHOT_STORE,
|
|
1089
|
+
EventBus,
|
|
1090
|
+
ProjectionManager,
|
|
1091
|
+
],
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
static forFeature(aggregates: any[] = []): DynamicModule {
|
|
1096
|
+
return {
|
|
1097
|
+
module: EventSourcingModule,
|
|
1098
|
+
providers: [...aggregates],
|
|
1099
|
+
exports: [...aggregates],
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
`;
|
|
1104
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'event-sourcing.module.ts'), content);
|
|
1105
|
+
console.log(chalk_1.default.green(' ✓ Event sourcing module'));
|
|
1106
|
+
}
|
|
1107
|
+
//# sourceMappingURL=event-sourcing-full.js.map
|