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