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