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