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,953 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Aggregate Root & Command Handler Validator Generator
|
|
4
|
+
* Generates comprehensive validators for aggregates
|
|
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.setupAggregateValidator = setupAggregateValidator;
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
47
|
+
async function setupAggregateValidator(basePath, options = {}) {
|
|
48
|
+
console.log(chalk_1.default.bold.blue('\n✅ Setting up Aggregate Validator Framework\n'));
|
|
49
|
+
const sharedPath = path.join(basePath, 'src/shared/domain/validation');
|
|
50
|
+
if (!fs.existsSync(sharedPath)) {
|
|
51
|
+
fs.mkdirSync(sharedPath, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
// Generate aggregate root base
|
|
54
|
+
const aggregateContent = generateAggregateRoot();
|
|
55
|
+
fs.writeFileSync(path.join(sharedPath, 'aggregate-root.ts'), aggregateContent);
|
|
56
|
+
console.log(chalk_1.default.green(` ✓ Created aggregate root base`));
|
|
57
|
+
// Generate invariant validator
|
|
58
|
+
const invariantContent = generateInvariantValidator();
|
|
59
|
+
fs.writeFileSync(path.join(sharedPath, 'invariant.validator.ts'), invariantContent);
|
|
60
|
+
console.log(chalk_1.default.green(` ✓ Created invariant validator`));
|
|
61
|
+
// Generate state machine
|
|
62
|
+
const stateMachineContent = generateStateMachine();
|
|
63
|
+
fs.writeFileSync(path.join(sharedPath, 'state-machine.ts'), stateMachineContent);
|
|
64
|
+
console.log(chalk_1.default.green(` ✓ Created state machine`));
|
|
65
|
+
// Generate business rules engine
|
|
66
|
+
const rulesContent = generateBusinessRulesEngine();
|
|
67
|
+
fs.writeFileSync(path.join(sharedPath, 'business-rules.ts'), rulesContent);
|
|
68
|
+
console.log(chalk_1.default.green(` ✓ Created business rules engine`));
|
|
69
|
+
// Generate command validator
|
|
70
|
+
const commandContent = generateCommandValidator();
|
|
71
|
+
fs.writeFileSync(path.join(sharedPath, 'command.validator.ts'), commandContent);
|
|
72
|
+
console.log(chalk_1.default.green(` ✓ Created command validator`));
|
|
73
|
+
console.log(chalk_1.default.bold.green('\n✅ Aggregate validator framework ready!\n'));
|
|
74
|
+
}
|
|
75
|
+
function generateAggregateRoot() {
|
|
76
|
+
return `/**
|
|
77
|
+
* Aggregate Root Base
|
|
78
|
+
* Foundation for all aggregate roots with invariant validation
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
import { InvariantValidator, InvariantRule } from './invariant.validator';
|
|
82
|
+
|
|
83
|
+
export interface DomainEvent {
|
|
84
|
+
eventType: string;
|
|
85
|
+
aggregateId: string;
|
|
86
|
+
aggregateType: string;
|
|
87
|
+
timestamp: Date;
|
|
88
|
+
version: number;
|
|
89
|
+
payload: any;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Base Aggregate Root
|
|
94
|
+
*/
|
|
95
|
+
export abstract class AggregateRoot<TId = string> {
|
|
96
|
+
private _id: TId;
|
|
97
|
+
private _version: number = 0;
|
|
98
|
+
private _domainEvents: DomainEvent[] = [];
|
|
99
|
+
private _invariantValidator: InvariantValidator<this>;
|
|
100
|
+
|
|
101
|
+
protected constructor(id: TId) {
|
|
102
|
+
this._id = id;
|
|
103
|
+
this._invariantValidator = new InvariantValidator<this>();
|
|
104
|
+
this.registerInvariants(this._invariantValidator);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get id(): TId {
|
|
108
|
+
return this._id;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get version(): number {
|
|
112
|
+
return this._version;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Register aggregate invariants
|
|
117
|
+
* Override in subclass to define invariants
|
|
118
|
+
*/
|
|
119
|
+
protected abstract registerInvariants(validator: InvariantValidator<this>): void;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validate all invariants
|
|
123
|
+
*/
|
|
124
|
+
protected validateInvariants(): void {
|
|
125
|
+
const result = this._invariantValidator.validate(this);
|
|
126
|
+
if (!result.valid) {
|
|
127
|
+
throw new InvariantViolationError(result.violations);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Apply a domain event
|
|
133
|
+
*/
|
|
134
|
+
protected apply(event: Omit<DomainEvent, 'version' | 'timestamp' | 'aggregateId' | 'aggregateType'>): void {
|
|
135
|
+
const fullEvent: DomainEvent = {
|
|
136
|
+
...event,
|
|
137
|
+
aggregateId: String(this._id),
|
|
138
|
+
aggregateType: this.constructor.name,
|
|
139
|
+
version: this._version + 1,
|
|
140
|
+
timestamp: new Date(),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
this.when(fullEvent);
|
|
144
|
+
this._domainEvents.push(fullEvent);
|
|
145
|
+
this._version++;
|
|
146
|
+
|
|
147
|
+
// Validate invariants after state change
|
|
148
|
+
this.validateInvariants();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Handle event application
|
|
153
|
+
* Override to implement event handlers
|
|
154
|
+
*/
|
|
155
|
+
protected abstract when(event: DomainEvent): void;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Load aggregate from event history
|
|
159
|
+
*/
|
|
160
|
+
public loadFromHistory(events: DomainEvent[]): void {
|
|
161
|
+
for (const event of events) {
|
|
162
|
+
this.when(event);
|
|
163
|
+
this._version = event.version;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get uncommitted domain events
|
|
169
|
+
*/
|
|
170
|
+
public getUncommittedEvents(): DomainEvent[] {
|
|
171
|
+
return [...this._domainEvents];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Clear uncommitted events after persistence
|
|
176
|
+
*/
|
|
177
|
+
public markEventsAsCommitted(): void {
|
|
178
|
+
this._domainEvents = [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Check if aggregate has uncommitted changes
|
|
183
|
+
*/
|
|
184
|
+
public hasUncommittedChanges(): boolean {
|
|
185
|
+
return this._domainEvents.length > 0;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Invariant violation error
|
|
191
|
+
*/
|
|
192
|
+
export class InvariantViolationError extends Error {
|
|
193
|
+
constructor(public readonly violations: string[]) {
|
|
194
|
+
super(\`Invariant violations: \${violations.join(', ')}\`);
|
|
195
|
+
this.name = 'InvariantViolationError';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Entity base class
|
|
201
|
+
*/
|
|
202
|
+
export abstract class Entity<TId = string> {
|
|
203
|
+
protected readonly _id: TId;
|
|
204
|
+
|
|
205
|
+
protected constructor(id: TId) {
|
|
206
|
+
this._id = id;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
get id(): TId {
|
|
210
|
+
return this._id;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
equals(entity: Entity<TId>): boolean {
|
|
214
|
+
if (entity === null || entity === undefined) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
if (this === entity) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
return this._id === entity._id;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Aggregate factory interface
|
|
226
|
+
*/
|
|
227
|
+
export interface AggregateFactory<T extends AggregateRoot, TId = string> {
|
|
228
|
+
create(id: TId, ...args: any[]): T;
|
|
229
|
+
reconstitute(id: TId, events: DomainEvent[]): T;
|
|
230
|
+
}
|
|
231
|
+
`;
|
|
232
|
+
}
|
|
233
|
+
function generateInvariantValidator() {
|
|
234
|
+
return `/**
|
|
235
|
+
* Invariant Validator
|
|
236
|
+
* Validates aggregate invariants and business rules
|
|
237
|
+
*/
|
|
238
|
+
|
|
239
|
+
export interface InvariantRule<T> {
|
|
240
|
+
name: string;
|
|
241
|
+
description?: string;
|
|
242
|
+
check: (aggregate: T) => boolean;
|
|
243
|
+
message: string | ((aggregate: T) => string);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface ValidationResult {
|
|
247
|
+
valid: boolean;
|
|
248
|
+
violations: string[];
|
|
249
|
+
warnings: string[];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Invariant Validator
|
|
254
|
+
*/
|
|
255
|
+
export class InvariantValidator<T> {
|
|
256
|
+
private rules: InvariantRule<T>[] = [];
|
|
257
|
+
private warningRules: InvariantRule<T>[] = [];
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Add invariant rule
|
|
261
|
+
*/
|
|
262
|
+
addRule(rule: InvariantRule<T>): this {
|
|
263
|
+
this.rules.push(rule);
|
|
264
|
+
return this;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Add warning rule (doesn't fail validation)
|
|
269
|
+
*/
|
|
270
|
+
addWarning(rule: InvariantRule<T>): this {
|
|
271
|
+
this.warningRules.push(rule);
|
|
272
|
+
return this;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Validate aggregate against all rules
|
|
277
|
+
*/
|
|
278
|
+
validate(aggregate: T): ValidationResult {
|
|
279
|
+
const violations: string[] = [];
|
|
280
|
+
const warnings: string[] = [];
|
|
281
|
+
|
|
282
|
+
// Check invariant rules
|
|
283
|
+
for (const rule of this.rules) {
|
|
284
|
+
if (!rule.check(aggregate)) {
|
|
285
|
+
const message = typeof rule.message === 'function'
|
|
286
|
+
? rule.message(aggregate)
|
|
287
|
+
: rule.message;
|
|
288
|
+
violations.push(\`[\${rule.name}] \${message}\`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Check warning rules
|
|
293
|
+
for (const rule of this.warningRules) {
|
|
294
|
+
if (!rule.check(aggregate)) {
|
|
295
|
+
const message = typeof rule.message === 'function'
|
|
296
|
+
? rule.message(aggregate)
|
|
297
|
+
: rule.message;
|
|
298
|
+
warnings.push(\`[\${rule.name}] \${message}\`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
valid: violations.length === 0,
|
|
304
|
+
violations,
|
|
305
|
+
warnings,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Validate and throw on violation
|
|
311
|
+
*/
|
|
312
|
+
validateOrThrow(aggregate: T): void {
|
|
313
|
+
const result = this.validate(aggregate);
|
|
314
|
+
if (!result.valid) {
|
|
315
|
+
throw new InvariantError(result.violations);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Invariant error
|
|
322
|
+
*/
|
|
323
|
+
export class InvariantError extends Error {
|
|
324
|
+
constructor(public readonly violations: string[]) {
|
|
325
|
+
super(violations.join('; '));
|
|
326
|
+
this.name = 'InvariantError';
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Invariant builder for fluent API
|
|
332
|
+
*/
|
|
333
|
+
export class InvariantBuilder<T> {
|
|
334
|
+
private rule: Partial<InvariantRule<T>> = {};
|
|
335
|
+
|
|
336
|
+
named(name: string): this {
|
|
337
|
+
this.rule.name = name;
|
|
338
|
+
return this;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
describedAs(description: string): this {
|
|
342
|
+
this.rule.description = description;
|
|
343
|
+
return this;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
check(predicate: (aggregate: T) => boolean): this {
|
|
347
|
+
this.rule.check = predicate;
|
|
348
|
+
return this;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
withMessage(message: string | ((aggregate: T) => string)): this {
|
|
352
|
+
this.rule.message = message;
|
|
353
|
+
return this;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
build(): InvariantRule<T> {
|
|
357
|
+
if (!this.rule.name || !this.rule.check || !this.rule.message) {
|
|
358
|
+
throw new Error('Invariant rule is incomplete');
|
|
359
|
+
}
|
|
360
|
+
return this.rule as InvariantRule<T>;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Create invariant builder
|
|
366
|
+
*/
|
|
367
|
+
export function invariant<T>(): InvariantBuilder<T> {
|
|
368
|
+
return new InvariantBuilder<T>();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Common invariant rules factory
|
|
373
|
+
*/
|
|
374
|
+
export const CommonInvariants = {
|
|
375
|
+
notNull<T, K extends keyof T>(field: K): InvariantRule<T> {
|
|
376
|
+
return {
|
|
377
|
+
name: \`\${String(field)}-not-null\`,
|
|
378
|
+
check: (aggregate) => aggregate[field] !== null && aggregate[field] !== undefined,
|
|
379
|
+
message: \`\${String(field)} cannot be null\`,
|
|
380
|
+
};
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
notEmpty<T, K extends keyof T>(field: K): InvariantRule<T> {
|
|
384
|
+
return {
|
|
385
|
+
name: \`\${String(field)}-not-empty\`,
|
|
386
|
+
check: (aggregate) => {
|
|
387
|
+
const value = aggregate[field];
|
|
388
|
+
if (typeof value === 'string') return value.length > 0;
|
|
389
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
390
|
+
return value !== null && value !== undefined;
|
|
391
|
+
},
|
|
392
|
+
message: \`\${String(field)} cannot be empty\`,
|
|
393
|
+
};
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
positive<T, K extends keyof T>(field: K): InvariantRule<T> {
|
|
397
|
+
return {
|
|
398
|
+
name: \`\${String(field)}-positive\`,
|
|
399
|
+
check: (aggregate) => (aggregate[field] as unknown as number) > 0,
|
|
400
|
+
message: \`\${String(field)} must be positive\`,
|
|
401
|
+
};
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
nonNegative<T, K extends keyof T>(field: K): InvariantRule<T> {
|
|
405
|
+
return {
|
|
406
|
+
name: \`\${String(field)}-non-negative\`,
|
|
407
|
+
check: (aggregate) => (aggregate[field] as unknown as number) >= 0,
|
|
408
|
+
message: \`\${String(field)} cannot be negative\`,
|
|
409
|
+
};
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
inRange<T, K extends keyof T>(field: K, min: number, max: number): InvariantRule<T> {
|
|
413
|
+
return {
|
|
414
|
+
name: \`\${String(field)}-in-range\`,
|
|
415
|
+
check: (aggregate) => {
|
|
416
|
+
const value = aggregate[field] as unknown as number;
|
|
417
|
+
return value >= min && value <= max;
|
|
418
|
+
},
|
|
419
|
+
message: \`\${String(field)} must be between \${min} and \${max}\`,
|
|
420
|
+
};
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
validState<T>(validStates: string[], stateField: keyof T): InvariantRule<T> {
|
|
424
|
+
return {
|
|
425
|
+
name: 'valid-state',
|
|
426
|
+
check: (aggregate) => validStates.includes(String(aggregate[stateField])),
|
|
427
|
+
message: (aggregate) => \`Invalid state: \${aggregate[stateField]}. Valid states: \${validStates.join(', ')}\`,
|
|
428
|
+
};
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
`;
|
|
432
|
+
}
|
|
433
|
+
function generateStateMachine() {
|
|
434
|
+
return `/**
|
|
435
|
+
* State Machine
|
|
436
|
+
* Manages state transitions with validation
|
|
437
|
+
*/
|
|
438
|
+
|
|
439
|
+
export interface StateTransition<TState extends string, TEvent extends string> {
|
|
440
|
+
from: TState | TState[];
|
|
441
|
+
event: TEvent;
|
|
442
|
+
to: TState;
|
|
443
|
+
guard?: () => boolean;
|
|
444
|
+
action?: () => void;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export interface StateMachineConfig<TState extends string, TEvent extends string> {
|
|
448
|
+
initial: TState;
|
|
449
|
+
transitions: StateTransition<TState, TEvent>[];
|
|
450
|
+
onEnter?: Partial<Record<TState, () => void>>;
|
|
451
|
+
onExit?: Partial<Record<TState, () => void>>;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* State Machine
|
|
456
|
+
*/
|
|
457
|
+
export class StateMachine<TState extends string, TEvent extends string> {
|
|
458
|
+
private currentState: TState;
|
|
459
|
+
private readonly config: StateMachineConfig<TState, TEvent>;
|
|
460
|
+
private history: { from: TState; event: TEvent; to: TState; timestamp: Date }[] = [];
|
|
461
|
+
|
|
462
|
+
constructor(config: StateMachineConfig<TState, TEvent>) {
|
|
463
|
+
this.config = config;
|
|
464
|
+
this.currentState = config.initial;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Get current state
|
|
469
|
+
*/
|
|
470
|
+
get state(): TState {
|
|
471
|
+
return this.currentState;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Check if transition is allowed
|
|
476
|
+
*/
|
|
477
|
+
canTransition(event: TEvent): boolean {
|
|
478
|
+
return this.findTransition(event) !== undefined;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Get allowed events from current state
|
|
483
|
+
*/
|
|
484
|
+
getAllowedEvents(): TEvent[] {
|
|
485
|
+
return this.config.transitions
|
|
486
|
+
.filter(t => this.matchesFrom(t.from, this.currentState))
|
|
487
|
+
.filter(t => !t.guard || t.guard())
|
|
488
|
+
.map(t => t.event);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Trigger a transition
|
|
493
|
+
*/
|
|
494
|
+
transition(event: TEvent): TState {
|
|
495
|
+
const transition = this.findTransition(event);
|
|
496
|
+
|
|
497
|
+
if (!transition) {
|
|
498
|
+
throw new InvalidTransitionError(this.currentState, event);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const fromState = this.currentState;
|
|
502
|
+
|
|
503
|
+
// Execute exit action
|
|
504
|
+
if (this.config.onExit?.[fromState]) {
|
|
505
|
+
this.config.onExit[fromState]!();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Execute transition action
|
|
509
|
+
if (transition.action) {
|
|
510
|
+
transition.action();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Update state
|
|
514
|
+
this.currentState = transition.to;
|
|
515
|
+
|
|
516
|
+
// Execute enter action
|
|
517
|
+
if (this.config.onEnter?.[this.currentState]) {
|
|
518
|
+
this.config.onEnter[this.currentState]!();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Record history
|
|
522
|
+
this.history.push({
|
|
523
|
+
from: fromState,
|
|
524
|
+
event,
|
|
525
|
+
to: this.currentState,
|
|
526
|
+
timestamp: new Date(),
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
return this.currentState;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Get transition history
|
|
534
|
+
*/
|
|
535
|
+
getHistory(): { from: TState; event: TEvent; to: TState; timestamp: Date }[] {
|
|
536
|
+
return [...this.history];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Check if in a specific state
|
|
541
|
+
*/
|
|
542
|
+
isInState(state: TState): boolean {
|
|
543
|
+
return this.currentState === state;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Reset to initial state
|
|
548
|
+
*/
|
|
549
|
+
reset(): void {
|
|
550
|
+
this.currentState = this.config.initial;
|
|
551
|
+
this.history = [];
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private findTransition(event: TEvent): StateTransition<TState, TEvent> | undefined {
|
|
555
|
+
return this.config.transitions.find(t => {
|
|
556
|
+
if (!this.matchesFrom(t.from, this.currentState)) return false;
|
|
557
|
+
if (t.event !== event) return false;
|
|
558
|
+
if (t.guard && !t.guard()) return false;
|
|
559
|
+
return true;
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private matchesFrom(from: TState | TState[], currentState: TState): boolean {
|
|
564
|
+
if (Array.isArray(from)) {
|
|
565
|
+
return from.includes(currentState);
|
|
566
|
+
}
|
|
567
|
+
return from === currentState;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Invalid transition error
|
|
573
|
+
*/
|
|
574
|
+
export class InvalidTransitionError extends Error {
|
|
575
|
+
constructor(
|
|
576
|
+
public readonly fromState: string,
|
|
577
|
+
public readonly event: string,
|
|
578
|
+
) {
|
|
579
|
+
super(\`Cannot transition from '\${fromState}' with event '\${event}'\`);
|
|
580
|
+
this.name = 'InvalidTransitionError';
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* State machine builder
|
|
586
|
+
*/
|
|
587
|
+
export class StateMachineBuilder<TState extends string, TEvent extends string> {
|
|
588
|
+
private config: StateMachineConfig<TState, TEvent> = {
|
|
589
|
+
initial: '' as TState,
|
|
590
|
+
transitions: [],
|
|
591
|
+
onEnter: {},
|
|
592
|
+
onExit: {},
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
initial(state: TState): this {
|
|
596
|
+
this.config.initial = state;
|
|
597
|
+
return this;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
transition(from: TState | TState[], event: TEvent, to: TState): this {
|
|
601
|
+
this.config.transitions.push({ from, event, to });
|
|
602
|
+
return this;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
transitionWithGuard(
|
|
606
|
+
from: TState | TState[],
|
|
607
|
+
event: TEvent,
|
|
608
|
+
to: TState,
|
|
609
|
+
guard: () => boolean,
|
|
610
|
+
): this {
|
|
611
|
+
this.config.transitions.push({ from, event, to, guard });
|
|
612
|
+
return this;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
onEnter(state: TState, action: () => void): this {
|
|
616
|
+
this.config.onEnter![state] = action;
|
|
617
|
+
return this;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
onExit(state: TState, action: () => void): this {
|
|
621
|
+
this.config.onExit![state] = action;
|
|
622
|
+
return this;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
build(): StateMachine<TState, TEvent> {
|
|
626
|
+
return new StateMachine(this.config);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Create state machine builder
|
|
632
|
+
*/
|
|
633
|
+
export function stateMachine<TState extends string, TEvent extends string>(): StateMachineBuilder<TState, TEvent> {
|
|
634
|
+
return new StateMachineBuilder<TState, TEvent>();
|
|
635
|
+
}
|
|
636
|
+
`;
|
|
637
|
+
}
|
|
638
|
+
function generateBusinessRulesEngine() {
|
|
639
|
+
return `/**
|
|
640
|
+
* Business Rules Engine
|
|
641
|
+
* Centralized business rule validation
|
|
642
|
+
*/
|
|
643
|
+
|
|
644
|
+
export interface BusinessRule<TContext = any> {
|
|
645
|
+
name: string;
|
|
646
|
+
description?: string;
|
|
647
|
+
priority?: number;
|
|
648
|
+
condition: (context: TContext) => boolean;
|
|
649
|
+
action: (context: TContext) => void | Promise<void>;
|
|
650
|
+
onFailure?: (context: TContext) => void;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export interface RuleResult {
|
|
654
|
+
rule: string;
|
|
655
|
+
passed: boolean;
|
|
656
|
+
message?: string;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Business Rules Engine
|
|
661
|
+
*/
|
|
662
|
+
export class BusinessRulesEngine<TContext = any> {
|
|
663
|
+
private rules: BusinessRule<TContext>[] = [];
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Add a rule
|
|
667
|
+
*/
|
|
668
|
+
addRule(rule: BusinessRule<TContext>): this {
|
|
669
|
+
this.rules.push(rule);
|
|
670
|
+
this.sortRules();
|
|
671
|
+
return this;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Remove a rule
|
|
676
|
+
*/
|
|
677
|
+
removeRule(name: string): this {
|
|
678
|
+
this.rules = this.rules.filter(r => r.name !== name);
|
|
679
|
+
return this;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Evaluate all rules
|
|
684
|
+
*/
|
|
685
|
+
async evaluate(context: TContext): Promise<RuleResult[]> {
|
|
686
|
+
const results: RuleResult[] = [];
|
|
687
|
+
|
|
688
|
+
for (const rule of this.rules) {
|
|
689
|
+
try {
|
|
690
|
+
const passed = rule.condition(context);
|
|
691
|
+
|
|
692
|
+
if (passed) {
|
|
693
|
+
await rule.action(context);
|
|
694
|
+
} else if (rule.onFailure) {
|
|
695
|
+
rule.onFailure(context);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
results.push({
|
|
699
|
+
rule: rule.name,
|
|
700
|
+
passed,
|
|
701
|
+
});
|
|
702
|
+
} catch (error) {
|
|
703
|
+
results.push({
|
|
704
|
+
rule: rule.name,
|
|
705
|
+
passed: false,
|
|
706
|
+
message: (error as Error).message,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return results;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Evaluate and throw on first failure
|
|
716
|
+
*/
|
|
717
|
+
async evaluateStrict(context: TContext): Promise<void> {
|
|
718
|
+
for (const rule of this.rules) {
|
|
719
|
+
if (!rule.condition(context)) {
|
|
720
|
+
throw new BusinessRuleViolationError(rule.name, rule.description);
|
|
721
|
+
}
|
|
722
|
+
await rule.action(context);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Check if all rules pass without executing actions
|
|
728
|
+
*/
|
|
729
|
+
check(context: TContext): boolean {
|
|
730
|
+
return this.rules.every(rule => rule.condition(context));
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Get failing rules
|
|
735
|
+
*/
|
|
736
|
+
getFailingRules(context: TContext): BusinessRule<TContext>[] {
|
|
737
|
+
return this.rules.filter(rule => !rule.condition(context));
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
private sortRules(): void {
|
|
741
|
+
this.rules.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Business rule violation error
|
|
747
|
+
*/
|
|
748
|
+
export class BusinessRuleViolationError extends Error {
|
|
749
|
+
constructor(
|
|
750
|
+
public readonly ruleName: string,
|
|
751
|
+
public readonly ruleDescription?: string,
|
|
752
|
+
) {
|
|
753
|
+
super(\`Business rule '\${ruleName}' violated\${ruleDescription ? \`: \${ruleDescription}\` : ''}\`);
|
|
754
|
+
this.name = 'BusinessRuleViolationError';
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Rule builder
|
|
760
|
+
*/
|
|
761
|
+
export class RuleBuilder<TContext = any> {
|
|
762
|
+
private rule: Partial<BusinessRule<TContext>> = {};
|
|
763
|
+
|
|
764
|
+
named(name: string): this {
|
|
765
|
+
this.rule.name = name;
|
|
766
|
+
return this;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
describedAs(description: string): this {
|
|
770
|
+
this.rule.description = description;
|
|
771
|
+
return this;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
withPriority(priority: number): this {
|
|
775
|
+
this.rule.priority = priority;
|
|
776
|
+
return this;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
when(condition: (context: TContext) => boolean): this {
|
|
780
|
+
this.rule.condition = condition;
|
|
781
|
+
return this;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
then(action: (context: TContext) => void | Promise<void>): this {
|
|
785
|
+
this.rule.action = action;
|
|
786
|
+
return this;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
otherwise(handler: (context: TContext) => void): this {
|
|
790
|
+
this.rule.onFailure = handler;
|
|
791
|
+
return this;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
build(): BusinessRule<TContext> {
|
|
795
|
+
if (!this.rule.name || !this.rule.condition || !this.rule.action) {
|
|
796
|
+
throw new Error('Business rule is incomplete');
|
|
797
|
+
}
|
|
798
|
+
return this.rule as BusinessRule<TContext>;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Create rule builder
|
|
804
|
+
*/
|
|
805
|
+
export function rule<TContext = any>(): RuleBuilder<TContext> {
|
|
806
|
+
return new RuleBuilder<TContext>();
|
|
807
|
+
}
|
|
808
|
+
`;
|
|
809
|
+
}
|
|
810
|
+
function generateCommandValidator() {
|
|
811
|
+
return `/**
|
|
812
|
+
* Command Validator
|
|
813
|
+
* Validates commands before execution
|
|
814
|
+
*/
|
|
815
|
+
|
|
816
|
+
import { validate, ValidationError } from 'class-validator';
|
|
817
|
+
import { plainToClass } from 'class-transformer';
|
|
818
|
+
|
|
819
|
+
export interface CommandValidationResult {
|
|
820
|
+
valid: boolean;
|
|
821
|
+
errors: CommandValidationError[];
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
export interface CommandValidationError {
|
|
825
|
+
property: string;
|
|
826
|
+
constraints: Record<string, string>;
|
|
827
|
+
value?: any;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Command Validator
|
|
832
|
+
*/
|
|
833
|
+
export class CommandValidator {
|
|
834
|
+
/**
|
|
835
|
+
* Validate a command object
|
|
836
|
+
*/
|
|
837
|
+
async validate<T extends object>(
|
|
838
|
+
command: T,
|
|
839
|
+
commandClass?: new () => T,
|
|
840
|
+
): Promise<CommandValidationResult> {
|
|
841
|
+
const instance = commandClass
|
|
842
|
+
? plainToClass(commandClass, command)
|
|
843
|
+
: command;
|
|
844
|
+
|
|
845
|
+
const errors = await validate(instance as object);
|
|
846
|
+
|
|
847
|
+
return {
|
|
848
|
+
valid: errors.length === 0,
|
|
849
|
+
errors: this.formatErrors(errors),
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Validate and throw on error
|
|
855
|
+
*/
|
|
856
|
+
async validateOrThrow<T extends object>(
|
|
857
|
+
command: T,
|
|
858
|
+
commandClass?: new () => T,
|
|
859
|
+
): Promise<void> {
|
|
860
|
+
const result = await this.validate(command, commandClass);
|
|
861
|
+
|
|
862
|
+
if (!result.valid) {
|
|
863
|
+
throw new CommandValidationException(result.errors);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
private formatErrors(errors: ValidationError[]): CommandValidationError[] {
|
|
868
|
+
return errors.map(error => ({
|
|
869
|
+
property: error.property,
|
|
870
|
+
constraints: error.constraints || {},
|
|
871
|
+
value: error.value,
|
|
872
|
+
}));
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Command validation exception
|
|
878
|
+
*/
|
|
879
|
+
export class CommandValidationException extends Error {
|
|
880
|
+
constructor(public readonly errors: CommandValidationError[]) {
|
|
881
|
+
super(\`Command validation failed: \${errors.map(e => e.property).join(', ')}\`);
|
|
882
|
+
this.name = 'CommandValidationException';
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
toJSON() {
|
|
886
|
+
return {
|
|
887
|
+
name: this.name,
|
|
888
|
+
message: this.message,
|
|
889
|
+
errors: this.errors,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Command handler decorator with validation
|
|
896
|
+
*/
|
|
897
|
+
export function ValidateCommand(commandClass: new () => any): MethodDecorator {
|
|
898
|
+
return function (
|
|
899
|
+
target: any,
|
|
900
|
+
propertyKey: string | symbol,
|
|
901
|
+
descriptor: PropertyDescriptor,
|
|
902
|
+
) {
|
|
903
|
+
const originalMethod = descriptor.value;
|
|
904
|
+
const validator = new CommandValidator();
|
|
905
|
+
|
|
906
|
+
descriptor.value = async function (...args: any[]) {
|
|
907
|
+
const command = args[0];
|
|
908
|
+
await validator.validateOrThrow(command, commandClass);
|
|
909
|
+
return originalMethod.apply(this, args);
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
return descriptor;
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Pre-condition decorator
|
|
918
|
+
*/
|
|
919
|
+
export function PreCondition(
|
|
920
|
+
condition: (command: any) => boolean,
|
|
921
|
+
message: string,
|
|
922
|
+
): MethodDecorator {
|
|
923
|
+
return function (
|
|
924
|
+
target: any,
|
|
925
|
+
propertyKey: string | symbol,
|
|
926
|
+
descriptor: PropertyDescriptor,
|
|
927
|
+
) {
|
|
928
|
+
const originalMethod = descriptor.value;
|
|
929
|
+
|
|
930
|
+
descriptor.value = async function (...args: any[]) {
|
|
931
|
+
const command = args[0];
|
|
932
|
+
if (!condition(command)) {
|
|
933
|
+
throw new PreConditionFailedError(message);
|
|
934
|
+
}
|
|
935
|
+
return originalMethod.apply(this, args);
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
return descriptor;
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Pre-condition failed error
|
|
944
|
+
*/
|
|
945
|
+
export class PreConditionFailedError extends Error {
|
|
946
|
+
constructor(message: string) {
|
|
947
|
+
super(message);
|
|
948
|
+
this.name = 'PreConditionFailedError';
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
`;
|
|
952
|
+
}
|
|
953
|
+
//# sourceMappingURL=aggregate-validator.js.map
|