@gravito/scaffold 4.0.0 → 4.1.2

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