@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 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/BootstrapGenerator.ts
2183
- var BootstrapGenerator = class {
2184
- context = null;
2185
- generate(context) {
2186
- this.context = context;
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: "Bootstrap",
2204
+ name: moduleName,
2190
2205
  children: [
2191
- { type: "file", name: "app.ts", content: this.generateBootstrapApp(context) },
2192
- { type: "file", name: "providers.ts", content: this.generateProvidersRegistry(context) },
2193
- { type: "file", name: "events.ts", content: this.generateEventsRegistry() },
2194
- { type: "file", name: "routes.ts", content: this.generateRoutesRegistry(context) }
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
- generateConfigDirectory(context) {
2199
- this.context = context;
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
- type: "directory",
2202
- name: "config",
2203
- children: [
2204
- { type: "file", name: "app.ts", content: this.generateAppConfig(context) },
2205
- { type: "file", name: "database.ts", content: this.generateDatabaseConfig() },
2206
- { type: "file", name: "modules.ts", content: this.generateModulesConfig() },
2207
- { type: "file", name: "cache.ts", content: this.generateCacheConfig() },
2208
- { type: "file", name: "logging.ts", content: this.generateLoggingConfig() }
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
- generateMainEntry(_context) {
4294
+ }
4295
+ `;
4296
+ }
4297
+ /**
4298
+ * 生成事件訂閱器
4299
+ */
4300
+ generateSubscriber(moduleName) {
2213
4301
  return `/**
2214
- * Application Entry Point
4302
+ * ${moduleName}ProjectionSubscriber - \u6295\u5F71\u4E8B\u4EF6\u8A02\u95B1\u5668
2215
4303
  *
2216
- * Start the HTTP server.
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 { createApp } from './Bootstrap/app'
4314
+ import type { DomainEvent } from '@/Shared/Domain/DomainEvent'
4315
+ import { ${moduleName}EventProjector } from '../../Domain/Projectors/${moduleName}EventProjector'
2220
4316
 
2221
- const app = await createApp()
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
- export default app.liftoff()
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
- generateBootstrapApp(_context) {
4353
+ /**
4354
+ * 生成快取層
4355
+ */
4356
+ generateCache(moduleName) {
2227
4357
  return `/**
2228
- * Application Bootstrap
2229
- *
2230
- * Central configuration and initialization using the ServiceProvider pattern.
4358
+ * ${moduleName}ReadModelCache - \u8B80\u6A21\u578B\u5FEB\u53D6\u5C64
2231
4359
  *
2232
- * Lifecycle:
2233
- * 1. Configure: Load app config and orbits
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 { defineConfig, PlanetCore } from '@gravito/core'
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
- export async function createApp(): Promise<PlanetCore> {
2246
- // 1. Configure
2247
- const config = defineConfig({
2248
- config: appConfig,
2249
- orbits: [
2250
- new OrbitAtlas() as unknown as import('@gravito/core').GravitoOrbit,
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
- // 2. Boot Core
2255
- const core = await PlanetCore.boot(config)
2256
- core.registerGlobalErrorHandlers()
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
- // 3. Register Providers
2259
- await registerProviders(core)
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
- // 4. Bootstrap All Providers
2262
- await core.bootstrap()
4390
+ return null
4391
+ }
2263
4392
 
2264
- // Register routes after bootstrap
2265
- registerRoutes(core.router)
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
- return core
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
- generateProvidersRegistry(_context) {
4434
+ /**
4435
+ * 生成 HTTP 控制器
4436
+ */
4437
+ generateController(moduleName) {
4438
+ const kebabName = this.toKebabCase(moduleName);
2272
4439
  return `/**
2273
- * Service Providers Registry
4440
+ * ${moduleName}QueryController - \u67E5\u8A62\u7AEF\u9EDE\u63A7\u5236\u5668
2274
4441
  *
2275
- * Register all service providers here.
2276
- * Include both global and module-specific providers.
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
- ServiceProvider,
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
- * Middleware Provider - Global middleware registration
4450
+ * ${moduleName} \u67E5\u8A62\u63A7\u5236\u5668
2291
4451
  */
2292
- export class MiddlewareProvider extends ServiceProvider {
2293
- register(_container: Container): void {}
4452
+ export class ${moduleName}QueryController {
4453
+ constructor(private queryService: Query${moduleName}Service) {}
2294
4454
 
2295
- boot(core: PlanetCore): void {
2296
- const isDev = process.env.NODE_ENV !== 'production'
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
- core.adapter.use('*', securityHeaders({
2299
- contentSecurityPolicy: isDev ? false : undefined,
2300
- }))
4463
+ if (!id) {
4464
+ ctx.response.status = 400
4465
+ ctx.response.json({ error: 'ID is required' })
4466
+ return
4467
+ }
2301
4468
 
2302
- core.adapter.use('*', bodySizeLimit(10 * 1024 * 1024))
4469
+ const dto = await this.queryService.findById(id)
2303
4470
 
2304
- core.logger.info('\u{1F6E1}\uFE0F Global middleware registered')
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
- export async function registerProviders(core: PlanetCore): Promise<void> {
2309
- // Global Providers
2310
- core.register(new MiddlewareProvider())
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
- // Module Providers
2313
- core.register(new OrderingServiceProvider())
2314
- core.register(new CatalogServiceProvider())
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
- // Add more providers as needed
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
- import { EventDispatcher } from '../Shared/Infrastructure/EventBus/EventDispatcher'
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
- export function registerEvents(dispatcher: EventDispatcher): void {
2330
- // Register event handlers
2331
- // dispatcher.subscribe('ordering.created', async (event) => { ... })
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
- generateRoutesRegistry(_context) {
4559
+ /**
4560
+ * 生成路由檔案
4561
+ */
4562
+ generateRoutes(moduleName) {
4563
+ const kebabName = this.toKebabCase(moduleName);
4564
+ const functionName = `register${moduleName}QueryRoutes`;
2336
4565
  return `/**
2337
- * Routes Registry
4566
+ * ${kebabName}.routes.ts - ${moduleName} \u67E5\u8A62\u8DEF\u7531
2338
4567
  *
2339
- * Register all module routes here.
4568
+ * \u8A3B\u518A ${moduleName} \u67E5\u8A62\u7AEF\u9EDE\u7684\u8DEF\u7531
2340
4569
  */
2341
4570
 
2342
- export function registerRoutes(router: any): void {
2343
- // Health check
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
- // Catalog module
2351
- router.get('/api/products', (c: any) => c.json({ message: 'Product list' }))
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
- generateModulesConfig() {
4598
+ /**
4599
+ * 生成模組索引檔案
4600
+ */
4601
+ generateIndex(moduleName) {
2356
4602
  return `/**
2357
- * Modules Configuration
4603
+ * ${moduleName} Query Module
2358
4604
  *
2359
- * Define module boundaries and their dependencies.
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
- export default {
2363
- modules: {
2364
- ordering: {
2365
- name: 'Ordering',
2366
- description: 'Order management module',
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
- // Module dependencies
2377
- dependencies: {
2378
- ordering: ['catalog'], // Ordering depends on Catalog
2379
- },
2380
- }
2381
- `;
2382
- }
2383
- generateAppConfig(context) {
2384
- return `export default {
2385
- name: process.env.APP_NAME ?? '${context.name}',
2386
- env: process.env.APP_ENV ?? 'development',
2387
- port: Number.parseInt(process.env.PORT ?? '3000', 10),
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
- generateDatabaseConfig() {
2395
- const driver = this.context?.profileConfig?.drivers?.database ?? "none";
2396
- return ConfigGenerator.generateDatabaseConfig(driver);
2397
- }
2398
- generateCacheConfig() {
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
- generateLoggingConfig() {
2406
- return `export default {
2407
- default: 'console',
2408
- channels: { console: { driver: 'console', level: 'debug' } },
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 "Domain-Driven Design (DDD)";
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 \u2514\u2500\u2500 Services/ # Domain services
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 Scaffold = class {
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(__dirname, "../templates");
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.architecture);
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(type) {
7857
+ createGenerator(options) {
5507
7858
  const config = {
5508
7859
  templatesDir: this.templatesDir,
5509
7860
  verbose: this.verbose
5510
7861
  };
5511
- switch (type) {
5512
- case "enterprise-mvc":
5513
- return new EnterpriseMvcGenerator(config);
5514
- case "clean":
5515
- return new CleanArchitectureGenerator(config);
5516
- case "ddd":
5517
- return new DddGenerator(config);
5518
- case "action-domain":
5519
- return new ActionDomainGenerator(config);
5520
- case "satellite":
5521
- return new SatelliteGenerator(config);
5522
- case "standalone-engine":
5523
- return new StandaloneEngineGenerator(config);
5524
- default:
5525
- throw new Error(`Unknown architecture type: ${type}`);
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,