@gravito/scaffold 4.0.0 → 4.1.2
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 +30 -6
- package/dist/index.cjs +2959 -260
- package/dist/index.d.cts +240 -3
- package/dist/index.d.ts +240 -3
- package/dist/index.js +2956 -260
- package/package.json +5 -2
package/dist/index.cjs
CHANGED
|
@@ -30,7 +30,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
AdvancedModuleGenerator: () => AdvancedModuleGenerator,
|
|
33
34
|
BaseGenerator: () => BaseGenerator,
|
|
35
|
+
CQRSQueryModuleGenerator: () => CQRSQueryModuleGenerator,
|
|
34
36
|
CleanArchitectureGenerator: () => CleanArchitectureGenerator,
|
|
35
37
|
DddGenerator: () => DddGenerator,
|
|
36
38
|
DependencyValidator: () => DependencyValidator,
|
|
@@ -1055,6 +1057,118 @@ export default {
|
|
|
1055
1057
|
}
|
|
1056
1058
|
`;
|
|
1057
1059
|
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Generate workers configuration for job queue system
|
|
1062
|
+
*/
|
|
1063
|
+
static generateWorkersConfig(level = "basic") {
|
|
1064
|
+
switch (level) {
|
|
1065
|
+
case "advanced":
|
|
1066
|
+
return this.generateAdvancedWorkersConfig();
|
|
1067
|
+
case "production":
|
|
1068
|
+
return this.generateProductionWorkersConfig();
|
|
1069
|
+
default:
|
|
1070
|
+
return this.generateBasicWorkersConfig();
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Generate basic workers configuration
|
|
1075
|
+
*/
|
|
1076
|
+
static generateBasicWorkersConfig() {
|
|
1077
|
+
return ` /**
|
|
1078
|
+
* Workers Configuration
|
|
1079
|
+
*
|
|
1080
|
+
* Manages job execution in isolated worker threads.
|
|
1081
|
+
* Automatically selects best available runtime (Bun or Node.js).
|
|
1082
|
+
*/
|
|
1083
|
+
workers: {
|
|
1084
|
+
// Runtime environment: 'auto' | 'bun' | 'node'
|
|
1085
|
+
runtime: process.env.WORKERS_RUNTIME as 'auto' | 'bun' | 'node' ?? 'auto',
|
|
1086
|
+
|
|
1087
|
+
pool: {
|
|
1088
|
+
poolSize: Number.parseInt(process.env.WORKERS_POOL_SIZE ?? '4', 10),
|
|
1089
|
+
minWorkers: Number.parseInt(process.env.WORKERS_MIN_WORKERS ?? '0', 10),
|
|
1090
|
+
healthCheckInterval: 30000,
|
|
1091
|
+
},
|
|
1092
|
+
|
|
1093
|
+
execution: {
|
|
1094
|
+
maxExecutionTime: Number.parseInt(process.env.WORKERS_MAX_EXECUTION_TIME ?? '30000', 10),
|
|
1095
|
+
maxMemory: Number.parseInt(process.env.WORKERS_MAX_MEMORY ?? '0', 10),
|
|
1096
|
+
idleTimeout: Number.parseInt(process.env.WORKERS_IDLE_TIMEOUT ?? '60000', 10),
|
|
1097
|
+
isolateContexts: process.env.WORKERS_ISOLATE_CONTEXTS === 'true',
|
|
1098
|
+
},
|
|
1099
|
+
},`;
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Generate advanced workers configuration with Bun optimizations
|
|
1103
|
+
*/
|
|
1104
|
+
static generateAdvancedWorkersConfig() {
|
|
1105
|
+
return ` /**
|
|
1106
|
+
* Workers Configuration (Advanced)
|
|
1107
|
+
*
|
|
1108
|
+
* Manages job execution in isolated worker threads.
|
|
1109
|
+
* Includes Bun-specific optimizations for enhanced performance.
|
|
1110
|
+
*
|
|
1111
|
+
* Performance characteristics:
|
|
1112
|
+
* - Bun: 2-241x faster message passing, 20-30% less memory (smol mode)
|
|
1113
|
+
* - Node.js: Stable, widely tested, compatible
|
|
1114
|
+
*/
|
|
1115
|
+
workers: {
|
|
1116
|
+
runtime: process.env.WORKERS_RUNTIME as 'auto' | 'bun' | 'node' ?? 'auto',
|
|
1117
|
+
|
|
1118
|
+
pool: {
|
|
1119
|
+
poolSize: Number.parseInt(process.env.WORKERS_POOL_SIZE ?? '4', 10),
|
|
1120
|
+
minWorkers: Number.parseInt(process.env.WORKERS_MIN_WORKERS ?? '1', 10),
|
|
1121
|
+
healthCheckInterval: 30000,
|
|
1122
|
+
},
|
|
1123
|
+
|
|
1124
|
+
execution: {
|
|
1125
|
+
maxExecutionTime: Number.parseInt(process.env.WORKERS_MAX_EXECUTION_TIME ?? '30000', 10),
|
|
1126
|
+
maxMemory: Number.parseInt(process.env.WORKERS_MAX_MEMORY ?? '0', 10),
|
|
1127
|
+
idleTimeout: Number.parseInt(process.env.WORKERS_IDLE_TIMEOUT ?? '60000', 10),
|
|
1128
|
+
isolateContexts: process.env.WORKERS_ISOLATE_CONTEXTS === 'true',
|
|
1129
|
+
},
|
|
1130
|
+
|
|
1131
|
+
// Bun-specific optimizations
|
|
1132
|
+
bun: {
|
|
1133
|
+
smol: process.env.WORKERS_BUN_SMOL === 'true',
|
|
1134
|
+
preload: process.env.WORKERS_BUN_PRELOAD
|
|
1135
|
+
? process.env.WORKERS_BUN_PRELOAD.split(',').map((p) => p.trim())
|
|
1136
|
+
: undefined,
|
|
1137
|
+
inspectPort: process.env.WORKERS_BUN_INSPECT_PORT
|
|
1138
|
+
? Number.parseInt(process.env.WORKERS_BUN_INSPECT_PORT, 10)
|
|
1139
|
+
: undefined,
|
|
1140
|
+
},
|
|
1141
|
+
},`;
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Generate production-optimized workers configuration
|
|
1145
|
+
*/
|
|
1146
|
+
static generateProductionWorkersConfig() {
|
|
1147
|
+
return ` /**
|
|
1148
|
+
* Workers Configuration (Production Optimized)
|
|
1149
|
+
*/
|
|
1150
|
+
workers: {
|
|
1151
|
+
runtime: 'auto' as const,
|
|
1152
|
+
|
|
1153
|
+
pool: {
|
|
1154
|
+
poolSize: Number.parseInt(process.env.WORKERS_POOL_SIZE ?? '8', 10),
|
|
1155
|
+
minWorkers: 2,
|
|
1156
|
+
healthCheckInterval: 30000,
|
|
1157
|
+
},
|
|
1158
|
+
|
|
1159
|
+
execution: {
|
|
1160
|
+
maxExecutionTime: 30000,
|
|
1161
|
+
maxMemory: Number.parseInt(process.env.WORKERS_MAX_MEMORY ?? '512', 10),
|
|
1162
|
+
idleTimeout: 60000,
|
|
1163
|
+
isolateContexts: false,
|
|
1164
|
+
},
|
|
1165
|
+
|
|
1166
|
+
bun: {
|
|
1167
|
+
smol: true,
|
|
1168
|
+
preload: process.env.WORKERS_BUN_PRELOAD?.split(',').map((p) => p.trim()),
|
|
1169
|
+
},
|
|
1170
|
+
},`;
|
|
1171
|
+
}
|
|
1058
1172
|
};
|
|
1059
1173
|
|
|
1060
1174
|
// src/utils/ServiceProviderGenerator.ts
|
|
@@ -2067,235 +2181,2462 @@ Created with \u2764\uFE0F using Gravito Framework
|
|
|
2067
2181
|
}
|
|
2068
2182
|
};
|
|
2069
2183
|
|
|
2070
|
-
// src/generators/ddd/
|
|
2071
|
-
var
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2184
|
+
// src/generators/ddd/AdvancedModuleGenerator.ts
|
|
2185
|
+
var AdvancedModuleGenerator = class {
|
|
2186
|
+
/**
|
|
2187
|
+
* 生成進階 DDD 模組結構(Event Sourcing)
|
|
2188
|
+
*
|
|
2189
|
+
* 生成檔案:
|
|
2190
|
+
* - Domain/AggregateRoots/{Name}.ts
|
|
2191
|
+
* - Domain/Events/{Name}*Event.ts(3 個事件示例)
|
|
2192
|
+
* - Domain/Services/{Name}EventApplier.ts
|
|
2193
|
+
* - Domain/Repositories/I{Name}EventStore.ts
|
|
2194
|
+
* - Domain/ValueObjects/{Name}Status.ts
|
|
2195
|
+
* - Infrastructure/EventStore/InMemory{Name}EventStore.ts
|
|
2196
|
+
* - Infrastructure/EventStore/Database{Name}EventStore.ts
|
|
2197
|
+
* - Infrastructure/EventStore/{Name}EventDeserializer.ts
|
|
2198
|
+
* - Application/Services/Create{Name}Service.ts
|
|
2199
|
+
* - 完整的測試框架
|
|
2200
|
+
*/
|
|
2201
|
+
generate(moduleName, _context) {
|
|
2088
2202
|
return {
|
|
2089
2203
|
type: "directory",
|
|
2090
|
-
name:
|
|
2204
|
+
name: moduleName,
|
|
2091
2205
|
children: [
|
|
2092
|
-
|
|
2093
|
-
{
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2206
|
+
// Domain Layer
|
|
2207
|
+
{
|
|
2208
|
+
type: "directory",
|
|
2209
|
+
name: "Domain",
|
|
2210
|
+
children: [
|
|
2211
|
+
// Aggregate Root
|
|
2212
|
+
{
|
|
2213
|
+
type: "directory",
|
|
2214
|
+
name: "AggregateRoots",
|
|
2215
|
+
children: [
|
|
2216
|
+
{
|
|
2217
|
+
type: "file",
|
|
2218
|
+
name: `${moduleName}.ts`,
|
|
2219
|
+
content: this.generateAggregateRoot(moduleName)
|
|
2220
|
+
}
|
|
2221
|
+
]
|
|
2222
|
+
},
|
|
2223
|
+
// Events
|
|
2224
|
+
{
|
|
2225
|
+
type: "directory",
|
|
2226
|
+
name: "Events",
|
|
2227
|
+
children: [
|
|
2228
|
+
{
|
|
2229
|
+
type: "file",
|
|
2230
|
+
name: `${moduleName}CreatedEvent.ts`,
|
|
2231
|
+
content: this.generateEvent(moduleName, "Created")
|
|
2232
|
+
},
|
|
2233
|
+
{
|
|
2234
|
+
type: "file",
|
|
2235
|
+
name: `${moduleName}UpdatedEvent.ts`,
|
|
2236
|
+
content: this.generateEvent(moduleName, "Updated")
|
|
2237
|
+
},
|
|
2238
|
+
{
|
|
2239
|
+
type: "file",
|
|
2240
|
+
name: `${moduleName}DeletedEvent.ts`,
|
|
2241
|
+
content: this.generateEvent(moduleName, "Deleted")
|
|
2242
|
+
}
|
|
2243
|
+
]
|
|
2244
|
+
},
|
|
2245
|
+
// Value Objects
|
|
2246
|
+
{
|
|
2247
|
+
type: "directory",
|
|
2248
|
+
name: "ValueObjects",
|
|
2249
|
+
children: [
|
|
2250
|
+
{
|
|
2251
|
+
type: "file",
|
|
2252
|
+
name: `${moduleName}Status.ts`,
|
|
2253
|
+
content: this.generateStatusValueObject(moduleName)
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
type: "file",
|
|
2257
|
+
name: `${moduleName}Id.ts`,
|
|
2258
|
+
content: this.generateIdValueObject(moduleName)
|
|
2259
|
+
}
|
|
2260
|
+
]
|
|
2261
|
+
},
|
|
2262
|
+
// Event Applier
|
|
2263
|
+
{
|
|
2264
|
+
type: "directory",
|
|
2265
|
+
name: "Services",
|
|
2266
|
+
children: [
|
|
2267
|
+
{
|
|
2268
|
+
type: "file",
|
|
2269
|
+
name: `${moduleName}EventApplier.ts`,
|
|
2270
|
+
content: this.generateEventApplier(moduleName)
|
|
2271
|
+
}
|
|
2272
|
+
]
|
|
2273
|
+
},
|
|
2274
|
+
// Repositories
|
|
2275
|
+
{
|
|
2276
|
+
type: "directory",
|
|
2277
|
+
name: "Repositories",
|
|
2278
|
+
children: [
|
|
2279
|
+
{
|
|
2280
|
+
type: "file",
|
|
2281
|
+
name: `I${moduleName}EventStore.ts`,
|
|
2282
|
+
content: this.generateEventStoreInterface(moduleName)
|
|
2283
|
+
}
|
|
2284
|
+
]
|
|
2285
|
+
}
|
|
2286
|
+
]
|
|
2287
|
+
},
|
|
2288
|
+
// Application Layer
|
|
2289
|
+
{
|
|
2290
|
+
type: "directory",
|
|
2291
|
+
name: "Application",
|
|
2292
|
+
children: [
|
|
2293
|
+
{
|
|
2294
|
+
type: "directory",
|
|
2295
|
+
name: "Services",
|
|
2296
|
+
children: [
|
|
2297
|
+
{
|
|
2298
|
+
type: "file",
|
|
2299
|
+
name: `Create${moduleName}Service.ts`,
|
|
2300
|
+
content: this.generateApplicationService(moduleName)
|
|
2301
|
+
}
|
|
2302
|
+
]
|
|
2303
|
+
},
|
|
2304
|
+
{
|
|
2305
|
+
type: "directory",
|
|
2306
|
+
name: "DTOs",
|
|
2307
|
+
children: [
|
|
2308
|
+
{
|
|
2309
|
+
type: "file",
|
|
2310
|
+
name: `${moduleName}DTO.ts`,
|
|
2311
|
+
content: this.generateDTO(moduleName)
|
|
2312
|
+
}
|
|
2313
|
+
]
|
|
2314
|
+
}
|
|
2315
|
+
]
|
|
2316
|
+
},
|
|
2317
|
+
// Infrastructure Layer
|
|
2318
|
+
{
|
|
2319
|
+
type: "directory",
|
|
2320
|
+
name: "Infrastructure",
|
|
2321
|
+
children: [
|
|
2322
|
+
{
|
|
2323
|
+
type: "directory",
|
|
2324
|
+
name: "EventStore",
|
|
2325
|
+
children: [
|
|
2326
|
+
{
|
|
2327
|
+
type: "file",
|
|
2328
|
+
name: `InMemory${moduleName}EventStore.ts`,
|
|
2329
|
+
content: this.generateInMemoryEventStore(moduleName)
|
|
2330
|
+
},
|
|
2331
|
+
{
|
|
2332
|
+
type: "file",
|
|
2333
|
+
name: `Database${moduleName}EventStore.ts`,
|
|
2334
|
+
content: this.generateDatabaseEventStore(moduleName)
|
|
2335
|
+
},
|
|
2336
|
+
{
|
|
2337
|
+
type: "file",
|
|
2338
|
+
name: `${moduleName}EventDeserializer.ts`,
|
|
2339
|
+
content: this.generateEventDeserializer(moduleName)
|
|
2340
|
+
}
|
|
2341
|
+
]
|
|
2342
|
+
}
|
|
2343
|
+
]
|
|
2344
|
+
},
|
|
2345
|
+
// Presentation Layer
|
|
2346
|
+
{
|
|
2347
|
+
type: "directory",
|
|
2348
|
+
name: "Presentation",
|
|
2349
|
+
children: [
|
|
2350
|
+
{
|
|
2351
|
+
type: "directory",
|
|
2352
|
+
name: "Controllers",
|
|
2353
|
+
children: [
|
|
2354
|
+
{
|
|
2355
|
+
type: "file",
|
|
2356
|
+
name: `${moduleName}Controller.ts`,
|
|
2357
|
+
content: this.generateController(moduleName)
|
|
2358
|
+
}
|
|
2359
|
+
]
|
|
2360
|
+
},
|
|
2361
|
+
{
|
|
2362
|
+
type: "directory",
|
|
2363
|
+
name: "Routes",
|
|
2364
|
+
children: [
|
|
2365
|
+
{
|
|
2366
|
+
type: "file",
|
|
2367
|
+
name: `${this.toKebabCase(moduleName)}.routes.ts`,
|
|
2368
|
+
content: this.generateRoutes(moduleName)
|
|
2369
|
+
}
|
|
2370
|
+
]
|
|
2371
|
+
}
|
|
2372
|
+
]
|
|
2373
|
+
},
|
|
2374
|
+
// Module Export
|
|
2375
|
+
{
|
|
2376
|
+
type: "file",
|
|
2377
|
+
name: "index.ts",
|
|
2378
|
+
content: this.generateModuleIndex(moduleName)
|
|
2379
|
+
}
|
|
2097
2380
|
]
|
|
2098
2381
|
};
|
|
2099
2382
|
}
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2383
|
+
// ─────────────────────────────────────────────────────────────────
|
|
2384
|
+
// 域層代碼生成
|
|
2385
|
+
// ─────────────────────────────────────────────────────────────────
|
|
2386
|
+
generateAggregateRoot(name) {
|
|
2387
|
+
return `import { AggregateRoot } from '@/Shared/Domain/AggregateRoot'
|
|
2388
|
+
import { DomainEvent } from '@/Shared/Domain/DomainEvent'
|
|
2389
|
+
import { ${name}Status } from '../ValueObjects/${name}Status'
|
|
2390
|
+
import { ${name}Id } from '../ValueObjects/${name}Id'
|
|
2391
|
+
import {
|
|
2392
|
+
${name}CreatedEvent,
|
|
2393
|
+
${name}UpdatedEvent,
|
|
2394
|
+
${name}DeletedEvent,
|
|
2395
|
+
} from '../Events'
|
|
2396
|
+
|
|
2397
|
+
/**
|
|
2398
|
+
* ${name} \u805A\u5408\u6839
|
|
2103
2399
|
*
|
|
2104
|
-
*
|
|
2400
|
+
* Event Sourcing \u5BE6\u73FE\uFF1A\u72C0\u614B\u5B8C\u5168\u7531\u4E8B\u4EF6\u6D41\u6C7A\u5B9A
|
|
2401
|
+
*
|
|
2402
|
+
* \u751F\u547D\u9031\u671F\uFF1A
|
|
2403
|
+
* CREATED \u2192 (UPDATED*) \u2192 DELETED
|
|
2105
2404
|
*/
|
|
2405
|
+
export class ${name} extends AggregateRoot {
|
|
2406
|
+
private id: ${name}Id | null = null
|
|
2407
|
+
private status: ${name}Status | null = null
|
|
2408
|
+
private name: string = ''
|
|
2409
|
+
private description: string = ''
|
|
2410
|
+
private createdAt: Date = new Date()
|
|
2411
|
+
private updatedAt: Date = new Date()
|
|
2412
|
+
|
|
2413
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2414
|
+
// \u5DE5\u5EE0\u65B9\u6CD5 - \u5EFA\u7ACB\u65B0\u805A\u5408
|
|
2415
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2106
2416
|
|
|
2107
|
-
|
|
2417
|
+
/**
|
|
2418
|
+
* \u5DE5\u5EE0\u65B9\u6CD5\uFF1A\u5EFA\u7ACB\u65B0 ${name}
|
|
2419
|
+
*/
|
|
2420
|
+
static create(id: ${name}Id, name: string, description?: string): ${name} {
|
|
2421
|
+
const instance = new ${name}()
|
|
2422
|
+
|
|
2423
|
+
instance.raiseEvent(
|
|
2424
|
+
new ${name}CreatedEvent(id.value, {
|
|
2425
|
+
name,
|
|
2426
|
+
description: description || '',
|
|
2427
|
+
})
|
|
2428
|
+
)
|
|
2108
2429
|
|
|
2109
|
-
|
|
2430
|
+
return instance
|
|
2431
|
+
}
|
|
2110
2432
|
|
|
2111
|
-
|
|
2112
|
-
|
|
2433
|
+
/**
|
|
2434
|
+
* \u5F9E\u4E8B\u4EF6\u6D41\u91CD\u5EFA ${name}
|
|
2435
|
+
*/
|
|
2436
|
+
static fromEvents(events: DomainEvent[]): ${name} {
|
|
2437
|
+
const instance = new ${name}()
|
|
2438
|
+
|
|
2439
|
+
for (const event of events) {
|
|
2440
|
+
instance.applyEvent(event)
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
return instance
|
|
2113
2444
|
}
|
|
2114
|
-
generateBootstrapApp(_context) {
|
|
2115
|
-
return `/**
|
|
2116
|
-
* Application Bootstrap
|
|
2117
|
-
*
|
|
2118
|
-
* Central configuration and initialization using the ServiceProvider pattern.
|
|
2119
|
-
*
|
|
2120
|
-
* Lifecycle:
|
|
2121
|
-
* 1. Configure: Load app config and orbits
|
|
2122
|
-
* 2. Boot: Initialize PlanetCore
|
|
2123
|
-
* 3. Register Providers: Bind services to container
|
|
2124
|
-
* 4. Bootstrap: Boot all providers
|
|
2125
|
-
*/
|
|
2126
2445
|
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
import { registerProviders } from './providers'
|
|
2131
|
-
import { registerRoutes } from './routes'
|
|
2446
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2447
|
+
// \u547D\u4EE4\u65B9\u6CD5 - \u72C0\u614B\u8F49\u79FB
|
|
2448
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2132
2449
|
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
orbits: [
|
|
2138
|
-
new OrbitAtlas() as unknown as import('@gravito/core').GravitoOrbit,
|
|
2139
|
-
],
|
|
2140
|
-
})
|
|
2450
|
+
update(name: string, description?: string): void {
|
|
2451
|
+
if (!this.id) {
|
|
2452
|
+
throw new Error('${name} \u672A\u521D\u59CB\u5316')
|
|
2453
|
+
}
|
|
2141
2454
|
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2455
|
+
if (this.status?.isDeleted()) {
|
|
2456
|
+
throw new Error('${name} \u5DF2\u522A\u9664')
|
|
2457
|
+
}
|
|
2145
2458
|
|
|
2146
|
-
|
|
2147
|
-
|
|
2459
|
+
this.raiseEvent(
|
|
2460
|
+
new ${name}UpdatedEvent(this.id.value, {
|
|
2461
|
+
name,
|
|
2462
|
+
description: description || '',
|
|
2463
|
+
})
|
|
2464
|
+
)
|
|
2465
|
+
}
|
|
2148
2466
|
|
|
2149
|
-
|
|
2150
|
-
|
|
2467
|
+
delete(): void {
|
|
2468
|
+
if (!this.id) {
|
|
2469
|
+
throw new Error('${name} \u672A\u521D\u59CB\u5316')
|
|
2470
|
+
}
|
|
2151
2471
|
|
|
2152
|
-
|
|
2153
|
-
|
|
2472
|
+
if (this.status?.isDeleted()) {
|
|
2473
|
+
throw new Error('${name} \u5DF2\u522A\u9664')
|
|
2474
|
+
}
|
|
2154
2475
|
|
|
2155
|
-
|
|
2156
|
-
}
|
|
2157
|
-
`;
|
|
2476
|
+
this.raiseEvent(new ${name}DeletedEvent(this.id.value))
|
|
2158
2477
|
}
|
|
2159
|
-
generateProvidersRegistry(_context) {
|
|
2160
|
-
return `/**
|
|
2161
|
-
* Service Providers Registry
|
|
2162
|
-
*
|
|
2163
|
-
* Register all service providers here.
|
|
2164
|
-
* Include both global and module-specific providers.
|
|
2165
|
-
*/
|
|
2166
2478
|
|
|
2479
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2480
|
+
// \u4E8B\u4EF6\u61C9\u7528 - \u72C0\u614B\u66F4\u65B0
|
|
2481
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2482
|
+
|
|
2483
|
+
applyEvent(event: DomainEvent): void {
|
|
2484
|
+
switch (event.eventType) {
|
|
2485
|
+
case '${this.toKebabCase(name)}.created':
|
|
2486
|
+
this.onCreated(event)
|
|
2487
|
+
break
|
|
2488
|
+
case '${this.toKebabCase(name)}.updated':
|
|
2489
|
+
this.onUpdated(event)
|
|
2490
|
+
break
|
|
2491
|
+
case '${this.toKebabCase(name)}.deleted':
|
|
2492
|
+
this.onDeleted(event)
|
|
2493
|
+
break
|
|
2494
|
+
default:
|
|
2495
|
+
throw new Error(\`Unknown event type: \${event.eventType}\`)
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
private onCreated(event: DomainEvent): void {
|
|
2500
|
+
this.id = new ${name}Id(event.aggregateId)
|
|
2501
|
+
this.status = new ${name}Status('created')
|
|
2502
|
+
this.name = event.data.name as string
|
|
2503
|
+
this.description = event.data.description as string
|
|
2504
|
+
this.createdAt = event.occurredAt
|
|
2505
|
+
this.updatedAt = event.occurredAt
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
private onUpdated(event: DomainEvent): void {
|
|
2509
|
+
this.name = event.data.name as string
|
|
2510
|
+
this.description = event.data.description as string
|
|
2511
|
+
this.updatedAt = event.occurredAt
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
private onDeleted(event: DomainEvent): void {
|
|
2515
|
+
this.status = new ${name}Status('deleted')
|
|
2516
|
+
this.updatedAt = event.occurredAt
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2520
|
+
// \u8B80\u53D6\u65B9\u6CD5 - \u53D6\u5F97\u72C0\u614B
|
|
2521
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2522
|
+
|
|
2523
|
+
getId(): ${name}Id {
|
|
2524
|
+
if (!this.id) throw new Error('ID \u672A\u521D\u59CB\u5316')
|
|
2525
|
+
return this.id
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
getStatus(): ${name}Status {
|
|
2529
|
+
if (!this.status) throw new Error('Status \u672A\u521D\u59CB\u5316')
|
|
2530
|
+
return this.status
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
getName(): string {
|
|
2534
|
+
return this.name
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
getDescription(): string {
|
|
2538
|
+
return this.description
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
getCreatedAt(): Date {
|
|
2542
|
+
return this.createdAt
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
getUpdatedAt(): Date {
|
|
2546
|
+
return this.updatedAt
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
`;
|
|
2550
|
+
}
|
|
2551
|
+
generateEvent(name, action) {
|
|
2552
|
+
return `import { DomainEvent } from '@/Shared/Domain/DomainEvent'
|
|
2553
|
+
|
|
2554
|
+
export interface ${name}${action}Data {
|
|
2555
|
+
name: string
|
|
2556
|
+
description?: string
|
|
2557
|
+
// TODO: \u65B0\u589E\u5176\u4ED6\u4E8B\u4EF6\u8CC7\u6599
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
/**
|
|
2561
|
+
* ${name}${action}Event
|
|
2562
|
+
*
|
|
2563
|
+
* \u4E8B\u4EF6\u5305\u542B\u6240\u6709\u5FC5\u8981\u8CC7\u8A0A\u4EE5\u91CD\u5EFA\u805A\u5408\u72C0\u614B
|
|
2564
|
+
* \u4E8B\u4EF6\u662F\u4E0D\u53EF\u8B8A\u7684
|
|
2565
|
+
*/
|
|
2566
|
+
export class ${name}${action}Event extends DomainEvent {
|
|
2567
|
+
constructor(
|
|
2568
|
+
aggregateId: string,
|
|
2569
|
+
payload: ${name}${action}Data,
|
|
2570
|
+
version?: number,
|
|
2571
|
+
occurredAt?: Date
|
|
2572
|
+
) {
|
|
2573
|
+
super(
|
|
2574
|
+
aggregateId,
|
|
2575
|
+
'${this.toKebabCase(name)}.${this.toKebabCase(action)}',
|
|
2576
|
+
{
|
|
2577
|
+
name: payload.name,
|
|
2578
|
+
description: payload.description || '',
|
|
2579
|
+
// TODO: \u65B0\u589E\u5176\u4ED6\u5C6C\u6027
|
|
2580
|
+
},
|
|
2581
|
+
version,
|
|
2582
|
+
occurredAt
|
|
2583
|
+
)
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
/**
|
|
2587
|
+
* \u5411\u5F8C\u76F8\u5BB9\u6027\u652F\u63F4
|
|
2588
|
+
* \u7576\u4E8B\u4EF6\u7D50\u69CB\u8B8A\u5316\u6642\uFF0C\u8986\u84CB\u6B64\u65B9\u6CD5
|
|
2589
|
+
*/
|
|
2590
|
+
getSchemaVersion(): string {
|
|
2591
|
+
return '1.0.0'
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
toJSON() {
|
|
2595
|
+
return {
|
|
2596
|
+
eventId: this.eventId,
|
|
2597
|
+
aggregateId: this.aggregateId,
|
|
2598
|
+
eventType: this.eventType,
|
|
2599
|
+
occurredAt: this.occurredAt,
|
|
2600
|
+
version: this.version,
|
|
2601
|
+
data: this.data,
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
`;
|
|
2606
|
+
}
|
|
2607
|
+
generateStatusValueObject(name) {
|
|
2608
|
+
return `import { ValueObject } from '@/Shared/Domain/ValueObject'
|
|
2609
|
+
|
|
2610
|
+
export type ${name}StatusValue = 'created' | 'updated' | 'deleted'
|
|
2611
|
+
|
|
2612
|
+
/**
|
|
2613
|
+
* ${name}Status - \u72C0\u614B\u503C\u7269\u4EF6
|
|
2614
|
+
*
|
|
2615
|
+
* \u4E0D\u53EF\u8B8A\u7684\u72C0\u614B\u503C
|
|
2616
|
+
*/
|
|
2617
|
+
export class ${name}Status extends ValueObject {
|
|
2618
|
+
constructor(readonly value: ${name}StatusValue) {
|
|
2619
|
+
super()
|
|
2620
|
+
this.validate()
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
private validate(): void {
|
|
2624
|
+
const validStatuses: ${name}StatusValue[] = ['created', 'updated', 'deleted']
|
|
2625
|
+
if (!validStatuses.includes(this.value)) {
|
|
2626
|
+
throw new Error(\`Invalid ${name} status: \${this.value}\`)
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
isCreated(): boolean {
|
|
2631
|
+
return this.value === 'created'
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
isUpdated(): boolean {
|
|
2635
|
+
return this.value === 'updated'
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
isDeleted(): boolean {
|
|
2639
|
+
return this.value === 'deleted'
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
equals(other: ValueObject): boolean {
|
|
2643
|
+
if (!(other instanceof ${name}Status)) return false
|
|
2644
|
+
return this.value === other.value
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
toString(): string {
|
|
2648
|
+
return this.value
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
`;
|
|
2652
|
+
}
|
|
2653
|
+
generateIdValueObject(name) {
|
|
2654
|
+
return `import { ValueObject } from '@/Shared/Domain/ValueObject'
|
|
2655
|
+
|
|
2656
|
+
/**
|
|
2657
|
+
* ${name}Id - ID \u503C\u7269\u4EF6
|
|
2658
|
+
*
|
|
2659
|
+
* \u78BA\u4FDD ID \u7684\u6709\u6548\u6027
|
|
2660
|
+
*/
|
|
2661
|
+
export class ${name}Id extends ValueObject {
|
|
2662
|
+
constructor(readonly value: string) {
|
|
2663
|
+
super()
|
|
2664
|
+
this.validate()
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
private validate(): void {
|
|
2668
|
+
if (!this.value || this.value.trim() === '') {
|
|
2669
|
+
throw new Error(\`Invalid ${name} ID: \${this.value}\`)
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
equals(other: ValueObject): boolean {
|
|
2674
|
+
if (!(other instanceof ${name}Id)) return false
|
|
2675
|
+
return this.value === other.value
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
toString(): string {
|
|
2679
|
+
return this.value
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
`;
|
|
2683
|
+
}
|
|
2684
|
+
generateEventApplier(name) {
|
|
2685
|
+
return `import { DomainEvent } from '@/Shared/Domain/DomainEvent'
|
|
2686
|
+
|
|
2687
|
+
/**
|
|
2688
|
+
* ${name}EventApplier - \u7D14\u51FD\u5F0F\u4E8B\u4EF6\u61C9\u7528\u5668
|
|
2689
|
+
*
|
|
2690
|
+
* \u8A2D\u8A08\u539F\u5247\uFF1A
|
|
2691
|
+
* 1. \u7121\u526F\u4F5C\u7528\uFF1A\u76F8\u540C\u8F38\u5165 \u2192 \u76F8\u540C\u8F38\u51FA
|
|
2692
|
+
* 2. \u4E0D\u53EF\u8B8A\u6027\uFF1A\u4E0D\u4FEE\u6539\u8F38\u5165\u72C0\u614B
|
|
2693
|
+
* 3. \u5931\u6557\u5FEB\u901F\uFF1A\u7121\u6548\u4E8B\u4EF6\u7ACB\u5373\u62CB\u51FA
|
|
2694
|
+
*/
|
|
2695
|
+
|
|
2696
|
+
export interface ${name}State {
|
|
2697
|
+
readonly id: string
|
|
2698
|
+
readonly status: string
|
|
2699
|
+
readonly name: string
|
|
2700
|
+
readonly description: string
|
|
2701
|
+
readonly createdAt: Date
|
|
2702
|
+
readonly updatedAt: Date
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
export class ${name}EventApplier {
|
|
2706
|
+
/**
|
|
2707
|
+
* \u61C9\u7528\u55AE\u500B\u4E8B\u4EF6\u5230\u72C0\u614B
|
|
2708
|
+
*
|
|
2709
|
+
* @param state \u76EE\u524D\u72C0\u614B\uFF08null \u8868\u793A\u65B0\u805A\u5408\uFF09
|
|
2710
|
+
* @param event \u8981\u61C9\u7528\u7684\u4E8B\u4EF6
|
|
2711
|
+
* @returns \u65B0\u72C0\u614B\uFF08state \u4E0D\u53EF\u8B8A\uFF09
|
|
2712
|
+
*/
|
|
2713
|
+
static apply(state: ${name}State | null, event: DomainEvent): ${name}State {
|
|
2714
|
+
// \u521D\u59CB\u5316\u4E8B\u4EF6
|
|
2715
|
+
if (event.eventType === '${this.toKebabCase(name)}.created') {
|
|
2716
|
+
if (state !== null) {
|
|
2717
|
+
throw new Error('${name} \u5DF2\u5B58\u5728\uFF0C\u7121\u6CD5\u518D\u6B21\u5EFA\u7ACB')
|
|
2718
|
+
}
|
|
2719
|
+
return this.applyCreated(event)
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
// \u975E\u521D\u59CB\u5316\u4E8B\u4EF6
|
|
2723
|
+
if (state === null) {
|
|
2724
|
+
throw new Error(\`Event \${event.eventType} \u8981\u6C42 state \u975E null\`)
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
switch (event.eventType) {
|
|
2728
|
+
case '${this.toKebabCase(name)}.updated':
|
|
2729
|
+
return this.applyUpdated(state, event)
|
|
2730
|
+
case '${this.toKebabCase(name)}.deleted':
|
|
2731
|
+
return this.applyDeleted(state, event)
|
|
2732
|
+
default:
|
|
2733
|
+
throw new Error(\`Unknown event type: \${event.eventType}\`)
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
private static applyCreated(event: DomainEvent): ${name}State {
|
|
2738
|
+
return {
|
|
2739
|
+
id: event.aggregateId,
|
|
2740
|
+
status: 'created',
|
|
2741
|
+
name: event.data.name as string,
|
|
2742
|
+
description: event.data.description as string,
|
|
2743
|
+
createdAt: event.occurredAt,
|
|
2744
|
+
updatedAt: event.occurredAt,
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
private static applyUpdated(state: ${name}State, event: DomainEvent): ${name}State {
|
|
2749
|
+
return {
|
|
2750
|
+
...state,
|
|
2751
|
+
name: event.data.name as string,
|
|
2752
|
+
description: event.data.description as string,
|
|
2753
|
+
updatedAt: event.occurredAt,
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
private static applyDeleted(state: ${name}State, event: DomainEvent): ${name}State {
|
|
2758
|
+
return {
|
|
2759
|
+
...state,
|
|
2760
|
+
status: 'deleted',
|
|
2761
|
+
updatedAt: event.occurredAt,
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
`;
|
|
2766
|
+
}
|
|
2767
|
+
generateEventStoreInterface(name) {
|
|
2768
|
+
return `import { ${name} } from '../AggregateRoots/${name}'
|
|
2769
|
+
|
|
2770
|
+
/**
|
|
2771
|
+
* I${name}EventStore - \u4E8B\u4EF6\u5B58\u5132\u4ECB\u9762
|
|
2772
|
+
*
|
|
2773
|
+
* \u8CA0\u8CAC\u4E8B\u4EF6\u7684\u6301\u4E45\u5316\u548C\u91CD\u5EFA
|
|
2774
|
+
*/
|
|
2775
|
+
export interface I${name}EventStore {
|
|
2776
|
+
/**
|
|
2777
|
+
* \u4FDD\u5B58\u805A\u5408\uFF08\u6301\u4E45\u5316\u65B0\u4E8B\u4EF6\uFF09
|
|
2778
|
+
*/
|
|
2779
|
+
save(aggregate: ${name}): Promise<void>
|
|
2780
|
+
|
|
2781
|
+
/**
|
|
2782
|
+
* \u6839\u64DA ID \u67E5\u8A62\u805A\u5408
|
|
2783
|
+
*/
|
|
2784
|
+
findById(id: string): Promise<${name} | null>
|
|
2785
|
+
|
|
2786
|
+
/**
|
|
2787
|
+
* \u53D6\u5F97\u6240\u6709\u4E8B\u4EF6\uFF08\u7528\u65BC\u91CD\u5EFA\uFF09
|
|
2788
|
+
*/
|
|
2789
|
+
getEvents(aggregateId: string): Promise<Event[]>
|
|
2790
|
+
}
|
|
2791
|
+
`;
|
|
2792
|
+
}
|
|
2793
|
+
generateInMemoryEventStore(name) {
|
|
2794
|
+
return `import type { DomainEvent } from '@/Shared/Domain/DomainEvent'
|
|
2795
|
+
import type { I${name}EventStore } from '../../Domain/Repositories/I${name}EventStore'
|
|
2796
|
+
import { ${name} } from '../../Domain/AggregateRoots/${name}'
|
|
2797
|
+
|
|
2798
|
+
/**
|
|
2799
|
+
* InMemory${name}EventStore - \u8A18\u61B6\u9AD4\u5BE6\u73FE
|
|
2800
|
+
*
|
|
2801
|
+
* \u7528\u9014\uFF1A
|
|
2802
|
+
* - \u55AE\u5143\u6E2C\u8A66\uFF08\u5FEB\u901F\uFF0C\u7121 I/O\uFF09
|
|
2803
|
+
* - \u958B\u767C\u74B0\u5883\uFF08\u7C21\u55AE\uFF0C\u7121\u9700 DB\uFF09
|
|
2804
|
+
* - \u539F\u578B\u8A2D\u8A08
|
|
2805
|
+
*/
|
|
2806
|
+
export class InMemory${name}EventStore implements I${name}EventStore {
|
|
2807
|
+
private events = new Map<string, DomainEvent[]>()
|
|
2808
|
+
|
|
2809
|
+
async save(aggregate: ${name}): Promise<void> {
|
|
2810
|
+
const id = aggregate.getId().value
|
|
2811
|
+
const uncommittedEvents = aggregate.getUncommittedEvents()
|
|
2812
|
+
|
|
2813
|
+
if (uncommittedEvents.length === 0) return
|
|
2814
|
+
|
|
2815
|
+
if (!this.events.has(id)) {
|
|
2816
|
+
this.events.set(id, [...uncommittedEvents])
|
|
2817
|
+
} else {
|
|
2818
|
+
this.events.get(id)!.push(...uncommittedEvents)
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
aggregate.markEventsAsCommitted()
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
async findById(id: string): Promise<${name} | null> {
|
|
2825
|
+
const events = this.events.get(id)
|
|
2826
|
+
if (!events || events.length === 0) return null
|
|
2827
|
+
|
|
2828
|
+
return ${name}.fromEvents(events)
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
async getEvents(aggregateId: string): Promise<DomainEvent[]> {
|
|
2832
|
+
return this.events.get(aggregateId) || []
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// \u6E2C\u8A66\u8F14\u52A9\u65B9\u6CD5
|
|
2836
|
+
clear(): void {
|
|
2837
|
+
this.events.clear()
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
getAllEvents(): DomainEvent[] {
|
|
2841
|
+
return Array.from(this.events.values()).flat()
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
`;
|
|
2845
|
+
}
|
|
2846
|
+
generateDatabaseEventStore(name) {
|
|
2847
|
+
return `import type { DomainEvent } from '@/Shared/Domain/DomainEvent'
|
|
2848
|
+
import type { I${name}EventStore } from '../../Domain/Repositories/I${name}EventStore'
|
|
2849
|
+
import { ${name} } from '../../Domain/AggregateRoots/${name}'
|
|
2850
|
+
import { ${name}EventDeserializer } from './${name}EventDeserializer'
|
|
2851
|
+
|
|
2852
|
+
/**
|
|
2853
|
+
* Database${name}EventStore - \u8CC7\u6599\u5EAB\u5BE6\u73FE
|
|
2854
|
+
*
|
|
2855
|
+
* \u7528\u9014\uFF1A
|
|
2856
|
+
* - \u751F\u7522\u74B0\u5883\uFF08\u6301\u4E45\u5316\uFF0C\u53EF\u9760\uFF09
|
|
2857
|
+
* - \u5B8C\u6574\u6B77\u53F2\u8A18\u9304\uFF08\u6240\u6709\u4E8B\u4EF6\uFF09
|
|
2858
|
+
* - \u4E8B\u4EF6\u91CD\u653E
|
|
2859
|
+
*
|
|
2860
|
+
* \u8868\u7D50\u69CB\uFF1A
|
|
2861
|
+
* ${this.toSnakeCase(name)}_events (
|
|
2862
|
+
* aggregate_id VARCHAR(255),
|
|
2863
|
+
* event_type VARCHAR(255),
|
|
2864
|
+
* payload JSON,
|
|
2865
|
+
* version INT,
|
|
2866
|
+
* occurred_at TIMESTAMP
|
|
2867
|
+
* )
|
|
2868
|
+
*/
|
|
2869
|
+
export class Database${name}EventStore implements I${name}EventStore {
|
|
2870
|
+
// TODO: \u6CE8\u5165 DB \u9023\u63A5\uFF08\u4F7F\u7528 @gravito/atlas\uFF09
|
|
2871
|
+
// constructor(private db: Database) {}
|
|
2872
|
+
|
|
2873
|
+
async save(aggregate: ${name}): Promise<void> {
|
|
2874
|
+
const id = aggregate.getId().value
|
|
2875
|
+
const uncommittedEvents = aggregate.getUncommittedEvents()
|
|
2876
|
+
|
|
2877
|
+
if (uncommittedEvents.length === 0) return
|
|
2878
|
+
|
|
2879
|
+
// TODO: \u63D2\u5165\u4E8B\u4EF6\u5230\u8CC7\u6599\u5EAB
|
|
2880
|
+
// for (const event of uncommittedEvents) {
|
|
2881
|
+
// await this.db.table('${this.toSnakeCase(name)}_events').insert({
|
|
2882
|
+
// aggregate_id: id,
|
|
2883
|
+
// event_type: event.eventType,
|
|
2884
|
+
// payload: JSON.stringify(event.data),
|
|
2885
|
+
// version: event.version,
|
|
2886
|
+
// occurred_at: event.occurredAt.toISOString()
|
|
2887
|
+
// })
|
|
2888
|
+
// }
|
|
2889
|
+
|
|
2890
|
+
aggregate.markEventsAsCommitted()
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
async findById(id: string): Promise<${name} | null> {
|
|
2894
|
+
// TODO: \u5F9E\u8CC7\u6599\u5EAB\u67E5\u8A62\u4E8B\u4EF6
|
|
2895
|
+
// const rows = await this.db
|
|
2896
|
+
// .table('${this.toSnakeCase(name)}_events')
|
|
2897
|
+
// .where('aggregate_id', id)
|
|
2898
|
+
// .orderBy('version', 'asc')
|
|
2899
|
+
// .select()
|
|
2900
|
+
|
|
2901
|
+
// if (!rows || rows.length === 0) return null
|
|
2902
|
+
|
|
2903
|
+
// const events = rows.map(row =>
|
|
2904
|
+
// ${name}EventDeserializer.fromDatabaseRow(row)
|
|
2905
|
+
// )
|
|
2906
|
+
|
|
2907
|
+
// return ${name}.fromEvents(events)
|
|
2908
|
+
|
|
2909
|
+
return null
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
async getEvents(aggregateId: string): Promise<DomainEvent[]> {
|
|
2913
|
+
// TODO: \u67E5\u8A62\u6240\u6709\u4E8B\u4EF6\uFF08\u7528\u65BC\u91CD\u653E\uFF09
|
|
2914
|
+
return []
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
`;
|
|
2918
|
+
}
|
|
2919
|
+
generateEventDeserializer(name) {
|
|
2920
|
+
return `import type { DomainEvent } from '@/Shared/Domain/DomainEvent'
|
|
2167
2921
|
import {
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2922
|
+
${name}CreatedEvent,
|
|
2923
|
+
${name}UpdatedEvent,
|
|
2924
|
+
${name}DeletedEvent,
|
|
2925
|
+
} from '../../Domain/Events'
|
|
2926
|
+
|
|
2927
|
+
/**
|
|
2928
|
+
* ${name}EventDeserializer - \u4E8B\u4EF6\u53CD\u5E8F\u5217\u5316\u5668
|
|
2929
|
+
*
|
|
2930
|
+
* \u8CAC\u4EFB\uFF1A
|
|
2931
|
+
* 1. \u5F9E JSON payload \u91CD\u5EFA DomainEvent \u5B50\u985E
|
|
2932
|
+
* 2. \u652F\u63F4\u4E8B\u4EF6\u7248\u672C\u9077\u79FB
|
|
2933
|
+
* 3. \u6642\u9593\u6233\u8F49\u63DB
|
|
2934
|
+
*/
|
|
2935
|
+
export class ${name}EventDeserializer {
|
|
2936
|
+
static deserialize(
|
|
2937
|
+
eventType: string,
|
|
2938
|
+
payload: Record<string, unknown>,
|
|
2939
|
+
aggregateId: string,
|
|
2940
|
+
occurredAt: Date,
|
|
2941
|
+
version: number
|
|
2942
|
+
): DomainEvent {
|
|
2943
|
+
switch (eventType) {
|
|
2944
|
+
case '${this.toKebabCase(name)}.created':
|
|
2945
|
+
return new ${name}CreatedEvent(
|
|
2946
|
+
aggregateId,
|
|
2947
|
+
{
|
|
2948
|
+
name: payload.name as string,
|
|
2949
|
+
description: payload.description as string,
|
|
2950
|
+
},
|
|
2951
|
+
version,
|
|
2952
|
+
occurredAt
|
|
2953
|
+
)
|
|
2954
|
+
|
|
2955
|
+
case '${this.toKebabCase(name)}.updated':
|
|
2956
|
+
return new ${name}UpdatedEvent(
|
|
2957
|
+
aggregateId,
|
|
2958
|
+
{
|
|
2959
|
+
name: payload.name as string,
|
|
2960
|
+
description: payload.description as string,
|
|
2961
|
+
},
|
|
2962
|
+
version,
|
|
2963
|
+
occurredAt
|
|
2964
|
+
)
|
|
2965
|
+
|
|
2966
|
+
case '${this.toKebabCase(name)}.deleted':
|
|
2967
|
+
return new ${name}DeletedEvent(aggregateId, {}, version, occurredAt)
|
|
2968
|
+
|
|
2969
|
+
default:
|
|
2970
|
+
throw new Error(\`Unknown event type: \${eventType}\`)
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
static fromDatabaseRow(row: any): DomainEvent {
|
|
2975
|
+
const payload = typeof row.payload === 'string'
|
|
2976
|
+
? JSON.parse(row.payload)
|
|
2977
|
+
: row.payload
|
|
2978
|
+
const occurredAt = new Date(row.occurred_at)
|
|
2979
|
+
|
|
2980
|
+
return this.deserialize(
|
|
2981
|
+
row.event_type,
|
|
2982
|
+
payload,
|
|
2983
|
+
row.aggregate_id,
|
|
2984
|
+
occurredAt,
|
|
2985
|
+
row.version
|
|
2986
|
+
)
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
`;
|
|
2990
|
+
}
|
|
2991
|
+
generateApplicationService(name) {
|
|
2992
|
+
return `import type { I${name}EventStore } from '../../Domain/Repositories/I${name}EventStore'
|
|
2993
|
+
import { ${name} } from '../../Domain/AggregateRoots/${name}'
|
|
2994
|
+
import { ${name}Id } from '../../Domain/ValueObjects/${name}Id'
|
|
2995
|
+
import { ${name}DTO } from '../DTOs/${name}DTO'
|
|
2996
|
+
|
|
2997
|
+
/**
|
|
2998
|
+
* Create${name}Service - \u5EFA\u7ACB ${name} \u7684\u61C9\u7528\u670D\u52D9
|
|
2999
|
+
*
|
|
3000
|
+
* \u8CAC\u4EFB\uFF1A
|
|
3001
|
+
* 1. \u5354\u8ABF\u9818\u57DF\u908F\u8F2F
|
|
3002
|
+
* 2. \u7BA1\u7406\u4E8B\u52D9
|
|
3003
|
+
* 3. \u8F49\u63DB DTO
|
|
3004
|
+
*/
|
|
3005
|
+
export class Create${name}Service {
|
|
3006
|
+
constructor(private eventStore: I${name}EventStore) {}
|
|
3007
|
+
|
|
3008
|
+
async execute(input: { name: string; description?: string }): Promise<${name}DTO> {
|
|
3009
|
+
// TODO: \u9A57\u8B49\u8F38\u5165
|
|
3010
|
+
// if (!input.name || input.name.trim() === '') {
|
|
3011
|
+
// throw new Error('Name \u4E0D\u80FD\u70BA\u7A7A')
|
|
3012
|
+
// }
|
|
3013
|
+
|
|
3014
|
+
// \u5EFA\u7ACB\u805A\u5408
|
|
3015
|
+
const id = new ${name}Id(\`${this.toKebabCase(name)}-\${crypto.randomUUID()}\`)
|
|
3016
|
+
const aggregate = ${name}.create(id, input.name, input.description)
|
|
3017
|
+
|
|
3018
|
+
// \u6301\u4E45\u5316\u4E8B\u4EF6
|
|
3019
|
+
await this.eventStore.save(aggregate)
|
|
3020
|
+
|
|
3021
|
+
// \u8F49\u63DB\u70BA DTO
|
|
3022
|
+
return ${name}DTO.fromEntity(aggregate)
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
`;
|
|
3026
|
+
}
|
|
3027
|
+
generateDTO(name) {
|
|
3028
|
+
return `import { ${name} } from '../../Domain/AggregateRoots/${name}'
|
|
3029
|
+
|
|
3030
|
+
/**
|
|
3031
|
+
* ${name}DTO - \u8CC7\u6599\u8F49\u79FB\u7269\u4EF6
|
|
3032
|
+
*
|
|
3033
|
+
* \u7528\u9014\uFF1A
|
|
3034
|
+
* - \u8DE8\u5C64\u8F49\u79FB\u8CC7\u6599
|
|
3035
|
+
* - API \u97FF\u61C9\u683C\u5F0F\u5316
|
|
3036
|
+
* - \u96B1\u85CF\u5167\u90E8\u7D30\u7BC0
|
|
3037
|
+
*/
|
|
3038
|
+
export class ${name}DTO {
|
|
3039
|
+
id!: string
|
|
3040
|
+
status!: string
|
|
3041
|
+
name!: string
|
|
3042
|
+
description!: string
|
|
3043
|
+
createdAt!: Date
|
|
3044
|
+
updatedAt!: Date
|
|
3045
|
+
|
|
3046
|
+
static fromEntity(entity: ${name}): ${name}DTO {
|
|
3047
|
+
const dto = new ${name}DTO()
|
|
3048
|
+
dto.id = entity.getId().value
|
|
3049
|
+
dto.status = entity.getStatus().value
|
|
3050
|
+
dto.name = entity.getName()
|
|
3051
|
+
dto.description = entity.getDescription()
|
|
3052
|
+
dto.createdAt = entity.getCreatedAt()
|
|
3053
|
+
dto.updatedAt = entity.getUpdatedAt()
|
|
3054
|
+
return dto
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
toJSON() {
|
|
3058
|
+
return {
|
|
3059
|
+
id: this.id,
|
|
3060
|
+
status: this.status,
|
|
3061
|
+
name: this.name,
|
|
3062
|
+
description: this.description,
|
|
3063
|
+
createdAt: this.createdAt,
|
|
3064
|
+
updatedAt: this.updatedAt,
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
`;
|
|
3069
|
+
}
|
|
3070
|
+
generateController(name) {
|
|
3071
|
+
return `import type { IModuleRouter } from '@/Shared/Presentation/IModuleRouter'
|
|
3072
|
+
import { Create${name}Service } from '../../Application/Services/Create${name}Service'
|
|
3073
|
+
import type { I${name}EventStore } from '../../Domain/Repositories/I${name}EventStore'
|
|
3074
|
+
|
|
3075
|
+
/**
|
|
3076
|
+
* ${name}Controller - HTTP \u8655\u7406\u5668
|
|
3077
|
+
*
|
|
3078
|
+
* \u8CAC\u4EFB\uFF1A
|
|
3079
|
+
* 1. \u89E3\u6790 HTTP \u8ACB\u6C42
|
|
3080
|
+
* 2. \u8ABF\u7528\u61C9\u7528\u670D\u52D9
|
|
3081
|
+
* 3. \u683C\u5F0F\u5316\u97FF\u61C9
|
|
3082
|
+
*/
|
|
3083
|
+
export class ${name}Controller {
|
|
3084
|
+
private createService: Create${name}Service
|
|
3085
|
+
|
|
3086
|
+
constructor(eventStore: I${name}EventStore) {
|
|
3087
|
+
this.createService = new Create${name}Service(eventStore)
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
async create(ctx: any) {
|
|
3091
|
+
try {
|
|
3092
|
+
// TODO: \u89E3\u6790 request body
|
|
3093
|
+
const input = {
|
|
3094
|
+
name: 'TODO',
|
|
3095
|
+
description: 'TODO',
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
const result = await this.createService.execute(input)
|
|
3099
|
+
return ctx.json(result, 201)
|
|
3100
|
+
} catch (error) {
|
|
3101
|
+
return ctx.json({ error: (error as Error).message }, 400)
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
// TODO: \u65B0\u589E\u5176\u4ED6\u8DEF\u7531\u8655\u7406\u5668
|
|
3106
|
+
// async update(ctx) { ... }
|
|
3107
|
+
// async delete(ctx) { ... }
|
|
3108
|
+
}
|
|
3109
|
+
`;
|
|
3110
|
+
}
|
|
3111
|
+
generateRoutes(name) {
|
|
3112
|
+
const kebabName = this.toKebabCase(name);
|
|
3113
|
+
return `import type { IModuleRouter } from '@/Shared/Presentation/IModuleRouter'
|
|
3114
|
+
import { ${name}Controller } from '../Controllers/${name}Controller'
|
|
3115
|
+
import type { I${name}EventStore } from '../../Domain/Repositories/I${name}EventStore'
|
|
3116
|
+
|
|
3117
|
+
/**
|
|
3118
|
+
* register${name}Routes - \u8DEF\u7531\u8A3B\u518A
|
|
3119
|
+
*
|
|
3120
|
+
* \u652F\u63F4\uFF1A
|
|
3121
|
+
* POST /api/${kebabName} - \u5EFA\u7ACB
|
|
3122
|
+
* GET /api/${kebabName}/:id - \u53D6\u5F97\u8A73\u60C5
|
|
3123
|
+
* PUT /api/${kebabName}/:id - \u66F4\u65B0
|
|
3124
|
+
* DELETE /api/${kebabName}/:id - \u522A\u9664
|
|
3125
|
+
*/
|
|
3126
|
+
export function register${name}Routes(
|
|
3127
|
+
router: IModuleRouter,
|
|
3128
|
+
eventStore: I${name}EventStore
|
|
3129
|
+
): void {
|
|
3130
|
+
const controller = new ${name}Controller(eventStore)
|
|
3131
|
+
|
|
3132
|
+
router.post('/api/${kebabName}', (ctx) => controller.create(ctx))
|
|
3133
|
+
|
|
3134
|
+
// TODO: \u65B0\u589E\u5176\u4ED6\u8DEF\u7531
|
|
3135
|
+
// router.get('/api/${kebabName}/:id', (ctx) => controller.getById(ctx))
|
|
3136
|
+
// router.put('/api/${kebabName}/:id', (ctx) => controller.update(ctx))
|
|
3137
|
+
// router.delete('/api/${kebabName}/:id', (ctx) => controller.delete(ctx))
|
|
3138
|
+
}
|
|
3139
|
+
`;
|
|
3140
|
+
}
|
|
3141
|
+
generateModuleIndex(name) {
|
|
3142
|
+
return `/**
|
|
3143
|
+
* ${name} \u6A21\u7D44\u5C0E\u51FA
|
|
3144
|
+
*
|
|
3145
|
+
* \u516C\u958B API
|
|
3146
|
+
*/
|
|
3147
|
+
|
|
3148
|
+
// Domain
|
|
3149
|
+
export { ${name} } from './Domain/AggregateRoots/${name}'
|
|
3150
|
+
export { ${name}Id } from './Domain/ValueObjects/${name}Id'
|
|
3151
|
+
export { ${name}Status } from './Domain/ValueObjects/${name}Status'
|
|
3152
|
+
export {
|
|
3153
|
+
${name}CreatedEvent,
|
|
3154
|
+
${name}UpdatedEvent,
|
|
3155
|
+
${name}DeletedEvent,
|
|
3156
|
+
} from './Domain/Events'
|
|
3157
|
+
export { ${name}EventApplier } from './Domain/Services/${name}EventApplier'
|
|
3158
|
+
export type { I${name}EventStore } from './Domain/Repositories/I${name}EventStore'
|
|
3159
|
+
|
|
3160
|
+
// Application
|
|
3161
|
+
export { Create${name}Service } from './Application/Services/Create${name}Service'
|
|
3162
|
+
export { ${name}DTO } from './Application/DTOs/${name}DTO'
|
|
3163
|
+
|
|
3164
|
+
// Infrastructure
|
|
3165
|
+
export { InMemory${name}EventStore } from './Infrastructure/EventStore/InMemory${name}EventStore'
|
|
3166
|
+
export { Database${name}EventStore } from './Infrastructure/EventStore/Database${name}EventStore'
|
|
3167
|
+
export { ${name}EventDeserializer } from './Infrastructure/EventStore/${name}EventDeserializer'
|
|
3168
|
+
|
|
3169
|
+
// Presentation
|
|
3170
|
+
export { ${name}Controller } from './Presentation/Controllers/${name}Controller'
|
|
3171
|
+
export { register${name}Routes } from './Presentation/Routes/${this.toKebabCase(name)}.routes'
|
|
3172
|
+
`;
|
|
3173
|
+
}
|
|
3174
|
+
// ─────────────────────────────────────────────────────────────────
|
|
3175
|
+
// 工具方法
|
|
3176
|
+
// ─────────────────────────────────────────────────────────────────
|
|
3177
|
+
toKebabCase(str) {
|
|
3178
|
+
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
3179
|
+
}
|
|
3180
|
+
toSnakeCase(str) {
|
|
3181
|
+
return str.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").toLowerCase();
|
|
3182
|
+
}
|
|
3183
|
+
};
|
|
3184
|
+
|
|
3185
|
+
// src/generators/ddd/BootstrapGenerator.ts
|
|
3186
|
+
var BootstrapGenerator = class {
|
|
3187
|
+
context = null;
|
|
3188
|
+
generate(context) {
|
|
3189
|
+
this.context = context;
|
|
3190
|
+
return {
|
|
3191
|
+
type: "directory",
|
|
3192
|
+
name: "Bootstrap",
|
|
3193
|
+
children: [
|
|
3194
|
+
{ type: "file", name: "app.ts", content: this.generateBootstrapApp(context) },
|
|
3195
|
+
{ type: "file", name: "providers.ts", content: this.generateProvidersRegistry(context) },
|
|
3196
|
+
{ type: "file", name: "events.ts", content: this.generateEventsRegistry() },
|
|
3197
|
+
{ type: "file", name: "routes.ts", content: this.generateRoutesRegistry(context) },
|
|
3198
|
+
{ type: "file", name: "auto-di.ts", content: this.generateAutoDiBootstrap() }
|
|
3199
|
+
]
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
generateConfigDirectory(context) {
|
|
3203
|
+
this.context = context;
|
|
3204
|
+
return {
|
|
3205
|
+
type: "directory",
|
|
3206
|
+
name: "config",
|
|
3207
|
+
children: [
|
|
3208
|
+
{ type: "file", name: "app.ts", content: this.generateAppConfig(context) },
|
|
3209
|
+
{ type: "file", name: "database.ts", content: this.generateDatabaseConfig() },
|
|
3210
|
+
{ type: "file", name: "modules.ts", content: this.generateModulesConfig() },
|
|
3211
|
+
{ type: "file", name: "cache.ts", content: this.generateCacheConfig() },
|
|
3212
|
+
{ type: "file", name: "logging.ts", content: this.generateLoggingConfig() }
|
|
3213
|
+
]
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
generateMainEntry(_context) {
|
|
3217
|
+
return `/**
|
|
3218
|
+
* Application Entry Point
|
|
3219
|
+
*
|
|
3220
|
+
* Start the HTTP server.
|
|
3221
|
+
*/
|
|
3222
|
+
|
|
3223
|
+
import { createApp } from './Bootstrap/app'
|
|
3224
|
+
|
|
3225
|
+
const app = await createApp()
|
|
3226
|
+
|
|
3227
|
+
export default app.liftoff()
|
|
3228
|
+
`;
|
|
3229
|
+
}
|
|
3230
|
+
generateBootstrapApp(_context) {
|
|
3231
|
+
return `/**
|
|
3232
|
+
* Application Bootstrap
|
|
3233
|
+
*
|
|
3234
|
+
* Central configuration and initialization using the ServiceProvider pattern.
|
|
3235
|
+
*
|
|
3236
|
+
* Lifecycle:
|
|
3237
|
+
* 1. Configure: Load app config and orbits
|
|
3238
|
+
* 2. Boot: Initialize PlanetCore
|
|
3239
|
+
* 3. Auto-discover and register services via AutoDiBootstrap
|
|
3240
|
+
* 4. Bootstrap: Boot all providers
|
|
3241
|
+
* 5. Register routes: Auto-register module routes
|
|
3242
|
+
*/
|
|
3243
|
+
|
|
3244
|
+
import { defineConfig, PlanetCore } from '@gravito/core'
|
|
3245
|
+
import { OrbitAtlas } from '@gravito/atlas'
|
|
3246
|
+
import appConfig from '../../config/app'
|
|
3247
|
+
import { AutoDiBootstrap } from '../../Bootstrap/auto-di'
|
|
3248
|
+
import { registerProviders } from './providers'
|
|
3249
|
+
import { registerRoutes } from './routes'
|
|
3250
|
+
|
|
3251
|
+
export async function createApp(): Promise<PlanetCore> {
|
|
3252
|
+
// 1. Configure
|
|
3253
|
+
const config = defineConfig({
|
|
3254
|
+
config: appConfig,
|
|
3255
|
+
orbits: [
|
|
3256
|
+
new OrbitAtlas() as unknown as import('@gravito/core').GravitoOrbit,
|
|
3257
|
+
],
|
|
3258
|
+
})
|
|
3259
|
+
|
|
3260
|
+
// 2. Boot Core
|
|
3261
|
+
const core = await PlanetCore.boot(config)
|
|
3262
|
+
core.registerGlobalErrorHandlers()
|
|
3263
|
+
|
|
3264
|
+
// 3. Auto-discover and register services (OPTIONAL)
|
|
3265
|
+
// \u53D6\u6D88\u4E0B\u5217\u8A3B\u89E3\u4EE5\u555F\u7528\u81EA\u52D5 DI \u6383\u63CF
|
|
3266
|
+
// \u512A\u9EDE\uFF1A\u7121\u9700\u624B\u52D5\u4FEE\u6539 registerProviders()
|
|
3267
|
+
// \u7F3A\u9EDE\uFF1A\u6383\u63CF\u9700\u8981\u6642\u9593\uFF08~100ms\uFF09
|
|
3268
|
+
// await AutoDiBootstrap.scanAndRegisterServices(core.container)
|
|
3269
|
+
|
|
3270
|
+
// 3b. \u6216\u4F7F\u7528\u50B3\u7D71\u7684\u986F\u5F0F\u63D0\u4F9B\u8005\u8A3B\u518A\uFF08\u63A8\u85A6\u7528\u65BC\u751F\u7522\uFF09
|
|
3271
|
+
await registerProviders(core)
|
|
3272
|
+
|
|
3273
|
+
// 4. Bootstrap All Providers
|
|
3274
|
+
await core.bootstrap()
|
|
3275
|
+
|
|
3276
|
+
// 5. Auto-register module routes (OPTIONAL)
|
|
3277
|
+
// await AutoDiBootstrap.scanAndRegisterRoutes(core)
|
|
3278
|
+
|
|
3279
|
+
// 5b. \u6216\u4F7F\u7528\u50B3\u7D71\u7684\u8DEF\u7531\u8A3B\u518A
|
|
3280
|
+
registerRoutes(core.router)
|
|
3281
|
+
|
|
3282
|
+
return core
|
|
3283
|
+
}
|
|
3284
|
+
`;
|
|
3285
|
+
}
|
|
3286
|
+
generateProvidersRegistry(_context) {
|
|
3287
|
+
return `/**
|
|
3288
|
+
* Service Providers Registry
|
|
3289
|
+
*
|
|
3290
|
+
* Register all service providers here.
|
|
3291
|
+
* Include both global and module-specific providers.
|
|
3292
|
+
*/
|
|
3293
|
+
|
|
3294
|
+
import {
|
|
3295
|
+
ServiceProvider,
|
|
3296
|
+
type Container,
|
|
3297
|
+
type PlanetCore,
|
|
3298
|
+
bodySizeLimit,
|
|
3299
|
+
securityHeaders,
|
|
3300
|
+
} from '@gravito/core'
|
|
3301
|
+
import { OrderingServiceProvider } from '../Modules/Ordering/Infrastructure/Providers/OrderingServiceProvider'
|
|
3302
|
+
import { CatalogServiceProvider } from '../Modules/Catalog/Infrastructure/Providers/CatalogServiceProvider'
|
|
3303
|
+
|
|
3304
|
+
/**
|
|
3305
|
+
* Middleware Provider - Global middleware registration
|
|
3306
|
+
*/
|
|
3307
|
+
export class MiddlewareProvider extends ServiceProvider {
|
|
3308
|
+
register(_container: Container): void {}
|
|
3309
|
+
|
|
3310
|
+
boot(core: PlanetCore): void {
|
|
3311
|
+
const isDev = process.env.NODE_ENV !== 'production'
|
|
3312
|
+
|
|
3313
|
+
core.adapter.use('*', securityHeaders({
|
|
3314
|
+
contentSecurityPolicy: isDev ? false : undefined,
|
|
3315
|
+
}))
|
|
3316
|
+
|
|
3317
|
+
core.adapter.use('*', bodySizeLimit(10 * 1024 * 1024))
|
|
3318
|
+
|
|
3319
|
+
core.logger.info('\u{1F6E1}\uFE0F Global middleware registered')
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
export async function registerProviders(core: PlanetCore): Promise<void> {
|
|
3324
|
+
// Global Providers
|
|
3325
|
+
core.register(new MiddlewareProvider())
|
|
3326
|
+
|
|
3327
|
+
// Module Providers
|
|
3328
|
+
core.register(new OrderingServiceProvider())
|
|
3329
|
+
core.register(new CatalogServiceProvider())
|
|
3330
|
+
|
|
3331
|
+
// Add more providers as needed
|
|
3332
|
+
}
|
|
3333
|
+
`;
|
|
3334
|
+
}
|
|
3335
|
+
generateEventsRegistry() {
|
|
3336
|
+
return `/**
|
|
3337
|
+
* Domain Events Registry
|
|
3338
|
+
*
|
|
3339
|
+
* Register all domain event handlers here.
|
|
3340
|
+
*/
|
|
3341
|
+
|
|
3342
|
+
import { EventDispatcher } from '../Shared/Infrastructure/EventBus/EventDispatcher'
|
|
3343
|
+
|
|
3344
|
+
export function registerEvents(dispatcher: EventDispatcher): void {
|
|
3345
|
+
// Register event handlers
|
|
3346
|
+
// dispatcher.subscribe('ordering.created', async (event) => { ... })
|
|
3347
|
+
}
|
|
3348
|
+
`;
|
|
3349
|
+
}
|
|
3350
|
+
generateRoutesRegistry(_context) {
|
|
3351
|
+
return `/**
|
|
3352
|
+
* Routes Registry
|
|
3353
|
+
*
|
|
3354
|
+
* Register all module routes here.
|
|
3355
|
+
*/
|
|
3356
|
+
|
|
3357
|
+
export function registerRoutes(router: any): void {
|
|
3358
|
+
// Health check
|
|
3359
|
+
router.get('/health', (c: any) => c.json({ status: 'healthy' }))
|
|
3360
|
+
|
|
3361
|
+
// Ordering module
|
|
3362
|
+
router.get('/api/orders', (c: any) => c.json({ message: 'Order list' }))
|
|
3363
|
+
router.post('/api/orders', (c: any) => c.json({ message: 'Order created' }, 201))
|
|
3364
|
+
|
|
3365
|
+
// Catalog module
|
|
3366
|
+
router.get('/api/products', (c: any) => c.json({ message: 'Product list' }))
|
|
3367
|
+
}
|
|
3368
|
+
`;
|
|
3369
|
+
}
|
|
3370
|
+
generateModulesConfig() {
|
|
3371
|
+
return `/**
|
|
3372
|
+
* Modules Configuration
|
|
3373
|
+
*
|
|
3374
|
+
* Define module boundaries and their dependencies.
|
|
3375
|
+
*/
|
|
3376
|
+
|
|
3377
|
+
export default {
|
|
3378
|
+
modules: {
|
|
3379
|
+
ordering: {
|
|
3380
|
+
name: 'Ordering',
|
|
3381
|
+
description: 'Order management module',
|
|
3382
|
+
prefix: '/api/orders',
|
|
3383
|
+
},
|
|
3384
|
+
catalog: {
|
|
3385
|
+
name: 'Catalog',
|
|
3386
|
+
description: 'Product catalog module',
|
|
3387
|
+
prefix: '/api/products',
|
|
3388
|
+
},
|
|
3389
|
+
},
|
|
3390
|
+
|
|
3391
|
+
// Module dependencies
|
|
3392
|
+
dependencies: {
|
|
3393
|
+
ordering: ['catalog'], // Ordering depends on Catalog
|
|
3394
|
+
},
|
|
3395
|
+
}
|
|
3396
|
+
`;
|
|
3397
|
+
}
|
|
3398
|
+
generateAppConfig(context) {
|
|
3399
|
+
return `export default {
|
|
3400
|
+
name: process.env.APP_NAME ?? '${context.name}',
|
|
3401
|
+
env: process.env.APP_ENV ?? 'development',
|
|
3402
|
+
port: Number.parseInt(process.env.PORT ?? '3000', 10),
|
|
3403
|
+
VIEW_DIR: process.env.VIEW_DIR ?? 'src/views',
|
|
3404
|
+
debug: process.env.APP_DEBUG === 'true',
|
|
3405
|
+
url: process.env.APP_URL ?? 'http://localhost:3000',
|
|
3406
|
+
}
|
|
3407
|
+
`;
|
|
3408
|
+
}
|
|
3409
|
+
generateDatabaseConfig() {
|
|
3410
|
+
const driver = this.context?.profileConfig?.drivers?.database ?? "none";
|
|
3411
|
+
return ConfigGenerator.generateDatabaseConfig(driver);
|
|
3412
|
+
}
|
|
3413
|
+
generateCacheConfig() {
|
|
3414
|
+
return `export default {
|
|
3415
|
+
default: process.env.CACHE_DRIVER ?? 'memory',
|
|
3416
|
+
stores: { memory: { driver: 'memory' } },
|
|
3417
|
+
}
|
|
3418
|
+
`;
|
|
3419
|
+
}
|
|
3420
|
+
generateLoggingConfig() {
|
|
3421
|
+
return `export default {
|
|
3422
|
+
default: 'console',
|
|
3423
|
+
channels: { console: { driver: 'console', level: 'debug' } },
|
|
3424
|
+
}
|
|
3425
|
+
`;
|
|
3426
|
+
}
|
|
3427
|
+
generateAutoDiBootstrap() {
|
|
3428
|
+
return `/**
|
|
3429
|
+
* AutoDiBootstrap - \u81EA\u52D5\u4F9D\u8CF4\u6CE8\u5165\u5F15\u5C0E\u5C64
|
|
3430
|
+
*
|
|
3431
|
+
* \u529F\u80FD\uFF1A
|
|
3432
|
+
* 1. \u81EA\u52D5\u6383\u63CF\u6A21\u7D44\u76EE\u9304\u767C\u73FE\u670D\u52D9
|
|
3433
|
+
* 2. \u81EA\u52D5\u8A3B\u518A\u5230 DI \u5BB9\u5668
|
|
3434
|
+
* 3. \u81EA\u52D5\u8A3B\u518A\u8DEF\u7531
|
|
3435
|
+
*
|
|
3436
|
+
* \u4F7F\u7528\u65B9\u5F0F\uFF1A
|
|
3437
|
+
* \u5728 app.ts \u4E2D\u53D6\u6D88\u4E0B\u5217\u8A3B\u89E3\uFF1A
|
|
3438
|
+
* await AutoDiBootstrap.scanAndRegisterServices(core.container)
|
|
3439
|
+
* await AutoDiBootstrap.scanAndRegisterRoutes(core)
|
|
3440
|
+
*
|
|
3441
|
+
* \u6CE8\u610F\uFF1A\u81EA\u52D5\u6383\u63CF\u6703\u589E\u52A0\u555F\u52D5\u6642\u9593\uFF08~100ms\uFF09
|
|
3442
|
+
* \u751F\u7522\u74B0\u5883\u5EFA\u8B70\u4F7F\u7528\u624B\u52D5\u8A3B\u518A\uFF08registerProviders\uFF09
|
|
3443
|
+
*/
|
|
3444
|
+
|
|
3445
|
+
import type { Container, PlanetCore } from '@gravito/core'
|
|
3446
|
+
import { glob } from 'bun'
|
|
3447
|
+
|
|
3448
|
+
interface DiscoveredService {
|
|
3449
|
+
type: 'domain-service' | 'application-service' | 'repository' | 'event-subscriber'
|
|
3450
|
+
filePath: string
|
|
3451
|
+
moduleName: string
|
|
3452
|
+
className: string
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
interface DiscoveredRoute {
|
|
3456
|
+
moduleName: string
|
|
3457
|
+
routeFilePath: string
|
|
3458
|
+
functionName: string
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
export class AutoDiBootstrap {
|
|
3462
|
+
/**
|
|
3463
|
+
* \u6383\u63CF\u4E26\u81EA\u52D5\u8A3B\u518A\u6240\u6709\u6A21\u7D44\u670D\u52D9
|
|
3464
|
+
*/
|
|
3465
|
+
static async scanAndRegisterServices(
|
|
3466
|
+
container: Container,
|
|
3467
|
+
projectRoot = process.cwd(),
|
|
3468
|
+
): Promise<void> {
|
|
3469
|
+
const services = await this.discoverServices(projectRoot)
|
|
3470
|
+
console.log(\`\u{1F50D} \u767C\u73FE \${services.length} \u500B\u670D\u52D9\`)
|
|
3471
|
+
|
|
3472
|
+
for (const service of services) {
|
|
3473
|
+
await this.registerService(container, service)
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
console.log(\`\u2705 \u5DF2\u8A3B\u518A \${services.length} \u500B\u670D\u52D9\u5230 DI \u5BB9\u5668\`)
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
/**
|
|
3480
|
+
* \u6383\u63CF\u4E26\u81EA\u52D5\u8A3B\u518A\u6240\u6709\u6A21\u7D44\u8DEF\u7531
|
|
3481
|
+
*/
|
|
3482
|
+
static async scanAndRegisterRoutes(core: PlanetCore, projectRoot = process.cwd()): Promise<void> {
|
|
3483
|
+
const routes = await this.discoverRoutes(projectRoot)
|
|
3484
|
+
console.log(\`\u{1F6E3}\uFE0F \u767C\u73FE \${routes.length} \u500B\u8DEF\u7531\u6A21\u7D44\`)
|
|
3485
|
+
|
|
3486
|
+
for (const route of routes) {
|
|
3487
|
+
try {
|
|
3488
|
+
const moduleUrl = new URL(\`file://\${route.routeFilePath}\`)
|
|
3489
|
+
const routeModule = await import(moduleUrl.href)
|
|
3490
|
+
const registerFunction = routeModule[route.functionName] || routeModule.default
|
|
3491
|
+
|
|
3492
|
+
if (typeof registerFunction === 'function') {
|
|
3493
|
+
registerFunction(core)
|
|
3494
|
+
console.log(\` \u2713 \${route.moduleName} \u8DEF\u7531\u5DF2\u8A3B\u518A\`)
|
|
3495
|
+
}
|
|
3496
|
+
} catch (error) {
|
|
3497
|
+
console.error(\` \u2717 \u7121\u6CD5\u8F09\u5165 \${route.routeFilePath}:\`, error)
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
console.log(\`\u2705 \u5DF2\u8A3B\u518A \${routes.length} \u500B\u8DEF\u7531\`)
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
private static async discoverServices(projectRoot: string): Promise<DiscoveredService[]> {
|
|
3505
|
+
const services: DiscoveredService[] = []
|
|
3506
|
+
|
|
3507
|
+
// \u6383\u63CF\u6240\u6709 Service \u548C Repository
|
|
3508
|
+
const patterns = [
|
|
3509
|
+
'src/Modules/*/Domain/Services/*Service.ts',
|
|
3510
|
+
'src/Modules/*/Application/Services/*Service.ts',
|
|
3511
|
+
'src/Modules/*/Infrastructure/Repositories/*Repository.ts',
|
|
3512
|
+
'src/Modules/*/Infrastructure/Subscribers/*Subscriber.ts',
|
|
3513
|
+
]
|
|
3514
|
+
|
|
3515
|
+
for (const pattern of patterns) {
|
|
3516
|
+
const files = await glob({ cwd: projectRoot, pattern })
|
|
3517
|
+
for (const filePath of files) {
|
|
3518
|
+
const moduleName = this.extractModuleName(filePath)
|
|
3519
|
+
const className = this.extractClassName(filePath)
|
|
3520
|
+
const type = this.inferServiceType(filePath) as DiscoveredService['type']
|
|
3521
|
+
|
|
3522
|
+
services.push({ type, filePath, moduleName, className })
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
return services
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
private static async discoverRoutes(projectRoot: string): Promise<DiscoveredRoute[]> {
|
|
3530
|
+
const routeFiles = await glob({
|
|
3531
|
+
cwd: projectRoot,
|
|
3532
|
+
pattern: 'src/Modules/*/Presentation/Routes/*.routes.ts',
|
|
3533
|
+
})
|
|
3534
|
+
|
|
3535
|
+
return routeFiles.map((filePath) => {
|
|
3536
|
+
const moduleName = this.extractModuleName(filePath)
|
|
3537
|
+
return {
|
|
3538
|
+
moduleName,
|
|
3539
|
+
routeFilePath: \`\${projectRoot}/\${filePath}\`,
|
|
3540
|
+
functionName: \`register\${moduleName}Routes\`,
|
|
3541
|
+
}
|
|
3542
|
+
})
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
private static async registerService(container: Container, service: DiscoveredService): Promise<void> {
|
|
3546
|
+
try {
|
|
3547
|
+
const moduleUrl = new URL(\`file://\${process.cwd()}/\${service.filePath}\`)
|
|
3548
|
+
const module = await import(moduleUrl.href)
|
|
3549
|
+
const ServiceClass = module[service.className] || module.default
|
|
3550
|
+
|
|
3551
|
+
if (!ServiceClass) {
|
|
3552
|
+
console.warn(\` \u26A0\uFE0F \u7121\u6CD5\u627E\u5230 \${service.className} \u5728 \${service.filePath}\`)
|
|
3553
|
+
return
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
const serviceKey = this.generateServiceKey(service.className)
|
|
3557
|
+
container.singleton(serviceKey, () => new ServiceClass())
|
|
3558
|
+
|
|
3559
|
+
console.log(\` \u2713 \${serviceKey}\`)
|
|
3560
|
+
} catch (error) {
|
|
3561
|
+
console.error(\` \u2717 \u7121\u6CD5\u8F09\u5165 \${service.filePath}:\`, error)
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
private static extractModuleName(filePath: string): string {
|
|
3566
|
+
const match = filePath.match(/Modules\\/([^\\/]+)/)
|
|
3567
|
+
return match ? match[1] : 'Unknown'
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
private static extractClassName(filePath: string): string {
|
|
3571
|
+
const fileName = filePath.split('/').pop() || ''
|
|
3572
|
+
return fileName.replace('.ts', '')
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
private static inferServiceType(filePath: string): string {
|
|
3576
|
+
if (filePath.includes('/Domain/Services/')) return 'domain-service'
|
|
3577
|
+
if (filePath.includes('/Application/Services/')) return 'application-service'
|
|
3578
|
+
if (filePath.includes('/Infrastructure/Repositories/')) return 'repository'
|
|
3579
|
+
if (filePath.includes('/Infrastructure/Subscribers/')) return 'event-subscriber'
|
|
3580
|
+
return 'unknown'
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
private static generateServiceKey(className: string): string {
|
|
3584
|
+
let name = className
|
|
3585
|
+
if (name.startsWith('I') && name.length > 1) name = name.slice(1)
|
|
3586
|
+
if (name.endsWith('Service')) name = name.slice(0, -7)
|
|
3587
|
+
if (name.endsWith('Repository')) name = name.slice(0, -10)
|
|
3588
|
+
return this.toKebabCase(name)
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
private static toKebabCase(str: string): string {
|
|
3592
|
+
return str
|
|
3593
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
3594
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
3595
|
+
.toLowerCase()
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
`;
|
|
3599
|
+
}
|
|
3600
|
+
};
|
|
3601
|
+
|
|
3602
|
+
// src/generators/ddd/CQRSQueryModuleGenerator.ts
|
|
3603
|
+
var CQRSQueryModuleGenerator = class {
|
|
3604
|
+
/**
|
|
3605
|
+
* 生成 CQRS 查詢側模組結構
|
|
3606
|
+
*
|
|
3607
|
+
* 生成檔案:
|
|
3608
|
+
* - Domain/ReadModels/{Name}ReadModel.ts
|
|
3609
|
+
* - Domain/Projectors/{Name}EventProjector.ts
|
|
3610
|
+
* - Application/Services/Query{Name}Service.ts
|
|
3611
|
+
* - Application/DTOs/{Name}ReadDTO.ts
|
|
3612
|
+
* - Infrastructure/Subscribers/{Name}ProjectionSubscriber.ts
|
|
3613
|
+
* - Infrastructure/Cache/{Name}ReadModelCache.ts
|
|
3614
|
+
* - Presentation/Controllers/{Name}QueryController.ts
|
|
3615
|
+
* - index.ts
|
|
3616
|
+
*
|
|
3617
|
+
* @param moduleName - 模組名稱(例:'Wallet')
|
|
3618
|
+
* @param _context - 生成器上下文(當前未使用,預留供未來擴展)
|
|
3619
|
+
* @returns 模組目錄結構
|
|
3620
|
+
*/
|
|
3621
|
+
generate(moduleName, _context) {
|
|
3622
|
+
return {
|
|
3623
|
+
type: "directory",
|
|
3624
|
+
name: moduleName,
|
|
3625
|
+
children: [
|
|
3626
|
+
// Domain Layer
|
|
3627
|
+
{
|
|
3628
|
+
type: "directory",
|
|
3629
|
+
name: "Domain",
|
|
3630
|
+
children: [
|
|
3631
|
+
// Read Models
|
|
3632
|
+
{
|
|
3633
|
+
type: "directory",
|
|
3634
|
+
name: "ReadModels",
|
|
3635
|
+
children: [
|
|
3636
|
+
{
|
|
3637
|
+
type: "file",
|
|
3638
|
+
name: `${moduleName}ReadModel.ts`,
|
|
3639
|
+
content: this.generateReadModel(moduleName)
|
|
3640
|
+
}
|
|
3641
|
+
]
|
|
3642
|
+
},
|
|
3643
|
+
// Projectors
|
|
3644
|
+
{
|
|
3645
|
+
type: "directory",
|
|
3646
|
+
name: "Projectors",
|
|
3647
|
+
children: [
|
|
3648
|
+
{
|
|
3649
|
+
type: "file",
|
|
3650
|
+
name: `${moduleName}EventProjector.ts`,
|
|
3651
|
+
content: this.generateProjector(moduleName)
|
|
3652
|
+
}
|
|
3653
|
+
]
|
|
3654
|
+
},
|
|
3655
|
+
// Repositories
|
|
3656
|
+
{
|
|
3657
|
+
type: "directory",
|
|
3658
|
+
name: "Repositories",
|
|
3659
|
+
children: [
|
|
3660
|
+
{
|
|
3661
|
+
type: "file",
|
|
3662
|
+
name: `I${moduleName}ReadModelRepository.ts`,
|
|
3663
|
+
content: this.generateRepositoryInterface(moduleName)
|
|
3664
|
+
}
|
|
3665
|
+
]
|
|
3666
|
+
}
|
|
3667
|
+
]
|
|
3668
|
+
},
|
|
3669
|
+
// Application Layer
|
|
3670
|
+
{
|
|
3671
|
+
type: "directory",
|
|
3672
|
+
name: "Application",
|
|
3673
|
+
children: [
|
|
3674
|
+
{
|
|
3675
|
+
type: "directory",
|
|
3676
|
+
name: "Services",
|
|
3677
|
+
children: [
|
|
3678
|
+
{
|
|
3679
|
+
type: "file",
|
|
3680
|
+
name: `Query${moduleName}Service.ts`,
|
|
3681
|
+
content: this.generateQueryService(moduleName)
|
|
3682
|
+
}
|
|
3683
|
+
]
|
|
3684
|
+
},
|
|
3685
|
+
{
|
|
3686
|
+
type: "directory",
|
|
3687
|
+
name: "DTOs",
|
|
3688
|
+
children: [
|
|
3689
|
+
{
|
|
3690
|
+
type: "file",
|
|
3691
|
+
name: `${moduleName}ReadDTO.ts`,
|
|
3692
|
+
content: this.generateQueryDTO(moduleName)
|
|
3693
|
+
}
|
|
3694
|
+
]
|
|
3695
|
+
}
|
|
3696
|
+
]
|
|
3697
|
+
},
|
|
3698
|
+
// Infrastructure Layer
|
|
3699
|
+
{
|
|
3700
|
+
type: "directory",
|
|
3701
|
+
name: "Infrastructure",
|
|
3702
|
+
children: [
|
|
3703
|
+
{
|
|
3704
|
+
type: "directory",
|
|
3705
|
+
name: "Repositories",
|
|
3706
|
+
children: [
|
|
3707
|
+
{
|
|
3708
|
+
type: "file",
|
|
3709
|
+
name: `${moduleName}ReadModelRepository.ts`,
|
|
3710
|
+
content: this.generateRepositoryImplementation(moduleName)
|
|
3711
|
+
}
|
|
3712
|
+
]
|
|
3713
|
+
},
|
|
3714
|
+
{
|
|
3715
|
+
type: "directory",
|
|
3716
|
+
name: "Subscribers",
|
|
3717
|
+
children: [
|
|
3718
|
+
{
|
|
3719
|
+
type: "file",
|
|
3720
|
+
name: `${moduleName}ProjectionSubscriber.ts`,
|
|
3721
|
+
content: this.generateSubscriber(moduleName)
|
|
3722
|
+
}
|
|
3723
|
+
]
|
|
3724
|
+
},
|
|
3725
|
+
{
|
|
3726
|
+
type: "directory",
|
|
3727
|
+
name: "Cache",
|
|
3728
|
+
children: [
|
|
3729
|
+
{
|
|
3730
|
+
type: "file",
|
|
3731
|
+
name: `${moduleName}ReadModelCache.ts`,
|
|
3732
|
+
content: this.generateCache(moduleName)
|
|
3733
|
+
}
|
|
3734
|
+
]
|
|
3735
|
+
}
|
|
3736
|
+
]
|
|
3737
|
+
},
|
|
3738
|
+
// Presentation Layer
|
|
3739
|
+
{
|
|
3740
|
+
type: "directory",
|
|
3741
|
+
name: "Presentation",
|
|
3742
|
+
children: [
|
|
3743
|
+
{
|
|
3744
|
+
type: "directory",
|
|
3745
|
+
name: "Controllers",
|
|
3746
|
+
children: [
|
|
3747
|
+
{
|
|
3748
|
+
type: "file",
|
|
3749
|
+
name: `${moduleName}QueryController.ts`,
|
|
3750
|
+
content: this.generateController(moduleName)
|
|
3751
|
+
}
|
|
3752
|
+
]
|
|
3753
|
+
},
|
|
3754
|
+
{
|
|
3755
|
+
type: "directory",
|
|
3756
|
+
name: "Routes",
|
|
3757
|
+
children: [
|
|
3758
|
+
{
|
|
3759
|
+
type: "file",
|
|
3760
|
+
name: `${this.toKebabCase(moduleName)}.routes.ts`,
|
|
3761
|
+
content: this.generateRoutes(moduleName)
|
|
3762
|
+
}
|
|
3763
|
+
]
|
|
3764
|
+
}
|
|
3765
|
+
]
|
|
3766
|
+
},
|
|
3767
|
+
// Module index
|
|
3768
|
+
{
|
|
3769
|
+
type: "file",
|
|
3770
|
+
name: "index.ts",
|
|
3771
|
+
content: this.generateIndex(moduleName)
|
|
3772
|
+
}
|
|
3773
|
+
]
|
|
3774
|
+
};
|
|
3775
|
+
}
|
|
3776
|
+
/**
|
|
3777
|
+
* 生成讀模型檔案
|
|
3778
|
+
*/
|
|
3779
|
+
generateReadModel(moduleName) {
|
|
3780
|
+
return `/**
|
|
3781
|
+
* ${moduleName}ReadModel - \u67E5\u8A62\u512A\u5316\u7684\u8B80\u6A21\u578B
|
|
3782
|
+
*
|
|
3783
|
+
* \u8B80\u6A21\u578B\u662F\u5F9E\u4E8B\u4EF6\u6295\u5F71\u800C\u4F86\u7684\u975E\u898F\u7BC4\u5316\u6578\u64DA\u7D50\u69CB\uFF0C
|
|
3784
|
+
* \u91DD\u5C0D\u7279\u5B9A\u67E5\u8A62\u5834\u666F\u9032\u884C\u512A\u5316\u3002
|
|
3785
|
+
*
|
|
3786
|
+
* \u8207 Aggregate Root \u4E0D\u540C\uFF1A
|
|
3787
|
+
* - Aggregate Root\uFF1A\u898F\u7BC4\u5316\uFF0C\u55AE\u4E00\u4F86\u6E90\uFF08\u4E8B\u4EF6\uFF09\uFF0C\u5F37\u4E00\u81F4\u6027
|
|
3788
|
+
* - ReadModel\uFF1A\u53CD\u898F\u7BC4\u5316\uFF0C\u4F9B\u67E5\u8A62\u4F7F\u7528\uFF0C\u6700\u7D42\u4E00\u81F4\u6027
|
|
3789
|
+
*
|
|
3790
|
+
* \u8A2D\u8A08\u539F\u5247\uFF1A
|
|
3791
|
+
* 1. \u4E0D\u53EF\u8B8A\u6027\uFF1A\u8B80\u6A21\u578B\u5B57\u6BB5\u901A\u904E\u6295\u5F71\u5668\u552F\u8B80\u4FEE\u6539
|
|
3792
|
+
* 2. \u6E05\u6670\u6027\uFF1A\u5B57\u6BB5\u540D\u7A31\u76F4\u89C0\u53CD\u6620\u67E5\u8A62\u9700\u6C42
|
|
3793
|
+
* 3. \u6027\u80FD\uFF1A\u5305\u542B\u9810\u8A08\u7B97\u5B57\u6BB5\uFF08\u5982\u7E3D\u984D\u3001\u8A08\u6578\uFF09
|
|
3794
|
+
* 4. \u7248\u672C\u63A7\u5236\uFF1A\u652F\u6301\u6295\u5F71\u5668\u7248\u672C\u9077\u79FB
|
|
3795
|
+
*/
|
|
3796
|
+
|
|
3797
|
+
import type { DomainEvent } from '@/Shared/Domain/DomainEvent'
|
|
3798
|
+
|
|
3799
|
+
/**
|
|
3800
|
+
* ${moduleName} \u8B80\u6A21\u578B\u4ECB\u9762
|
|
3801
|
+
* \u4EE3\u8868\u512A\u5316\u7528\u65BC\u67E5\u8A62\u7684\u6578\u64DA\u7D50\u69CB
|
|
3802
|
+
*/
|
|
3803
|
+
export interface ${moduleName}ReadModel {
|
|
3804
|
+
// TODO: \u6839\u64DA\u67E5\u8A62\u9700\u6C42\u6DFB\u52A0\u5B57\u6BB5
|
|
3805
|
+
// \u793A\u4F8B\uFF1A
|
|
3806
|
+
// id: string
|
|
3807
|
+
// customerId: string
|
|
3808
|
+
// totalAmount: string (decimal as string for precision)
|
|
3809
|
+
// transactionCount: number
|
|
3810
|
+
// lastUpdated: Date
|
|
3811
|
+
// status: string
|
|
3812
|
+
|
|
3813
|
+
// \u6295\u5F71\u5143\u6578\u64DA
|
|
3814
|
+
/** \u6700\u5F8C\u8655\u7406\u7684\u4E8B\u4EF6 ID\uFF08\u7528\u65BC\u51AA\u7B49\u6027\uFF09 */
|
|
3815
|
+
lastProcessedEventId?: string
|
|
3816
|
+
/** \u6295\u5F71\u5668\u7248\u672C */
|
|
3817
|
+
projectionVersion: number
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
/**
|
|
3821
|
+
* \u5DE5\u5EE0\u65B9\u6CD5\uFF1A\u5275\u5EFA\u65B0\u7684\u8B80\u6A21\u578B
|
|
3822
|
+
*/
|
|
3823
|
+
export function create${moduleName}ReadModel(data: Partial<${moduleName}ReadModel>): ${moduleName}ReadModel {
|
|
3824
|
+
return {
|
|
3825
|
+
...data,
|
|
3826
|
+
projectionVersion: 1,
|
|
3827
|
+
} as ${moduleName}ReadModel
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
/**
|
|
3831
|
+
* \u9A57\u8B49\u8B80\u6A21\u578B\u6578\u64DA\u5B8C\u6574\u6027
|
|
3832
|
+
*/
|
|
3833
|
+
export function validate${moduleName}ReadModel(data: any): boolean {
|
|
3834
|
+
// TODO: \u6DFB\u52A0\u9A57\u8B49\u908F\u8F2F
|
|
3835
|
+
// - \u5FC5\u586B\u5B57\u6BB5\u6AA2\u67E5
|
|
3836
|
+
// - \u985E\u578B\u9A57\u8B49
|
|
3837
|
+
// - \u696D\u52D9\u898F\u5247\u9A57\u8B49
|
|
3838
|
+
return typeof data === 'object' && data !== null
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
/**
|
|
3842
|
+
* \u5F9E\u8B80\u6A21\u578B\u63D0\u53D6\u67E5\u8A62\u4FE1\u606F
|
|
3843
|
+
* \u7528\u65BC DTO \u8F49\u63DB\u548C\u5E8F\u5217\u5316
|
|
3844
|
+
*/
|
|
3845
|
+
export function extract${moduleName}Data(model: ${moduleName}ReadModel): Record<string, any> {
|
|
3846
|
+
return {
|
|
3847
|
+
// TODO: \u63D0\u53D6\u76F8\u95DC\u5B57\u6BB5\u7528\u65BC DTO \u8F49\u63DB
|
|
3848
|
+
projectionVersion: model.projectionVersion,
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
`;
|
|
3852
|
+
}
|
|
3853
|
+
/**
|
|
3854
|
+
* 生成事件投影器檔案
|
|
3855
|
+
*/
|
|
3856
|
+
generateProjector(moduleName) {
|
|
3857
|
+
return `/**
|
|
3858
|
+
* ${moduleName}EventProjector - \u4E8B\u4EF6\u6295\u5F71\u5668
|
|
3859
|
+
*
|
|
3860
|
+
* \u6295\u5F71\u5668\u662F\u7D14\u51FD\u6578\uFF0C\u8CA0\u8CAC\u5C07\u9818\u57DF\u4E8B\u4EF6\u8F49\u5316\u70BA\u8B80\u6A21\u578B\u3002
|
|
3861
|
+
* \u8A2D\u8A08\u70BA\u51AA\u7B49\u64CD\u4F5C\uFF1A\u540C\u4E00\u4E8B\u4EF6\u6295\u5F71\u591A\u6B21\u7D50\u679C\u76F8\u540C\u3002
|
|
3862
|
+
*
|
|
3863
|
+
* \u6838\u5FC3\u8077\u8CAC\uFF1A
|
|
3864
|
+
* 1. \u76E3\u807D\u7279\u5B9A\u7684\u9818\u57DF\u4E8B\u4EF6
|
|
3865
|
+
* 2. \u6839\u64DA\u4E8B\u4EF6\u5167\u5BB9\u66F4\u65B0\u8B80\u6A21\u578B
|
|
3866
|
+
* 3. \u78BA\u4FDD\u64CD\u4F5C\u51AA\u7B49\uFF08\u9632\u6B62\u91CD\u8907\u8655\u7406\uFF09
|
|
3867
|
+
* 4. \u8A18\u9304\u6295\u5F71\u5BE9\u8A08\u4FE1\u606F
|
|
3868
|
+
*
|
|
3869
|
+
* \u512A\u52E2\uFF1A
|
|
3870
|
+
* - \u7D14\u51FD\u6578\u908F\u8F2F\u6613\u65BC\u6E2C\u8A66
|
|
3871
|
+
* - \u652F\u6301\u4E8B\u4EF6\u91CD\u653E\u548C\u6295\u5F71\u91CD\u5EFA
|
|
3872
|
+
* - \u89E3\u8026\u4E8B\u4EF6\u4F86\u6E90\u548C\u6295\u5F71\u76EE\u6A19
|
|
3873
|
+
* - \u6613\u65BC\u65B0\u589E\u65B0\u7684\u8B80\u6A21\u578B\u800C\u7121\u9700\u4FEE\u6539\u805A\u5408\u6839
|
|
3874
|
+
*/
|
|
3875
|
+
|
|
3876
|
+
import type { DomainEvent } from '@/Shared/Domain/DomainEvent'
|
|
3877
|
+
import type { I${moduleName}ReadModelRepository } from '../Repositories/I${moduleName}ReadModelRepository'
|
|
3878
|
+
import type { ${moduleName}ReadModel } from '../ReadModels/${moduleName}ReadModel'
|
|
3879
|
+
|
|
3880
|
+
/**
|
|
3881
|
+
* ${moduleName} \u4E8B\u4EF6\u6295\u5F71\u5668
|
|
3882
|
+
*
|
|
3883
|
+
* \u8CA0\u8CAC\u8A02\u95B1\u9818\u57DF\u4E8B\u4EF6\u4E26\u6295\u5F71\u5230\u8B80\u6A21\u578B
|
|
3884
|
+
*/
|
|
3885
|
+
export class ${moduleName}EventProjector {
|
|
3886
|
+
constructor(private repository: I${moduleName}ReadModelRepository) {}
|
|
3887
|
+
|
|
3888
|
+
/**
|
|
3889
|
+
* \u6295\u5F71\u4E8B\u4EF6\u5230\u8B80\u6A21\u578B
|
|
3890
|
+
*
|
|
3891
|
+
* \u5DE5\u4F5C\u6D41\u7A0B\uFF1A
|
|
3892
|
+
* 1. \u6839\u64DA\u4E8B\u4EF6\u985E\u578B\u5206\u6D3E\u5230\u5C0D\u61C9\u7684 handler
|
|
3893
|
+
* 2. \u67E5\u8A62\u6216\u5275\u5EFA\u8B80\u6A21\u578B
|
|
3894
|
+
* 3. \u61C9\u7528\u6295\u5F71\u898F\u5247\u66F4\u65B0\u8B80\u6A21\u578B
|
|
3895
|
+
* 4. \u6AA2\u67E5\u51AA\u7B49\u6027\uFF08\u662F\u5426\u5DF2\u8655\u7406\u904E\uFF09
|
|
3896
|
+
* 5. \u4FDD\u5B58\u66F4\u65B0\u7D50\u679C
|
|
3897
|
+
*
|
|
3898
|
+
* @param event - \u8981\u6295\u5F71\u7684\u9818\u57DF\u4E8B\u4EF6
|
|
3899
|
+
* @throws \u5982\u679C\u4E8B\u4EF6\u8655\u7406\u5931\u6557
|
|
3900
|
+
*/
|
|
3901
|
+
async projectEvent(event: DomainEvent): Promise<void> {
|
|
3902
|
+
try {
|
|
3903
|
+
// TODO: \u6839\u64DA\u4E8B\u4EF6\u985E\u578B\u5206\u6D3E
|
|
3904
|
+
// switch (event.constructor.name) {
|
|
3905
|
+
// case 'SomeEvent':
|
|
3906
|
+
// await this.handleSomeEvent(event as SomeEvent)
|
|
3907
|
+
// break
|
|
3908
|
+
// default:
|
|
3909
|
+
// console.warn(\`Unknown event type: \${event.constructor.name}\`)
|
|
3910
|
+
// }
|
|
3911
|
+
} catch (error) {
|
|
3912
|
+
console.error(
|
|
3913
|
+
\`Failed to project \${event.constructor.name} in ${moduleName}Projector:\`,
|
|
3914
|
+
error
|
|
3915
|
+
)
|
|
3916
|
+
throw error
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
/**
|
|
3921
|
+
* TODO: \u70BA\u6BCF\u500B\u8981\u8A02\u95B1\u7684\u4E8B\u4EF6\u6DFB\u52A0 handler \u65B9\u6CD5
|
|
3922
|
+
*
|
|
3923
|
+
* \u793A\u4F8B\uFF1A
|
|
3924
|
+
* private async handleSomeEvent(event: SomeEvent): Promise<void> {
|
|
3925
|
+
* // 1. \u67E5\u8A62\u73FE\u6709\u8B80\u6A21\u578B\u6216\u5275\u5EFA\u65B0\u7684
|
|
3926
|
+
* let model = await this.repository.findById(event.aggregateId)
|
|
3927
|
+
* if (!model) {
|
|
3928
|
+
* model = createReadModel({ id: event.aggregateId })
|
|
3929
|
+
* }
|
|
3930
|
+
*
|
|
3931
|
+
* // 2. \u6AA2\u67E5\u51AA\u7B49\u6027
|
|
3932
|
+
* if (model.lastProcessedEventId === event.eventId) {
|
|
3933
|
+
* return // \u5DF2\u8655\u7406\u904E\u6B64\u4E8B\u4EF6
|
|
3934
|
+
* }
|
|
3935
|
+
*
|
|
3936
|
+
* // 3. \u61C9\u7528\u6295\u5F71\u898F\u5247\u66F4\u65B0\u5B57\u6BB5
|
|
3937
|
+
* model = {
|
|
3938
|
+
* ...model,
|
|
3939
|
+
* field1: event.data.newValue,
|
|
3940
|
+
* lastProcessedEventId: event.eventId,
|
|
3941
|
+
* projectionVersion: model.projectionVersion + 1,
|
|
3942
|
+
* }
|
|
3943
|
+
*
|
|
3944
|
+
* // 4. \u4FDD\u5B58\u66F4\u65B0
|
|
3945
|
+
* await this.repository.save(model)
|
|
3946
|
+
* }
|
|
3947
|
+
*/
|
|
3948
|
+
|
|
3949
|
+
/**
|
|
3950
|
+
* \u7372\u53D6\u6B64\u6295\u5F71\u5668\u8A02\u95B1\u7684\u6240\u6709\u4E8B\u4EF6\u985E\u578B
|
|
3951
|
+
*
|
|
3952
|
+
* \u7528\u65BC\u81EA\u52D5\u4E8B\u4EF6\u8A02\u95B1\u8A3B\u518A
|
|
3953
|
+
*/
|
|
3954
|
+
getSubscribedEventTypes(): string[] {
|
|
3955
|
+
// TODO: \u8FD4\u56DE\u6B64\u6295\u5F71\u5668\u8A02\u95B1\u7684\u6240\u6709\u4E8B\u4EF6\u985E\u578B\u540D\u7A31
|
|
3956
|
+
return [
|
|
3957
|
+
// 'SomeEvent',
|
|
3958
|
+
// 'AnotherEvent',
|
|
3959
|
+
]
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3962
|
+
/**
|
|
3963
|
+
* \u5224\u65B7\u6295\u5F71\u5668\u662F\u5426\u652F\u63F4\u67D0\u500B\u4E8B\u4EF6
|
|
3964
|
+
*/
|
|
3965
|
+
supportsEvent(eventType: string): boolean {
|
|
3966
|
+
return this.getSubscribedEventTypes().includes(eventType)
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
`;
|
|
3970
|
+
}
|
|
3971
|
+
/**
|
|
3972
|
+
* 生成查詢服務檔案
|
|
3973
|
+
*/
|
|
3974
|
+
generateQueryService(moduleName) {
|
|
3975
|
+
return `/**
|
|
3976
|
+
* Query${moduleName}Service - \u67E5\u8A62\u670D\u52D9
|
|
3977
|
+
*
|
|
3978
|
+
* \u61C9\u7528\u5C64\u670D\u52D9\uFF0C\u63D0\u4F9B\u67E5\u8A62\u63A5\u53E3\u3002
|
|
3979
|
+
* \u901A\u904E\u5009\u5EAB\u8A2A\u554F\u8B80\u6A21\u578B\uFF0C\u652F\u6301\u591A\u7A2E\u67E5\u8A62\u65B9\u6CD5\u3002
|
|
3980
|
+
*
|
|
3981
|
+
* \u8A2D\u8A08\u7279\u9EDE\uFF1A
|
|
3982
|
+
* 1. \u7121\u526F\u4F5C\u7528\uFF1A\u53EA\u8B80\u64CD\u4F5C\uFF0C\u4E0D\u4FEE\u6539\u72C0\u614B
|
|
3983
|
+
* 2. \u9AD8\u6548\uFF1A\u76F4\u63A5\u67E5\u8A62\u512A\u5316\u7684\u8B80\u6A21\u578B
|
|
3984
|
+
* 3. \u4E00\u81F4\u6027\uFF1A\u652F\u6301\u7DE9\u5B58\u63A7\u5236
|
|
3985
|
+
* 4. \u932F\u8AA4\u8655\u7406\uFF1A\u9069\u7576\u7684\u7570\u5E38\u8655\u7406\u548C\u65E5\u8A8C
|
|
3986
|
+
*/
|
|
3987
|
+
|
|
3988
|
+
import type { I${moduleName}ReadModelRepository } from '../../Infrastructure/Repositories/I${moduleName}ReadModelRepository'
|
|
3989
|
+
import { ${moduleName}ReadDTO } from '../DTOs/${moduleName}ReadDTO'
|
|
3990
|
+
|
|
3991
|
+
/**
|
|
3992
|
+
* \u67E5\u8A62\u904E\u6FFE\u689D\u4EF6
|
|
3993
|
+
*/
|
|
3994
|
+
export interface QueryFilters {
|
|
3995
|
+
[key: string]: any
|
|
3996
|
+
// TODO: \u6839\u64DA\u67E5\u8A62\u9700\u6C42\u5B9A\u7FA9\u904E\u6FFE\u5B57\u6BB5
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
/**
|
|
4000
|
+
* ${moduleName} \u67E5\u8A62\u670D\u52D9
|
|
4001
|
+
* \u63D0\u4F9B\u8B80\u6A21\u578B\u7684\u5404\u7A2E\u67E5\u8A62\u65B9\u6CD5
|
|
4002
|
+
*/
|
|
4003
|
+
export class Query${moduleName}Service {
|
|
4004
|
+
constructor(private repository: I${moduleName}ReadModelRepository) {}
|
|
4005
|
+
|
|
4006
|
+
/**
|
|
4007
|
+
* \u6839\u64DA ID \u67E5\u8A62\u55AE\u500B\u8B80\u6A21\u578B
|
|
4008
|
+
* @param id - \u8B80\u6A21\u578B ID
|
|
4009
|
+
* @returns \u8B80\u6A21\u578B DTO\uFF0C\u5982\u679C\u4E0D\u5B58\u5728\u8FD4\u56DE null
|
|
4010
|
+
*/
|
|
4011
|
+
async findById(id: string): Promise<${moduleName}ReadDTO | null> {
|
|
4012
|
+
try {
|
|
4013
|
+
const model = await this.repository.findById(id)
|
|
4014
|
+
return model ? ${moduleName}ReadDTO.fromReadModel(model) : null
|
|
4015
|
+
} catch (error) {
|
|
4016
|
+
console.error(\`Error finding ${moduleName} by ID \${id}:\`, error)
|
|
4017
|
+
throw error
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
/**
|
|
4022
|
+
* \u67E5\u8A62\u6240\u6709\u8B80\u6A21\u578B
|
|
4023
|
+
* @param filters - \u67E5\u8A62\u904E\u6FFE\u689D\u4EF6\uFF08\u53EF\u9078\uFF09
|
|
4024
|
+
* @returns \u8B80\u6A21\u578B DTO \u5217\u8868
|
|
4025
|
+
*/
|
|
4026
|
+
async findAll(filters?: QueryFilters): Promise<${moduleName}ReadDTO[]> {
|
|
4027
|
+
try {
|
|
4028
|
+
const models = await this.repository.findAll(filters)
|
|
4029
|
+
return models.map(model => ${moduleName}ReadDTO.fromReadModel(model))
|
|
4030
|
+
} catch (error) {
|
|
4031
|
+
console.error('Error finding all ${moduleName}:', error)
|
|
4032
|
+
throw error
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
/**
|
|
4037
|
+
* \u57FA\u65BC\u689D\u4EF6\u641C\u7D22
|
|
4038
|
+
* @param criteria - \u641C\u7D22\u689D\u4EF6
|
|
4039
|
+
* @returns \u5339\u914D\u7684\u8B80\u6A21\u578B DTO \u5217\u8868
|
|
4040
|
+
*/
|
|
4041
|
+
async search(criteria: QueryFilters): Promise<${moduleName}ReadDTO[]> {
|
|
4042
|
+
try {
|
|
4043
|
+
// TODO: \u5BE6\u73FE\u81EA\u5B9A\u7FA9\u641C\u7D22\u908F\u8F2F
|
|
4044
|
+
return this.findAll(criteria)
|
|
4045
|
+
} catch (error) {
|
|
4046
|
+
console.error('Error searching ${moduleName}:', error)
|
|
4047
|
+
throw error
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
/**
|
|
4052
|
+
* \u7372\u53D6\u7D71\u8A08\u4FE1\u606F
|
|
4053
|
+
* @returns \u7D71\u8A08\u6578\u64DA
|
|
4054
|
+
*/
|
|
4055
|
+
async getStatistics(): Promise<Record<string, any>> {
|
|
4056
|
+
try {
|
|
4057
|
+
// TODO: \u5BE6\u73FE\u7D71\u8A08\u908F\u8F2F
|
|
4058
|
+
// - \u7E3D\u6578\u91CF
|
|
4059
|
+
// - \u805A\u5408\u5B57\u6BB5\uFF08\u7E3D\u984D\u3001\u5E73\u5747\u503C\u7B49\uFF09
|
|
4060
|
+
// - \u5206\u7D44\u7D71\u8A08
|
|
4061
|
+
|
|
4062
|
+
return {
|
|
4063
|
+
totalCount: 0,
|
|
4064
|
+
// \u66F4\u591A\u7D71\u8A08\u5B57\u6BB5...
|
|
4065
|
+
}
|
|
4066
|
+
} catch (error) {
|
|
4067
|
+
console.error('Error getting ${moduleName} statistics:', error)
|
|
4068
|
+
throw error
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
`;
|
|
4073
|
+
}
|
|
4074
|
+
/**
|
|
4075
|
+
* 生成查詢 DTO 檔案
|
|
4076
|
+
*/
|
|
4077
|
+
generateQueryDTO(moduleName) {
|
|
4078
|
+
return `/**
|
|
4079
|
+
* ${moduleName}ReadDTO - \u67E5\u8A62\u7D50\u679C DTO
|
|
4080
|
+
*
|
|
4081
|
+
* \u6578\u64DA\u50B3\u8F38\u5C0D\u8C61\uFF0C\u7528\u65BC\u5C55\u793A\u5C64\u8207\u61C9\u7528\u5C64\u4E4B\u9593\u7684\u901A\u4FE1\u3002
|
|
4082
|
+
* \u57FA\u65BC\u8B80\u6A21\u578B\u4F46\u53EF\u4EE5\u5305\u542B\u984D\u5916\u7684\u8A08\u7B97\u5B57\u6BB5\u3002
|
|
4083
|
+
*/
|
|
4084
|
+
|
|
4085
|
+
import { BaseDTO } from '@/Shared/Application/BaseDTO'
|
|
4086
|
+
import type { ${moduleName}ReadModel } from '../../Domain/ReadModels/${moduleName}ReadModel'
|
|
2176
4087
|
|
|
2177
4088
|
/**
|
|
2178
|
-
*
|
|
4089
|
+
* ${moduleName} \u8B80\u6A21\u578B DTO
|
|
2179
4090
|
*/
|
|
2180
|
-
export class
|
|
2181
|
-
|
|
4091
|
+
export class ${moduleName}ReadDTO extends BaseDTO {
|
|
4092
|
+
// TODO: \u6839\u64DA\u67E5\u8A62\u9700\u6C42\u6DFB\u52A0\u5B57\u6BB5
|
|
4093
|
+
// \u793A\u4F8B\uFF1A
|
|
4094
|
+
// id: string
|
|
4095
|
+
// customerId: string
|
|
4096
|
+
// totalAmount: string
|
|
4097
|
+
// transactionCount: number
|
|
4098
|
+
// lastUpdated: Date
|
|
2182
4099
|
|
|
2183
|
-
|
|
2184
|
-
|
|
4100
|
+
/**
|
|
4101
|
+
* \u5F9E\u8B80\u6A21\u578B\u8F49\u63DB\u70BA DTO
|
|
4102
|
+
*/
|
|
4103
|
+
static fromReadModel(model: ${moduleName}ReadModel): ${moduleName}ReadDTO {
|
|
4104
|
+
const dto = new ${moduleName}ReadDTO()
|
|
4105
|
+
// TODO: \u6620\u5C04\u8B80\u6A21\u578B\u5B57\u6BB5\u5230 DTO
|
|
4106
|
+
// dto.id = model.id
|
|
4107
|
+
// dto.customerId = model.customerId
|
|
4108
|
+
// ...
|
|
4109
|
+
return dto
|
|
4110
|
+
}
|
|
2185
4111
|
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
4112
|
+
/**
|
|
4113
|
+
* \u5E8F\u5217\u5316\u70BA JSON
|
|
4114
|
+
*/
|
|
4115
|
+
toJSON(): Record<string, any> {
|
|
4116
|
+
return {
|
|
4117
|
+
// TODO: \u8FD4\u56DE DTO \u7684 JSON \u8868\u793A
|
|
4118
|
+
// id: this.id,
|
|
4119
|
+
// customerId: this.customerId,
|
|
4120
|
+
// ...
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
`;
|
|
4125
|
+
}
|
|
4126
|
+
/**
|
|
4127
|
+
* 生成倉庫介面
|
|
4128
|
+
*/
|
|
4129
|
+
generateRepositoryInterface(moduleName) {
|
|
4130
|
+
return `/**
|
|
4131
|
+
* I${moduleName}ReadModelRepository - \u8B80\u6A21\u578B\u5009\u5EAB\u4ECB\u9762
|
|
4132
|
+
*
|
|
4133
|
+
* \u5B9A\u7FA9\u8B80\u6A21\u578B\u5B58\u5132\u548C\u67E5\u8A62\u7684\u63A5\u53E3\u3002
|
|
4134
|
+
* \u5BE6\u73FE\u985E\u8CA0\u8CAC\u8207\u6578\u64DA\u5EAB\u6216\u5FEB\u53D6\u7684\u4EA4\u4E92\u3002
|
|
4135
|
+
*/
|
|
2189
4136
|
|
|
2190
|
-
|
|
4137
|
+
import type { ${moduleName}ReadModel } from '../ReadModels/${moduleName}ReadModel'
|
|
2191
4138
|
|
|
2192
|
-
|
|
4139
|
+
/**
|
|
4140
|
+
* ${moduleName} \u8B80\u6A21\u578B\u5009\u5EAB\u4ECB\u9762
|
|
4141
|
+
*/
|
|
4142
|
+
export interface I${moduleName}ReadModelRepository {
|
|
4143
|
+
/**
|
|
4144
|
+
* \u6839\u64DA ID \u67E5\u8A62\u8B80\u6A21\u578B
|
|
4145
|
+
*/
|
|
4146
|
+
findById(id: string): Promise<${moduleName}ReadModel | null>
|
|
4147
|
+
|
|
4148
|
+
/**
|
|
4149
|
+
* \u67E5\u8A62\u6240\u6709\u8B80\u6A21\u578B
|
|
4150
|
+
*/
|
|
4151
|
+
findAll(filters?: Record<string, any>): Promise<${moduleName}ReadModel[]>
|
|
4152
|
+
|
|
4153
|
+
/**
|
|
4154
|
+
* \u4FDD\u5B58\u8B80\u6A21\u578B\uFF08\u65B0\u589E\u6216\u66F4\u65B0\uFF09
|
|
4155
|
+
*/
|
|
4156
|
+
save(model: ${moduleName}ReadModel): Promise<void>
|
|
4157
|
+
|
|
4158
|
+
/**
|
|
4159
|
+
* \u522A\u9664\u8B80\u6A21\u578B
|
|
4160
|
+
*/
|
|
4161
|
+
delete(id: string): Promise<void>
|
|
4162
|
+
|
|
4163
|
+
/**
|
|
4164
|
+
* \u6839\u64DA\u81EA\u5B9A\u7FA9\u689D\u4EF6\u67E5\u8A62
|
|
4165
|
+
*/
|
|
4166
|
+
query(condition: Record<string, any>): Promise<${moduleName}ReadModel[]>
|
|
4167
|
+
|
|
4168
|
+
/**
|
|
4169
|
+
* \u8A08\u6578
|
|
4170
|
+
*/
|
|
4171
|
+
count(condition?: Record<string, any>): Promise<number>
|
|
4172
|
+
}
|
|
4173
|
+
`;
|
|
4174
|
+
}
|
|
4175
|
+
/**
|
|
4176
|
+
* 生成倉庫實現
|
|
4177
|
+
*/
|
|
4178
|
+
generateRepositoryImplementation(moduleName) {
|
|
4179
|
+
return `/**
|
|
4180
|
+
* ${moduleName}ReadModelRepository - \u8B80\u6A21\u578B\u5009\u5EAB\u5BE6\u73FE
|
|
4181
|
+
*
|
|
4182
|
+
* \u5BE6\u73FE\u8B80\u6A21\u578B\u7684\u6301\u4E45\u5316\u548C\u67E5\u8A62\u3002
|
|
4183
|
+
* \u4F7F\u7528 Atlas ORM \u8207\u6578\u64DA\u5EAB\u4EA4\u4E92\u3002
|
|
4184
|
+
*/
|
|
4185
|
+
|
|
4186
|
+
import type { I${moduleName}ReadModelRepository } from '../Repositories/I${moduleName}ReadModelRepository'
|
|
4187
|
+
import type { ${moduleName}ReadModel } from '../../Domain/ReadModels/${moduleName}ReadModel'
|
|
4188
|
+
|
|
4189
|
+
/**
|
|
4190
|
+
* ${moduleName} \u8B80\u6A21\u578B\u5009\u5EAB\u5BE6\u73FE
|
|
4191
|
+
* \u4F7F\u7528 Atlas ORM \u8207\u6578\u64DA\u5EAB\u4EA4\u4E92
|
|
4192
|
+
*/
|
|
4193
|
+
export class ${moduleName}ReadModelRepository implements I${moduleName}ReadModelRepository {
|
|
4194
|
+
private readonly tableName = '${this.toSnakeCase(moduleName)}_read_models'
|
|
4195
|
+
|
|
4196
|
+
/**
|
|
4197
|
+
* \u6839\u64DA ID \u67E5\u8A62\u8B80\u6A21\u578B
|
|
4198
|
+
*/
|
|
4199
|
+
async findById(id: string): Promise<${moduleName}ReadModel | null> {
|
|
4200
|
+
try {
|
|
4201
|
+
// TODO: \u4F7F\u7528 Atlas ORM \u67E5\u8A62
|
|
4202
|
+
// const db = getDatabase() // \u5F9E DI \u5BB9\u5668\u7372\u53D6
|
|
4203
|
+
// return await db.table(this.tableName).where('id', id).first()
|
|
4204
|
+
return null
|
|
4205
|
+
} catch (error) {
|
|
4206
|
+
console.error(\`Error finding ${moduleName} by ID \${id}:\`, error)
|
|
4207
|
+
throw error
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
|
|
4211
|
+
/**
|
|
4212
|
+
* \u67E5\u8A62\u6240\u6709\u8B80\u6A21\u578B
|
|
4213
|
+
*/
|
|
4214
|
+
async findAll(filters?: Record<string, any>): Promise<${moduleName}ReadModel[]> {
|
|
4215
|
+
try {
|
|
4216
|
+
// TODO: \u4F7F\u7528 Atlas ORM \u67E5\u8A62
|
|
4217
|
+
// const db = getDatabase()
|
|
4218
|
+
// let query = db.table(this.tableName)
|
|
4219
|
+
//
|
|
4220
|
+
// if (filters) {
|
|
4221
|
+
// for (const [key, value] of Object.entries(filters)) {
|
|
4222
|
+
// query = query.where(key, value)
|
|
4223
|
+
// }
|
|
4224
|
+
// }
|
|
4225
|
+
//
|
|
4226
|
+
// return await query.get()
|
|
4227
|
+
return []
|
|
4228
|
+
} catch (error) {
|
|
4229
|
+
console.error('Error finding all ${moduleName}:', error)
|
|
4230
|
+
throw error
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4233
|
+
|
|
4234
|
+
/**
|
|
4235
|
+
* \u4FDD\u5B58\u8B80\u6A21\u578B
|
|
4236
|
+
*/
|
|
4237
|
+
async save(model: ${moduleName}ReadModel): Promise<void> {
|
|
4238
|
+
try {
|
|
4239
|
+
// TODO: \u4F7F\u7528 Atlas ORM \u4FDD\u5B58
|
|
4240
|
+
// const db = getDatabase()
|
|
4241
|
+
// await db.table(this.tableName).updateOrInsert(
|
|
4242
|
+
// { id: model.id },
|
|
4243
|
+
// model
|
|
4244
|
+
// )
|
|
4245
|
+
} catch (error) {
|
|
4246
|
+
console.error('Error saving ${moduleName}:', error)
|
|
4247
|
+
throw error
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4251
|
+
/**
|
|
4252
|
+
* \u522A\u9664\u8B80\u6A21\u578B
|
|
4253
|
+
*/
|
|
4254
|
+
async delete(id: string): Promise<void> {
|
|
4255
|
+
try {
|
|
4256
|
+
// TODO: \u4F7F\u7528 Atlas ORM \u522A\u9664
|
|
4257
|
+
// const db = getDatabase()
|
|
4258
|
+
// await db.table(this.tableName).where('id', id).delete()
|
|
4259
|
+
} catch (error) {
|
|
4260
|
+
console.error(\`Error deleting ${moduleName} ID \${id}:\`, error)
|
|
4261
|
+
throw error
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
/**
|
|
4266
|
+
* \u6839\u64DA\u81EA\u5B9A\u7FA9\u689D\u4EF6\u67E5\u8A62
|
|
4267
|
+
*/
|
|
4268
|
+
async query(condition: Record<string, any>): Promise<${moduleName}ReadModel[]> {
|
|
4269
|
+
return this.findAll(condition)
|
|
4270
|
+
}
|
|
4271
|
+
|
|
4272
|
+
/**
|
|
4273
|
+
* \u8A08\u6578
|
|
4274
|
+
*/
|
|
4275
|
+
async count(condition?: Record<string, any>): Promise<number> {
|
|
4276
|
+
try {
|
|
4277
|
+
// TODO: \u4F7F\u7528 Atlas ORM \u8A08\u6578
|
|
4278
|
+
// const db = getDatabase()
|
|
4279
|
+
// let query = db.table(this.tableName)
|
|
4280
|
+
//
|
|
4281
|
+
// if (condition) {
|
|
4282
|
+
// for (const [key, value] of Object.entries(condition)) {
|
|
4283
|
+
// query = query.where(key, value)
|
|
4284
|
+
// }
|
|
4285
|
+
// }
|
|
4286
|
+
//
|
|
4287
|
+
// return await query.count()
|
|
4288
|
+
return 0
|
|
4289
|
+
} catch (error) {
|
|
4290
|
+
console.error('Error counting ${moduleName}:', error)
|
|
4291
|
+
throw error
|
|
4292
|
+
}
|
|
2193
4293
|
}
|
|
2194
4294
|
}
|
|
4295
|
+
`;
|
|
4296
|
+
}
|
|
4297
|
+
/**
|
|
4298
|
+
* 生成事件訂閱器
|
|
4299
|
+
*/
|
|
4300
|
+
generateSubscriber(moduleName) {
|
|
4301
|
+
return `/**
|
|
4302
|
+
* ${moduleName}ProjectionSubscriber - \u6295\u5F71\u4E8B\u4EF6\u8A02\u95B1\u5668
|
|
4303
|
+
*
|
|
4304
|
+
* \u8A02\u95B1\u9818\u57DF\u4E8B\u4EF6\u4E26\u8ABF\u7528\u6295\u5F71\u5668\u66F4\u65B0\u8B80\u6A21\u578B\u3002
|
|
4305
|
+
* \u5145\u7576\u4E8B\u4EF6\u6E90\u8207\u67E5\u8A62\u5074\u7684\u6A4B\u6881\u3002
|
|
4306
|
+
*
|
|
4307
|
+
* \u5DE5\u4F5C\u6D41\u7A0B\uFF1A
|
|
4308
|
+
* 1. \u61C9\u7528\u555F\u52D5\u6642\u8A3B\u518A\u5230\u4E8B\u4EF6\u532F\u6D41\u6392
|
|
4309
|
+
* 2. \u63A5\u6536\u76F8\u95DC\u9818\u57DF\u4E8B\u4EF6
|
|
4310
|
+
* 3. \u8ABF\u7528\u6295\u5F71\u5668\u8655\u7406\u4E8B\u4EF6
|
|
4311
|
+
* 4. \u66F4\u65B0\u8B80\u6A21\u578B
|
|
4312
|
+
*/
|
|
2195
4313
|
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
core.register(new MiddlewareProvider())
|
|
4314
|
+
import type { DomainEvent } from '@/Shared/Domain/DomainEvent'
|
|
4315
|
+
import { ${moduleName}EventProjector } from '../../Domain/Projectors/${moduleName}EventProjector'
|
|
2199
4316
|
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
4317
|
+
/**
|
|
4318
|
+
* ${moduleName} \u6295\u5F71\u4E8B\u4EF6\u8A02\u95B1\u5668
|
|
4319
|
+
* \u76E3\u807D\u4E8B\u4EF6\u4E26\u9A45\u52D5\u8B80\u6A21\u578B\u6295\u5F71
|
|
4320
|
+
*/
|
|
4321
|
+
export class ${moduleName}ProjectionSubscriber {
|
|
4322
|
+
constructor(private projector: ${moduleName}EventProjector) {}
|
|
2203
4323
|
|
|
2204
|
-
|
|
4324
|
+
/**
|
|
4325
|
+
* \u8655\u7406\u4E8B\u4EF6
|
|
4326
|
+
* @param event - \u63A5\u6536\u5230\u7684\u9818\u57DF\u4E8B\u4EF6
|
|
4327
|
+
*/
|
|
4328
|
+
async handle(event: DomainEvent): Promise<void> {
|
|
4329
|
+
try {
|
|
4330
|
+
// \u6AA2\u67E5\u6B64\u8A02\u95B1\u5668\u662F\u5426\u61C9\u8655\u7406\u8A72\u4E8B\u4EF6
|
|
4331
|
+
if (this.projector.supportsEvent(event.constructor.name)) {
|
|
4332
|
+
await this.projector.projectEvent(event)
|
|
4333
|
+
}
|
|
4334
|
+
} catch (error) {
|
|
4335
|
+
console.error(
|
|
4336
|
+
\`${moduleName}ProjectionSubscriber failed to handle \${event.constructor.name}:\`,
|
|
4337
|
+
error
|
|
4338
|
+
)
|
|
4339
|
+
throw error
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4343
|
+
/**
|
|
4344
|
+
* \u7372\u53D6\u8A02\u95B1\u7684\u4E8B\u4EF6\u985E\u578B\u5217\u8868
|
|
4345
|
+
* \u7528\u65BC\u4E8B\u4EF6\u532F\u6D41\u6392\u8A3B\u518A
|
|
4346
|
+
*/
|
|
4347
|
+
getSubscribedEventTypes(): string[] {
|
|
4348
|
+
return this.projector.getSubscribedEventTypes()
|
|
4349
|
+
}
|
|
2205
4350
|
}
|
|
2206
4351
|
`;
|
|
2207
4352
|
}
|
|
2208
|
-
|
|
4353
|
+
/**
|
|
4354
|
+
* 生成快取層
|
|
4355
|
+
*/
|
|
4356
|
+
generateCache(moduleName) {
|
|
2209
4357
|
return `/**
|
|
2210
|
-
*
|
|
4358
|
+
* ${moduleName}ReadModelCache - \u8B80\u6A21\u578B\u5FEB\u53D6\u5C64
|
|
2211
4359
|
*
|
|
2212
|
-
*
|
|
4360
|
+
* \u53EF\u9078\u7684\u5FEB\u53D6\u5C64\uFF0C\u7528\u65BC\u63D0\u9AD8\u67E5\u8A62\u6027\u80FD\u3002
|
|
4361
|
+
* \u652F\u6301\u96D9\u5C64\u5FEB\u53D6\uFF1A\u9032\u7A0B\u5167\u5FEB\u53D6 + Redis \u5FEB\u53D6
|
|
2213
4362
|
*/
|
|
2214
4363
|
|
|
2215
|
-
import {
|
|
4364
|
+
import type { ${moduleName}ReadModel } from '../../Domain/ReadModels/${moduleName}ReadModel'
|
|
2216
4365
|
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
4366
|
+
/**
|
|
4367
|
+
* ${moduleName} \u8B80\u6A21\u578B\u5FEB\u53D6
|
|
4368
|
+
*/
|
|
4369
|
+
export class ${moduleName}ReadModelCache {
|
|
4370
|
+
private memoryCache: Map<string, ${moduleName}ReadModel> = new Map()
|
|
4371
|
+
private readonly cacheTTL = 3600 // 1 \u5C0F\u6642
|
|
4372
|
+
|
|
4373
|
+
/**
|
|
4374
|
+
* \u5F9E\u5FEB\u53D6\u7372\u53D6\u8B80\u6A21\u578B
|
|
4375
|
+
*/
|
|
4376
|
+
async get(key: string): Promise<${moduleName}ReadModel | null> {
|
|
4377
|
+
// \u5148\u5617\u8A66\u9032\u7A0B\u8A18\u61B6\u9AD4\u5FEB\u53D6
|
|
4378
|
+
const cached = this.memoryCache.get(key)
|
|
4379
|
+
if (cached) {
|
|
4380
|
+
return cached
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4383
|
+
// TODO: \u5617\u8A66 Redis \u5FEB\u53D6
|
|
4384
|
+
// const redis = getRedisClient()
|
|
4385
|
+
// const cached = await redis.get(\`${moduleName}:\${key}\`)
|
|
4386
|
+
// if (cached) {
|
|
4387
|
+
// return JSON.parse(cached)
|
|
4388
|
+
// }
|
|
4389
|
+
|
|
4390
|
+
return null
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
/**
|
|
4394
|
+
* \u8A2D\u7F6E\u5FEB\u53D6
|
|
4395
|
+
*/
|
|
4396
|
+
async set(key: string, value: ${moduleName}ReadModel, ttl?: number): Promise<void> {
|
|
4397
|
+
// \u8A2D\u7F6E\u9032\u7A0B\u8A18\u61B6\u9AD4\u5FEB\u53D6
|
|
4398
|
+
this.memoryCache.set(key, value)
|
|
4399
|
+
|
|
4400
|
+
// TODO: \u8A2D\u7F6E Redis \u5FEB\u53D6
|
|
4401
|
+
// const redis = getRedisClient()
|
|
4402
|
+
// await redis.setex(
|
|
4403
|
+
// \`${moduleName}:\${key}\`,
|
|
4404
|
+
// ttl || this.cacheTTL,
|
|
4405
|
+
// JSON.stringify(value)
|
|
4406
|
+
// )
|
|
4407
|
+
}
|
|
4408
|
+
|
|
4409
|
+
/**
|
|
4410
|
+
* \u5931\u6548\u5FEB\u53D6
|
|
4411
|
+
*/
|
|
4412
|
+
async invalidate(key: string): Promise<void> {
|
|
4413
|
+
// \u6E05\u9664\u9032\u7A0B\u5FEB\u53D6
|
|
4414
|
+
this.memoryCache.delete(key)
|
|
4415
|
+
|
|
4416
|
+
// TODO: \u6E05\u9664 Redis \u5FEB\u53D6
|
|
4417
|
+
// const redis = getRedisClient()
|
|
4418
|
+
// await redis.del(\`${moduleName}:\${key}\`)
|
|
4419
|
+
}
|
|
4420
|
+
|
|
4421
|
+
/**
|
|
4422
|
+
* \u6E05\u7A7A\u6240\u6709\u5FEB\u53D6
|
|
4423
|
+
*/
|
|
4424
|
+
async clear(): Promise<void> {
|
|
4425
|
+
this.memoryCache.clear()
|
|
4426
|
+
|
|
4427
|
+
// TODO: \u6E05\u7A7A Redis \u5FEB\u53D6
|
|
4428
|
+
// const redis = getRedisClient()
|
|
4429
|
+
// await redis.del(\`${moduleName}:*\`)
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
`;
|
|
4433
|
+
}
|
|
4434
|
+
/**
|
|
4435
|
+
* 生成 HTTP 控制器
|
|
4436
|
+
*/
|
|
4437
|
+
generateController(moduleName) {
|
|
4438
|
+
const kebabName = this.toKebabCase(moduleName);
|
|
4439
|
+
return `/**
|
|
4440
|
+
* ${moduleName}QueryController - \u67E5\u8A62\u7AEF\u9EDE\u63A7\u5236\u5668
|
|
4441
|
+
*
|
|
4442
|
+
* HTTP \u7AEF\u9EDE\uFF0C\u70BA\u5BA2\u6236\u7AEF\u63D0\u4F9B\u8B80\u6A21\u578B\u67E5\u8A62\u63A5\u53E3\u3002
|
|
4443
|
+
* \u8ABF\u7528\u67E5\u8A62\u670D\u52D9\u4E26\u683C\u5F0F\u5316\u97FF\u61C9\u3002
|
|
4444
|
+
*/
|
|
4445
|
+
|
|
4446
|
+
import type { IHttpContext } from '@/Shared/Http/IHttpContext'
|
|
4447
|
+
import { Query${moduleName}Service } from '../../Application/Services/Query${moduleName}Service'
|
|
4448
|
+
|
|
4449
|
+
/**
|
|
4450
|
+
* ${moduleName} \u67E5\u8A62\u63A7\u5236\u5668
|
|
4451
|
+
*/
|
|
4452
|
+
export class ${moduleName}QueryController {
|
|
4453
|
+
constructor(private queryService: Query${moduleName}Service) {}
|
|
4454
|
+
|
|
4455
|
+
/**
|
|
4456
|
+
* GET /${kebabName}/:id
|
|
4457
|
+
* \u6839\u64DA ID \u67E5\u8A62\u55AE\u500B ${moduleName}
|
|
4458
|
+
*/
|
|
4459
|
+
async findById(ctx: IHttpContext): Promise<void> {
|
|
4460
|
+
try {
|
|
4461
|
+
const { id } = ctx.request.params as { id: string }
|
|
4462
|
+
|
|
4463
|
+
if (!id) {
|
|
4464
|
+
ctx.response.status = 400
|
|
4465
|
+
ctx.response.json({ error: 'ID is required' })
|
|
4466
|
+
return
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4469
|
+
const dto = await this.queryService.findById(id)
|
|
4470
|
+
|
|
4471
|
+
if (!dto) {
|
|
4472
|
+
ctx.response.status = 404
|
|
4473
|
+
ctx.response.json({ error: '${moduleName} not found' })
|
|
4474
|
+
return
|
|
4475
|
+
}
|
|
4476
|
+
|
|
4477
|
+
ctx.response.status = 200
|
|
4478
|
+
ctx.response.json({ data: dto })
|
|
4479
|
+
} catch (error) {
|
|
4480
|
+
this.handleError(ctx, error)
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
/**
|
|
4485
|
+
* GET /${kebabName}
|
|
4486
|
+
* \u67E5\u8A62\u6240\u6709 ${moduleName}
|
|
4487
|
+
*/
|
|
4488
|
+
async findAll(ctx: IHttpContext): Promise<void> {
|
|
4489
|
+
try {
|
|
4490
|
+
const filters = ctx.request.query as Record<string, any> | undefined
|
|
4491
|
+
const dtos = await this.queryService.findAll(filters)
|
|
4492
|
+
|
|
4493
|
+
ctx.response.status = 200
|
|
4494
|
+
ctx.response.json({
|
|
4495
|
+
data: dtos,
|
|
4496
|
+
meta: { count: dtos.length },
|
|
4497
|
+
})
|
|
4498
|
+
} catch (error) {
|
|
4499
|
+
this.handleError(ctx, error)
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4502
|
+
|
|
4503
|
+
/**
|
|
4504
|
+
* GET /${kebabName}/search
|
|
4505
|
+
* \u641C\u7D22 ${moduleName}
|
|
4506
|
+
*/
|
|
4507
|
+
async search(ctx: IHttpContext): Promise<void> {
|
|
4508
|
+
try {
|
|
4509
|
+
const criteria = ctx.request.query as Record<string, any> | undefined
|
|
4510
|
+
|
|
4511
|
+
if (!criteria) {
|
|
4512
|
+
ctx.response.status = 400
|
|
4513
|
+
ctx.response.json({ error: 'Search criteria required' })
|
|
4514
|
+
return
|
|
4515
|
+
}
|
|
4516
|
+
|
|
4517
|
+
const dtos = await this.queryService.search(criteria)
|
|
4518
|
+
|
|
4519
|
+
ctx.response.status = 200
|
|
4520
|
+
ctx.response.json({
|
|
4521
|
+
data: dtos,
|
|
4522
|
+
meta: { count: dtos.length },
|
|
4523
|
+
})
|
|
4524
|
+
} catch (error) {
|
|
4525
|
+
this.handleError(ctx, error)
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4529
|
+
/**
|
|
4530
|
+
* GET /${kebabName}/statistics
|
|
4531
|
+
* \u7372\u53D6\u7D71\u8A08\u4FE1\u606F
|
|
4532
|
+
*/
|
|
4533
|
+
async getStatistics(ctx: IHttpContext): Promise<void> {
|
|
4534
|
+
try {
|
|
4535
|
+
const stats = await this.queryService.getStatistics()
|
|
4536
|
+
|
|
4537
|
+
ctx.response.status = 200
|
|
4538
|
+
ctx.response.json({ data: stats })
|
|
4539
|
+
} catch (error) {
|
|
4540
|
+
this.handleError(ctx, error)
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4543
|
+
|
|
4544
|
+
/**
|
|
4545
|
+
* \u7D71\u4E00\u932F\u8AA4\u8655\u7406
|
|
4546
|
+
*/
|
|
4547
|
+
private handleError(ctx: IHttpContext, error: unknown): void {
|
|
4548
|
+
console.error('${moduleName}QueryController error:', error)
|
|
4549
|
+
|
|
4550
|
+
ctx.response.status = 500
|
|
4551
|
+
ctx.response.json({
|
|
4552
|
+
error: 'Internal server error',
|
|
4553
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
4554
|
+
})
|
|
4555
|
+
}
|
|
2220
4556
|
}
|
|
2221
4557
|
`;
|
|
2222
4558
|
}
|
|
2223
|
-
|
|
4559
|
+
/**
|
|
4560
|
+
* 生成路由檔案
|
|
4561
|
+
*/
|
|
4562
|
+
generateRoutes(moduleName) {
|
|
4563
|
+
const kebabName = this.toKebabCase(moduleName);
|
|
4564
|
+
const functionName = `register${moduleName}QueryRoutes`;
|
|
2224
4565
|
return `/**
|
|
2225
|
-
*
|
|
4566
|
+
* ${kebabName}.routes.ts - ${moduleName} \u67E5\u8A62\u8DEF\u7531
|
|
2226
4567
|
*
|
|
2227
|
-
*
|
|
4568
|
+
* \u8A3B\u518A ${moduleName} \u67E5\u8A62\u7AEF\u9EDE\u7684\u8DEF\u7531
|
|
2228
4569
|
*/
|
|
2229
4570
|
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
router.get('/health', (c: any) => c.json({ status: 'healthy' }))
|
|
2233
|
-
|
|
2234
|
-
// Ordering module
|
|
2235
|
-
router.get('/api/orders', (c: any) => c.json({ message: 'Order list' }))
|
|
2236
|
-
router.post('/api/orders', (c: any) => c.json({ message: 'Order created' }, 201))
|
|
4571
|
+
import type { PlanetCore } from '@gravito/core'
|
|
4572
|
+
import { ${moduleName}QueryController } from '../Controllers/${moduleName}QueryController'
|
|
2237
4573
|
|
|
2238
|
-
|
|
2239
|
-
|
|
4574
|
+
/**
|
|
4575
|
+
* \u8A3B\u518A ${moduleName} \u67E5\u8A62\u8DEF\u7531
|
|
4576
|
+
*
|
|
4577
|
+
* \u7AEF\u9EDE\uFF1A
|
|
4578
|
+
* - GET /${kebabName}/:id - \u7372\u53D6\u55AE\u500B ${moduleName}
|
|
4579
|
+
* - GET /${kebabName} - \u5217\u8868\u67E5\u8A62
|
|
4580
|
+
* - GET /${kebabName}/search - \u641C\u7D22
|
|
4581
|
+
* - GET /${kebabName}/statistics - \u7D71\u8A08\u4FE1\u606F
|
|
4582
|
+
*/
|
|
4583
|
+
export function ${functionName}(core: PlanetCore): void {
|
|
4584
|
+
// TODO: \u5F9E DI \u5BB9\u5668\u7372\u53D6\u4F9D\u8CF4\u4E26\u5275\u5EFA\u63A7\u5236\u5668
|
|
4585
|
+
// const queryService = core.container.make('query-${kebabName}-service')
|
|
4586
|
+
// const controller = new ${moduleName}QueryController(queryService)
|
|
4587
|
+
|
|
4588
|
+
// TODO: \u8A3B\u518A\u8DEF\u7531
|
|
4589
|
+
// core.router.get('/${kebabName}/:id', (ctx) => controller.findById(ctx))
|
|
4590
|
+
// core.router.get('/${kebabName}', (ctx) => controller.findAll(ctx))
|
|
4591
|
+
// core.router.get('/${kebabName}/search', (ctx) => controller.search(ctx))
|
|
4592
|
+
// core.router.get('/${kebabName}/statistics', (ctx) => controller.getStatistics(ctx))
|
|
4593
|
+
|
|
4594
|
+
console.log(\`\u2713 ${moduleName} query routes registered\`)
|
|
2240
4595
|
}
|
|
2241
4596
|
`;
|
|
2242
4597
|
}
|
|
2243
|
-
|
|
4598
|
+
/**
|
|
4599
|
+
* 生成模組索引檔案
|
|
4600
|
+
*/
|
|
4601
|
+
generateIndex(moduleName) {
|
|
2244
4602
|
return `/**
|
|
2245
|
-
*
|
|
4603
|
+
* ${moduleName} Query Module
|
|
2246
4604
|
*
|
|
2247
|
-
*
|
|
4605
|
+
* CQRS \u67E5\u8A62\u5074\u6A21\u7D44\uFF0C\u63D0\u4F9B\u8B80\u6A21\u578B\u548C\u67E5\u8A62\u670D\u52D9\u3002
|
|
4606
|
+
* \u8A02\u95B1\u4F86\u81EA\u5BEB\u5074\uFF08Event Sourcing\uFF09\u6A21\u7D44\u7684\u4E8B\u4EF6\u3002
|
|
2248
4607
|
*/
|
|
2249
4608
|
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
prefix: '/api/orders',
|
|
2256
|
-
},
|
|
2257
|
-
catalog: {
|
|
2258
|
-
name: 'Catalog',
|
|
2259
|
-
description: 'Product catalog module',
|
|
2260
|
-
prefix: '/api/products',
|
|
2261
|
-
},
|
|
2262
|
-
},
|
|
4609
|
+
// Domain exports
|
|
4610
|
+
export type { ${moduleName}ReadModel } from './Domain/ReadModels/${moduleName}ReadModel'
|
|
4611
|
+
export { create${moduleName}ReadModel, validate${moduleName}ReadModel } from './Domain/ReadModels/${moduleName}ReadModel'
|
|
4612
|
+
export { ${moduleName}EventProjector } from './Domain/Projectors/${moduleName}EventProjector'
|
|
4613
|
+
export type { I${moduleName}ReadModelRepository } from './Domain/Repositories/I${moduleName}ReadModelRepository'
|
|
2263
4614
|
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
VIEW_DIR: process.env.VIEW_DIR ?? 'src/views',
|
|
2277
|
-
debug: process.env.APP_DEBUG === 'true',
|
|
2278
|
-
url: process.env.APP_URL ?? 'http://localhost:3000',
|
|
2279
|
-
}
|
|
4615
|
+
// Application exports
|
|
4616
|
+
export { Query${moduleName}Service } from './Application/Services/Query${moduleName}Service'
|
|
4617
|
+
export { ${moduleName}ReadDTO } from './Application/DTOs/${moduleName}ReadDTO'
|
|
4618
|
+
|
|
4619
|
+
// Infrastructure exports
|
|
4620
|
+
export { ${moduleName}ReadModelRepository } from './Infrastructure/Repositories/${moduleName}ReadModelRepository'
|
|
4621
|
+
export { ${moduleName}ProjectionSubscriber } from './Infrastructure/Subscribers/${moduleName}ProjectionSubscriber'
|
|
4622
|
+
export { ${moduleName}ReadModelCache } from './Infrastructure/Cache/${moduleName}ReadModelCache'
|
|
4623
|
+
|
|
4624
|
+
// Presentation exports
|
|
4625
|
+
export { ${moduleName}QueryController } from './Presentation/Controllers/${moduleName}QueryController'
|
|
4626
|
+
export { register${moduleName}QueryRoutes } from './Presentation/Routes/${this.toKebabCase(moduleName)}.routes'
|
|
2280
4627
|
`;
|
|
2281
4628
|
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
return `export default {
|
|
2288
|
-
default: process.env.CACHE_DRIVER ?? 'memory',
|
|
2289
|
-
stores: { memory: { driver: 'memory' } },
|
|
2290
|
-
}
|
|
2291
|
-
`;
|
|
4629
|
+
/**
|
|
4630
|
+
* 將字符串轉換為 kebab-case
|
|
4631
|
+
*/
|
|
4632
|
+
toKebabCase(str) {
|
|
4633
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
2292
4634
|
}
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
`;
|
|
4635
|
+
/**
|
|
4636
|
+
* 將字符串轉換為 snake_case
|
|
4637
|
+
*/
|
|
4638
|
+
toSnakeCase(str) {
|
|
4639
|
+
return str.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").toLowerCase();
|
|
2299
4640
|
}
|
|
2300
4641
|
};
|
|
2301
4642
|
|
|
@@ -2442,6 +4783,30 @@ var ModuleGenerator = class {
|
|
|
2442
4783
|
]
|
|
2443
4784
|
}
|
|
2444
4785
|
]
|
|
4786
|
+
},
|
|
4787
|
+
// UserInterface Layer
|
|
4788
|
+
{
|
|
4789
|
+
type: "directory",
|
|
4790
|
+
name: "UserInterface",
|
|
4791
|
+
children: [
|
|
4792
|
+
{
|
|
4793
|
+
type: "directory",
|
|
4794
|
+
name: "Http",
|
|
4795
|
+
children: [
|
|
4796
|
+
{
|
|
4797
|
+
type: "directory",
|
|
4798
|
+
name: "Controllers",
|
|
4799
|
+
children: [
|
|
4800
|
+
{
|
|
4801
|
+
type: "file",
|
|
4802
|
+
name: `${name}Controller.ts`,
|
|
4803
|
+
content: this.generateController(name)
|
|
4804
|
+
}
|
|
4805
|
+
]
|
|
4806
|
+
}
|
|
4807
|
+
]
|
|
4808
|
+
}
|
|
4809
|
+
]
|
|
2445
4810
|
}
|
|
2446
4811
|
]
|
|
2447
4812
|
};
|
|
@@ -2457,17 +4822,13 @@ import { ${name}Created } from '../../Events/${name}Created'
|
|
|
2457
4822
|
import { ${name}Status } from './${name}Status'
|
|
2458
4823
|
|
|
2459
4824
|
export interface ${name}Props {
|
|
2460
|
-
// Add properties here
|
|
2461
4825
|
status: ${name}Status
|
|
2462
4826
|
createdAt: Date
|
|
2463
4827
|
}
|
|
2464
4828
|
|
|
2465
4829
|
export class ${name} extends AggregateRoot<Id> {
|
|
2466
|
-
private props: ${name}Props
|
|
2467
|
-
|
|
2468
|
-
private constructor(id: Id, props: ${name}Props) {
|
|
4830
|
+
private constructor(id: Id, private props: ${name}Props) {
|
|
2469
4831
|
super(id)
|
|
2470
|
-
this.props = props
|
|
2471
4832
|
}
|
|
2472
4833
|
|
|
2473
4834
|
static create(id: Id): ${name} {
|
|
@@ -2485,7 +4846,12 @@ export class ${name} extends AggregateRoot<Id> {
|
|
|
2485
4846
|
return this.props.status
|
|
2486
4847
|
}
|
|
2487
4848
|
|
|
2488
|
-
|
|
4849
|
+
/**
|
|
4850
|
+
* Complete the ${name} process
|
|
4851
|
+
*/
|
|
4852
|
+
complete(): void {
|
|
4853
|
+
this.props.status = ${name}Status.COMPLETED
|
|
4854
|
+
}
|
|
2489
4855
|
}
|
|
2490
4856
|
`;
|
|
2491
4857
|
}
|
|
@@ -2510,17 +4876,13 @@ export enum ${name}Status {
|
|
|
2510
4876
|
import { DomainEvent } from '@gravito/enterprise'
|
|
2511
4877
|
|
|
2512
4878
|
export class ${name}Created extends DomainEvent {
|
|
2513
|
-
constructor(public readonly
|
|
4879
|
+
constructor(public readonly aggregateId: string) {
|
|
2514
4880
|
super()
|
|
2515
4881
|
}
|
|
2516
4882
|
|
|
2517
4883
|
override get eventName(): string {
|
|
2518
4884
|
return '${name.toLowerCase()}.created'
|
|
2519
4885
|
}
|
|
2520
|
-
|
|
2521
|
-
get aggregateId(): string {
|
|
2522
|
-
return this.${name.toLowerCase()}Id
|
|
2523
|
-
}
|
|
2524
4886
|
}
|
|
2525
4887
|
`;
|
|
2526
4888
|
}
|
|
@@ -2547,7 +4909,6 @@ import { Command } from '@gravito/enterprise'
|
|
|
2547
4909
|
|
|
2548
4910
|
export class Create${name}Command extends Command {
|
|
2549
4911
|
constructor(
|
|
2550
|
-
// Add command properties
|
|
2551
4912
|
public readonly id?: string
|
|
2552
4913
|
) {
|
|
2553
4914
|
super()
|
|
@@ -2602,13 +4963,15 @@ export class Get${name}ByIdQuery extends Query {
|
|
|
2602
4963
|
import { QueryHandler } from '@gravito/enterprise'
|
|
2603
4964
|
import type { I${name}Repository } from '../../../Domain/Repositories/I${name}Repository'
|
|
2604
4965
|
import type { ${name}DTO } from '../../DTOs/${name}DTO'
|
|
4966
|
+
import { Id } from '../../../../../Shared/Domain/ValueObjects/Id'
|
|
2605
4967
|
import type { Get${name}ByIdQuery } from './Get${name}ByIdQuery'
|
|
2606
4968
|
|
|
2607
4969
|
export class Get${name}ByIdHandler implements QueryHandler<Get${name}ByIdQuery, ${name}DTO | null> {
|
|
2608
4970
|
constructor(private repository: I${name}Repository) {}
|
|
2609
4971
|
|
|
2610
4972
|
async handle(query: Get${name}ByIdQuery): Promise<${name}DTO | null> {
|
|
2611
|
-
const
|
|
4973
|
+
const id = Id.from(query.id)
|
|
4974
|
+
const aggregate = await this.repository.findById(id)
|
|
2612
4975
|
if (!aggregate) return null
|
|
2613
4976
|
|
|
2614
4977
|
return {
|
|
@@ -2629,7 +4992,6 @@ import type { ${name}Status } from '../../Domain/Aggregates/${name}/${name}Statu
|
|
|
2629
4992
|
export interface ${name}DTO {
|
|
2630
4993
|
id: string
|
|
2631
4994
|
status: ${name}Status
|
|
2632
|
-
// Add more fields
|
|
2633
4995
|
}
|
|
2634
4996
|
`;
|
|
2635
4997
|
}
|
|
@@ -2640,29 +5002,29 @@ export interface ${name}DTO {
|
|
|
2640
5002
|
|
|
2641
5003
|
import type { ${name} } from '../../Domain/Aggregates/${name}/${name}'
|
|
2642
5004
|
import type { I${name}Repository } from '../../Domain/Repositories/I${name}Repository'
|
|
2643
|
-
import
|
|
2644
|
-
|
|
2645
|
-
const store = new Map<string, ${name}>()
|
|
5005
|
+
import { Id } from '../../../../../Shared/Domain/ValueObjects/Id'
|
|
2646
5006
|
|
|
2647
5007
|
export class ${name}Repository implements I${name}Repository {
|
|
5008
|
+
private store = new Map<string, ${name}>()
|
|
5009
|
+
|
|
2648
5010
|
async findById(id: Id): Promise<${name} | null> {
|
|
2649
|
-
return store.get(id.value) ?? null
|
|
5011
|
+
return this.store.get(id.value) ?? null
|
|
2650
5012
|
}
|
|
2651
5013
|
|
|
2652
5014
|
async save(aggregate: ${name}): Promise<void> {
|
|
2653
|
-
store.set(aggregate.id.value, aggregate)
|
|
5015
|
+
this.store.set(aggregate.id.value, aggregate)
|
|
2654
5016
|
}
|
|
2655
5017
|
|
|
2656
5018
|
async delete(id: Id): Promise<void> {
|
|
2657
|
-
store.delete(id.value)
|
|
5019
|
+
this.store.delete(id.value)
|
|
2658
5020
|
}
|
|
2659
5021
|
|
|
2660
5022
|
async findAll(): Promise<${name}[]> {
|
|
2661
|
-
return Array.from(store.values())
|
|
5023
|
+
return Array.from(this.store.values())
|
|
2662
5024
|
}
|
|
2663
5025
|
|
|
2664
5026
|
async exists(id: Id): Promise<boolean> {
|
|
2665
|
-
return store.has(id.value)
|
|
5027
|
+
return this.store.has(id.value)
|
|
2666
5028
|
}
|
|
2667
5029
|
}
|
|
2668
5030
|
`;
|
|
@@ -2684,6 +5046,37 @@ export class ${name}ServiceProvider extends ServiceProvider {
|
|
|
2684
5046
|
console.log('[${name}] Module loaded')
|
|
2685
5047
|
}
|
|
2686
5048
|
}
|
|
5049
|
+
`;
|
|
5050
|
+
}
|
|
5051
|
+
generateController(name) {
|
|
5052
|
+
return `/**
|
|
5053
|
+
* ${name} Controller
|
|
5054
|
+
*/
|
|
5055
|
+
|
|
5056
|
+
import type { GravitoContext } from '@gravito/core'
|
|
5057
|
+
|
|
5058
|
+
export class ${name}Controller {
|
|
5059
|
+
/**
|
|
5060
|
+
* GET /${name.toLowerCase()}
|
|
5061
|
+
*/
|
|
5062
|
+
async index(ctx: GravitoContext) {
|
|
5063
|
+
return ctx.json({
|
|
5064
|
+
success: true,
|
|
5065
|
+
data: []
|
|
5066
|
+
})
|
|
5067
|
+
}
|
|
5068
|
+
|
|
5069
|
+
/**
|
|
5070
|
+
* GET /${name.toLowerCase()}/:id
|
|
5071
|
+
*/
|
|
5072
|
+
async show(ctx: GravitoContext) {
|
|
5073
|
+
const id = ctx.req.param('id')
|
|
5074
|
+
return ctx.json({
|
|
5075
|
+
success: true,
|
|
5076
|
+
data: { id }
|
|
5077
|
+
})
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
2687
5080
|
`;
|
|
2688
5081
|
}
|
|
2689
5082
|
};
|
|
@@ -2858,24 +5251,39 @@ export class Email extends ValueObject<EmailProps> {
|
|
|
2858
5251
|
|
|
2859
5252
|
import type { DomainEvent } from '@gravito/enterprise'
|
|
2860
5253
|
|
|
2861
|
-
type EventHandler = (event:
|
|
5254
|
+
type EventHandler<T extends DomainEvent = any> = (event: T) => void | Promise<void>
|
|
2862
5255
|
|
|
2863
5256
|
export class EventDispatcher {
|
|
2864
5257
|
private handlers: Map<string, EventHandler[]> = new Map()
|
|
2865
5258
|
|
|
2866
|
-
|
|
5259
|
+
/**
|
|
5260
|
+
* Subscribe to an event
|
|
5261
|
+
*/
|
|
5262
|
+
subscribe<T extends DomainEvent>(eventName: string, handler: EventHandler<T>): void {
|
|
2867
5263
|
const handlers = this.handlers.get(eventName) ?? []
|
|
2868
5264
|
handlers.push(handler)
|
|
2869
5265
|
this.handlers.set(eventName, handlers)
|
|
2870
5266
|
}
|
|
2871
5267
|
|
|
5268
|
+
/**
|
|
5269
|
+
* Dispatch a single event
|
|
5270
|
+
*/
|
|
2872
5271
|
async dispatch(event: DomainEvent): Promise<void> {
|
|
2873
5272
|
const handlers = this.handlers.get(event.eventName) ?? []
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
5273
|
+
const promises = handlers.map(handler => {
|
|
5274
|
+
try {
|
|
5275
|
+
return handler(event)
|
|
5276
|
+
} catch (error) {
|
|
5277
|
+
console.error(\`[EventDispatcher] Error in handler for \${event.eventName}:\`, error)
|
|
5278
|
+
}
|
|
5279
|
+
})
|
|
5280
|
+
|
|
5281
|
+
await Promise.all(promises)
|
|
2877
5282
|
}
|
|
2878
5283
|
|
|
5284
|
+
/**
|
|
5285
|
+
* Dispatch multiple events sequentially
|
|
5286
|
+
*/
|
|
2879
5287
|
async dispatchAll(events: DomainEvent[]): Promise<void> {
|
|
2880
5288
|
for (const event of events) {
|
|
2881
5289
|
await this.dispatch(event)
|
|
@@ -2899,24 +5307,38 @@ export function report(error: unknown): void {
|
|
|
2899
5307
|
// src/generators/DddGenerator.ts
|
|
2900
5308
|
var DddGenerator = class extends BaseGenerator {
|
|
2901
5309
|
moduleGenerator;
|
|
5310
|
+
advancedModuleGenerator;
|
|
5311
|
+
cqrsQueryModuleGenerator;
|
|
2902
5312
|
sharedKernelGenerator;
|
|
2903
5313
|
bootstrapGenerator;
|
|
5314
|
+
moduleType = "simple";
|
|
2904
5315
|
constructor(config) {
|
|
2905
5316
|
super(config);
|
|
2906
5317
|
this.moduleGenerator = new ModuleGenerator();
|
|
5318
|
+
this.advancedModuleGenerator = new AdvancedModuleGenerator();
|
|
5319
|
+
this.cqrsQueryModuleGenerator = new CQRSQueryModuleGenerator();
|
|
2907
5320
|
this.sharedKernelGenerator = new SharedKernelGenerator();
|
|
2908
5321
|
this.bootstrapGenerator = new BootstrapGenerator();
|
|
2909
5322
|
}
|
|
5323
|
+
/**
|
|
5324
|
+
* Set the module type for generated modules
|
|
5325
|
+
* @param type - 'simple' for basic CRUD, 'advanced' for Event Sourcing, 'cqrs-query' for CQRS Read Side
|
|
5326
|
+
* @default 'simple'
|
|
5327
|
+
*/
|
|
5328
|
+
setModuleType(type) {
|
|
5329
|
+
this.moduleType = type;
|
|
5330
|
+
}
|
|
2910
5331
|
get architectureType() {
|
|
2911
5332
|
return "ddd";
|
|
2912
5333
|
}
|
|
2913
5334
|
get displayName() {
|
|
2914
|
-
return
|
|
5335
|
+
return `Domain-Driven Design (DDD)${this.moduleType === "cqrs-query" ? " + CQRS" : ""}`;
|
|
2915
5336
|
}
|
|
2916
5337
|
get description() {
|
|
2917
5338
|
return "Full DDD with Bounded Contexts, Aggregates, and Event-Driven patterns";
|
|
2918
5339
|
}
|
|
2919
5340
|
getDirectoryStructure(context) {
|
|
5341
|
+
const moduleGenerator = this.getModuleGenerator();
|
|
2920
5342
|
return [
|
|
2921
5343
|
this.bootstrapGenerator.generateConfigDirectory(context),
|
|
2922
5344
|
{
|
|
@@ -2932,8 +5354,8 @@ var DddGenerator = class extends BaseGenerator {
|
|
|
2932
5354
|
type: "directory",
|
|
2933
5355
|
name: "Modules",
|
|
2934
5356
|
children: [
|
|
2935
|
-
this.moduleGenerator.generate("Ordering", context),
|
|
2936
|
-
this.moduleGenerator.generate("Catalog", context)
|
|
5357
|
+
moduleGenerator === "cqrs-query" ? this.cqrsQueryModuleGenerator.generate("Ordering", context) : moduleGenerator === "advanced" ? this.advancedModuleGenerator.generate("Ordering", context) : this.moduleGenerator.generate("Ordering", context),
|
|
5358
|
+
moduleGenerator === "cqrs-query" ? this.cqrsQueryModuleGenerator.generate("Catalog", context) : moduleGenerator === "advanced" ? this.advancedModuleGenerator.generate("Catalog", context) : this.moduleGenerator.generate("Catalog", context)
|
|
2937
5359
|
]
|
|
2938
5360
|
},
|
|
2939
5361
|
{
|
|
@@ -2989,6 +5411,12 @@ var DddGenerator = class extends BaseGenerator {
|
|
|
2989
5411
|
}
|
|
2990
5412
|
];
|
|
2991
5413
|
}
|
|
5414
|
+
/**
|
|
5415
|
+
* Get the active module generator type
|
|
5416
|
+
*/
|
|
5417
|
+
getModuleGenerator() {
|
|
5418
|
+
return this.moduleType;
|
|
5419
|
+
}
|
|
2992
5420
|
/**
|
|
2993
5421
|
* Override package.json for DDD architecture (uses main.ts instead of bootstrap.ts)
|
|
2994
5422
|
*/
|
|
@@ -3028,6 +5456,69 @@ var DddGenerator = class extends BaseGenerator {
|
|
|
3028
5456
|
|
|
3029
5457
|
This project follows **Domain-Driven Design (DDD)** with strategic and tactical patterns.
|
|
3030
5458
|
|
|
5459
|
+
${this.moduleType === "cqrs-query" ? `
|
|
5460
|
+
## Module Types
|
|
5461
|
+
|
|
5462
|
+
This project uses **CQRS Query Module Template** with event projection:
|
|
5463
|
+
- **Read Models**: Denormalized, query-optimized data structures
|
|
5464
|
+
- **Event Projectors**: Pure functions transforming events to read models
|
|
5465
|
+
- **Query Services**: Dedicated read-side use cases
|
|
5466
|
+
- **Event Subscribers**: Subscribe to write-side domain events
|
|
5467
|
+
- **Optional Caching**: Dual-tier caching (memory + Redis)
|
|
5468
|
+
|
|
5469
|
+
See each module's Domain/Projectors/{ModuleName}EventProjector.ts for projection patterns.
|
|
5470
|
+
|
|
5471
|
+
## CQRS Architecture
|
|
5472
|
+
|
|
5473
|
+
\`\`\`
|
|
5474
|
+
Write Side (Command) Read Side (Query)
|
|
5475
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
5476
|
+
\u2502 Aggregate Root \u2502 \u2502 Read Model \u2502
|
|
5477
|
+
\u2502 (Event Sourcing) \u2502 \u2502 (Denormalized) \u2502
|
|
5478
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
5479
|
+
\u2502 \u25B2
|
|
5480
|
+
\u2502 Domain Events \u2502
|
|
5481
|
+
\u25BC \u2502
|
|
5482
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
5483
|
+
\u2502 Event Store \u2502\u2500\u2500\u2500\u2500\u2500\u25B6\u2502 Event Subscriber \u2502
|
|
5484
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
5485
|
+
\u2502
|
|
5486
|
+
\u2502 Projects Events
|
|
5487
|
+
\u25BC
|
|
5488
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
5489
|
+
\u2502 Event Projector \u2502
|
|
5490
|
+
\u2502 (Pure Functions) \u2502
|
|
5491
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
5492
|
+
\u2502
|
|
5493
|
+
\u25BC
|
|
5494
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
5495
|
+
\u2502 Read Model DB \u2502
|
|
5496
|
+
\u2502 (Eventually Consistent)
|
|
5497
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
5498
|
+
\`\`\`
|
|
5499
|
+
` : this.moduleType === "advanced" ? `
|
|
5500
|
+
## Module Types
|
|
5501
|
+
|
|
5502
|
+
This project uses **Advanced Module Template** with Event Sourcing:
|
|
5503
|
+
- **Event Sourcing**: Complete event stream as source of truth
|
|
5504
|
+
- **Aggregate Roots**: Domain objects managing state through events
|
|
5505
|
+
- **Domain Events**: Rich, expressive events capturing domain changes
|
|
5506
|
+
- **Event Store**: Persistent event log for state reconstruction
|
|
5507
|
+
- **Event Applier**: Pure functions for immutable state transitions
|
|
5508
|
+
|
|
5509
|
+
See each module's Domain/Services/{ModuleName}EventApplier.ts for event handling patterns.
|
|
5510
|
+
` : `
|
|
5511
|
+
## Module Types
|
|
5512
|
+
|
|
5513
|
+
This project uses **Simple Module Template** with basic CRUD:
|
|
5514
|
+
- **Aggregates**: Standard entity-based domain objects
|
|
5515
|
+
- **Repositories**: Data access abstractions
|
|
5516
|
+
- **Services**: Domain and application logic
|
|
5517
|
+
- **Events**: Optional domain event support
|
|
5518
|
+
|
|
5519
|
+
To upgrade a module to Event Sourcing, use the Advanced template when scaffolding new modules.
|
|
5520
|
+
`}
|
|
5521
|
+
|
|
3031
5522
|
## Service Providers
|
|
3032
5523
|
|
|
3033
5524
|
Service providers are the central place to configure your application and modules. They follow the ServiceProvider pattern with \`register()\` and \`boot()\` lifecycle methods.
|
|
@@ -3058,12 +5549,38 @@ Service providers are the central place to configure your application and module
|
|
|
3058
5549
|
Each bounded context follows this structure:
|
|
3059
5550
|
|
|
3060
5551
|
\`\`\`
|
|
5552
|
+
${this.moduleType === "cqrs-query" ? `
|
|
5553
|
+
Context/ # CQRS Read Side Module
|
|
5554
|
+
\u251C\u2500\u2500 Domain/ # Query domain logic
|
|
5555
|
+
\u2502 \u251C\u2500\u2500 ReadModels/ # Denormalized data structures (immutable)
|
|
5556
|
+
\u2502 \u251C\u2500\u2500 Projectors/ # Pure functions: Event \u2192 ReadModel
|
|
5557
|
+
\u2502 \u2514\u2500\u2500 Repositories/ # Read model access interfaces
|
|
5558
|
+
\u251C\u2500\u2500 Application/ # Query use cases
|
|
5559
|
+
\u2502 \u251C\u2500\u2500 Services/ # Query services (findById, findAll, search, etc.)
|
|
5560
|
+
\u2502 \u2514\u2500\u2500 DTOs/ # Data transfer objects for responses
|
|
5561
|
+
\u251C\u2500\u2500 Infrastructure/ # Data access & external services
|
|
5562
|
+
\u2502 \u251C\u2500\u2500 Repositories/ # Read model implementations (ORM)
|
|
5563
|
+
\u2502 \u251C\u2500\u2500 Subscribers/ # Event subscribers (trigger projections)
|
|
5564
|
+
\u2502 \u2514\u2500\u2500 Cache/ # Optional caching layer (memory + Redis)
|
|
5565
|
+
\u2514\u2500\u2500 Presentation/ # Entry points
|
|
5566
|
+
\u251C\u2500\u2500 Controllers/ # HTTP query endpoints
|
|
5567
|
+
\u2514\u2500\u2500 Routes/ # Route registration
|
|
5568
|
+
\`\`\`
|
|
5569
|
+
|
|
5570
|
+
**Key Differences from Write Side:**
|
|
5571
|
+
- No Commands (read-only)
|
|
5572
|
+
- No EventStore (subscribes to write-side events)
|
|
5573
|
+
- Pure projectors (deterministic, testable)
|
|
5574
|
+
- Read models optimized for specific queries
|
|
5575
|
+
- Eventual consistency (projections may lag behind events)
|
|
5576
|
+
` : `
|
|
3061
5577
|
Context/
|
|
3062
5578
|
\u251C\u2500\u2500 Domain/ # Core business logic
|
|
3063
5579
|
\u2502 \u251C\u2500\u2500 Aggregates/ # Aggregate roots + entities
|
|
3064
5580
|
\u2502 \u251C\u2500\u2500 Events/ # Domain events
|
|
3065
5581
|
\u2502 \u251C\u2500\u2500 Repositories/ # Repository interfaces
|
|
3066
|
-
\u2502 \
|
|
5582
|
+
\u2502 \u251C\u2500\u2500 Services/ # Domain services (${this.moduleType === "advanced" ? "EventApplier for Event Sourcing" : "domain logic"})
|
|
5583
|
+
\u2502 \u2514\u2500\u2500 ValueObjects/ # Domain value objects
|
|
3067
5584
|
\u251C\u2500\u2500 Application/ # Use cases
|
|
3068
5585
|
\u2502 \u251C\u2500\u2500 Commands/ # Write operations
|
|
3069
5586
|
\u2502 \u251C\u2500\u2500 Queries/ # Read operations
|
|
@@ -3071,11 +5588,13 @@ Context/
|
|
|
3071
5588
|
\u2502 \u2514\u2500\u2500 DTOs/ # Data transfer objects
|
|
3072
5589
|
\u251C\u2500\u2500 Infrastructure/ # External concerns
|
|
3073
5590
|
\u2502 \u251C\u2500\u2500 Persistence/ # Repository implementations
|
|
5591
|
+
\u2502 \u251C\u2500\u2500 EventStore/ # ${this.moduleType === "advanced" ? "Event storage and reconstruction" : "(optional) Event storage"}
|
|
3074
5592
|
\u2502 \u2514\u2500\u2500 Providers/ # DI configuration
|
|
3075
5593
|
\u2514\u2500\u2500 UserInterface/ # Entry points
|
|
3076
5594
|
\u251C\u2500\u2500 Http/ # REST controllers
|
|
3077
5595
|
\u2514\u2500\u2500 Cli/ # CLI commands
|
|
3078
5596
|
\`\`\`
|
|
5597
|
+
`}
|
|
3079
5598
|
|
|
3080
5599
|
## SharedKernel
|
|
3081
5600
|
|
|
@@ -3091,6 +5610,10 @@ Contains types shared across contexts:
|
|
|
3091
5610
|
2. **Domain Events**: Inter-context communication
|
|
3092
5611
|
3. **CQRS**: Separate read/write models
|
|
3093
5612
|
4. **Repository Pattern**: Persistence abstraction
|
|
5613
|
+
${this.moduleType === "cqrs-query" ? `5. **Event Projection**: Transforming events to denormalized read models
|
|
5614
|
+
6. **Pure Projectors**: Deterministic, testable event handlers
|
|
5615
|
+
7. **Eventual Consistency**: Read models eventually consistent with events
|
|
5616
|
+
8. **Query Optimization**: Read models optimized for specific use cases` : this.moduleType === "advanced" ? "5. **Event Sourcing**: Event stream as single source of truth\n6. **Event Applier**: Pure functions for state transitions" : ""}
|
|
3094
5617
|
|
|
3095
5618
|
Created with \u2764\uFE0F using Gravito Framework
|
|
3096
5619
|
`;
|
|
@@ -3985,9 +6508,9 @@ var SatelliteGenerator = class extends BaseGenerator {
|
|
|
3985
6508
|
children: [
|
|
3986
6509
|
{
|
|
3987
6510
|
type: "directory",
|
|
3988
|
-
name: "
|
|
6511
|
+
name: "Aggregates",
|
|
3989
6512
|
children: [
|
|
3990
|
-
{ type: "file", name: `${name}.ts`, content: this.
|
|
6513
|
+
{ type: "file", name: `${name}.ts`, content: this.generateAggregate(name) }
|
|
3991
6514
|
]
|
|
3992
6515
|
},
|
|
3993
6516
|
{
|
|
@@ -4001,8 +6524,24 @@ var SatelliteGenerator = class extends BaseGenerator {
|
|
|
4001
6524
|
}
|
|
4002
6525
|
]
|
|
4003
6526
|
},
|
|
4004
|
-
{
|
|
4005
|
-
|
|
6527
|
+
{
|
|
6528
|
+
type: "directory",
|
|
6529
|
+
name: "ValueObjects",
|
|
6530
|
+
children: [
|
|
6531
|
+
{ type: "file", name: `${name}Id.ts`, content: this.generateIdValueObject(name) }
|
|
6532
|
+
]
|
|
6533
|
+
},
|
|
6534
|
+
{
|
|
6535
|
+
type: "directory",
|
|
6536
|
+
name: "Events",
|
|
6537
|
+
children: [
|
|
6538
|
+
{
|
|
6539
|
+
type: "file",
|
|
6540
|
+
name: `${name}Created.ts`,
|
|
6541
|
+
content: this.generateCreatedEvent(name)
|
|
6542
|
+
}
|
|
6543
|
+
]
|
|
6544
|
+
}
|
|
4006
6545
|
]
|
|
4007
6546
|
},
|
|
4008
6547
|
// Application Layer
|
|
@@ -4043,6 +6582,31 @@ var SatelliteGenerator = class extends BaseGenerator {
|
|
|
4043
6582
|
}
|
|
4044
6583
|
]
|
|
4045
6584
|
},
|
|
6585
|
+
// Interface Layer (HTTP/API)
|
|
6586
|
+
{
|
|
6587
|
+
type: "directory",
|
|
6588
|
+
name: "Interface",
|
|
6589
|
+
children: [
|
|
6590
|
+
{
|
|
6591
|
+
type: "directory",
|
|
6592
|
+
name: "Http",
|
|
6593
|
+
children: [
|
|
6594
|
+
{
|
|
6595
|
+
type: "directory",
|
|
6596
|
+
name: "Controllers",
|
|
6597
|
+
children: [
|
|
6598
|
+
{
|
|
6599
|
+
type: "file",
|
|
6600
|
+
name: `${name}Controller.ts`,
|
|
6601
|
+
content: this.generateController(name)
|
|
6602
|
+
}
|
|
6603
|
+
]
|
|
6604
|
+
},
|
|
6605
|
+
{ type: "directory", name: "Middleware", children: [] }
|
|
6606
|
+
]
|
|
6607
|
+
}
|
|
6608
|
+
]
|
|
6609
|
+
},
|
|
4046
6610
|
// Entry Point
|
|
4047
6611
|
{ type: "file", name: "index.ts", content: this.generateEntryPoint(name) },
|
|
4048
6612
|
{
|
|
@@ -4060,13 +6624,12 @@ var SatelliteGenerator = class extends BaseGenerator {
|
|
|
4060
6624
|
{
|
|
4061
6625
|
type: "file",
|
|
4062
6626
|
name: "unit.test.ts",
|
|
4063
|
-
content:
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
});`
|
|
6627
|
+
content: this.generateUnitTest(name)
|
|
6628
|
+
},
|
|
6629
|
+
{
|
|
6630
|
+
type: "file",
|
|
6631
|
+
name: "integration.test.ts",
|
|
6632
|
+
content: this.generateIntegrationTest(name)
|
|
4070
6633
|
}
|
|
4071
6634
|
]
|
|
4072
6635
|
}
|
|
@@ -4075,35 +6638,76 @@ describe("${name}", () => {
|
|
|
4075
6638
|
// ─────────────────────────────────────────────────────────────
|
|
4076
6639
|
// Domain Templates
|
|
4077
6640
|
// ─────────────────────────────────────────────────────────────
|
|
4078
|
-
|
|
4079
|
-
return `import {
|
|
6641
|
+
generateIdValueObject(name) {
|
|
6642
|
+
return `import { ValueObject } from '@gravito/enterprise'
|
|
6643
|
+
|
|
6644
|
+
interface IdProps {
|
|
6645
|
+
value: string
|
|
6646
|
+
}
|
|
6647
|
+
|
|
6648
|
+
export class ${name}Id extends ValueObject<IdProps> {
|
|
6649
|
+
constructor(value: string) {
|
|
6650
|
+
super({ value })
|
|
6651
|
+
}
|
|
6652
|
+
|
|
6653
|
+
static create(): ${name}Id {
|
|
6654
|
+
return new ${name}Id(crypto.randomUUID())
|
|
6655
|
+
}
|
|
6656
|
+
|
|
6657
|
+
get value(): string { return this.props.value }
|
|
6658
|
+
}
|
|
6659
|
+
`;
|
|
6660
|
+
}
|
|
6661
|
+
generateAggregate(name) {
|
|
6662
|
+
return `import { AggregateRoot } from '@gravito/enterprise'
|
|
6663
|
+
import { ${name}Id } from '../ValueObjects/${name}Id'
|
|
6664
|
+
import { ${name}Created } from '../Events/${name}Created'
|
|
4080
6665
|
|
|
4081
6666
|
export interface ${name}Props {
|
|
4082
6667
|
name: string
|
|
4083
6668
|
createdAt: Date
|
|
4084
6669
|
}
|
|
4085
6670
|
|
|
4086
|
-
export class ${name} extends
|
|
4087
|
-
constructor(id:
|
|
6671
|
+
export class ${name} extends AggregateRoot<${name}Id> {
|
|
6672
|
+
constructor(id: ${name}Id, private props: ${name}Props) {
|
|
4088
6673
|
super(id)
|
|
4089
6674
|
}
|
|
4090
6675
|
|
|
4091
|
-
static create(id:
|
|
4092
|
-
|
|
6676
|
+
static create(id: ${name}Id, name: string): ${name} {
|
|
6677
|
+
const aggregate = new ${name}(id, {
|
|
4093
6678
|
name,
|
|
4094
6679
|
createdAt: new Date()
|
|
4095
6680
|
})
|
|
6681
|
+
|
|
6682
|
+
aggregate.addDomainEvent(new ${name}Created(id.value))
|
|
6683
|
+
|
|
6684
|
+
return aggregate
|
|
4096
6685
|
}
|
|
4097
6686
|
|
|
4098
6687
|
get name() { return this.props.name }
|
|
4099
6688
|
}
|
|
6689
|
+
`;
|
|
6690
|
+
}
|
|
6691
|
+
generateCreatedEvent(name) {
|
|
6692
|
+
return `import { DomainEvent } from '@gravito/enterprise'
|
|
6693
|
+
|
|
6694
|
+
export class ${name}Created extends DomainEvent {
|
|
6695
|
+
constructor(public readonly aggregateId: string) {
|
|
6696
|
+
super()
|
|
6697
|
+
}
|
|
6698
|
+
|
|
6699
|
+
get eventName(): string {
|
|
6700
|
+
return '${this.context?.nameKebabCase}.created'
|
|
6701
|
+
}
|
|
6702
|
+
}
|
|
4100
6703
|
`;
|
|
4101
6704
|
}
|
|
4102
6705
|
generateRepositoryInterface(name) {
|
|
4103
6706
|
return `import { Repository } from '@gravito/enterprise'
|
|
4104
|
-
import { ${name} } from '../
|
|
6707
|
+
import { ${name} } from '../Aggregates/${name}'
|
|
6708
|
+
import { ${name}Id } from '../ValueObjects/${name}Id'
|
|
4105
6709
|
|
|
4106
|
-
export interface I${name}Repository extends Repository<${name},
|
|
6710
|
+
export interface I${name}Repository extends Repository<${name}, ${name}Id> {
|
|
4107
6711
|
// Add custom methods here
|
|
4108
6712
|
}
|
|
4109
6713
|
`;
|
|
@@ -4114,7 +6718,8 @@ export interface I${name}Repository extends Repository<${name}, string> {
|
|
|
4114
6718
|
generateUseCase(name) {
|
|
4115
6719
|
return `import { UseCase } from '@gravito/enterprise'
|
|
4116
6720
|
import { I${name}Repository } from '../../Domain/Contracts/I${name}Repository'
|
|
4117
|
-
import { ${name} } from '../../Domain/
|
|
6721
|
+
import { ${name} } from '../../Domain/Aggregates/${name}'
|
|
6722
|
+
import { ${name}Id } from '../../Domain/ValueObjects/${name}Id'
|
|
4118
6723
|
|
|
4119
6724
|
export interface Create${name}Input {
|
|
4120
6725
|
name: string
|
|
@@ -4126,12 +6731,12 @@ export class Create${name} extends UseCase<Create${name}Input, string> {
|
|
|
4126
6731
|
}
|
|
4127
6732
|
|
|
4128
6733
|
async execute(input: Create${name}Input): Promise<string> {
|
|
4129
|
-
const id =
|
|
6734
|
+
const id = ${name}Id.create()
|
|
4130
6735
|
const entity = ${name}.create(id, input.name)
|
|
4131
6736
|
|
|
4132
6737
|
await this.repository.save(entity)
|
|
4133
6738
|
|
|
4134
|
-
return id
|
|
6739
|
+
return id.value
|
|
4135
6740
|
}
|
|
4136
6741
|
}
|
|
4137
6742
|
`;
|
|
@@ -4141,17 +6746,16 @@ export class Create${name} extends UseCase<Create${name}Input, string> {
|
|
|
4141
6746
|
// ─────────────────────────────────────────────────────────────
|
|
4142
6747
|
generateAtlasRepository(name) {
|
|
4143
6748
|
return `import { I${name}Repository } from '../../Domain/Contracts/I${name}Repository'
|
|
4144
|
-
import { ${name} } from '../../Domain/
|
|
4145
|
-
import {
|
|
6749
|
+
import { ${name} } from '../../Domain/Aggregates/${name}'
|
|
6750
|
+
import { ${name}Id } from '../../Domain/ValueObjects/${name}Id'
|
|
4146
6751
|
|
|
4147
6752
|
export class Atlas${name}Repository implements I${name}Repository {
|
|
4148
6753
|
async save(entity: ${name}): Promise<void> {
|
|
4149
|
-
//
|
|
4150
|
-
console.log('[Atlas] Saving
|
|
4151
|
-
// await DB.table('${name.toLowerCase()}s').insert({ ... })
|
|
6754
|
+
// Implementation using @gravito/atlas
|
|
6755
|
+
console.log('[Atlas] Saving aggregate:', entity.id.value)
|
|
4152
6756
|
}
|
|
4153
6757
|
|
|
4154
|
-
async findById(id:
|
|
6758
|
+
async findById(id: ${name}Id): Promise<${name} | null> {
|
|
4155
6759
|
return null
|
|
4156
6760
|
}
|
|
4157
6761
|
|
|
@@ -4159,12 +6763,77 @@ export class Atlas${name}Repository implements I${name}Repository {
|
|
|
4159
6763
|
return []
|
|
4160
6764
|
}
|
|
4161
6765
|
|
|
4162
|
-
async delete(id:
|
|
6766
|
+
async delete(id: ${name}Id): Promise<void> {}
|
|
4163
6767
|
|
|
4164
|
-
async exists(id:
|
|
6768
|
+
async exists(id: ${name}Id): Promise<boolean> {
|
|
4165
6769
|
return false
|
|
4166
6770
|
}
|
|
4167
6771
|
}
|
|
6772
|
+
`;
|
|
6773
|
+
}
|
|
6774
|
+
// ─────────────────────────────────────────────────────────────
|
|
6775
|
+
// Test Templates
|
|
6776
|
+
// ─────────────────────────────────────────────────────────────
|
|
6777
|
+
generateUnitTest(name) {
|
|
6778
|
+
return `import { describe, it, expect } from "bun:test";
|
|
6779
|
+
import { ${name} } from "../src/Domain/Aggregates/${name}";
|
|
6780
|
+
import { ${name}Id } from "../src/Domain/ValueObjects/${name}Id";
|
|
6781
|
+
|
|
6782
|
+
describe("${name} Aggregate", () => {
|
|
6783
|
+
it("should create a new aggregate with a domain event", () => {
|
|
6784
|
+
const id = ${name}Id.create();
|
|
6785
|
+
const aggregate = ${name}.create(id, "Test Name");
|
|
6786
|
+
|
|
6787
|
+
expect(aggregate.id).toBe(id);
|
|
6788
|
+
expect(aggregate.name).toBe("Test Name");
|
|
6789
|
+
expect(aggregate.pullDomainEvents()).toHaveLength(1);
|
|
6790
|
+
});
|
|
6791
|
+
});`;
|
|
6792
|
+
}
|
|
6793
|
+
generateIntegrationTest(name) {
|
|
6794
|
+
return `import { describe, it, expect, beforeAll } from "bun:test";
|
|
6795
|
+
import { PlanetCore } from "@gravito/core";
|
|
6796
|
+
|
|
6797
|
+
describe("${name} Integration", () => {
|
|
6798
|
+
let core: PlanetCore;
|
|
6799
|
+
|
|
6800
|
+
beforeAll(async () => {
|
|
6801
|
+
core = new PlanetCore();
|
|
6802
|
+
// Setup dependencies here
|
|
6803
|
+
});
|
|
6804
|
+
|
|
6805
|
+
it("should handle the creation flow", async () => {
|
|
6806
|
+
expect(true).toBe(true); // Placeholder for actual integration logic
|
|
6807
|
+
});
|
|
6808
|
+
});`;
|
|
6809
|
+
}
|
|
6810
|
+
// ─────────────────────────────────────────────────────────────
|
|
6811
|
+
// Interface Templates
|
|
6812
|
+
// ─────────────────────────────────────────────────────────────
|
|
6813
|
+
generateController(name) {
|
|
6814
|
+
return `import type { GravitoContext } from '@gravito/core'
|
|
6815
|
+
import { Create${name} } from '../../../Application/UseCases/Create${name}'
|
|
6816
|
+
|
|
6817
|
+
export class ${name}Controller {
|
|
6818
|
+
constructor(private createUseCase: Create${name}) {}
|
|
6819
|
+
|
|
6820
|
+
async store(ctx: GravitoContext) {
|
|
6821
|
+
const body = await ctx.req.json()
|
|
6822
|
+
const id = await this.createUseCase.execute({ name: body.name })
|
|
6823
|
+
|
|
6824
|
+
return ctx.json({
|
|
6825
|
+
success: true,
|
|
6826
|
+
data: { id }
|
|
6827
|
+
}, 201)
|
|
6828
|
+
}
|
|
6829
|
+
|
|
6830
|
+
async index(ctx: GravitoContext) {
|
|
6831
|
+
return ctx.json({
|
|
6832
|
+
success: true,
|
|
6833
|
+
data: []
|
|
6834
|
+
})
|
|
6835
|
+
}
|
|
6836
|
+
}
|
|
4168
6837
|
`;
|
|
4169
6838
|
}
|
|
4170
6839
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -4173,17 +6842,22 @@ export class Atlas${name}Repository implements I${name}Repository {
|
|
|
4173
6842
|
generateEntryPoint(name) {
|
|
4174
6843
|
return `import { ServiceProvider, type Container } from '@gravito/core'
|
|
4175
6844
|
import { Atlas${name}Repository } from './Infrastructure/Persistence/Atlas${name}Repository'
|
|
6845
|
+
import { Create${name} } from './Application/UseCases/Create${name}'
|
|
6846
|
+
import { ${name}Controller } from './Interface/Http/Controllers/${name}Controller'
|
|
4176
6847
|
|
|
4177
6848
|
export class ${name}ServiceProvider extends ServiceProvider {
|
|
4178
6849
|
register(container: Container): void {
|
|
4179
|
-
// Bind Repository
|
|
6850
|
+
// 1. Bind Repository (Infrastructure)
|
|
4180
6851
|
container.singleton('${name.toLowerCase()}.repo', () => new Atlas${name}Repository())
|
|
4181
6852
|
|
|
4182
|
-
// Bind UseCases
|
|
4183
|
-
container.singleton('${name.toLowerCase()}.create', () => {
|
|
4184
|
-
return new
|
|
4185
|
-
|
|
4186
|
-
|
|
6853
|
+
// 2. Bind UseCases (Application)
|
|
6854
|
+
container.singleton('${name.toLowerCase()}.usecase.create', (c) => {
|
|
6855
|
+
return new Create${name}(c.make('${name.toLowerCase()}.repo'))
|
|
6856
|
+
})
|
|
6857
|
+
|
|
6858
|
+
// 3. Bind Controllers (Interface)
|
|
6859
|
+
container.singleton('${name.toLowerCase()}.controller', (c) => {
|
|
6860
|
+
return new ${name}Controller(c.make('${name.toLowerCase()}.usecase.create'))
|
|
4187
6861
|
})
|
|
4188
6862
|
}
|
|
4189
6863
|
|
|
@@ -4294,7 +6968,9 @@ var ProfileResolver = class _ProfileResolver {
|
|
|
4294
6968
|
storage: "local",
|
|
4295
6969
|
session: "file"
|
|
4296
6970
|
},
|
|
4297
|
-
features: []
|
|
6971
|
+
features: [],
|
|
6972
|
+
workers: "basic"
|
|
6973
|
+
// Basic workers configuration for core profile
|
|
4298
6974
|
},
|
|
4299
6975
|
scale: {
|
|
4300
6976
|
drivers: {
|
|
@@ -4304,7 +6980,9 @@ var ProfileResolver = class _ProfileResolver {
|
|
|
4304
6980
|
storage: "s3",
|
|
4305
6981
|
session: "redis"
|
|
4306
6982
|
},
|
|
4307
|
-
features: ["stream", "nebula"]
|
|
6983
|
+
features: ["stream", "nebula"],
|
|
6984
|
+
workers: "advanced"
|
|
6985
|
+
// Advanced workers with Bun optimizations
|
|
4308
6986
|
},
|
|
4309
6987
|
enterprise: {
|
|
4310
6988
|
drivers: {
|
|
@@ -4314,14 +6992,17 @@ var ProfileResolver = class _ProfileResolver {
|
|
|
4314
6992
|
storage: "s3",
|
|
4315
6993
|
session: "redis"
|
|
4316
6994
|
},
|
|
4317
|
-
features: ["stream", "nebula", "monitor", "sentinel", "fortify"]
|
|
6995
|
+
features: ["stream", "nebula", "monitor", "sentinel", "fortify"],
|
|
6996
|
+
workers: "production"
|
|
6997
|
+
// Production-optimized workers
|
|
4318
6998
|
}
|
|
4319
6999
|
};
|
|
4320
7000
|
resolve(profile = "core", withFeatures = []) {
|
|
4321
7001
|
const base = _ProfileResolver.DEFAULTS[profile] || _ProfileResolver.DEFAULTS.core;
|
|
4322
7002
|
const config = {
|
|
4323
7003
|
drivers: { ...base.drivers },
|
|
4324
|
-
features: [...base.features]
|
|
7004
|
+
features: [...base.features],
|
|
7005
|
+
workers: base.workers
|
|
4325
7006
|
};
|
|
4326
7007
|
for (const feature of withFeatures) {
|
|
4327
7008
|
this.applyFeature(config, feature);
|
|
@@ -4389,6 +7070,7 @@ var ProfileResolver = class _ProfileResolver {
|
|
|
4389
7070
|
|
|
4390
7071
|
// src/Scaffold.ts
|
|
4391
7072
|
var import_node_path6 = __toESM(require("path"), 1);
|
|
7073
|
+
var import_node_url = require("url");
|
|
4392
7074
|
|
|
4393
7075
|
// src/generators/ActionDomainGenerator.ts
|
|
4394
7076
|
var ActionDomainGenerator = class extends BaseGenerator {
|
|
@@ -5062,11 +7744,16 @@ bun start
|
|
|
5062
7744
|
};
|
|
5063
7745
|
|
|
5064
7746
|
// src/Scaffold.ts
|
|
5065
|
-
var
|
|
7747
|
+
var import_meta = {};
|
|
7748
|
+
var Scaffold = class _Scaffold {
|
|
5066
7749
|
templatesDir;
|
|
5067
7750
|
verbose;
|
|
7751
|
+
static packageDir = import_node_path6.default.resolve(
|
|
7752
|
+
import_node_path6.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url)),
|
|
7753
|
+
".."
|
|
7754
|
+
);
|
|
5068
7755
|
constructor(options = {}) {
|
|
5069
|
-
this.templatesDir = options.templatesDir ?? import_node_path6.default.resolve(
|
|
7756
|
+
this.templatesDir = options.templatesDir ?? import_node_path6.default.resolve(_Scaffold.packageDir, "templates");
|
|
5070
7757
|
this.verbose = options.verbose ?? false;
|
|
5071
7758
|
}
|
|
5072
7759
|
/**
|
|
@@ -5118,7 +7805,7 @@ var Scaffold = class {
|
|
|
5118
7805
|
* @returns {Promise<ScaffoldResult>}
|
|
5119
7806
|
*/
|
|
5120
7807
|
async create(options) {
|
|
5121
|
-
const generator = this.createGenerator(options
|
|
7808
|
+
const generator = this.createGenerator(options);
|
|
5122
7809
|
const fs5 = await import("fs/promises");
|
|
5123
7810
|
const profileResolver = new ProfileResolver();
|
|
5124
7811
|
const profileConfig = profileResolver.resolve(options.profile, options.features);
|
|
@@ -5164,29 +7851,39 @@ var Scaffold = class {
|
|
|
5164
7851
|
}
|
|
5165
7852
|
}
|
|
5166
7853
|
/**
|
|
5167
|
-
* Create a generator for the specified architecture.
|
|
7854
|
+
* Create a generator for the specified architecture with options.
|
|
7855
|
+
* @param options - Scaffold options including architecture type and module type for DDD
|
|
5168
7856
|
*/
|
|
5169
|
-
createGenerator(
|
|
7857
|
+
createGenerator(options) {
|
|
5170
7858
|
const config = {
|
|
5171
7859
|
templatesDir: this.templatesDir,
|
|
5172
7860
|
verbose: this.verbose
|
|
5173
7861
|
};
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
7862
|
+
const type = typeof options === "string" ? options : options.architecture;
|
|
7863
|
+
const generator = (() => {
|
|
7864
|
+
switch (type) {
|
|
7865
|
+
case "enterprise-mvc":
|
|
7866
|
+
return new EnterpriseMvcGenerator(config);
|
|
7867
|
+
case "clean":
|
|
7868
|
+
return new CleanArchitectureGenerator(config);
|
|
7869
|
+
case "ddd": {
|
|
7870
|
+
const dddGen = new DddGenerator(config);
|
|
7871
|
+
if (typeof options !== "string" && options.dddModuleType) {
|
|
7872
|
+
dddGen.setModuleType(options.dddModuleType);
|
|
7873
|
+
}
|
|
7874
|
+
return dddGen;
|
|
7875
|
+
}
|
|
7876
|
+
case "action-domain":
|
|
7877
|
+
return new ActionDomainGenerator(config);
|
|
7878
|
+
case "satellite":
|
|
7879
|
+
return new SatelliteGenerator(config);
|
|
7880
|
+
case "standalone-engine":
|
|
7881
|
+
return new StandaloneEngineGenerator(config);
|
|
7882
|
+
default:
|
|
7883
|
+
throw new Error(`Unknown architecture type: ${type}`);
|
|
7884
|
+
}
|
|
7885
|
+
})();
|
|
7886
|
+
return generator;
|
|
5190
7887
|
}
|
|
5191
7888
|
/**
|
|
5192
7889
|
* Generate a single module (for DDD bounded context).
|
|
@@ -5203,7 +7900,9 @@ var Scaffold = class {
|
|
|
5203
7900
|
};
|
|
5204
7901
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5205
7902
|
0 && (module.exports = {
|
|
7903
|
+
AdvancedModuleGenerator,
|
|
5206
7904
|
BaseGenerator,
|
|
7905
|
+
CQRSQueryModuleGenerator,
|
|
5207
7906
|
CleanArchitectureGenerator,
|
|
5208
7907
|
DddGenerator,
|
|
5209
7908
|
DependencyValidator,
|