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