nestjs-ddd-cli 2.2.0 → 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 +26 -3
- 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,889 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.applyEventSourcingRecipe = applyEventSourcingRecipe;
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const file_utils_1 = require("../../utils/file.utils");
|
|
43
|
+
async function applyEventSourcingRecipe(basePath) {
|
|
44
|
+
const sharedPath = path.join(basePath, 'src/shared');
|
|
45
|
+
const esPath = path.join(sharedPath, 'event-sourcing');
|
|
46
|
+
await (0, file_utils_1.ensureDir)(esPath);
|
|
47
|
+
await (0, file_utils_1.ensureDir)(path.join(esPath, 'store'));
|
|
48
|
+
await (0, file_utils_1.ensureDir)(path.join(esPath, 'aggregate'));
|
|
49
|
+
await (0, file_utils_1.ensureDir)(path.join(esPath, 'projections'));
|
|
50
|
+
// Event sourcing types
|
|
51
|
+
const typesContent = `export interface DomainEvent<T = any> {
|
|
52
|
+
eventId: string;
|
|
53
|
+
eventType: string;
|
|
54
|
+
aggregateId: string;
|
|
55
|
+
aggregateType: string;
|
|
56
|
+
version: number;
|
|
57
|
+
timestamp: Date;
|
|
58
|
+
payload: T;
|
|
59
|
+
metadata: EventMetadata;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface EventMetadata {
|
|
63
|
+
correlationId?: string;
|
|
64
|
+
causationId?: string;
|
|
65
|
+
userId?: string;
|
|
66
|
+
tenantId?: string;
|
|
67
|
+
[key: string]: any;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface StoredEvent {
|
|
71
|
+
id: string;
|
|
72
|
+
streamId: string;
|
|
73
|
+
eventType: string;
|
|
74
|
+
version: number;
|
|
75
|
+
data: string;
|
|
76
|
+
metadata: string;
|
|
77
|
+
timestamp: Date;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface EventStream {
|
|
81
|
+
streamId: string;
|
|
82
|
+
version: number;
|
|
83
|
+
events: DomainEvent[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface Snapshot<T = any> {
|
|
87
|
+
aggregateId: string;
|
|
88
|
+
aggregateType: string;
|
|
89
|
+
version: number;
|
|
90
|
+
state: T;
|
|
91
|
+
timestamp: Date;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ProjectionState<T = any> {
|
|
95
|
+
projectionName: string;
|
|
96
|
+
position: number;
|
|
97
|
+
state: T;
|
|
98
|
+
lastUpdated: Date;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type EventHandler<T = any> = (event: DomainEvent<T>) => Promise<void>;
|
|
102
|
+
|
|
103
|
+
export interface Projection {
|
|
104
|
+
name: string;
|
|
105
|
+
init(): Promise<void>;
|
|
106
|
+
handle(event: DomainEvent): Promise<void>;
|
|
107
|
+
getPosition(): Promise<number>;
|
|
108
|
+
reset(): Promise<void>;
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'event-sourcing.types.ts'), typesContent);
|
|
112
|
+
// Event Store
|
|
113
|
+
const eventStoreContent = `import { Injectable, Logger } from "@nestjs/common";
|
|
114
|
+
import { v4 as uuid } from "uuid";
|
|
115
|
+
import { DomainEvent, StoredEvent, EventStream, EventMetadata } from "../event-sourcing.types";
|
|
116
|
+
|
|
117
|
+
export interface EventStoreConfig {
|
|
118
|
+
snapshotFrequency?: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@Injectable()
|
|
122
|
+
export class EventStore {
|
|
123
|
+
private readonly logger = new Logger(EventStore.name);
|
|
124
|
+
private events: Map<string, StoredEvent[]> = new Map();
|
|
125
|
+
private globalPosition = 0;
|
|
126
|
+
private subscribers: Map<string, Array<(event: DomainEvent) => Promise<void>>> = new Map();
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Append events to a stream
|
|
130
|
+
*/
|
|
131
|
+
async append(
|
|
132
|
+
streamId: string,
|
|
133
|
+
events: Array<{ eventType: string; payload: any }>,
|
|
134
|
+
expectedVersion: number,
|
|
135
|
+
metadata: EventMetadata = {}
|
|
136
|
+
): Promise<DomainEvent[]> {
|
|
137
|
+
const stream = this.events.get(streamId) || [];
|
|
138
|
+
const currentVersion = stream.length;
|
|
139
|
+
|
|
140
|
+
// Optimistic concurrency check
|
|
141
|
+
if (expectedVersion !== -1 && expectedVersion !== currentVersion) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
\`Concurrency conflict: expected version \${expectedVersion}, but stream is at \${currentVersion}\`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const domainEvents: DomainEvent[] = [];
|
|
148
|
+
|
|
149
|
+
for (let i = 0; i < events.length; i++) {
|
|
150
|
+
const { eventType, payload } = events[i];
|
|
151
|
+
const version = currentVersion + i + 1;
|
|
152
|
+
this.globalPosition++;
|
|
153
|
+
|
|
154
|
+
const event: DomainEvent = {
|
|
155
|
+
eventId: uuid(),
|
|
156
|
+
eventType,
|
|
157
|
+
aggregateId: streamId.split("-")[1] || streamId,
|
|
158
|
+
aggregateType: streamId.split("-")[0],
|
|
159
|
+
version,
|
|
160
|
+
timestamp: new Date(),
|
|
161
|
+
payload,
|
|
162
|
+
metadata: {
|
|
163
|
+
...metadata,
|
|
164
|
+
position: this.globalPosition,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const storedEvent: StoredEvent = {
|
|
169
|
+
id: event.eventId,
|
|
170
|
+
streamId,
|
|
171
|
+
eventType,
|
|
172
|
+
version,
|
|
173
|
+
data: JSON.stringify(payload),
|
|
174
|
+
metadata: JSON.stringify(event.metadata),
|
|
175
|
+
timestamp: event.timestamp,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
stream.push(storedEvent);
|
|
179
|
+
domainEvents.push(event);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.events.set(streamId, stream);
|
|
183
|
+
|
|
184
|
+
// Notify subscribers
|
|
185
|
+
for (const event of domainEvents) {
|
|
186
|
+
await this.notifySubscribers(event);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.logger.debug(\`Appended \${events.length} events to stream \${streamId}\`);
|
|
190
|
+
return domainEvents;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Read events from a stream
|
|
195
|
+
*/
|
|
196
|
+
async readStream(
|
|
197
|
+
streamId: string,
|
|
198
|
+
fromVersion: number = 0,
|
|
199
|
+
toVersion?: number
|
|
200
|
+
): Promise<EventStream> {
|
|
201
|
+
const stored = this.events.get(streamId) || [];
|
|
202
|
+
const filtered = stored.filter(
|
|
203
|
+
(e) => e.version > fromVersion && (!toVersion || e.version <= toVersion)
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const events: DomainEvent[] = filtered.map((e) => ({
|
|
207
|
+
eventId: e.id,
|
|
208
|
+
eventType: e.eventType,
|
|
209
|
+
aggregateId: streamId.split("-")[1] || streamId,
|
|
210
|
+
aggregateType: streamId.split("-")[0],
|
|
211
|
+
version: e.version,
|
|
212
|
+
timestamp: e.timestamp,
|
|
213
|
+
payload: JSON.parse(e.data),
|
|
214
|
+
metadata: JSON.parse(e.metadata),
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
streamId,
|
|
219
|
+
version: stored.length,
|
|
220
|
+
events,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Read all events from a position (for projections)
|
|
226
|
+
*/
|
|
227
|
+
async readAll(fromPosition: number = 0, limit: number = 100): Promise<DomainEvent[]> {
|
|
228
|
+
const allEvents: DomainEvent[] = [];
|
|
229
|
+
|
|
230
|
+
for (const [streamId, stored] of this.events) {
|
|
231
|
+
for (const e of stored) {
|
|
232
|
+
const metadata = JSON.parse(e.metadata);
|
|
233
|
+
if (metadata.position > fromPosition) {
|
|
234
|
+
allEvents.push({
|
|
235
|
+
eventId: e.id,
|
|
236
|
+
eventType: e.eventType,
|
|
237
|
+
aggregateId: streamId.split("-")[1] || streamId,
|
|
238
|
+
aggregateType: streamId.split("-")[0],
|
|
239
|
+
version: e.version,
|
|
240
|
+
timestamp: e.timestamp,
|
|
241
|
+
payload: JSON.parse(e.data),
|
|
242
|
+
metadata,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return allEvents
|
|
249
|
+
.sort((a, b) => (a.metadata.position || 0) - (b.metadata.position || 0))
|
|
250
|
+
.slice(0, limit);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Subscribe to events
|
|
255
|
+
*/
|
|
256
|
+
subscribe(
|
|
257
|
+
eventType: string | "*",
|
|
258
|
+
handler: (event: DomainEvent) => Promise<void>
|
|
259
|
+
): () => void {
|
|
260
|
+
if (!this.subscribers.has(eventType)) {
|
|
261
|
+
this.subscribers.set(eventType, []);
|
|
262
|
+
}
|
|
263
|
+
this.subscribers.get(eventType)!.push(handler);
|
|
264
|
+
|
|
265
|
+
// Return unsubscribe function
|
|
266
|
+
return () => {
|
|
267
|
+
const handlers = this.subscribers.get(eventType);
|
|
268
|
+
if (handlers) {
|
|
269
|
+
const index = handlers.indexOf(handler);
|
|
270
|
+
if (index > -1) {
|
|
271
|
+
handlers.splice(index, 1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get current stream version
|
|
279
|
+
*/
|
|
280
|
+
async getStreamVersion(streamId: string): Promise<number> {
|
|
281
|
+
const stream = this.events.get(streamId) || [];
|
|
282
|
+
return stream.length;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Check if stream exists
|
|
287
|
+
*/
|
|
288
|
+
async streamExists(streamId: string): Promise<boolean> {
|
|
289
|
+
return this.events.has(streamId) && (this.events.get(streamId)?.length || 0) > 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Delete stream (soft delete - mark as deleted)
|
|
294
|
+
*/
|
|
295
|
+
async deleteStream(streamId: string): Promise<void> {
|
|
296
|
+
await this.append(
|
|
297
|
+
streamId,
|
|
298
|
+
[{ eventType: "StreamDeleted", payload: { deletedAt: new Date() } }],
|
|
299
|
+
-1
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private async notifySubscribers(event: DomainEvent): Promise<void> {
|
|
304
|
+
const handlers = [
|
|
305
|
+
...(this.subscribers.get(event.eventType) || []),
|
|
306
|
+
...(this.subscribers.get("*") || []),
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
for (const handler of handlers) {
|
|
310
|
+
try {
|
|
311
|
+
await handler(event);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
this.logger.error(\`Subscriber error for \${event.eventType}:\`, error);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
`;
|
|
319
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'store/event.store.ts'), eventStoreContent);
|
|
320
|
+
// Snapshot Store
|
|
321
|
+
const snapshotStoreContent = `import { Injectable, Logger } from "@nestjs/common";
|
|
322
|
+
import { Snapshot } from "../event-sourcing.types";
|
|
323
|
+
|
|
324
|
+
@Injectable()
|
|
325
|
+
export class SnapshotStore {
|
|
326
|
+
private readonly logger = new Logger(SnapshotStore.name);
|
|
327
|
+
private snapshots: Map<string, Snapshot[]> = new Map();
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Save a snapshot
|
|
331
|
+
*/
|
|
332
|
+
async save<T>(snapshot: Snapshot<T>): Promise<void> {
|
|
333
|
+
const key = \`\${snapshot.aggregateType}-\${snapshot.aggregateId}\`;
|
|
334
|
+
|
|
335
|
+
if (!this.snapshots.has(key)) {
|
|
336
|
+
this.snapshots.set(key, []);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
this.snapshots.get(key)!.push(snapshot);
|
|
340
|
+
this.logger.debug(\`Saved snapshot for \${key} at version \${snapshot.version}\`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Get latest snapshot for an aggregate
|
|
345
|
+
*/
|
|
346
|
+
async getLatest<T>(
|
|
347
|
+
aggregateType: string,
|
|
348
|
+
aggregateId: string
|
|
349
|
+
): Promise<Snapshot<T> | null> {
|
|
350
|
+
const key = \`\${aggregateType}-\${aggregateId}\`;
|
|
351
|
+
const snapshots = this.snapshots.get(key);
|
|
352
|
+
|
|
353
|
+
if (!snapshots || snapshots.length === 0) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return snapshots[snapshots.length - 1] as Snapshot<T>;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get snapshot at specific version
|
|
362
|
+
*/
|
|
363
|
+
async getAtVersion<T>(
|
|
364
|
+
aggregateType: string,
|
|
365
|
+
aggregateId: string,
|
|
366
|
+
version: number
|
|
367
|
+
): Promise<Snapshot<T> | null> {
|
|
368
|
+
const key = \`\${aggregateType}-\${aggregateId}\`;
|
|
369
|
+
const snapshots = this.snapshots.get(key);
|
|
370
|
+
|
|
371
|
+
if (!snapshots) return null;
|
|
372
|
+
|
|
373
|
+
// Find snapshot at or before the requested version
|
|
374
|
+
for (let i = snapshots.length - 1; i >= 0; i--) {
|
|
375
|
+
if (snapshots[i].version <= version) {
|
|
376
|
+
return snapshots[i] as Snapshot<T>;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Delete old snapshots, keeping only the latest N
|
|
385
|
+
*/
|
|
386
|
+
async pruneSnapshots(
|
|
387
|
+
aggregateType: string,
|
|
388
|
+
aggregateId: string,
|
|
389
|
+
keepCount: number = 3
|
|
390
|
+
): Promise<number> {
|
|
391
|
+
const key = \`\${aggregateType}-\${aggregateId}\`;
|
|
392
|
+
const snapshots = this.snapshots.get(key);
|
|
393
|
+
|
|
394
|
+
if (!snapshots || snapshots.length <= keepCount) {
|
|
395
|
+
return 0;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const removed = snapshots.length - keepCount;
|
|
399
|
+
this.snapshots.set(key, snapshots.slice(-keepCount));
|
|
400
|
+
|
|
401
|
+
this.logger.debug(\`Pruned \${removed} old snapshots for \${key}\`);
|
|
402
|
+
return removed;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
`;
|
|
406
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'store/snapshot.store.ts'), snapshotStoreContent);
|
|
407
|
+
// Aggregate Root
|
|
408
|
+
const aggregateRootContent = `import { DomainEvent, EventMetadata } from "../event-sourcing.types";
|
|
409
|
+
|
|
410
|
+
export abstract class AggregateRoot<TState = any> {
|
|
411
|
+
protected _id: string;
|
|
412
|
+
protected _version: number = 0;
|
|
413
|
+
protected _state: TState;
|
|
414
|
+
protected _uncommittedEvents: Array<{ eventType: string; payload: any }> = [];
|
|
415
|
+
|
|
416
|
+
get id(): string {
|
|
417
|
+
return this._id;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
get version(): number {
|
|
421
|
+
return this._version;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
get state(): TState {
|
|
425
|
+
return this._state;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get the aggregate type name
|
|
430
|
+
*/
|
|
431
|
+
abstract getType(): string;
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Create initial state
|
|
435
|
+
*/
|
|
436
|
+
protected abstract createInitialState(): TState;
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Apply an event to update state
|
|
440
|
+
*/
|
|
441
|
+
protected abstract applyEvent(event: DomainEvent): void;
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Initialize a new aggregate
|
|
445
|
+
*/
|
|
446
|
+
protected initialize(id: string): void {
|
|
447
|
+
this._id = id;
|
|
448
|
+
this._state = this.createInitialState();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Record a new event
|
|
453
|
+
*/
|
|
454
|
+
protected recordEvent(eventType: string, payload: any): void {
|
|
455
|
+
this._uncommittedEvents.push({ eventType, payload });
|
|
456
|
+
|
|
457
|
+
// Apply immediately to update state
|
|
458
|
+
const event: DomainEvent = {
|
|
459
|
+
eventId: "",
|
|
460
|
+
eventType,
|
|
461
|
+
aggregateId: this._id,
|
|
462
|
+
aggregateType: this.getType(),
|
|
463
|
+
version: this._version + this._uncommittedEvents.length,
|
|
464
|
+
timestamp: new Date(),
|
|
465
|
+
payload,
|
|
466
|
+
metadata: {},
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
this.applyEvent(event);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Get uncommitted events
|
|
474
|
+
*/
|
|
475
|
+
getUncommittedEvents(): Array<{ eventType: string; payload: any }> {
|
|
476
|
+
return [...this._uncommittedEvents];
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Clear uncommitted events after persisting
|
|
481
|
+
*/
|
|
482
|
+
clearUncommittedEvents(): void {
|
|
483
|
+
this._uncommittedEvents = [];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Reconstitute aggregate from events
|
|
488
|
+
*/
|
|
489
|
+
loadFromHistory(events: DomainEvent[]): void {
|
|
490
|
+
for (const event of events) {
|
|
491
|
+
this.applyEvent(event);
|
|
492
|
+
this._version = event.version;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Load from snapshot and subsequent events
|
|
498
|
+
*/
|
|
499
|
+
loadFromSnapshot(state: TState, version: number, events: DomainEvent[]): void {
|
|
500
|
+
this._state = state;
|
|
501
|
+
this._version = version;
|
|
502
|
+
|
|
503
|
+
for (const event of events) {
|
|
504
|
+
this.applyEvent(event);
|
|
505
|
+
this._version = event.version;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Create a snapshot of current state
|
|
511
|
+
*/
|
|
512
|
+
createSnapshot(): { state: TState; version: number } {
|
|
513
|
+
return {
|
|
514
|
+
state: { ...this._state },
|
|
515
|
+
version: this._version,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
`;
|
|
520
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'aggregate/aggregate-root.ts'), aggregateRootContent);
|
|
521
|
+
// Aggregate Repository
|
|
522
|
+
const aggregateRepoContent = `import { Logger } from "@nestjs/common";
|
|
523
|
+
import { EventStore } from "../store/event.store";
|
|
524
|
+
import { SnapshotStore } from "../store/snapshot.store";
|
|
525
|
+
import { AggregateRoot } from "./aggregate-root";
|
|
526
|
+
import { DomainEvent, EventMetadata, Snapshot } from "../event-sourcing.types";
|
|
527
|
+
|
|
528
|
+
export interface AggregateRepositoryConfig {
|
|
529
|
+
snapshotFrequency?: number;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export abstract class AggregateRepository<T extends AggregateRoot> {
|
|
533
|
+
protected readonly logger = new Logger(this.constructor.name);
|
|
534
|
+
protected config: AggregateRepositoryConfig = {
|
|
535
|
+
snapshotFrequency: 10,
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
constructor(
|
|
539
|
+
protected readonly eventStore: EventStore,
|
|
540
|
+
protected readonly snapshotStore: SnapshotStore
|
|
541
|
+
) {}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Get the aggregate type
|
|
545
|
+
*/
|
|
546
|
+
protected abstract getAggregateType(): string;
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Create a new aggregate instance
|
|
550
|
+
*/
|
|
551
|
+
protected abstract createAggregate(): T;
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Get stream ID for an aggregate
|
|
555
|
+
*/
|
|
556
|
+
protected getStreamId(aggregateId: string): string {
|
|
557
|
+
return \`\${this.getAggregateType()}-\${aggregateId}\`;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Load an aggregate by ID
|
|
562
|
+
*/
|
|
563
|
+
async load(aggregateId: string): Promise<T | null> {
|
|
564
|
+
const streamId = this.getStreamId(aggregateId);
|
|
565
|
+
const exists = await this.eventStore.streamExists(streamId);
|
|
566
|
+
|
|
567
|
+
if (!exists) {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const aggregate = this.createAggregate();
|
|
572
|
+
(aggregate as any)._id = aggregateId;
|
|
573
|
+
|
|
574
|
+
// Try to load from snapshot
|
|
575
|
+
const snapshot = await this.snapshotStore.getLatest(
|
|
576
|
+
this.getAggregateType(),
|
|
577
|
+
aggregateId
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
let fromVersion = 0;
|
|
581
|
+
if (snapshot) {
|
|
582
|
+
(aggregate as any)._state = snapshot.state;
|
|
583
|
+
(aggregate as any)._version = snapshot.version;
|
|
584
|
+
fromVersion = snapshot.version;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Load events after snapshot
|
|
588
|
+
const { events } = await this.eventStore.readStream(streamId, fromVersion);
|
|
589
|
+
|
|
590
|
+
if (snapshot) {
|
|
591
|
+
aggregate.loadFromSnapshot(snapshot.state, snapshot.version, events);
|
|
592
|
+
} else {
|
|
593
|
+
aggregate.loadFromHistory(events);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return aggregate;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Save an aggregate
|
|
601
|
+
*/
|
|
602
|
+
async save(aggregate: T, metadata: EventMetadata = {}): Promise<void> {
|
|
603
|
+
const uncommittedEvents = aggregate.getUncommittedEvents();
|
|
604
|
+
|
|
605
|
+
if (uncommittedEvents.length === 0) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const streamId = this.getStreamId(aggregate.id);
|
|
610
|
+
const expectedVersion = aggregate.version - uncommittedEvents.length;
|
|
611
|
+
|
|
612
|
+
await this.eventStore.append(
|
|
613
|
+
streamId,
|
|
614
|
+
uncommittedEvents,
|
|
615
|
+
expectedVersion,
|
|
616
|
+
metadata
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
aggregate.clearUncommittedEvents();
|
|
620
|
+
|
|
621
|
+
// Create snapshot if needed
|
|
622
|
+
const newVersion = aggregate.version;
|
|
623
|
+
if (
|
|
624
|
+
this.config.snapshotFrequency &&
|
|
625
|
+
newVersion % this.config.snapshotFrequency === 0
|
|
626
|
+
) {
|
|
627
|
+
await this.createSnapshot(aggregate);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Create a snapshot of the aggregate
|
|
633
|
+
*/
|
|
634
|
+
protected async createSnapshot(aggregate: T): Promise<void> {
|
|
635
|
+
const { state, version } = aggregate.createSnapshot();
|
|
636
|
+
|
|
637
|
+
const snapshot: Snapshot = {
|
|
638
|
+
aggregateId: aggregate.id,
|
|
639
|
+
aggregateType: this.getAggregateType(),
|
|
640
|
+
version,
|
|
641
|
+
state,
|
|
642
|
+
timestamp: new Date(),
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
await this.snapshotStore.save(snapshot);
|
|
646
|
+
this.logger.debug(\`Created snapshot for \${aggregate.id} at version \${version}\`);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Check if aggregate exists
|
|
651
|
+
*/
|
|
652
|
+
async exists(aggregateId: string): Promise<boolean> {
|
|
653
|
+
const streamId = this.getStreamId(aggregateId);
|
|
654
|
+
return this.eventStore.streamExists(streamId);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
`;
|
|
658
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'aggregate/aggregate-repository.ts'), aggregateRepoContent);
|
|
659
|
+
// Projection Manager
|
|
660
|
+
const projectionManagerContent = `import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
|
|
661
|
+
import { EventStore } from "../store/event.store";
|
|
662
|
+
import { DomainEvent, Projection, ProjectionState } from "../event-sourcing.types";
|
|
663
|
+
|
|
664
|
+
@Injectable()
|
|
665
|
+
export class ProjectionManager implements OnModuleInit {
|
|
666
|
+
private readonly logger = new Logger(ProjectionManager.name);
|
|
667
|
+
private projections: Map<string, Projection> = new Map();
|
|
668
|
+
private positions: Map<string, number> = new Map();
|
|
669
|
+
private isRunning = false;
|
|
670
|
+
|
|
671
|
+
constructor(private eventStore: EventStore) {}
|
|
672
|
+
|
|
673
|
+
async onModuleInit() {
|
|
674
|
+
// Start projection processing after a short delay
|
|
675
|
+
setTimeout(() => this.start(), 1000);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Register a projection
|
|
680
|
+
*/
|
|
681
|
+
register(projection: Projection): void {
|
|
682
|
+
this.projections.set(projection.name, projection);
|
|
683
|
+
this.logger.log(\`Registered projection: \${projection.name}\`);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Start processing projections
|
|
688
|
+
*/
|
|
689
|
+
async start(): Promise<void> {
|
|
690
|
+
if (this.isRunning) return;
|
|
691
|
+
this.isRunning = true;
|
|
692
|
+
|
|
693
|
+
// Initialize all projections
|
|
694
|
+
for (const [name, projection] of this.projections) {
|
|
695
|
+
await projection.init();
|
|
696
|
+
const position = await projection.getPosition();
|
|
697
|
+
this.positions.set(name, position);
|
|
698
|
+
this.logger.log(\`Projection \${name} at position \${position}\`);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Subscribe to all events
|
|
702
|
+
this.eventStore.subscribe("*", async (event) => {
|
|
703
|
+
await this.processEvent(event);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
this.logger.log("Projection manager started");
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Stop processing projections
|
|
711
|
+
*/
|
|
712
|
+
stop(): void {
|
|
713
|
+
this.isRunning = false;
|
|
714
|
+
this.logger.log("Projection manager stopped");
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Process a single event for all projections
|
|
719
|
+
*/
|
|
720
|
+
private async processEvent(event: DomainEvent): Promise<void> {
|
|
721
|
+
const eventPosition = event.metadata.position || 0;
|
|
722
|
+
|
|
723
|
+
for (const [name, projection] of this.projections) {
|
|
724
|
+
const currentPosition = this.positions.get(name) || 0;
|
|
725
|
+
|
|
726
|
+
if (eventPosition > currentPosition) {
|
|
727
|
+
try {
|
|
728
|
+
await projection.handle(event);
|
|
729
|
+
this.positions.set(name, eventPosition);
|
|
730
|
+
} catch (error) {
|
|
731
|
+
this.logger.error(\`Projection \${name} failed on event \${event.eventType}:\`, error);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Rebuild a projection from scratch
|
|
739
|
+
*/
|
|
740
|
+
async rebuild(projectionName: string): Promise<void> {
|
|
741
|
+
const projection = this.projections.get(projectionName);
|
|
742
|
+
if (!projection) {
|
|
743
|
+
throw new Error(\`Projection \${projectionName} not found\`);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
this.logger.log(\`Rebuilding projection: \${projectionName}\`);
|
|
747
|
+
|
|
748
|
+
await projection.reset();
|
|
749
|
+
this.positions.set(projectionName, 0);
|
|
750
|
+
|
|
751
|
+
// Process all events
|
|
752
|
+
let position = 0;
|
|
753
|
+
const batchSize = 100;
|
|
754
|
+
|
|
755
|
+
while (true) {
|
|
756
|
+
const events = await this.eventStore.readAll(position, batchSize);
|
|
757
|
+
if (events.length === 0) break;
|
|
758
|
+
|
|
759
|
+
for (const event of events) {
|
|
760
|
+
await projection.handle(event);
|
|
761
|
+
position = event.metadata.position || position + 1;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
this.positions.set(projectionName, position);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
this.logger.log(\`Projection \${projectionName} rebuilt to position \${position}\`);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Get projection status
|
|
772
|
+
*/
|
|
773
|
+
getStatus(): Array<{ name: string; position: number }> {
|
|
774
|
+
return Array.from(this.projections.keys()).map((name) => ({
|
|
775
|
+
name,
|
|
776
|
+
position: this.positions.get(name) || 0,
|
|
777
|
+
}));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
`;
|
|
781
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'projections/projection-manager.ts'), projectionManagerContent);
|
|
782
|
+
// Base Projection
|
|
783
|
+
const baseProjectionContent = `import { Logger } from "@nestjs/common";
|
|
784
|
+
import { DomainEvent, Projection } from "../event-sourcing.types";
|
|
785
|
+
|
|
786
|
+
export abstract class BaseProjection implements Projection {
|
|
787
|
+
protected readonly logger = new Logger(this.constructor.name);
|
|
788
|
+
protected position = 0;
|
|
789
|
+
|
|
790
|
+
abstract readonly name: string;
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Initialize the projection
|
|
794
|
+
*/
|
|
795
|
+
async init(): Promise<void> {
|
|
796
|
+
this.logger.log(\`Initializing projection: \${this.name}\`);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Handle an event
|
|
801
|
+
*/
|
|
802
|
+
async handle(event: DomainEvent): Promise<void> {
|
|
803
|
+
const handlerName = \`on\${event.eventType}\`;
|
|
804
|
+
const handler = (this as any)[handlerName];
|
|
805
|
+
|
|
806
|
+
if (typeof handler === "function") {
|
|
807
|
+
await handler.call(this, event);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
this.position = event.metadata.position || this.position + 1;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Get current position
|
|
815
|
+
*/
|
|
816
|
+
async getPosition(): Promise<number> {
|
|
817
|
+
return this.position;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Reset the projection
|
|
822
|
+
*/
|
|
823
|
+
async reset(): Promise<void> {
|
|
824
|
+
this.position = 0;
|
|
825
|
+
this.logger.log(\`Reset projection: \${this.name}\`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
`;
|
|
829
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'projections/base-projection.ts'), baseProjectionContent);
|
|
830
|
+
// Event Sourcing Module
|
|
831
|
+
const moduleContent = `import { Module, Global, DynamicModule } from "@nestjs/common";
|
|
832
|
+
import { EventStore } from "./store/event.store";
|
|
833
|
+
import { SnapshotStore } from "./store/snapshot.store";
|
|
834
|
+
import { ProjectionManager } from "./projections/projection-manager";
|
|
835
|
+
|
|
836
|
+
export interface EventSourcingModuleOptions {
|
|
837
|
+
snapshotFrequency?: number;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
@Global()
|
|
841
|
+
@Module({})
|
|
842
|
+
export class EventSourcingModule {
|
|
843
|
+
static forRoot(options: EventSourcingModuleOptions = {}): DynamicModule {
|
|
844
|
+
return {
|
|
845
|
+
module: EventSourcingModule,
|
|
846
|
+
providers: [
|
|
847
|
+
EventStore,
|
|
848
|
+
SnapshotStore,
|
|
849
|
+
ProjectionManager,
|
|
850
|
+
{
|
|
851
|
+
provide: "EVENT_SOURCING_OPTIONS",
|
|
852
|
+
useValue: options,
|
|
853
|
+
},
|
|
854
|
+
],
|
|
855
|
+
exports: [EventStore, SnapshotStore, ProjectionManager],
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
`;
|
|
860
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'event-sourcing.module.ts'), moduleContent);
|
|
861
|
+
// Index exports
|
|
862
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'index.ts'), `export * from "./event-sourcing.types";
|
|
863
|
+
export * from "./store/event.store";
|
|
864
|
+
export * from "./store/snapshot.store";
|
|
865
|
+
export * from "./aggregate/aggregate-root";
|
|
866
|
+
export * from "./aggregate/aggregate-repository";
|
|
867
|
+
export * from "./projections/projection-manager";
|
|
868
|
+
export * from "./projections/base-projection";
|
|
869
|
+
export * from "./event-sourcing.module";
|
|
870
|
+
`);
|
|
871
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'store/index.ts'), `export * from "./event.store";
|
|
872
|
+
export * from "./snapshot.store";
|
|
873
|
+
`);
|
|
874
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'aggregate/index.ts'), `export * from "./aggregate-root";
|
|
875
|
+
export * from "./aggregate-repository";
|
|
876
|
+
`);
|
|
877
|
+
await (0, file_utils_1.writeFile)(path.join(esPath, 'projections/index.ts'), `export * from "./projection-manager";
|
|
878
|
+
export * from "./base-projection";
|
|
879
|
+
`);
|
|
880
|
+
console.log(chalk_1.default.green(' ✓ Event sourcing types'));
|
|
881
|
+
console.log(chalk_1.default.green(' ✓ Event store with optimistic concurrency'));
|
|
882
|
+
console.log(chalk_1.default.green(' ✓ Snapshot store'));
|
|
883
|
+
console.log(chalk_1.default.green(' ✓ Aggregate root base class'));
|
|
884
|
+
console.log(chalk_1.default.green(' ✓ Aggregate repository with snapshot support'));
|
|
885
|
+
console.log(chalk_1.default.green(' ✓ Projection manager with rebuild capability'));
|
|
886
|
+
console.log(chalk_1.default.green(' ✓ Base projection class'));
|
|
887
|
+
console.log(chalk_1.default.green(' ✓ Event sourcing module'));
|
|
888
|
+
}
|
|
889
|
+
//# sourceMappingURL=event-sourcing.recipe.js.map
|