@gravito/scaffold 1.0.0-beta.1
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/README.md +152 -0
- package/dist/index.cjs +3269 -0
- package/dist/index.d.cts +462 -0
- package/dist/index.d.ts +462 -0
- package/dist/index.js +3227 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3227 @@
|
|
|
1
|
+
// src/generators/BaseGenerator.ts
|
|
2
|
+
import fs2 from "fs/promises";
|
|
3
|
+
import path2 from "path";
|
|
4
|
+
|
|
5
|
+
// src/generators/StubGenerator.ts
|
|
6
|
+
import fs from "fs/promises";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import Handlebars from "handlebars";
|
|
9
|
+
var StubGenerator = class {
|
|
10
|
+
config;
|
|
11
|
+
handlebars;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.handlebars = Handlebars.create();
|
|
15
|
+
this.registerBuiltinHelpers();
|
|
16
|
+
if (config.helpers) {
|
|
17
|
+
for (const [name, helper] of Object.entries(config.helpers)) {
|
|
18
|
+
this.handlebars.registerHelper(name, helper);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Register built-in Handlebars helpers.
|
|
24
|
+
*/
|
|
25
|
+
registerBuiltinHelpers() {
|
|
26
|
+
this.handlebars.registerHelper("capitalize", (str) => {
|
|
27
|
+
if (!str) {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
31
|
+
});
|
|
32
|
+
this.handlebars.registerHelper("lowercase", (str) => {
|
|
33
|
+
return str?.toLowerCase() ?? "";
|
|
34
|
+
});
|
|
35
|
+
this.handlebars.registerHelper("uppercase", (str) => {
|
|
36
|
+
return str?.toUpperCase() ?? "";
|
|
37
|
+
});
|
|
38
|
+
this.handlebars.registerHelper("camelCase", (str) => {
|
|
39
|
+
if (!str) {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toLowerCase());
|
|
43
|
+
});
|
|
44
|
+
this.handlebars.registerHelper("pascalCase", (str) => {
|
|
45
|
+
if (!str) {
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase());
|
|
49
|
+
});
|
|
50
|
+
this.handlebars.registerHelper("snakeCase", (str) => {
|
|
51
|
+
if (!str) {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
return str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "").replace(/[-\s]+/g, "_");
|
|
55
|
+
});
|
|
56
|
+
this.handlebars.registerHelper("kebabCase", (str) => {
|
|
57
|
+
if (!str) {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
return str.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "").replace(/[_\s]+/g, "-");
|
|
61
|
+
});
|
|
62
|
+
this.handlebars.registerHelper("pluralize", (str) => {
|
|
63
|
+
if (!str) {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
if (str.endsWith("y")) {
|
|
67
|
+
return `${str.slice(0, -1)}ies`;
|
|
68
|
+
}
|
|
69
|
+
if (str.endsWith("s") || str.endsWith("x") || str.endsWith("ch") || str.endsWith("sh")) {
|
|
70
|
+
return `${str}es`;
|
|
71
|
+
}
|
|
72
|
+
return `${str}s`;
|
|
73
|
+
});
|
|
74
|
+
this.handlebars.registerHelper("date", (format) => {
|
|
75
|
+
const now = /* @__PURE__ */ new Date();
|
|
76
|
+
if (format === "iso") {
|
|
77
|
+
return now.toISOString();
|
|
78
|
+
}
|
|
79
|
+
if (format === "year") {
|
|
80
|
+
return now.getFullYear().toString();
|
|
81
|
+
}
|
|
82
|
+
return now.toISOString().split("T")[0];
|
|
83
|
+
});
|
|
84
|
+
this.handlebars.registerHelper("eq", (a, b) => a === b);
|
|
85
|
+
this.handlebars.registerHelper("neq", (a, b) => a !== b);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Generate a file from a stub template.
|
|
89
|
+
*
|
|
90
|
+
* @param stubName - Name of the stub file (relative to stubsDir)
|
|
91
|
+
* @param outputPath - Output path (relative to outputDir)
|
|
92
|
+
* @param variables - Template variables
|
|
93
|
+
* @returns Path to the generated file
|
|
94
|
+
*/
|
|
95
|
+
async generate(stubName, outputPath, variables = {}) {
|
|
96
|
+
const stubPath = path.resolve(this.config.stubsDir, stubName);
|
|
97
|
+
const template = await fs.readFile(stubPath, "utf-8");
|
|
98
|
+
const compiled = this.handlebars.compile(template);
|
|
99
|
+
const content = compiled({
|
|
100
|
+
...this.config.defaultVariables,
|
|
101
|
+
...variables
|
|
102
|
+
});
|
|
103
|
+
const fullOutputPath = path.resolve(this.config.outputDir, outputPath);
|
|
104
|
+
await fs.mkdir(path.dirname(fullOutputPath), { recursive: true });
|
|
105
|
+
await fs.writeFile(fullOutputPath, content, "utf-8");
|
|
106
|
+
return fullOutputPath;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Generate multiple files from a stub template.
|
|
110
|
+
*
|
|
111
|
+
* @param stubName - Name of the stub file
|
|
112
|
+
* @param outputs - Array of [outputPath, variables] tuples
|
|
113
|
+
* @returns Array of generated file paths
|
|
114
|
+
*/
|
|
115
|
+
async generateMany(stubName, outputs) {
|
|
116
|
+
const results = [];
|
|
117
|
+
for (const [outputPath, variables] of outputs) {
|
|
118
|
+
const result = await this.generate(stubName, outputPath, variables);
|
|
119
|
+
results.push(result);
|
|
120
|
+
}
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Render a template string directly.
|
|
125
|
+
*
|
|
126
|
+
* @param template - Template string
|
|
127
|
+
* @param variables - Template variables
|
|
128
|
+
* @returns Rendered content
|
|
129
|
+
*/
|
|
130
|
+
render(template, variables = {}) {
|
|
131
|
+
const compiled = this.handlebars.compile(template);
|
|
132
|
+
return compiled({
|
|
133
|
+
...this.config.defaultVariables,
|
|
134
|
+
...variables
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Register a custom Handlebars helper.
|
|
139
|
+
*
|
|
140
|
+
* @param name - Helper name
|
|
141
|
+
* @param helper - Helper function
|
|
142
|
+
*/
|
|
143
|
+
registerHelper(name, helper) {
|
|
144
|
+
this.handlebars.registerHelper(name, helper);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Register a Handlebars partial.
|
|
148
|
+
*
|
|
149
|
+
* @param name - Partial name
|
|
150
|
+
* @param partial - Partial template string
|
|
151
|
+
*/
|
|
152
|
+
registerPartial(name, partial) {
|
|
153
|
+
this.handlebars.registerPartial(name, partial);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// src/generators/BaseGenerator.ts
|
|
158
|
+
var BaseGenerator = class {
|
|
159
|
+
config;
|
|
160
|
+
stubGenerator;
|
|
161
|
+
filesCreated = [];
|
|
162
|
+
constructor(config) {
|
|
163
|
+
this.config = config;
|
|
164
|
+
this.stubGenerator = new StubGenerator({
|
|
165
|
+
stubsDir: config.templatesDir,
|
|
166
|
+
outputDir: ""
|
|
167
|
+
// Set per-generation
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Generate the project scaffold.
|
|
172
|
+
*
|
|
173
|
+
* @param context - Generator context
|
|
174
|
+
* @returns Array of created file paths
|
|
175
|
+
*/
|
|
176
|
+
async generate(context) {
|
|
177
|
+
this.filesCreated = [];
|
|
178
|
+
const structure = this.getDirectoryStructure(context);
|
|
179
|
+
await this.createStructure(context.targetDir, structure, context);
|
|
180
|
+
await this.generateCommonFiles(context);
|
|
181
|
+
return this.filesCreated;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Create directory structure recursively.
|
|
185
|
+
*/
|
|
186
|
+
async createStructure(basePath, nodes, context) {
|
|
187
|
+
for (const node of nodes) {
|
|
188
|
+
const fullPath = path2.resolve(basePath, node.name);
|
|
189
|
+
if (node.type === "directory") {
|
|
190
|
+
await fs2.mkdir(fullPath, { recursive: true });
|
|
191
|
+
this.log(`\u{1F4C1} Created directory: ${node.name}`);
|
|
192
|
+
if (node.children) {
|
|
193
|
+
await this.createStructure(fullPath, node.children, context);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
await fs2.mkdir(path2.dirname(fullPath), { recursive: true });
|
|
197
|
+
if (node.template) {
|
|
198
|
+
const templatePath = path2.resolve(this.config.templatesDir, node.template);
|
|
199
|
+
try {
|
|
200
|
+
const template = await fs2.readFile(templatePath, "utf-8");
|
|
201
|
+
const content = this.stubGenerator.render(template, context);
|
|
202
|
+
await fs2.writeFile(fullPath, content, "utf-8");
|
|
203
|
+
} catch {
|
|
204
|
+
await fs2.writeFile(fullPath, node.content ?? "", "utf-8");
|
|
205
|
+
}
|
|
206
|
+
} else if (node.content) {
|
|
207
|
+
await fs2.writeFile(fullPath, node.content, "utf-8");
|
|
208
|
+
} else {
|
|
209
|
+
await fs2.writeFile(fullPath, "", "utf-8");
|
|
210
|
+
}
|
|
211
|
+
this.filesCreated.push(fullPath);
|
|
212
|
+
this.log(`\u{1F4C4} Created file: ${node.name}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Generate common files (package.json, .env, etc.)
|
|
218
|
+
*/
|
|
219
|
+
async generateCommonFiles(context) {
|
|
220
|
+
await this.writeFile(context.targetDir, "package.json", this.generatePackageJson(context));
|
|
221
|
+
await this.writeFile(context.targetDir, ".env.example", this.generateEnvExample(context));
|
|
222
|
+
await this.writeFile(context.targetDir, ".env", this.generateEnvExample(context));
|
|
223
|
+
await this.writeFile(context.targetDir, ".gitignore", this.generateGitignore());
|
|
224
|
+
await this.writeFile(context.targetDir, "tsconfig.json", this.generateTsConfig());
|
|
225
|
+
await this.writeFile(
|
|
226
|
+
context.targetDir,
|
|
227
|
+
"ARCHITECTURE.md",
|
|
228
|
+
this.generateArchitectureDoc(context)
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Write a file and track it.
|
|
233
|
+
*/
|
|
234
|
+
async writeFile(basePath, relativePath, content) {
|
|
235
|
+
const fullPath = path2.resolve(basePath, relativePath);
|
|
236
|
+
await fs2.mkdir(path2.dirname(fullPath), { recursive: true });
|
|
237
|
+
await fs2.writeFile(fullPath, content, "utf-8");
|
|
238
|
+
this.filesCreated.push(fullPath);
|
|
239
|
+
this.log(`\u{1F4C4} Created file: ${relativePath}`);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Generate package.json content.
|
|
243
|
+
*/
|
|
244
|
+
generatePackageJson(context) {
|
|
245
|
+
const pkg = {
|
|
246
|
+
name: context.nameKebabCase,
|
|
247
|
+
version: "0.1.0",
|
|
248
|
+
type: "module",
|
|
249
|
+
scripts: {
|
|
250
|
+
dev: "bun run --watch src/bootstrap.ts",
|
|
251
|
+
build: "bun build ./src/bootstrap.ts --outdir ./dist --target bun",
|
|
252
|
+
start: "bun run dist/bootstrap.js",
|
|
253
|
+
test: "bun test",
|
|
254
|
+
typecheck: "tsc --noEmit"
|
|
255
|
+
},
|
|
256
|
+
dependencies: {
|
|
257
|
+
"gravito-core": "^1.0.0-beta.5"
|
|
258
|
+
},
|
|
259
|
+
devDependencies: {
|
|
260
|
+
"@types/bun": "latest",
|
|
261
|
+
typescript: "^5.0.0"
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
return JSON.stringify(pkg, null, 2);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Generate .env.example content.
|
|
268
|
+
*/
|
|
269
|
+
generateEnvExample(context) {
|
|
270
|
+
return `# Application
|
|
271
|
+
APP_NAME="${context.name}"
|
|
272
|
+
APP_ENV=development
|
|
273
|
+
APP_KEY=
|
|
274
|
+
APP_DEBUG=true
|
|
275
|
+
APP_URL=http://localhost:3000
|
|
276
|
+
|
|
277
|
+
# Server
|
|
278
|
+
PORT=3000
|
|
279
|
+
|
|
280
|
+
# Database
|
|
281
|
+
DB_CONNECTION=sqlite
|
|
282
|
+
DB_DATABASE=database/database.sqlite
|
|
283
|
+
|
|
284
|
+
# Cache
|
|
285
|
+
CACHE_DRIVER=memory
|
|
286
|
+
|
|
287
|
+
# Logging
|
|
288
|
+
LOG_LEVEL=debug
|
|
289
|
+
`;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Generate .gitignore content.
|
|
293
|
+
*/
|
|
294
|
+
generateGitignore() {
|
|
295
|
+
return `# Dependencies
|
|
296
|
+
node_modules/
|
|
297
|
+
|
|
298
|
+
# Build output
|
|
299
|
+
dist/
|
|
300
|
+
|
|
301
|
+
# Environment
|
|
302
|
+
.env
|
|
303
|
+
.env.local
|
|
304
|
+
.env.*.local
|
|
305
|
+
|
|
306
|
+
# IDE
|
|
307
|
+
.idea/
|
|
308
|
+
.vscode/
|
|
309
|
+
*.swp
|
|
310
|
+
*.swo
|
|
311
|
+
|
|
312
|
+
# System
|
|
313
|
+
.DS_Store
|
|
314
|
+
Thumbs.db
|
|
315
|
+
|
|
316
|
+
# Logs
|
|
317
|
+
*.log
|
|
318
|
+
logs/
|
|
319
|
+
|
|
320
|
+
# Database
|
|
321
|
+
*.sqlite
|
|
322
|
+
*.sqlite-journal
|
|
323
|
+
|
|
324
|
+
# Coverage
|
|
325
|
+
coverage/
|
|
326
|
+
`;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Generate tsconfig.json content.
|
|
330
|
+
*/
|
|
331
|
+
generateTsConfig() {
|
|
332
|
+
const config = {
|
|
333
|
+
compilerOptions: {
|
|
334
|
+
target: "ESNext",
|
|
335
|
+
module: "ESNext",
|
|
336
|
+
moduleResolution: "bundler",
|
|
337
|
+
esModuleInterop: true,
|
|
338
|
+
strict: true,
|
|
339
|
+
skipLibCheck: true,
|
|
340
|
+
declaration: true,
|
|
341
|
+
outDir: "./dist",
|
|
342
|
+
rootDir: "./src",
|
|
343
|
+
baseUrl: ".",
|
|
344
|
+
paths: {
|
|
345
|
+
"@/*": ["./src/*"]
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
include: ["src/**/*"],
|
|
349
|
+
exclude: ["node_modules", "dist"]
|
|
350
|
+
};
|
|
351
|
+
return JSON.stringify(config, null, 2);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Log a message if verbose mode is enabled.
|
|
355
|
+
*/
|
|
356
|
+
log(message) {
|
|
357
|
+
if (this.config.verbose) {
|
|
358
|
+
console.log(message);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Create generator context from options.
|
|
363
|
+
*/
|
|
364
|
+
static createContext(name, targetDir, architecture, packageManager = "bun", extra = {}) {
|
|
365
|
+
const now = /* @__PURE__ */ new Date();
|
|
366
|
+
const pascalCase = name.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase());
|
|
367
|
+
const camelCase = pascalCase.replace(/^./, (c) => c.toLowerCase());
|
|
368
|
+
const snakeCase = name.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "").replace(/[-\s]+/g, "_");
|
|
369
|
+
const kebabCase = name.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "").replace(/[_\s]+/g, "-");
|
|
370
|
+
return {
|
|
371
|
+
name,
|
|
372
|
+
namePascalCase: pascalCase,
|
|
373
|
+
nameCamelCase: camelCase,
|
|
374
|
+
nameSnakeCase: snakeCase,
|
|
375
|
+
nameKebabCase: kebabCase,
|
|
376
|
+
targetDir,
|
|
377
|
+
architecture,
|
|
378
|
+
packageManager,
|
|
379
|
+
year: now.getFullYear().toString(),
|
|
380
|
+
date: now.toISOString().split("T")[0] ?? now.toISOString().slice(0, 10),
|
|
381
|
+
...extra
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// src/generators/CleanArchitectureGenerator.ts
|
|
387
|
+
var CleanArchitectureGenerator = class extends BaseGenerator {
|
|
388
|
+
get architectureType() {
|
|
389
|
+
return "clean";
|
|
390
|
+
}
|
|
391
|
+
get displayName() {
|
|
392
|
+
return "Clean Architecture";
|
|
393
|
+
}
|
|
394
|
+
get description() {
|
|
395
|
+
return "Uncle Bob's Clean Architecture with strict dependency rules and pure domain layer";
|
|
396
|
+
}
|
|
397
|
+
getDirectoryStructure(context) {
|
|
398
|
+
return [
|
|
399
|
+
{
|
|
400
|
+
type: "directory",
|
|
401
|
+
name: "config",
|
|
402
|
+
children: [
|
|
403
|
+
{ type: "file", name: "app.ts", content: this.generateAppConfig(context) },
|
|
404
|
+
{ type: "file", name: "database.ts", content: this.generateDatabaseConfig() },
|
|
405
|
+
{ type: "file", name: "auth.ts", content: this.generateAuthConfig() },
|
|
406
|
+
{ type: "file", name: "cache.ts", content: this.generateCacheConfig() },
|
|
407
|
+
{ type: "file", name: "logging.ts", content: this.generateLoggingConfig() }
|
|
408
|
+
]
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
type: "directory",
|
|
412
|
+
name: "src",
|
|
413
|
+
children: [
|
|
414
|
+
// Domain Layer (innermost - no dependencies)
|
|
415
|
+
{
|
|
416
|
+
type: "directory",
|
|
417
|
+
name: "Domain",
|
|
418
|
+
children: [
|
|
419
|
+
{
|
|
420
|
+
type: "directory",
|
|
421
|
+
name: "Entities",
|
|
422
|
+
children: [
|
|
423
|
+
{ type: "file", name: "Entity.ts", content: this.generateBaseEntity() },
|
|
424
|
+
{ type: "file", name: "User.ts", content: this.generateUserEntity() }
|
|
425
|
+
]
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
type: "directory",
|
|
429
|
+
name: "ValueObjects",
|
|
430
|
+
children: [
|
|
431
|
+
{ type: "file", name: "ValueObject.ts", content: this.generateBaseValueObject() },
|
|
432
|
+
{ type: "file", name: "Email.ts", content: this.generateEmailValueObject() }
|
|
433
|
+
]
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
type: "directory",
|
|
437
|
+
name: "Interfaces",
|
|
438
|
+
children: [
|
|
439
|
+
{
|
|
440
|
+
type: "file",
|
|
441
|
+
name: "IUserRepository.ts",
|
|
442
|
+
content: this.generateUserRepositoryInterface()
|
|
443
|
+
}
|
|
444
|
+
]
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
type: "directory",
|
|
448
|
+
name: "Exceptions",
|
|
449
|
+
children: [
|
|
450
|
+
{
|
|
451
|
+
type: "file",
|
|
452
|
+
name: "DomainException.ts",
|
|
453
|
+
content: this.generateDomainException()
|
|
454
|
+
}
|
|
455
|
+
]
|
|
456
|
+
}
|
|
457
|
+
]
|
|
458
|
+
},
|
|
459
|
+
// Application Layer (use cases)
|
|
460
|
+
{
|
|
461
|
+
type: "directory",
|
|
462
|
+
name: "Application",
|
|
463
|
+
children: [
|
|
464
|
+
{
|
|
465
|
+
type: "directory",
|
|
466
|
+
name: "UseCases",
|
|
467
|
+
children: [
|
|
468
|
+
{
|
|
469
|
+
type: "directory",
|
|
470
|
+
name: "User",
|
|
471
|
+
children: [
|
|
472
|
+
{
|
|
473
|
+
type: "file",
|
|
474
|
+
name: "CreateUser.ts",
|
|
475
|
+
content: this.generateCreateUserUseCase()
|
|
476
|
+
},
|
|
477
|
+
{ type: "file", name: "GetUser.ts", content: this.generateGetUserUseCase() }
|
|
478
|
+
]
|
|
479
|
+
}
|
|
480
|
+
]
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
type: "directory",
|
|
484
|
+
name: "DTOs",
|
|
485
|
+
children: [{ type: "file", name: "UserDTO.ts", content: this.generateUserDTO() }]
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
type: "directory",
|
|
489
|
+
name: "Interfaces",
|
|
490
|
+
children: [
|
|
491
|
+
{
|
|
492
|
+
type: "file",
|
|
493
|
+
name: "IMailService.ts",
|
|
494
|
+
content: this.generateMailServiceInterface()
|
|
495
|
+
}
|
|
496
|
+
]
|
|
497
|
+
}
|
|
498
|
+
]
|
|
499
|
+
},
|
|
500
|
+
// Infrastructure Layer (external implementations)
|
|
501
|
+
{
|
|
502
|
+
type: "directory",
|
|
503
|
+
name: "Infrastructure",
|
|
504
|
+
children: [
|
|
505
|
+
{
|
|
506
|
+
type: "directory",
|
|
507
|
+
name: "Persistence",
|
|
508
|
+
children: [
|
|
509
|
+
{
|
|
510
|
+
type: "directory",
|
|
511
|
+
name: "Repositories",
|
|
512
|
+
children: [
|
|
513
|
+
{
|
|
514
|
+
type: "file",
|
|
515
|
+
name: "UserRepository.ts",
|
|
516
|
+
content: this.generateUserRepository()
|
|
517
|
+
}
|
|
518
|
+
]
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
type: "directory",
|
|
522
|
+
name: "Migrations",
|
|
523
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
524
|
+
}
|
|
525
|
+
]
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
type: "directory",
|
|
529
|
+
name: "ExternalServices",
|
|
530
|
+
children: [
|
|
531
|
+
{ type: "file", name: "MailService.ts", content: this.generateMailService() }
|
|
532
|
+
]
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
type: "directory",
|
|
536
|
+
name: "Providers",
|
|
537
|
+
children: [
|
|
538
|
+
{
|
|
539
|
+
type: "file",
|
|
540
|
+
name: "AppServiceProvider.ts",
|
|
541
|
+
content: this.generateAppServiceProvider(context)
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
type: "file",
|
|
545
|
+
name: "RepositoryServiceProvider.ts",
|
|
546
|
+
content: this.generateRepositoryServiceProvider()
|
|
547
|
+
}
|
|
548
|
+
]
|
|
549
|
+
}
|
|
550
|
+
]
|
|
551
|
+
},
|
|
552
|
+
// Interface Layer (controllers, presenters)
|
|
553
|
+
{
|
|
554
|
+
type: "directory",
|
|
555
|
+
name: "Interface",
|
|
556
|
+
children: [
|
|
557
|
+
{
|
|
558
|
+
type: "directory",
|
|
559
|
+
name: "Http",
|
|
560
|
+
children: [
|
|
561
|
+
{
|
|
562
|
+
type: "directory",
|
|
563
|
+
name: "Controllers",
|
|
564
|
+
children: [
|
|
565
|
+
{
|
|
566
|
+
type: "file",
|
|
567
|
+
name: "UserController.ts",
|
|
568
|
+
content: this.generateUserController()
|
|
569
|
+
}
|
|
570
|
+
]
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
type: "directory",
|
|
574
|
+
name: "Middleware",
|
|
575
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
type: "directory",
|
|
579
|
+
name: "Routes",
|
|
580
|
+
children: [{ type: "file", name: "api.ts", content: this.generateApiRoutes() }]
|
|
581
|
+
}
|
|
582
|
+
]
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
type: "directory",
|
|
586
|
+
name: "Presenters",
|
|
587
|
+
children: [
|
|
588
|
+
{ type: "file", name: "UserPresenter.ts", content: this.generateUserPresenter() }
|
|
589
|
+
]
|
|
590
|
+
}
|
|
591
|
+
]
|
|
592
|
+
},
|
|
593
|
+
{ type: "file", name: "bootstrap.ts", content: this.generateBootstrap(context) }
|
|
594
|
+
]
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
type: "directory",
|
|
598
|
+
name: "tests",
|
|
599
|
+
children: [
|
|
600
|
+
{
|
|
601
|
+
type: "directory",
|
|
602
|
+
name: "Unit",
|
|
603
|
+
children: [
|
|
604
|
+
{
|
|
605
|
+
type: "directory",
|
|
606
|
+
name: "Domain",
|
|
607
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
type: "directory",
|
|
611
|
+
name: "Application",
|
|
612
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
613
|
+
}
|
|
614
|
+
]
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
type: "directory",
|
|
618
|
+
name: "Integration",
|
|
619
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
620
|
+
}
|
|
621
|
+
]
|
|
622
|
+
}
|
|
623
|
+
];
|
|
624
|
+
}
|
|
625
|
+
// ─────────────────────────────────────────────────────────────
|
|
626
|
+
// Config Generators (similar to MVC but simplified)
|
|
627
|
+
// ─────────────────────────────────────────────────────────────
|
|
628
|
+
generateAppConfig(context) {
|
|
629
|
+
return `export default {
|
|
630
|
+
name: process.env.APP_NAME ?? '${context.name}',
|
|
631
|
+
env: process.env.APP_ENV ?? 'development',
|
|
632
|
+
debug: process.env.APP_DEBUG === 'true',
|
|
633
|
+
url: process.env.APP_URL ?? 'http://localhost:3000',
|
|
634
|
+
key: process.env.APP_KEY,
|
|
635
|
+
}
|
|
636
|
+
`;
|
|
637
|
+
}
|
|
638
|
+
generateDatabaseConfig() {
|
|
639
|
+
return `export default {
|
|
640
|
+
default: process.env.DB_CONNECTION ?? 'sqlite',
|
|
641
|
+
connections: {
|
|
642
|
+
sqlite: {
|
|
643
|
+
driver: 'sqlite',
|
|
644
|
+
database: process.env.DB_DATABASE ?? 'database/database.sqlite',
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
}
|
|
648
|
+
`;
|
|
649
|
+
}
|
|
650
|
+
generateAuthConfig() {
|
|
651
|
+
return `export default {
|
|
652
|
+
defaults: { guard: 'web' },
|
|
653
|
+
guards: {
|
|
654
|
+
web: { driver: 'session', provider: 'users' },
|
|
655
|
+
api: { driver: 'token', provider: 'users' },
|
|
656
|
+
},
|
|
657
|
+
}
|
|
658
|
+
`;
|
|
659
|
+
}
|
|
660
|
+
generateCacheConfig() {
|
|
661
|
+
return `export default {
|
|
662
|
+
default: process.env.CACHE_DRIVER ?? 'memory',
|
|
663
|
+
stores: {
|
|
664
|
+
memory: { driver: 'memory' },
|
|
665
|
+
},
|
|
666
|
+
}
|
|
667
|
+
`;
|
|
668
|
+
}
|
|
669
|
+
generateLoggingConfig() {
|
|
670
|
+
return `export default {
|
|
671
|
+
default: process.env.LOG_CHANNEL ?? 'console',
|
|
672
|
+
channels: {
|
|
673
|
+
console: { driver: 'console', level: process.env.LOG_LEVEL ?? 'debug' },
|
|
674
|
+
},
|
|
675
|
+
}
|
|
676
|
+
`;
|
|
677
|
+
}
|
|
678
|
+
// ─────────────────────────────────────────────────────────────
|
|
679
|
+
// Domain Layer
|
|
680
|
+
// ─────────────────────────────────────────────────────────────
|
|
681
|
+
generateBaseEntity() {
|
|
682
|
+
return `/**
|
|
683
|
+
* Base Entity
|
|
684
|
+
*
|
|
685
|
+
* All domain entities extend this base class.
|
|
686
|
+
* Entities have identity and lifecycle.
|
|
687
|
+
*
|
|
688
|
+
* IMPORTANT: This layer must have NO external dependencies.
|
|
689
|
+
*/
|
|
690
|
+
|
|
691
|
+
export abstract class Entity<T> {
|
|
692
|
+
protected readonly _id: T
|
|
693
|
+
|
|
694
|
+
constructor(id: T) {
|
|
695
|
+
this._id = id
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
get id(): T {
|
|
699
|
+
return this._id
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
equals(other: Entity<T>): boolean {
|
|
703
|
+
if (other === null || other === undefined) {
|
|
704
|
+
return false
|
|
705
|
+
}
|
|
706
|
+
if (!(other instanceof Entity)) {
|
|
707
|
+
return false
|
|
708
|
+
}
|
|
709
|
+
return this._id === other._id
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
`;
|
|
713
|
+
}
|
|
714
|
+
generateUserEntity() {
|
|
715
|
+
return `/**
|
|
716
|
+
* User Entity
|
|
717
|
+
*
|
|
718
|
+
* Core domain entity representing a user.
|
|
719
|
+
* Contains business logic related to users.
|
|
720
|
+
*/
|
|
721
|
+
|
|
722
|
+
import { Entity } from './Entity'
|
|
723
|
+
import { Email } from '../ValueObjects/Email'
|
|
724
|
+
|
|
725
|
+
export interface UserProps {
|
|
726
|
+
name: string
|
|
727
|
+
email: Email
|
|
728
|
+
createdAt: Date
|
|
729
|
+
updatedAt?: Date
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export class User extends Entity<string> {
|
|
733
|
+
private props: UserProps
|
|
734
|
+
|
|
735
|
+
private constructor(id: string, props: UserProps) {
|
|
736
|
+
super(id)
|
|
737
|
+
this.props = props
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
static create(id: string, name: string, email: string): User {
|
|
741
|
+
return new User(id, {
|
|
742
|
+
name,
|
|
743
|
+
email: Email.create(email),
|
|
744
|
+
createdAt: new Date(),
|
|
745
|
+
})
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
static reconstitute(id: string, props: UserProps): User {
|
|
749
|
+
return new User(id, props)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
get name(): string {
|
|
753
|
+
return this.props.name
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
get email(): Email {
|
|
757
|
+
return this.props.email
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
get createdAt(): Date {
|
|
761
|
+
return this.props.createdAt
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
updateName(name: string): void {
|
|
765
|
+
if (!name || name.trim().length === 0) {
|
|
766
|
+
throw new Error('Name cannot be empty')
|
|
767
|
+
}
|
|
768
|
+
this.props.name = name.trim()
|
|
769
|
+
this.props.updatedAt = new Date()
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
`;
|
|
773
|
+
}
|
|
774
|
+
generateBaseValueObject() {
|
|
775
|
+
return `/**
|
|
776
|
+
* Base Value Object
|
|
777
|
+
*
|
|
778
|
+
* Value objects are immutable and compared by value.
|
|
779
|
+
* They have no identity of their own.
|
|
780
|
+
*/
|
|
781
|
+
|
|
782
|
+
export abstract class ValueObject<T> {
|
|
783
|
+
protected readonly props: T
|
|
784
|
+
|
|
785
|
+
constructor(props: T) {
|
|
786
|
+
this.props = Object.freeze(props)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
equals(other: ValueObject<T>): boolean {
|
|
790
|
+
if (other === null || other === undefined) {
|
|
791
|
+
return false
|
|
792
|
+
}
|
|
793
|
+
return JSON.stringify(this.props) === JSON.stringify(other.props)
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
`;
|
|
797
|
+
}
|
|
798
|
+
generateEmailValueObject() {
|
|
799
|
+
return `/**
|
|
800
|
+
* Email Value Object
|
|
801
|
+
*
|
|
802
|
+
* Encapsulates email validation and comparison.
|
|
803
|
+
*/
|
|
804
|
+
|
|
805
|
+
import { ValueObject } from './ValueObject'
|
|
806
|
+
|
|
807
|
+
interface EmailProps {
|
|
808
|
+
value: string
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export class Email extends ValueObject<EmailProps> {
|
|
812
|
+
private constructor(props: EmailProps) {
|
|
813
|
+
super(props)
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
static create(email: string): Email {
|
|
817
|
+
if (!Email.isValid(email)) {
|
|
818
|
+
throw new Error(\`Invalid email: \${email}\`)
|
|
819
|
+
}
|
|
820
|
+
return new Email({ value: email.toLowerCase().trim() })
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
static isValid(email: string): boolean {
|
|
824
|
+
const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/
|
|
825
|
+
return emailRegex.test(email)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
get value(): string {
|
|
829
|
+
return this.props.value
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
toString(): string {
|
|
833
|
+
return this.value
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
`;
|
|
837
|
+
}
|
|
838
|
+
generateUserRepositoryInterface() {
|
|
839
|
+
return `/**
|
|
840
|
+
* User Repository Interface
|
|
841
|
+
*
|
|
842
|
+
* Defines the contract for user persistence.
|
|
843
|
+
* Implementations are in Infrastructure layer.
|
|
844
|
+
*/
|
|
845
|
+
|
|
846
|
+
import type { User } from '../Entities/User'
|
|
847
|
+
|
|
848
|
+
export interface IUserRepository {
|
|
849
|
+
findById(id: string): Promise<User | null>
|
|
850
|
+
findByEmail(email: string): Promise<User | null>
|
|
851
|
+
save(user: User): Promise<void>
|
|
852
|
+
delete(id: string): Promise<void>
|
|
853
|
+
findAll(): Promise<User[]>
|
|
854
|
+
}
|
|
855
|
+
`;
|
|
856
|
+
}
|
|
857
|
+
generateDomainException() {
|
|
858
|
+
return `/**
|
|
859
|
+
* Domain Exception
|
|
860
|
+
*
|
|
861
|
+
* Base exception for domain-level errors.
|
|
862
|
+
*/
|
|
863
|
+
|
|
864
|
+
export class DomainException extends Error {
|
|
865
|
+
constructor(message: string) {
|
|
866
|
+
super(message)
|
|
867
|
+
this.name = 'DomainException'
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
export class EntityNotFoundException extends DomainException {
|
|
872
|
+
constructor(entity: string, id: string) {
|
|
873
|
+
super(\`\${entity} with id \${id} not found\`)
|
|
874
|
+
this.name = 'EntityNotFoundException'
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
export class InvalidValueException extends DomainException {
|
|
879
|
+
constructor(message: string) {
|
|
880
|
+
super(message)
|
|
881
|
+
this.name = 'InvalidValueException'
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
`;
|
|
885
|
+
}
|
|
886
|
+
// ─────────────────────────────────────────────────────────────
|
|
887
|
+
// Application Layer
|
|
888
|
+
// ─────────────────────────────────────────────────────────────
|
|
889
|
+
generateCreateUserUseCase() {
|
|
890
|
+
return `/**
|
|
891
|
+
* Create User Use Case
|
|
892
|
+
*
|
|
893
|
+
* Application service for creating new users.
|
|
894
|
+
*/
|
|
895
|
+
|
|
896
|
+
import { User } from '../../../Domain/Entities/User'
|
|
897
|
+
import type { IUserRepository } from '../../../Domain/Interfaces/IUserRepository'
|
|
898
|
+
import type { UserDTO } from '../../DTOs/UserDTO'
|
|
899
|
+
|
|
900
|
+
export interface CreateUserInput {
|
|
901
|
+
name: string
|
|
902
|
+
email: string
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
export interface CreateUserOutput {
|
|
906
|
+
user: UserDTO
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
export class CreateUserUseCase {
|
|
910
|
+
constructor(private userRepository: IUserRepository) {}
|
|
911
|
+
|
|
912
|
+
async execute(input: CreateUserInput): Promise<CreateUserOutput> {
|
|
913
|
+
// Check if email already exists
|
|
914
|
+
const existing = await this.userRepository.findByEmail(input.email)
|
|
915
|
+
if (existing) {
|
|
916
|
+
throw new Error('User with this email already exists')
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Create user entity
|
|
920
|
+
const user = User.create(
|
|
921
|
+
crypto.randomUUID(),
|
|
922
|
+
input.name,
|
|
923
|
+
input.email
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
// Persist
|
|
927
|
+
await this.userRepository.save(user)
|
|
928
|
+
|
|
929
|
+
// Return DTO
|
|
930
|
+
return {
|
|
931
|
+
user: {
|
|
932
|
+
id: user.id,
|
|
933
|
+
name: user.name,
|
|
934
|
+
email: user.email.value,
|
|
935
|
+
createdAt: user.createdAt.toISOString(),
|
|
936
|
+
},
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
`;
|
|
941
|
+
}
|
|
942
|
+
generateGetUserUseCase() {
|
|
943
|
+
return `/**
|
|
944
|
+
* Get User Use Case
|
|
945
|
+
*/
|
|
946
|
+
|
|
947
|
+
import type { IUserRepository } from '../../../Domain/Interfaces/IUserRepository'
|
|
948
|
+
import { EntityNotFoundException } from '../../../Domain/Exceptions/DomainException'
|
|
949
|
+
import type { UserDTO } from '../../DTOs/UserDTO'
|
|
950
|
+
|
|
951
|
+
export interface GetUserInput {
|
|
952
|
+
id: string
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
export interface GetUserOutput {
|
|
956
|
+
user: UserDTO
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
export class GetUserUseCase {
|
|
960
|
+
constructor(private userRepository: IUserRepository) {}
|
|
961
|
+
|
|
962
|
+
async execute(input: GetUserInput): Promise<GetUserOutput> {
|
|
963
|
+
const user = await this.userRepository.findById(input.id)
|
|
964
|
+
|
|
965
|
+
if (!user) {
|
|
966
|
+
throw new EntityNotFoundException('User', input.id)
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return {
|
|
970
|
+
user: {
|
|
971
|
+
id: user.id,
|
|
972
|
+
name: user.name,
|
|
973
|
+
email: user.email.value,
|
|
974
|
+
createdAt: user.createdAt.toISOString(),
|
|
975
|
+
},
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
`;
|
|
980
|
+
}
|
|
981
|
+
generateUserDTO() {
|
|
982
|
+
return `/**
|
|
983
|
+
* User Data Transfer Object
|
|
984
|
+
*
|
|
985
|
+
* Used for transferring user data across boundaries.
|
|
986
|
+
*/
|
|
987
|
+
|
|
988
|
+
export interface UserDTO {
|
|
989
|
+
id: string
|
|
990
|
+
name: string
|
|
991
|
+
email: string
|
|
992
|
+
createdAt: string
|
|
993
|
+
updatedAt?: string
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
export interface CreateUserDTO {
|
|
997
|
+
name: string
|
|
998
|
+
email: string
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
export interface UpdateUserDTO {
|
|
1002
|
+
name?: string
|
|
1003
|
+
email?: string
|
|
1004
|
+
}
|
|
1005
|
+
`;
|
|
1006
|
+
}
|
|
1007
|
+
generateMailServiceInterface() {
|
|
1008
|
+
return `/**
|
|
1009
|
+
* Mail Service Interface
|
|
1010
|
+
*
|
|
1011
|
+
* Contract for email sending functionality.
|
|
1012
|
+
*/
|
|
1013
|
+
|
|
1014
|
+
export interface MailMessage {
|
|
1015
|
+
to: string
|
|
1016
|
+
subject: string
|
|
1017
|
+
body: string
|
|
1018
|
+
html?: string
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
export interface IMailService {
|
|
1022
|
+
send(message: MailMessage): Promise<void>
|
|
1023
|
+
}
|
|
1024
|
+
`;
|
|
1025
|
+
}
|
|
1026
|
+
// ─────────────────────────────────────────────────────────────
|
|
1027
|
+
// Infrastructure Layer
|
|
1028
|
+
// ─────────────────────────────────────────────────────────────
|
|
1029
|
+
generateUserRepository() {
|
|
1030
|
+
return `/**
|
|
1031
|
+
* User Repository Implementation
|
|
1032
|
+
*
|
|
1033
|
+
* Concrete implementation of IUserRepository.
|
|
1034
|
+
*/
|
|
1035
|
+
|
|
1036
|
+
import type { User } from '../../../Domain/Entities/User'
|
|
1037
|
+
import type { IUserRepository } from '../../../Domain/Interfaces/IUserRepository'
|
|
1038
|
+
|
|
1039
|
+
// In-memory store for demo purposes
|
|
1040
|
+
const users = new Map<string, User>()
|
|
1041
|
+
|
|
1042
|
+
export class UserRepository implements IUserRepository {
|
|
1043
|
+
async findById(id: string): Promise<User | null> {
|
|
1044
|
+
return users.get(id) ?? null
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
1048
|
+
for (const user of users.values()) {
|
|
1049
|
+
if (user.email.value === email) {
|
|
1050
|
+
return user
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return null
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
async save(user: User): Promise<void> {
|
|
1057
|
+
users.set(user.id, user)
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
async delete(id: string): Promise<void> {
|
|
1061
|
+
users.delete(id)
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async findAll(): Promise<User[]> {
|
|
1065
|
+
return Array.from(users.values())
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
`;
|
|
1069
|
+
}
|
|
1070
|
+
generateMailService() {
|
|
1071
|
+
return `/**
|
|
1072
|
+
* Mail Service Implementation
|
|
1073
|
+
*/
|
|
1074
|
+
|
|
1075
|
+
import type { IMailService, MailMessage } from '../../Application/Interfaces/IMailService'
|
|
1076
|
+
|
|
1077
|
+
export class MailService implements IMailService {
|
|
1078
|
+
async send(message: MailMessage): Promise<void> {
|
|
1079
|
+
// TODO: Implement actual email sending
|
|
1080
|
+
console.log(\`[Mail] Sending to \${message.to}: \${message.subject}\`)
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
`;
|
|
1084
|
+
}
|
|
1085
|
+
generateAppServiceProvider(context) {
|
|
1086
|
+
return `/**
|
|
1087
|
+
* App Service Provider
|
|
1088
|
+
*/
|
|
1089
|
+
|
|
1090
|
+
import { ServiceProvider, type Container, type PlanetCore } from 'gravito-core'
|
|
1091
|
+
|
|
1092
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
1093
|
+
register(_container: Container): void {
|
|
1094
|
+
// Register application services
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
boot(_core: PlanetCore): void {
|
|
1098
|
+
console.log('${context.name} (Clean Architecture) booted!')
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
`;
|
|
1102
|
+
}
|
|
1103
|
+
generateRepositoryServiceProvider() {
|
|
1104
|
+
return `/**
|
|
1105
|
+
* Repository Service Provider
|
|
1106
|
+
*
|
|
1107
|
+
* Binds repository interfaces to implementations.
|
|
1108
|
+
*/
|
|
1109
|
+
|
|
1110
|
+
import { ServiceProvider, type Container } from 'gravito-core'
|
|
1111
|
+
import { UserRepository } from '../Persistence/Repositories/UserRepository'
|
|
1112
|
+
import { MailService } from '../ExternalServices/MailService'
|
|
1113
|
+
|
|
1114
|
+
export class RepositoryServiceProvider extends ServiceProvider {
|
|
1115
|
+
register(container: Container): void {
|
|
1116
|
+
// Bind repositories
|
|
1117
|
+
container.singleton('userRepository', () => new UserRepository())
|
|
1118
|
+
|
|
1119
|
+
// Bind external services
|
|
1120
|
+
container.singleton('mailService', () => new MailService())
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
`;
|
|
1124
|
+
}
|
|
1125
|
+
// ─────────────────────────────────────────────────────────────
|
|
1126
|
+
// Interface Layer
|
|
1127
|
+
// ─────────────────────────────────────────────────────────────
|
|
1128
|
+
generateUserController() {
|
|
1129
|
+
return `/**
|
|
1130
|
+
* User Controller
|
|
1131
|
+
*/
|
|
1132
|
+
|
|
1133
|
+
import type { GravitoContext } from 'gravito-core'
|
|
1134
|
+
import { CreateUserUseCase } from '../../../Application/UseCases/User/CreateUser'
|
|
1135
|
+
import { GetUserUseCase } from '../../../Application/UseCases/User/GetUser'
|
|
1136
|
+
import { UserRepository } from '../../../Infrastructure/Persistence/Repositories/UserRepository'
|
|
1137
|
+
|
|
1138
|
+
const userRepository = new UserRepository()
|
|
1139
|
+
|
|
1140
|
+
export class UserController {
|
|
1141
|
+
async create(c: GravitoContext) {
|
|
1142
|
+
const body = await c.req.json() as { name: string; email: string }
|
|
1143
|
+
const useCase = new CreateUserUseCase(userRepository)
|
|
1144
|
+
|
|
1145
|
+
const result = await useCase.execute({
|
|
1146
|
+
name: body.name,
|
|
1147
|
+
email: body.email,
|
|
1148
|
+
})
|
|
1149
|
+
|
|
1150
|
+
return c.json({ success: true, data: result.user }, 201)
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
async show(c: GravitoContext) {
|
|
1154
|
+
const id = c.req.param('id') ?? ''
|
|
1155
|
+
const useCase = new GetUserUseCase(userRepository)
|
|
1156
|
+
|
|
1157
|
+
const result = await useCase.execute({ id })
|
|
1158
|
+
|
|
1159
|
+
return c.json({ success: true, data: result.user })
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
`;
|
|
1163
|
+
}
|
|
1164
|
+
generateApiRoutes() {
|
|
1165
|
+
return `/**
|
|
1166
|
+
* API Routes
|
|
1167
|
+
*/
|
|
1168
|
+
|
|
1169
|
+
import { UserController } from '../Controllers/UserController'
|
|
1170
|
+
|
|
1171
|
+
export function registerApiRoutes(router: any): void {
|
|
1172
|
+
const users = new UserController()
|
|
1173
|
+
|
|
1174
|
+
router.post('/api/users', (c: any) => users.create(c))
|
|
1175
|
+
router.get('/api/users/:id', (c: any) => users.show(c))
|
|
1176
|
+
}
|
|
1177
|
+
`;
|
|
1178
|
+
}
|
|
1179
|
+
generateUserPresenter() {
|
|
1180
|
+
return `/**
|
|
1181
|
+
* User Presenter
|
|
1182
|
+
*
|
|
1183
|
+
* Formats user data for different output formats.
|
|
1184
|
+
*/
|
|
1185
|
+
|
|
1186
|
+
import type { UserDTO } from '../../Application/DTOs/UserDTO'
|
|
1187
|
+
|
|
1188
|
+
export class UserPresenter {
|
|
1189
|
+
static toJson(user: UserDTO): object {
|
|
1190
|
+
return {
|
|
1191
|
+
id: user.id,
|
|
1192
|
+
name: user.name,
|
|
1193
|
+
email: user.email,
|
|
1194
|
+
created_at: user.createdAt,
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
static toList(users: UserDTO[]): object {
|
|
1199
|
+
return {
|
|
1200
|
+
data: users.map((u) => this.toJson(u)),
|
|
1201
|
+
total: users.length,
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
`;
|
|
1206
|
+
}
|
|
1207
|
+
generateBootstrap(context) {
|
|
1208
|
+
return `/**
|
|
1209
|
+
* Application Bootstrap
|
|
1210
|
+
*/
|
|
1211
|
+
|
|
1212
|
+
import { PlanetCore } from 'gravito-core'
|
|
1213
|
+
import { AppServiceProvider } from './Infrastructure/Providers/AppServiceProvider'
|
|
1214
|
+
import { RepositoryServiceProvider } from './Infrastructure/Providers/RepositoryServiceProvider'
|
|
1215
|
+
import { registerApiRoutes } from './Interface/Http/Routes/api'
|
|
1216
|
+
|
|
1217
|
+
const core = new PlanetCore({
|
|
1218
|
+
config: { APP_NAME: '${context.name}' },
|
|
1219
|
+
})
|
|
1220
|
+
|
|
1221
|
+
core.register(new RepositoryServiceProvider())
|
|
1222
|
+
core.register(new AppServiceProvider())
|
|
1223
|
+
|
|
1224
|
+
await core.bootstrap()
|
|
1225
|
+
|
|
1226
|
+
// Register routes
|
|
1227
|
+
registerApiRoutes(core.router)
|
|
1228
|
+
|
|
1229
|
+
export default core.liftoff()
|
|
1230
|
+
`;
|
|
1231
|
+
}
|
|
1232
|
+
generateArchitectureDoc(context) {
|
|
1233
|
+
return `# ${context.name} - Clean Architecture Guide
|
|
1234
|
+
|
|
1235
|
+
## Overview
|
|
1236
|
+
|
|
1237
|
+
This project follows **Clean Architecture** (by Robert C. Martin).
|
|
1238
|
+
The key principle is the **Dependency Rule**: dependencies point inward.
|
|
1239
|
+
|
|
1240
|
+
## Layer Structure
|
|
1241
|
+
|
|
1242
|
+
\`\`\`
|
|
1243
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
1244
|
+
\u2502 Interface Layer \u2502
|
|
1245
|
+
\u2502 (Controllers, Presenters) \u2502
|
|
1246
|
+
\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
|
|
1247
|
+
\u2502 Infrastructure Layer \u2502
|
|
1248
|
+
\u2502 (Repositories, External Services) \u2502
|
|
1249
|
+
\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
|
|
1250
|
+
\u2502 Application Layer \u2502
|
|
1251
|
+
\u2502 (Use Cases, DTOs) \u2502
|
|
1252
|
+
\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
|
|
1253
|
+
\u2502 Domain Layer \u2502
|
|
1254
|
+
\u2502 (Entities, Value Objects, Interfaces) \u2502
|
|
1255
|
+
\u2502 \u2B50 PURE \u2B50 \u2502
|
|
1256
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
1257
|
+
\`\`\`
|
|
1258
|
+
|
|
1259
|
+
## Dependency Rule
|
|
1260
|
+
|
|
1261
|
+
> Inner layers know nothing about outer layers.
|
|
1262
|
+
|
|
1263
|
+
- **Domain**: No dependencies (pure TypeScript only)
|
|
1264
|
+
- **Application**: Depends only on Domain
|
|
1265
|
+
- **Infrastructure**: Implements Domain interfaces
|
|
1266
|
+
- **Interface**: Calls Application use cases
|
|
1267
|
+
|
|
1268
|
+
## Directory Structure
|
|
1269
|
+
|
|
1270
|
+
\`\`\`
|
|
1271
|
+
src/
|
|
1272
|
+
\u251C\u2500\u2500 Domain/ # Enterprise Business Rules
|
|
1273
|
+
\u2502 \u251C\u2500\u2500 Entities/ # Business objects with identity
|
|
1274
|
+
\u2502 \u251C\u2500\u2500 ValueObjects/ # Immutable value types
|
|
1275
|
+
\u2502 \u251C\u2500\u2500 Interfaces/ # Repository contracts
|
|
1276
|
+
\u2502 \u2514\u2500\u2500 Exceptions/ # Domain errors
|
|
1277
|
+
\u251C\u2500\u2500 Application/ # Application Business Rules
|
|
1278
|
+
\u2502 \u251C\u2500\u2500 UseCases/ # Business operations
|
|
1279
|
+
\u2502 \u251C\u2500\u2500 DTOs/ # Data transfer objects
|
|
1280
|
+
\u2502 \u2514\u2500\u2500 Interfaces/ # Service contracts
|
|
1281
|
+
\u251C\u2500\u2500 Infrastructure/ # Frameworks & Drivers
|
|
1282
|
+
\u2502 \u251C\u2500\u2500 Persistence/ # Database implementations
|
|
1283
|
+
\u2502 \u251C\u2500\u2500 ExternalServices/ # Third-party integrations
|
|
1284
|
+
\u2502 \u2514\u2500\u2500 Providers/ # Service providers
|
|
1285
|
+
\u2514\u2500\u2500 Interface/ # Interface Adapters
|
|
1286
|
+
\u251C\u2500\u2500 Http/ # HTTP controllers & routes
|
|
1287
|
+
\u2514\u2500\u2500 Presenters/ # Output formatters
|
|
1288
|
+
\`\`\`
|
|
1289
|
+
|
|
1290
|
+
## Key Benefits
|
|
1291
|
+
|
|
1292
|
+
1. **Testable**: Domain and Use Cases can be tested without frameworks
|
|
1293
|
+
2. **Independent**: Business logic is framework-agnostic
|
|
1294
|
+
3. **Maintainable**: Changes in outer layers don't affect inner layers
|
|
1295
|
+
4. **Flexible**: Easy to swap databases or frameworks
|
|
1296
|
+
|
|
1297
|
+
Created with \u2764\uFE0F using Gravito Framework
|
|
1298
|
+
`;
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
// src/generators/DddGenerator.ts
|
|
1303
|
+
var DddGenerator = class extends BaseGenerator {
|
|
1304
|
+
get architectureType() {
|
|
1305
|
+
return "ddd";
|
|
1306
|
+
}
|
|
1307
|
+
get displayName() {
|
|
1308
|
+
return "Domain-Driven Design (DDD)";
|
|
1309
|
+
}
|
|
1310
|
+
get description() {
|
|
1311
|
+
return "Full DDD with Bounded Contexts, Aggregates, and Event-Driven patterns";
|
|
1312
|
+
}
|
|
1313
|
+
getDirectoryStructure(context) {
|
|
1314
|
+
return [
|
|
1315
|
+
{
|
|
1316
|
+
type: "directory",
|
|
1317
|
+
name: "config",
|
|
1318
|
+
children: [
|
|
1319
|
+
{ type: "file", name: "app.ts", content: this.generateAppConfig(context) },
|
|
1320
|
+
{ type: "file", name: "database.ts", content: this.generateDatabaseConfig() },
|
|
1321
|
+
{ type: "file", name: "modules.ts", content: this.generateModulesConfig() },
|
|
1322
|
+
{ type: "file", name: "cache.ts", content: this.generateCacheConfig() },
|
|
1323
|
+
{ type: "file", name: "logging.ts", content: this.generateLoggingConfig() }
|
|
1324
|
+
]
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
type: "directory",
|
|
1328
|
+
name: "src",
|
|
1329
|
+
children: [
|
|
1330
|
+
// Bootstrap - Application startup and configuration
|
|
1331
|
+
this.generateBootstrapDirectory(context),
|
|
1332
|
+
// Shared - Cross-module shared components
|
|
1333
|
+
this.generateShared(),
|
|
1334
|
+
// Modules - Bounded Contexts
|
|
1335
|
+
{
|
|
1336
|
+
type: "directory",
|
|
1337
|
+
name: "Modules",
|
|
1338
|
+
children: [
|
|
1339
|
+
this.generateModule("Ordering", context),
|
|
1340
|
+
this.generateModule("Catalog", context)
|
|
1341
|
+
]
|
|
1342
|
+
},
|
|
1343
|
+
{ type: "file", name: "main.ts", content: this.generateMainEntry(context) }
|
|
1344
|
+
]
|
|
1345
|
+
},
|
|
1346
|
+
{
|
|
1347
|
+
type: "directory",
|
|
1348
|
+
name: "tests",
|
|
1349
|
+
children: [
|
|
1350
|
+
{
|
|
1351
|
+
type: "directory",
|
|
1352
|
+
name: "Modules",
|
|
1353
|
+
children: [
|
|
1354
|
+
{
|
|
1355
|
+
type: "directory",
|
|
1356
|
+
name: "Ordering",
|
|
1357
|
+
children: [
|
|
1358
|
+
{
|
|
1359
|
+
type: "directory",
|
|
1360
|
+
name: "Unit",
|
|
1361
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
type: "directory",
|
|
1365
|
+
name: "Integration",
|
|
1366
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
1367
|
+
}
|
|
1368
|
+
]
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
type: "directory",
|
|
1372
|
+
name: "Catalog",
|
|
1373
|
+
children: [
|
|
1374
|
+
{
|
|
1375
|
+
type: "directory",
|
|
1376
|
+
name: "Unit",
|
|
1377
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
1378
|
+
}
|
|
1379
|
+
]
|
|
1380
|
+
}
|
|
1381
|
+
]
|
|
1382
|
+
},
|
|
1383
|
+
{
|
|
1384
|
+
type: "directory",
|
|
1385
|
+
name: "Shared",
|
|
1386
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
1387
|
+
}
|
|
1388
|
+
]
|
|
1389
|
+
}
|
|
1390
|
+
];
|
|
1391
|
+
}
|
|
1392
|
+
// ─────────────────────────────────────────────────────────────
|
|
1393
|
+
// Bootstrap Directory
|
|
1394
|
+
// ─────────────────────────────────────────────────────────────
|
|
1395
|
+
generateBootstrapDirectory(context) {
|
|
1396
|
+
return {
|
|
1397
|
+
type: "directory",
|
|
1398
|
+
name: "Bootstrap",
|
|
1399
|
+
children: [
|
|
1400
|
+
{ type: "file", name: "app.ts", content: this.generateBootstrapApp(context) },
|
|
1401
|
+
{ type: "file", name: "providers.ts", content: this.generateProvidersRegistry(context) },
|
|
1402
|
+
{ type: "file", name: "events.ts", content: this.generateEventsRegistry() },
|
|
1403
|
+
{ type: "file", name: "routes.ts", content: this.generateRoutesRegistry(context) }
|
|
1404
|
+
]
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
// ─────────────────────────────────────────────────────────────
|
|
1408
|
+
// Shared Directory (replaces SharedKernel with user's structure)
|
|
1409
|
+
// ─────────────────────────────────────────────────────────────
|
|
1410
|
+
generateShared() {
|
|
1411
|
+
return {
|
|
1412
|
+
type: "directory",
|
|
1413
|
+
name: "Shared",
|
|
1414
|
+
children: [
|
|
1415
|
+
{
|
|
1416
|
+
type: "directory",
|
|
1417
|
+
name: "Domain",
|
|
1418
|
+
children: [
|
|
1419
|
+
{
|
|
1420
|
+
type: "directory",
|
|
1421
|
+
name: "ValueObjects",
|
|
1422
|
+
children: [
|
|
1423
|
+
{ type: "file", name: "Id.ts", content: this.generateIdValueObject() },
|
|
1424
|
+
{ type: "file", name: "Money.ts", content: this.generateMoneyValueObject() },
|
|
1425
|
+
{ type: "file", name: "Email.ts", content: this.generateEmailValueObject() }
|
|
1426
|
+
]
|
|
1427
|
+
},
|
|
1428
|
+
{
|
|
1429
|
+
type: "directory",
|
|
1430
|
+
name: "Events",
|
|
1431
|
+
children: [
|
|
1432
|
+
{ type: "file", name: "DomainEvent.ts", content: this.generateDomainEvent() }
|
|
1433
|
+
]
|
|
1434
|
+
},
|
|
1435
|
+
{
|
|
1436
|
+
type: "directory",
|
|
1437
|
+
name: "Primitives",
|
|
1438
|
+
children: [
|
|
1439
|
+
{ type: "file", name: "AggregateRoot.ts", content: this.generateAggregateRoot() },
|
|
1440
|
+
{ type: "file", name: "Entity.ts", content: this.generateEntity() },
|
|
1441
|
+
{ type: "file", name: "ValueObject.ts", content: this.generateValueObject() }
|
|
1442
|
+
]
|
|
1443
|
+
}
|
|
1444
|
+
]
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
type: "directory",
|
|
1448
|
+
name: "Infrastructure",
|
|
1449
|
+
children: [
|
|
1450
|
+
{
|
|
1451
|
+
type: "directory",
|
|
1452
|
+
name: "EventBus",
|
|
1453
|
+
children: [
|
|
1454
|
+
{
|
|
1455
|
+
type: "file",
|
|
1456
|
+
name: "EventDispatcher.ts",
|
|
1457
|
+
content: this.generateEventDispatcher()
|
|
1458
|
+
}
|
|
1459
|
+
]
|
|
1460
|
+
}
|
|
1461
|
+
]
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
type: "directory",
|
|
1465
|
+
name: "Exceptions",
|
|
1466
|
+
children: [
|
|
1467
|
+
{ type: "file", name: "Handler.ts", content: this.generateExceptionHandler() }
|
|
1468
|
+
]
|
|
1469
|
+
}
|
|
1470
|
+
]
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
// ─────────────────────────────────────────────────────────────
|
|
1474
|
+
// Module Generator (replaces Bounded Context)
|
|
1475
|
+
// ─────────────────────────────────────────────────────────────
|
|
1476
|
+
generateModule(name, context) {
|
|
1477
|
+
return {
|
|
1478
|
+
type: "directory",
|
|
1479
|
+
name,
|
|
1480
|
+
children: [
|
|
1481
|
+
// Domain Layer
|
|
1482
|
+
{
|
|
1483
|
+
type: "directory",
|
|
1484
|
+
name: "Domain",
|
|
1485
|
+
children: [
|
|
1486
|
+
{
|
|
1487
|
+
type: "directory",
|
|
1488
|
+
name: "Aggregates",
|
|
1489
|
+
children: [
|
|
1490
|
+
{
|
|
1491
|
+
type: "directory",
|
|
1492
|
+
name,
|
|
1493
|
+
children: [
|
|
1494
|
+
{ type: "file", name: `${name}.ts`, content: this.generateAggregate(name) },
|
|
1495
|
+
{
|
|
1496
|
+
type: "file",
|
|
1497
|
+
name: `${name}Status.ts`,
|
|
1498
|
+
content: this.generateAggregateStatus(name)
|
|
1499
|
+
}
|
|
1500
|
+
]
|
|
1501
|
+
}
|
|
1502
|
+
]
|
|
1503
|
+
},
|
|
1504
|
+
{
|
|
1505
|
+
type: "directory",
|
|
1506
|
+
name: "Events",
|
|
1507
|
+
children: [
|
|
1508
|
+
{
|
|
1509
|
+
type: "file",
|
|
1510
|
+
name: `${name}Created.ts`,
|
|
1511
|
+
content: this.generateCreatedEvent(name)
|
|
1512
|
+
}
|
|
1513
|
+
]
|
|
1514
|
+
},
|
|
1515
|
+
{
|
|
1516
|
+
type: "directory",
|
|
1517
|
+
name: "Repositories",
|
|
1518
|
+
children: [
|
|
1519
|
+
{
|
|
1520
|
+
type: "file",
|
|
1521
|
+
name: `I${name}Repository.ts`,
|
|
1522
|
+
content: this.generateRepositoryInterface(name)
|
|
1523
|
+
}
|
|
1524
|
+
]
|
|
1525
|
+
},
|
|
1526
|
+
{
|
|
1527
|
+
type: "directory",
|
|
1528
|
+
name: "Services",
|
|
1529
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
1530
|
+
}
|
|
1531
|
+
]
|
|
1532
|
+
},
|
|
1533
|
+
// Application Layer
|
|
1534
|
+
{
|
|
1535
|
+
type: "directory",
|
|
1536
|
+
name: "Application",
|
|
1537
|
+
children: [
|
|
1538
|
+
{
|
|
1539
|
+
type: "directory",
|
|
1540
|
+
name: "Commands",
|
|
1541
|
+
children: [
|
|
1542
|
+
{
|
|
1543
|
+
type: "directory",
|
|
1544
|
+
name: `Create${name}`,
|
|
1545
|
+
children: [
|
|
1546
|
+
{
|
|
1547
|
+
type: "file",
|
|
1548
|
+
name: `Create${name}Command.ts`,
|
|
1549
|
+
content: this.generateCommand(name)
|
|
1550
|
+
},
|
|
1551
|
+
{
|
|
1552
|
+
type: "file",
|
|
1553
|
+
name: `Create${name}Handler.ts`,
|
|
1554
|
+
content: this.generateCommandHandler(name)
|
|
1555
|
+
}
|
|
1556
|
+
]
|
|
1557
|
+
}
|
|
1558
|
+
]
|
|
1559
|
+
},
|
|
1560
|
+
{
|
|
1561
|
+
type: "directory",
|
|
1562
|
+
name: "Queries",
|
|
1563
|
+
children: [
|
|
1564
|
+
{
|
|
1565
|
+
type: "directory",
|
|
1566
|
+
name: `Get${name}ById`,
|
|
1567
|
+
children: [
|
|
1568
|
+
{
|
|
1569
|
+
type: "file",
|
|
1570
|
+
name: `Get${name}ByIdQuery.ts`,
|
|
1571
|
+
content: this.generateQuery(name)
|
|
1572
|
+
},
|
|
1573
|
+
{
|
|
1574
|
+
type: "file",
|
|
1575
|
+
name: `Get${name}ByIdHandler.ts`,
|
|
1576
|
+
content: this.generateQueryHandler(name)
|
|
1577
|
+
}
|
|
1578
|
+
]
|
|
1579
|
+
}
|
|
1580
|
+
]
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
type: "directory",
|
|
1584
|
+
name: "DTOs",
|
|
1585
|
+
children: [{ type: "file", name: `${name}DTO.ts`, content: this.generateDTO(name) }]
|
|
1586
|
+
}
|
|
1587
|
+
]
|
|
1588
|
+
},
|
|
1589
|
+
// Infrastructure Layer
|
|
1590
|
+
{
|
|
1591
|
+
type: "directory",
|
|
1592
|
+
name: "Infrastructure",
|
|
1593
|
+
children: [
|
|
1594
|
+
{
|
|
1595
|
+
type: "directory",
|
|
1596
|
+
name: "Persistence",
|
|
1597
|
+
children: [
|
|
1598
|
+
{
|
|
1599
|
+
type: "file",
|
|
1600
|
+
name: `${name}Repository.ts`,
|
|
1601
|
+
content: this.generateRepository(name)
|
|
1602
|
+
}
|
|
1603
|
+
]
|
|
1604
|
+
},
|
|
1605
|
+
{
|
|
1606
|
+
type: "directory",
|
|
1607
|
+
name: "Providers",
|
|
1608
|
+
children: [
|
|
1609
|
+
{
|
|
1610
|
+
type: "file",
|
|
1611
|
+
name: `${name}ServiceProvider.ts`,
|
|
1612
|
+
content: this.generateModuleServiceProvider(name, context)
|
|
1613
|
+
}
|
|
1614
|
+
]
|
|
1615
|
+
}
|
|
1616
|
+
]
|
|
1617
|
+
}
|
|
1618
|
+
]
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
// ─────────────────────────────────────────────────────────────
|
|
1622
|
+
// Bootstrap File Generators
|
|
1623
|
+
// ─────────────────────────────────────────────────────────────
|
|
1624
|
+
generateBootstrapApp(context) {
|
|
1625
|
+
return `/**
|
|
1626
|
+
* Application Bootstrap
|
|
1627
|
+
*
|
|
1628
|
+
* Central configuration and initialization of the application.
|
|
1629
|
+
*/
|
|
1630
|
+
|
|
1631
|
+
import { PlanetCore } from 'gravito-core'
|
|
1632
|
+
import { registerProviders } from './providers'
|
|
1633
|
+
import { registerRoutes } from './routes'
|
|
1634
|
+
|
|
1635
|
+
export async function createApp(): Promise<PlanetCore> {
|
|
1636
|
+
const core = new PlanetCore({
|
|
1637
|
+
config: {
|
|
1638
|
+
APP_NAME: '${context.name}',
|
|
1639
|
+
},
|
|
1640
|
+
})
|
|
1641
|
+
|
|
1642
|
+
// Register all service providers
|
|
1643
|
+
await registerProviders(core)
|
|
1644
|
+
|
|
1645
|
+
// Bootstrap the application
|
|
1646
|
+
await core.bootstrap()
|
|
1647
|
+
|
|
1648
|
+
// Register routes
|
|
1649
|
+
registerRoutes(core.router)
|
|
1650
|
+
|
|
1651
|
+
return core
|
|
1652
|
+
}
|
|
1653
|
+
`;
|
|
1654
|
+
}
|
|
1655
|
+
generateProvidersRegistry(_context) {
|
|
1656
|
+
return `/**
|
|
1657
|
+
* Service Providers Registry
|
|
1658
|
+
*
|
|
1659
|
+
* Register all module service providers here.
|
|
1660
|
+
*/
|
|
1661
|
+
|
|
1662
|
+
import type { PlanetCore } from 'gravito-core'
|
|
1663
|
+
import { OrderingServiceProvider } from '../Modules/Ordering/Infrastructure/Providers/OrderingServiceProvider'
|
|
1664
|
+
import { CatalogServiceProvider } from '../Modules/Catalog/Infrastructure/Providers/CatalogServiceProvider'
|
|
1665
|
+
|
|
1666
|
+
export async function registerProviders(core: PlanetCore): Promise<void> {
|
|
1667
|
+
// Register module providers
|
|
1668
|
+
core.register(new OrderingServiceProvider())
|
|
1669
|
+
core.register(new CatalogServiceProvider())
|
|
1670
|
+
|
|
1671
|
+
// Add more providers as needed
|
|
1672
|
+
}
|
|
1673
|
+
`;
|
|
1674
|
+
}
|
|
1675
|
+
generateEventsRegistry() {
|
|
1676
|
+
return `/**
|
|
1677
|
+
* Domain Events Registry
|
|
1678
|
+
*
|
|
1679
|
+
* Register all domain event handlers here.
|
|
1680
|
+
*/
|
|
1681
|
+
|
|
1682
|
+
import { EventDispatcher } from '../Shared/Infrastructure/EventBus/EventDispatcher'
|
|
1683
|
+
|
|
1684
|
+
export function registerEvents(dispatcher: EventDispatcher): void {
|
|
1685
|
+
// Register event handlers
|
|
1686
|
+
// dispatcher.subscribe('ordering.created', async (event) => { ... })
|
|
1687
|
+
}
|
|
1688
|
+
`;
|
|
1689
|
+
}
|
|
1690
|
+
generateRoutesRegistry(_context) {
|
|
1691
|
+
return `/**
|
|
1692
|
+
* Routes Registry
|
|
1693
|
+
*
|
|
1694
|
+
* Register all module routes here.
|
|
1695
|
+
*/
|
|
1696
|
+
|
|
1697
|
+
export function registerRoutes(router: any): void {
|
|
1698
|
+
// Health check
|
|
1699
|
+
router.get('/health', (c: any) => c.json({ status: 'healthy' }))
|
|
1700
|
+
|
|
1701
|
+
// Ordering module
|
|
1702
|
+
router.get('/api/orders', (c: any) => c.json({ message: 'Order list' }))
|
|
1703
|
+
router.post('/api/orders', (c: any) => c.json({ message: 'Order created' }, 201))
|
|
1704
|
+
|
|
1705
|
+
// Catalog module
|
|
1706
|
+
router.get('/api/products', (c: any) => c.json({ message: 'Product list' }))
|
|
1707
|
+
}
|
|
1708
|
+
`;
|
|
1709
|
+
}
|
|
1710
|
+
generateMainEntry(_context) {
|
|
1711
|
+
return `/**
|
|
1712
|
+
* Application Entry Point
|
|
1713
|
+
*
|
|
1714
|
+
* Start the HTTP server.
|
|
1715
|
+
*/
|
|
1716
|
+
|
|
1717
|
+
import { createApp } from './Bootstrap/app'
|
|
1718
|
+
|
|
1719
|
+
const app = await createApp()
|
|
1720
|
+
|
|
1721
|
+
export default app.liftoff()
|
|
1722
|
+
`;
|
|
1723
|
+
}
|
|
1724
|
+
generateModulesConfig() {
|
|
1725
|
+
return `/**
|
|
1726
|
+
* Modules Configuration
|
|
1727
|
+
*
|
|
1728
|
+
* Define module boundaries and their dependencies.
|
|
1729
|
+
*/
|
|
1730
|
+
|
|
1731
|
+
export default {
|
|
1732
|
+
modules: {
|
|
1733
|
+
ordering: {
|
|
1734
|
+
name: 'Ordering',
|
|
1735
|
+
description: 'Order management module',
|
|
1736
|
+
prefix: '/api/orders',
|
|
1737
|
+
},
|
|
1738
|
+
catalog: {
|
|
1739
|
+
name: 'Catalog',
|
|
1740
|
+
description: 'Product catalog module',
|
|
1741
|
+
prefix: '/api/products',
|
|
1742
|
+
},
|
|
1743
|
+
},
|
|
1744
|
+
|
|
1745
|
+
// Module dependencies
|
|
1746
|
+
dependencies: {
|
|
1747
|
+
ordering: ['catalog'], // Ordering depends on Catalog
|
|
1748
|
+
},
|
|
1749
|
+
}
|
|
1750
|
+
`;
|
|
1751
|
+
}
|
|
1752
|
+
generateModuleServiceProvider(name, _context) {
|
|
1753
|
+
return `/**
|
|
1754
|
+
* ${name} Service Provider
|
|
1755
|
+
*/
|
|
1756
|
+
|
|
1757
|
+
import { ServiceProvider, type Container, type PlanetCore } from 'gravito-core'
|
|
1758
|
+
import { ${name}Repository } from '../Persistence/${name}Repository'
|
|
1759
|
+
|
|
1760
|
+
export class ${name}ServiceProvider extends ServiceProvider {
|
|
1761
|
+
register(container: Container): void {
|
|
1762
|
+
container.singleton('${name.toLowerCase()}Repository', () => new ${name}Repository())
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
boot(_core: PlanetCore): void {
|
|
1766
|
+
console.log('[${name}] Module loaded')
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
`;
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Override package.json for DDD architecture (uses main.ts instead of bootstrap.ts)
|
|
1773
|
+
*/
|
|
1774
|
+
generatePackageJson(context) {
|
|
1775
|
+
const pkg = {
|
|
1776
|
+
name: context.nameKebabCase,
|
|
1777
|
+
version: "0.1.0",
|
|
1778
|
+
type: "module",
|
|
1779
|
+
scripts: {
|
|
1780
|
+
dev: "bun run --watch src/main.ts",
|
|
1781
|
+
build: "bun build ./src/main.ts --outdir ./dist --target bun",
|
|
1782
|
+
start: "bun run dist/main.js",
|
|
1783
|
+
test: "bun test",
|
|
1784
|
+
typecheck: "tsc --noEmit"
|
|
1785
|
+
},
|
|
1786
|
+
dependencies: {
|
|
1787
|
+
"gravito-core": "^1.0.0-beta.5"
|
|
1788
|
+
},
|
|
1789
|
+
devDependencies: {
|
|
1790
|
+
"@types/bun": "latest",
|
|
1791
|
+
typescript: "^5.0.0"
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
return JSON.stringify(pkg, null, 2);
|
|
1795
|
+
}
|
|
1796
|
+
// ─────────────────────────────────────────────────────────────
|
|
1797
|
+
// Config Generators
|
|
1798
|
+
// ─────────────────────────────────────────────────────────────
|
|
1799
|
+
generateAppConfig(context) {
|
|
1800
|
+
return `export default {
|
|
1801
|
+
name: process.env.APP_NAME ?? '${context.name}',
|
|
1802
|
+
env: process.env.APP_ENV ?? 'development',
|
|
1803
|
+
debug: process.env.APP_DEBUG === 'true',
|
|
1804
|
+
url: process.env.APP_URL ?? 'http://localhost:3000',
|
|
1805
|
+
}
|
|
1806
|
+
`;
|
|
1807
|
+
}
|
|
1808
|
+
generateDatabaseConfig() {
|
|
1809
|
+
return `export default {
|
|
1810
|
+
default: process.env.DB_CONNECTION ?? 'sqlite',
|
|
1811
|
+
connections: {
|
|
1812
|
+
sqlite: { driver: 'sqlite', database: 'database/database.sqlite' },
|
|
1813
|
+
},
|
|
1814
|
+
}
|
|
1815
|
+
`;
|
|
1816
|
+
}
|
|
1817
|
+
generateCacheConfig() {
|
|
1818
|
+
return `export default {
|
|
1819
|
+
default: process.env.CACHE_DRIVER ?? 'memory',
|
|
1820
|
+
stores: { memory: { driver: 'memory' } },
|
|
1821
|
+
}
|
|
1822
|
+
`;
|
|
1823
|
+
}
|
|
1824
|
+
generateLoggingConfig() {
|
|
1825
|
+
return `export default {
|
|
1826
|
+
default: 'console',
|
|
1827
|
+
channels: { console: { driver: 'console', level: 'debug' } },
|
|
1828
|
+
}
|
|
1829
|
+
`;
|
|
1830
|
+
}
|
|
1831
|
+
// ─────────────────────────────────────────────────────────────
|
|
1832
|
+
// Shared Kernel Files
|
|
1833
|
+
// ─────────────────────────────────────────────────────────────
|
|
1834
|
+
generateIdValueObject() {
|
|
1835
|
+
return `/**
|
|
1836
|
+
* ID Value Object
|
|
1837
|
+
*
|
|
1838
|
+
* Shared identifier across all contexts.
|
|
1839
|
+
*/
|
|
1840
|
+
|
|
1841
|
+
export class Id {
|
|
1842
|
+
private readonly _value: string
|
|
1843
|
+
|
|
1844
|
+
private constructor(value: string) {
|
|
1845
|
+
this._value = value
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
static create(): Id {
|
|
1849
|
+
return new Id(crypto.randomUUID())
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
static from(value: string): Id {
|
|
1853
|
+
if (!value) throw new Error('Id cannot be empty')
|
|
1854
|
+
return new Id(value)
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
get value(): string {
|
|
1858
|
+
return this._value
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
equals(other: Id): boolean {
|
|
1862
|
+
return this._value === other._value
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
toString(): string {
|
|
1866
|
+
return this._value
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
`;
|
|
1870
|
+
}
|
|
1871
|
+
generateMoneyValueObject() {
|
|
1872
|
+
return `/**
|
|
1873
|
+
* Money Value Object
|
|
1874
|
+
*/
|
|
1875
|
+
|
|
1876
|
+
export class Money {
|
|
1877
|
+
constructor(
|
|
1878
|
+
public readonly amount: number,
|
|
1879
|
+
public readonly currency: string = 'USD'
|
|
1880
|
+
) {
|
|
1881
|
+
if (amount < 0) throw new Error('Amount cannot be negative')
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
add(other: Money): Money {
|
|
1885
|
+
this.assertSameCurrency(other)
|
|
1886
|
+
return new Money(this.amount + other.amount, this.currency)
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
subtract(other: Money): Money {
|
|
1890
|
+
this.assertSameCurrency(other)
|
|
1891
|
+
return new Money(this.amount - other.amount, this.currency)
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
private assertSameCurrency(other: Money): void {
|
|
1895
|
+
if (this.currency !== other.currency) {
|
|
1896
|
+
throw new Error('Cannot operate on different currencies')
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
equals(other: Money): boolean {
|
|
1901
|
+
return this.amount === other.amount && this.currency === other.currency
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
`;
|
|
1905
|
+
}
|
|
1906
|
+
generateEmailValueObject() {
|
|
1907
|
+
return `/**
|
|
1908
|
+
* Email Value Object
|
|
1909
|
+
*/
|
|
1910
|
+
|
|
1911
|
+
export class Email {
|
|
1912
|
+
private readonly _value: string
|
|
1913
|
+
|
|
1914
|
+
private constructor(value: string) {
|
|
1915
|
+
this._value = value.toLowerCase().trim()
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
static create(email: string): Email {
|
|
1919
|
+
if (!Email.isValid(email)) {
|
|
1920
|
+
throw new Error(\`Invalid email: \${email}\`)
|
|
1921
|
+
}
|
|
1922
|
+
return new Email(email)
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
static isValid(email: string): boolean {
|
|
1926
|
+
return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
get value(): string {
|
|
1930
|
+
return this._value
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
`;
|
|
1934
|
+
}
|
|
1935
|
+
generateDomainEvent() {
|
|
1936
|
+
return `/**
|
|
1937
|
+
* Domain Event Base
|
|
1938
|
+
*/
|
|
1939
|
+
|
|
1940
|
+
export abstract class DomainEvent {
|
|
1941
|
+
readonly occurredOn: Date
|
|
1942
|
+
readonly eventId: string
|
|
1943
|
+
|
|
1944
|
+
constructor() {
|
|
1945
|
+
this.occurredOn = new Date()
|
|
1946
|
+
this.eventId = crypto.randomUUID()
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
abstract get eventName(): string
|
|
1950
|
+
abstract get aggregateId(): string
|
|
1951
|
+
}
|
|
1952
|
+
`;
|
|
1953
|
+
}
|
|
1954
|
+
generateAggregateRoot() {
|
|
1955
|
+
return `/**
|
|
1956
|
+
* Aggregate Root Base
|
|
1957
|
+
*/
|
|
1958
|
+
|
|
1959
|
+
import type { DomainEvent } from '../Events/DomainEvent'
|
|
1960
|
+
import type { Id } from '../ValueObjects/Id'
|
|
1961
|
+
|
|
1962
|
+
export abstract class AggregateRoot<T extends Id = Id> {
|
|
1963
|
+
private _domainEvents: DomainEvent[] = []
|
|
1964
|
+
|
|
1965
|
+
protected constructor(protected readonly _id: T) {}
|
|
1966
|
+
|
|
1967
|
+
get id(): T {
|
|
1968
|
+
return this._id
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
get domainEvents(): DomainEvent[] {
|
|
1972
|
+
return [...this._domainEvents]
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
protected addDomainEvent(event: DomainEvent): void {
|
|
1976
|
+
this._domainEvents.push(event)
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
clearDomainEvents(): DomainEvent[] {
|
|
1980
|
+
const events = [...this._domainEvents]
|
|
1981
|
+
this._domainEvents = []
|
|
1982
|
+
return events
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
`;
|
|
1986
|
+
}
|
|
1987
|
+
generateEntity() {
|
|
1988
|
+
return `/**
|
|
1989
|
+
* Entity Base
|
|
1990
|
+
*/
|
|
1991
|
+
|
|
1992
|
+
import type { Id } from '../ValueObjects/Id'
|
|
1993
|
+
|
|
1994
|
+
export abstract class Entity<T extends Id = Id> {
|
|
1995
|
+
protected constructor(protected readonly _id: T) {}
|
|
1996
|
+
|
|
1997
|
+
get id(): T {
|
|
1998
|
+
return this._id
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
equals(other: Entity<T>): boolean {
|
|
2002
|
+
return this._id.equals(other._id)
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
`;
|
|
2006
|
+
}
|
|
2007
|
+
generateValueObject() {
|
|
2008
|
+
return `/**
|
|
2009
|
+
* Value Object Base
|
|
2010
|
+
*/
|
|
2011
|
+
|
|
2012
|
+
export abstract class ValueObject<T> {
|
|
2013
|
+
protected readonly props: T
|
|
2014
|
+
|
|
2015
|
+
constructor(props: T) {
|
|
2016
|
+
this.props = Object.freeze(props)
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
equals(other: ValueObject<T>): boolean {
|
|
2020
|
+
return JSON.stringify(this.props) === JSON.stringify(other.props)
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
`;
|
|
2024
|
+
}
|
|
2025
|
+
generateEventDispatcher() {
|
|
2026
|
+
return `/**
|
|
2027
|
+
* Event Dispatcher
|
|
2028
|
+
*/
|
|
2029
|
+
|
|
2030
|
+
import type { DomainEvent } from '../../Domain/Events/DomainEvent'
|
|
2031
|
+
|
|
2032
|
+
type EventHandler = (event: DomainEvent) => void | Promise<void>
|
|
2033
|
+
|
|
2034
|
+
export class EventDispatcher {
|
|
2035
|
+
private handlers: Map<string, EventHandler[]> = new Map()
|
|
2036
|
+
|
|
2037
|
+
subscribe(eventName: string, handler: EventHandler): void {
|
|
2038
|
+
const handlers = this.handlers.get(eventName) ?? []
|
|
2039
|
+
handlers.push(handler)
|
|
2040
|
+
this.handlers.set(eventName, handlers)
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
async dispatch(event: DomainEvent): Promise<void> {
|
|
2044
|
+
const handlers = this.handlers.get(event.eventName) ?? []
|
|
2045
|
+
for (const handler of handlers) {
|
|
2046
|
+
await handler(event)
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
async dispatchAll(events: DomainEvent[]): Promise<void> {
|
|
2051
|
+
for (const event of events) {
|
|
2052
|
+
await this.dispatch(event)
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
`;
|
|
2057
|
+
}
|
|
2058
|
+
// ─────────────────────────────────────────────────────────────
|
|
2059
|
+
// Bounded Context Templates
|
|
2060
|
+
// ─────────────────────────────────────────────────────────────
|
|
2061
|
+
generateAggregate(name) {
|
|
2062
|
+
return `/**
|
|
2063
|
+
* ${name} Aggregate Root
|
|
2064
|
+
*/
|
|
2065
|
+
|
|
2066
|
+
import { AggregateRoot } from '../../../../../Shared/Domain/Primitives/AggregateRoot'
|
|
2067
|
+
import { Id } from '../../../../../Shared/Domain/ValueObjects/Id'
|
|
2068
|
+
import { ${name}Created } from '../../Events/${name}Created'
|
|
2069
|
+
import { ${name}Status } from './${name}Status'
|
|
2070
|
+
|
|
2071
|
+
export interface ${name}Props {
|
|
2072
|
+
// Add properties here
|
|
2073
|
+
status: ${name}Status
|
|
2074
|
+
createdAt: Date
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
export class ${name} extends AggregateRoot {
|
|
2078
|
+
private props: ${name}Props
|
|
2079
|
+
|
|
2080
|
+
private constructor(id: Id, props: ${name}Props) {
|
|
2081
|
+
super(id)
|
|
2082
|
+
this.props = props
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
static create(id: Id): ${name} {
|
|
2086
|
+
const aggregate = new ${name}(id, {
|
|
2087
|
+
status: ${name}Status.PENDING,
|
|
2088
|
+
createdAt: new Date(),
|
|
2089
|
+
})
|
|
2090
|
+
|
|
2091
|
+
aggregate.addDomainEvent(new ${name}Created(id.value))
|
|
2092
|
+
|
|
2093
|
+
return aggregate
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
get status(): ${name}Status {
|
|
2097
|
+
return this.props.status
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
// Add domain methods here
|
|
2101
|
+
}
|
|
2102
|
+
`;
|
|
2103
|
+
}
|
|
2104
|
+
generateAggregateStatus(name) {
|
|
2105
|
+
return `/**
|
|
2106
|
+
* ${name} Status
|
|
2107
|
+
*/
|
|
2108
|
+
|
|
2109
|
+
export enum ${name}Status {
|
|
2110
|
+
PENDING = 'pending',
|
|
2111
|
+
ACTIVE = 'active',
|
|
2112
|
+
COMPLETED = 'completed',
|
|
2113
|
+
CANCELLED = 'cancelled',
|
|
2114
|
+
}
|
|
2115
|
+
`;
|
|
2116
|
+
}
|
|
2117
|
+
generateCreatedEvent(name) {
|
|
2118
|
+
return `/**
|
|
2119
|
+
* ${name} Created Event
|
|
2120
|
+
*/
|
|
2121
|
+
|
|
2122
|
+
import { DomainEvent } from '../../../../Shared/Domain/Events/DomainEvent'
|
|
2123
|
+
|
|
2124
|
+
export class ${name}Created extends DomainEvent {
|
|
2125
|
+
constructor(public readonly ${name.toLowerCase()}Id: string) {
|
|
2126
|
+
super()
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
get eventName(): string {
|
|
2130
|
+
return '${name.toLowerCase()}.created'
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
get aggregateId(): string {
|
|
2134
|
+
return this.${name.toLowerCase()}Id
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
`;
|
|
2138
|
+
}
|
|
2139
|
+
generateRepositoryInterface(name) {
|
|
2140
|
+
return `/**
|
|
2141
|
+
* ${name} Repository Interface
|
|
2142
|
+
*/
|
|
2143
|
+
|
|
2144
|
+
import type { ${name} } from '../Aggregates/${name}/${name}'
|
|
2145
|
+
|
|
2146
|
+
export interface I${name}Repository {
|
|
2147
|
+
findById(id: string): Promise<${name} | null>
|
|
2148
|
+
save(aggregate: ${name}): Promise<void>
|
|
2149
|
+
delete(id: string): Promise<void>
|
|
2150
|
+
}
|
|
2151
|
+
`;
|
|
2152
|
+
}
|
|
2153
|
+
generateCommand(name) {
|
|
2154
|
+
return `/**
|
|
2155
|
+
* Create ${name} Command
|
|
2156
|
+
*/
|
|
2157
|
+
|
|
2158
|
+
export class Create${name}Command {
|
|
2159
|
+
constructor(
|
|
2160
|
+
// Add command properties
|
|
2161
|
+
public readonly id?: string
|
|
2162
|
+
) {}
|
|
2163
|
+
}
|
|
2164
|
+
`;
|
|
2165
|
+
}
|
|
2166
|
+
generateCommandHandler(name) {
|
|
2167
|
+
return `/**
|
|
2168
|
+
* Create ${name} Handler
|
|
2169
|
+
*/
|
|
2170
|
+
|
|
2171
|
+
import type { I${name}Repository } from '../../../Domain/Repositories/I${name}Repository'
|
|
2172
|
+
import { ${name} } from '../../../Domain/Aggregates/${name}/${name}'
|
|
2173
|
+
import { Id } from '../../../../../Shared/Domain/ValueObjects/Id'
|
|
2174
|
+
import type { Create${name}Command } from './Create${name}Command'
|
|
2175
|
+
|
|
2176
|
+
export class Create${name}Handler {
|
|
2177
|
+
constructor(private repository: I${name}Repository) {}
|
|
2178
|
+
|
|
2179
|
+
async handle(command: Create${name}Command): Promise<string> {
|
|
2180
|
+
const id = command.id ? Id.from(command.id) : Id.create()
|
|
2181
|
+
const aggregate = ${name}.create(id)
|
|
2182
|
+
|
|
2183
|
+
await this.repository.save(aggregate)
|
|
2184
|
+
|
|
2185
|
+
return id.value
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
`;
|
|
2189
|
+
}
|
|
2190
|
+
generateQuery(name) {
|
|
2191
|
+
return `/**
|
|
2192
|
+
* Get ${name} By Id Query
|
|
2193
|
+
*/
|
|
2194
|
+
|
|
2195
|
+
export class Get${name}ByIdQuery {
|
|
2196
|
+
constructor(public readonly id: string) {}
|
|
2197
|
+
}
|
|
2198
|
+
`;
|
|
2199
|
+
}
|
|
2200
|
+
generateQueryHandler(name) {
|
|
2201
|
+
return `/**
|
|
2202
|
+
* Get ${name} By Id Handler
|
|
2203
|
+
*/
|
|
2204
|
+
|
|
2205
|
+
import type { I${name}Repository } from '../../../Domain/Repositories/I${name}Repository'
|
|
2206
|
+
import type { ${name}DTO } from '../../DTOs/${name}DTO'
|
|
2207
|
+
import type { Get${name}ByIdQuery } from './Get${name}ByIdQuery'
|
|
2208
|
+
|
|
2209
|
+
export class Get${name}ByIdHandler {
|
|
2210
|
+
constructor(private repository: I${name}Repository) {}
|
|
2211
|
+
|
|
2212
|
+
async handle(query: Get${name}ByIdQuery): Promise<${name}DTO | null> {
|
|
2213
|
+
const aggregate = await this.repository.findById(query.id)
|
|
2214
|
+
if (!aggregate) return null
|
|
2215
|
+
|
|
2216
|
+
return {
|
|
2217
|
+
id: aggregate.id.value,
|
|
2218
|
+
status: aggregate.status,
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
`;
|
|
2223
|
+
}
|
|
2224
|
+
generateDTO(name) {
|
|
2225
|
+
return `/**
|
|
2226
|
+
* ${name} DTO
|
|
2227
|
+
*/
|
|
2228
|
+
|
|
2229
|
+
import type { ${name}Status } from '../../Domain/Aggregates/${name}/${name}Status'
|
|
2230
|
+
|
|
2231
|
+
export interface ${name}DTO {
|
|
2232
|
+
id: string
|
|
2233
|
+
status: ${name}Status
|
|
2234
|
+
// Add more fields
|
|
2235
|
+
}
|
|
2236
|
+
`;
|
|
2237
|
+
}
|
|
2238
|
+
generateRepository(name) {
|
|
2239
|
+
return `/**
|
|
2240
|
+
* ${name} Repository Implementation
|
|
2241
|
+
*/
|
|
2242
|
+
|
|
2243
|
+
import type { ${name} } from '../../Domain/Aggregates/${name}/${name}'
|
|
2244
|
+
import type { I${name}Repository } from '../../Domain/Repositories/I${name}Repository'
|
|
2245
|
+
|
|
2246
|
+
const store = new Map<string, ${name}>()
|
|
2247
|
+
|
|
2248
|
+
export class ${name}Repository implements I${name}Repository {
|
|
2249
|
+
async findById(id: string): Promise<${name} | null> {
|
|
2250
|
+
return store.get(id) ?? null
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
async save(aggregate: ${name}): Promise<void> {
|
|
2254
|
+
store.set(aggregate.id.value, aggregate)
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
async delete(id: string): Promise<void> {
|
|
2258
|
+
store.delete(id)
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
`;
|
|
2262
|
+
}
|
|
2263
|
+
generateExceptionHandler() {
|
|
2264
|
+
return `/**
|
|
2265
|
+
* Exception Handler
|
|
2266
|
+
*/
|
|
2267
|
+
|
|
2268
|
+
export function report(error: unknown): void {
|
|
2269
|
+
console.error('[Exception]', error)
|
|
2270
|
+
}
|
|
2271
|
+
`;
|
|
2272
|
+
}
|
|
2273
|
+
generateArchitectureDoc(context) {
|
|
2274
|
+
return `# ${context.name} - DDD Architecture Guide
|
|
2275
|
+
|
|
2276
|
+
## Overview
|
|
2277
|
+
|
|
2278
|
+
This project follows **Domain-Driven Design (DDD)** with strategic and tactical patterns.
|
|
2279
|
+
|
|
2280
|
+
## Bounded Contexts
|
|
2281
|
+
|
|
2282
|
+
\`\`\`
|
|
2283
|
+
\u250C\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\u2510
|
|
2284
|
+
\u2502 Ordering \u2502\u2500\u2500\u2500\u2500\u25B6\u2502 Catalog \u2502
|
|
2285
|
+
\u2502 (Core Domain) \u2502 \u2502 (Supporting) \u2502
|
|
2286
|
+
\u2514\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\u2518
|
|
2287
|
+
\u2502
|
|
2288
|
+
\u25BC
|
|
2289
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
2290
|
+
\u2502 SharedKernel \u2502
|
|
2291
|
+
\u2502 (Shared Types) \u2502
|
|
2292
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
2293
|
+
\`\`\`
|
|
2294
|
+
|
|
2295
|
+
## Context Structure
|
|
2296
|
+
|
|
2297
|
+
Each bounded context follows this structure:
|
|
2298
|
+
|
|
2299
|
+
\`\`\`
|
|
2300
|
+
Context/
|
|
2301
|
+
\u251C\u2500\u2500 Domain/ # Core business logic
|
|
2302
|
+
\u2502 \u251C\u2500\u2500 Aggregates/ # Aggregate roots + entities
|
|
2303
|
+
\u2502 \u251C\u2500\u2500 Events/ # Domain events
|
|
2304
|
+
\u2502 \u251C\u2500\u2500 Repositories/ # Repository interfaces
|
|
2305
|
+
\u2502 \u2514\u2500\u2500 Services/ # Domain services
|
|
2306
|
+
\u251C\u2500\u2500 Application/ # Use cases
|
|
2307
|
+
\u2502 \u251C\u2500\u2500 Commands/ # Write operations
|
|
2308
|
+
\u2502 \u251C\u2500\u2500 Queries/ # Read operations
|
|
2309
|
+
\u2502 \u251C\u2500\u2500 EventHandlers/ # Event reactions
|
|
2310
|
+
\u2502 \u2514\u2500\u2500 DTOs/ # Data transfer objects
|
|
2311
|
+
\u251C\u2500\u2500 Infrastructure/ # External concerns
|
|
2312
|
+
\u2502 \u251C\u2500\u2500 Persistence/ # Repository implementations
|
|
2313
|
+
\u2502 \u2514\u2500\u2500 Providers/ # DI configuration
|
|
2314
|
+
\u2514\u2500\u2500 UserInterface/ # Entry points
|
|
2315
|
+
\u251C\u2500\u2500 Http/ # REST controllers
|
|
2316
|
+
\u2514\u2500\u2500 Cli/ # CLI commands
|
|
2317
|
+
\`\`\`
|
|
2318
|
+
|
|
2319
|
+
## SharedKernel
|
|
2320
|
+
|
|
2321
|
+
Contains types shared across contexts:
|
|
2322
|
+
- **ValueObjects**: Id, Money, Email
|
|
2323
|
+
- **Primitives**: AggregateRoot, Entity, ValueObject
|
|
2324
|
+
- **Events**: DomainEvent base class
|
|
2325
|
+
- **EventBus**: Event dispatcher
|
|
2326
|
+
|
|
2327
|
+
## Key Patterns
|
|
2328
|
+
|
|
2329
|
+
1. **Aggregates**: Consistency boundaries
|
|
2330
|
+
2. **Domain Events**: Inter-context communication
|
|
2331
|
+
3. **CQRS**: Separate read/write models
|
|
2332
|
+
4. **Repository Pattern**: Persistence abstraction
|
|
2333
|
+
|
|
2334
|
+
Created with \u2764\uFE0F using Gravito Framework
|
|
2335
|
+
`;
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
|
|
2339
|
+
// src/generators/EnterpriseMvcGenerator.ts
|
|
2340
|
+
var EnterpriseMvcGenerator = class extends BaseGenerator {
|
|
2341
|
+
get architectureType() {
|
|
2342
|
+
return "enterprise-mvc";
|
|
2343
|
+
}
|
|
2344
|
+
get displayName() {
|
|
2345
|
+
return "Enterprise MVC";
|
|
2346
|
+
}
|
|
2347
|
+
get description() {
|
|
2348
|
+
return "Laravel-inspired MVC with Services and Repositories for enterprise applications";
|
|
2349
|
+
}
|
|
2350
|
+
getDirectoryStructure(context) {
|
|
2351
|
+
return [
|
|
2352
|
+
{
|
|
2353
|
+
type: "directory",
|
|
2354
|
+
name: "config",
|
|
2355
|
+
children: [
|
|
2356
|
+
{ type: "file", name: "app.ts", content: this.generateAppConfig(context) },
|
|
2357
|
+
{ type: "file", name: "database.ts", content: this.generateDatabaseConfig() },
|
|
2358
|
+
{ type: "file", name: "auth.ts", content: this.generateAuthConfig() },
|
|
2359
|
+
{ type: "file", name: "cache.ts", content: this.generateCacheConfig() },
|
|
2360
|
+
{ type: "file", name: "logging.ts", content: this.generateLoggingConfig() }
|
|
2361
|
+
]
|
|
2362
|
+
},
|
|
2363
|
+
{
|
|
2364
|
+
type: "directory",
|
|
2365
|
+
name: "src",
|
|
2366
|
+
children: [
|
|
2367
|
+
{
|
|
2368
|
+
type: "directory",
|
|
2369
|
+
name: "Http",
|
|
2370
|
+
children: [
|
|
2371
|
+
{ type: "file", name: "Kernel.ts", content: this.generateHttpKernel() },
|
|
2372
|
+
{
|
|
2373
|
+
type: "directory",
|
|
2374
|
+
name: "Controllers",
|
|
2375
|
+
children: [
|
|
2376
|
+
{ type: "file", name: "Controller.ts", content: this.generateBaseController() },
|
|
2377
|
+
{
|
|
2378
|
+
type: "file",
|
|
2379
|
+
name: "HomeController.ts",
|
|
2380
|
+
content: this.generateHomeController(context)
|
|
2381
|
+
}
|
|
2382
|
+
]
|
|
2383
|
+
},
|
|
2384
|
+
{
|
|
2385
|
+
type: "directory",
|
|
2386
|
+
name: "Middleware",
|
|
2387
|
+
children: [
|
|
2388
|
+
{ type: "file", name: "Authenticate.ts", content: this.generateAuthMiddleware() }
|
|
2389
|
+
]
|
|
2390
|
+
}
|
|
2391
|
+
]
|
|
2392
|
+
},
|
|
2393
|
+
{
|
|
2394
|
+
type: "directory",
|
|
2395
|
+
name: "Services",
|
|
2396
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
2397
|
+
},
|
|
2398
|
+
{
|
|
2399
|
+
type: "directory",
|
|
2400
|
+
name: "Repositories",
|
|
2401
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
2402
|
+
},
|
|
2403
|
+
{
|
|
2404
|
+
type: "directory",
|
|
2405
|
+
name: "Models",
|
|
2406
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
2407
|
+
},
|
|
2408
|
+
{
|
|
2409
|
+
type: "directory",
|
|
2410
|
+
name: "Resources",
|
|
2411
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
2412
|
+
},
|
|
2413
|
+
{
|
|
2414
|
+
type: "directory",
|
|
2415
|
+
name: "Providers",
|
|
2416
|
+
children: [
|
|
2417
|
+
{
|
|
2418
|
+
type: "file",
|
|
2419
|
+
name: "AppServiceProvider.ts",
|
|
2420
|
+
content: this.generateAppServiceProvider(context)
|
|
2421
|
+
},
|
|
2422
|
+
{
|
|
2423
|
+
type: "file",
|
|
2424
|
+
name: "RouteServiceProvider.ts",
|
|
2425
|
+
content: this.generateRouteServiceProvider(context)
|
|
2426
|
+
}
|
|
2427
|
+
]
|
|
2428
|
+
},
|
|
2429
|
+
{
|
|
2430
|
+
type: "directory",
|
|
2431
|
+
name: "Exceptions",
|
|
2432
|
+
children: [
|
|
2433
|
+
{ type: "file", name: "Handler.ts", content: this.generateExceptionHandler() }
|
|
2434
|
+
]
|
|
2435
|
+
},
|
|
2436
|
+
{ type: "file", name: "bootstrap.ts", content: this.generateBootstrap(context) },
|
|
2437
|
+
{ type: "file", name: "routes.ts", content: this.generateRoutes(context) }
|
|
2438
|
+
]
|
|
2439
|
+
},
|
|
2440
|
+
{
|
|
2441
|
+
type: "directory",
|
|
2442
|
+
name: "tests",
|
|
2443
|
+
children: [
|
|
2444
|
+
{
|
|
2445
|
+
type: "directory",
|
|
2446
|
+
name: "Unit",
|
|
2447
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
2448
|
+
},
|
|
2449
|
+
{
|
|
2450
|
+
type: "directory",
|
|
2451
|
+
name: "Feature",
|
|
2452
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
2453
|
+
}
|
|
2454
|
+
]
|
|
2455
|
+
},
|
|
2456
|
+
{
|
|
2457
|
+
type: "directory",
|
|
2458
|
+
name: "database",
|
|
2459
|
+
children: [
|
|
2460
|
+
{
|
|
2461
|
+
type: "directory",
|
|
2462
|
+
name: "migrations",
|
|
2463
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
2464
|
+
},
|
|
2465
|
+
{
|
|
2466
|
+
type: "directory",
|
|
2467
|
+
name: "seeders",
|
|
2468
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
2469
|
+
}
|
|
2470
|
+
]
|
|
2471
|
+
}
|
|
2472
|
+
];
|
|
2473
|
+
}
|
|
2474
|
+
// ─────────────────────────────────────────────────────────────
|
|
2475
|
+
// Config Generators
|
|
2476
|
+
// ─────────────────────────────────────────────────────────────
|
|
2477
|
+
generateAppConfig(context) {
|
|
2478
|
+
return `/**
|
|
2479
|
+
* Application Configuration
|
|
2480
|
+
*/
|
|
2481
|
+
export default {
|
|
2482
|
+
/**
|
|
2483
|
+
* Application name
|
|
2484
|
+
*/
|
|
2485
|
+
name: process.env.APP_NAME ?? '${context.name}',
|
|
2486
|
+
|
|
2487
|
+
/**
|
|
2488
|
+
* Application environment
|
|
2489
|
+
*/
|
|
2490
|
+
env: process.env.APP_ENV ?? 'development',
|
|
2491
|
+
|
|
2492
|
+
/**
|
|
2493
|
+
* Debug mode
|
|
2494
|
+
*/
|
|
2495
|
+
debug: process.env.APP_DEBUG === 'true',
|
|
2496
|
+
|
|
2497
|
+
/**
|
|
2498
|
+
* Application URL
|
|
2499
|
+
*/
|
|
2500
|
+
url: process.env.APP_URL ?? 'http://localhost:3000',
|
|
2501
|
+
|
|
2502
|
+
/**
|
|
2503
|
+
* Timezone
|
|
2504
|
+
*/
|
|
2505
|
+
timezone: 'UTC',
|
|
2506
|
+
|
|
2507
|
+
/**
|
|
2508
|
+
* Locale
|
|
2509
|
+
*/
|
|
2510
|
+
locale: 'en',
|
|
2511
|
+
|
|
2512
|
+
/**
|
|
2513
|
+
* Fallback locale
|
|
2514
|
+
*/
|
|
2515
|
+
fallbackLocale: 'en',
|
|
2516
|
+
|
|
2517
|
+
/**
|
|
2518
|
+
* Encryption key
|
|
2519
|
+
*/
|
|
2520
|
+
key: process.env.APP_KEY,
|
|
2521
|
+
|
|
2522
|
+
/**
|
|
2523
|
+
* Service providers to register
|
|
2524
|
+
*/
|
|
2525
|
+
providers: [
|
|
2526
|
+
// Framework providers
|
|
2527
|
+
// 'RouteServiceProvider',
|
|
2528
|
+
|
|
2529
|
+
// Application providers
|
|
2530
|
+
// 'AppServiceProvider',
|
|
2531
|
+
],
|
|
2532
|
+
}
|
|
2533
|
+
`;
|
|
2534
|
+
}
|
|
2535
|
+
generateDatabaseConfig() {
|
|
2536
|
+
return `/**
|
|
2537
|
+
* Database Configuration
|
|
2538
|
+
*/
|
|
2539
|
+
export default {
|
|
2540
|
+
/**
|
|
2541
|
+
* Default connection
|
|
2542
|
+
*/
|
|
2543
|
+
default: process.env.DB_CONNECTION ?? 'sqlite',
|
|
2544
|
+
|
|
2545
|
+
/**
|
|
2546
|
+
* Database connections
|
|
2547
|
+
*/
|
|
2548
|
+
connections: {
|
|
2549
|
+
sqlite: {
|
|
2550
|
+
driver: 'sqlite',
|
|
2551
|
+
database: process.env.DB_DATABASE ?? 'database/database.sqlite',
|
|
2552
|
+
},
|
|
2553
|
+
|
|
2554
|
+
mysql: {
|
|
2555
|
+
driver: 'mysql',
|
|
2556
|
+
host: process.env.DB_HOST ?? 'localhost',
|
|
2557
|
+
port: Number(process.env.DB_PORT ?? 3306),
|
|
2558
|
+
database: process.env.DB_DATABASE ?? 'forge',
|
|
2559
|
+
username: process.env.DB_USERNAME ?? 'forge',
|
|
2560
|
+
password: process.env.DB_PASSWORD ?? '',
|
|
2561
|
+
},
|
|
2562
|
+
|
|
2563
|
+
postgres: {
|
|
2564
|
+
driver: 'postgres',
|
|
2565
|
+
host: process.env.DB_HOST ?? 'localhost',
|
|
2566
|
+
port: Number(process.env.DB_PORT ?? 5432),
|
|
2567
|
+
database: process.env.DB_DATABASE ?? 'forge',
|
|
2568
|
+
username: process.env.DB_USERNAME ?? 'forge',
|
|
2569
|
+
password: process.env.DB_PASSWORD ?? '',
|
|
2570
|
+
},
|
|
2571
|
+
},
|
|
2572
|
+
|
|
2573
|
+
/**
|
|
2574
|
+
* Migration settings
|
|
2575
|
+
*/
|
|
2576
|
+
migrations: {
|
|
2577
|
+
table: 'migrations',
|
|
2578
|
+
path: 'database/migrations',
|
|
2579
|
+
},
|
|
2580
|
+
}
|
|
2581
|
+
`;
|
|
2582
|
+
}
|
|
2583
|
+
generateAuthConfig() {
|
|
2584
|
+
return `/**
|
|
2585
|
+
* Authentication Configuration
|
|
2586
|
+
*/
|
|
2587
|
+
export default {
|
|
2588
|
+
/**
|
|
2589
|
+
* Default guard
|
|
2590
|
+
*/
|
|
2591
|
+
defaults: {
|
|
2592
|
+
guard: 'web',
|
|
2593
|
+
},
|
|
2594
|
+
|
|
2595
|
+
/**
|
|
2596
|
+
* Authentication guards
|
|
2597
|
+
*/
|
|
2598
|
+
guards: {
|
|
2599
|
+
web: {
|
|
2600
|
+
driver: 'session',
|
|
2601
|
+
provider: 'users',
|
|
2602
|
+
},
|
|
2603
|
+
|
|
2604
|
+
api: {
|
|
2605
|
+
driver: 'token',
|
|
2606
|
+
provider: 'users',
|
|
2607
|
+
},
|
|
2608
|
+
},
|
|
2609
|
+
|
|
2610
|
+
/**
|
|
2611
|
+
* User providers
|
|
2612
|
+
*/
|
|
2613
|
+
providers: {
|
|
2614
|
+
users: {
|
|
2615
|
+
driver: 'database',
|
|
2616
|
+
table: 'users',
|
|
2617
|
+
},
|
|
2618
|
+
},
|
|
2619
|
+
}
|
|
2620
|
+
`;
|
|
2621
|
+
}
|
|
2622
|
+
generateCacheConfig() {
|
|
2623
|
+
return `/**
|
|
2624
|
+
* Cache Configuration
|
|
2625
|
+
*/
|
|
2626
|
+
export default {
|
|
2627
|
+
/**
|
|
2628
|
+
* Default cache driver
|
|
2629
|
+
*/
|
|
2630
|
+
default: process.env.CACHE_DRIVER ?? 'memory',
|
|
2631
|
+
|
|
2632
|
+
/**
|
|
2633
|
+
* Cache stores
|
|
2634
|
+
*/
|
|
2635
|
+
stores: {
|
|
2636
|
+
memory: {
|
|
2637
|
+
driver: 'memory',
|
|
2638
|
+
},
|
|
2639
|
+
|
|
2640
|
+
file: {
|
|
2641
|
+
driver: 'file',
|
|
2642
|
+
path: 'storage/cache',
|
|
2643
|
+
},
|
|
2644
|
+
|
|
2645
|
+
redis: {
|
|
2646
|
+
driver: 'redis',
|
|
2647
|
+
host: process.env.REDIS_HOST ?? 'localhost',
|
|
2648
|
+
port: Number(process.env.REDIS_PORT ?? 6379),
|
|
2649
|
+
password: process.env.REDIS_PASSWORD ?? null,
|
|
2650
|
+
database: Number(process.env.REDIS_CACHE_DB ?? 0),
|
|
2651
|
+
},
|
|
2652
|
+
},
|
|
2653
|
+
|
|
2654
|
+
/**
|
|
2655
|
+
* Cache key prefix
|
|
2656
|
+
*/
|
|
2657
|
+
prefix: 'app_cache_',
|
|
2658
|
+
}
|
|
2659
|
+
`;
|
|
2660
|
+
}
|
|
2661
|
+
generateLoggingConfig() {
|
|
2662
|
+
return `/**
|
|
2663
|
+
* Logging Configuration
|
|
2664
|
+
*/
|
|
2665
|
+
export default {
|
|
2666
|
+
/**
|
|
2667
|
+
* Default log channel
|
|
2668
|
+
*/
|
|
2669
|
+
default: process.env.LOG_CHANNEL ?? 'console',
|
|
2670
|
+
|
|
2671
|
+
/**
|
|
2672
|
+
* Log channels
|
|
2673
|
+
*/
|
|
2674
|
+
channels: {
|
|
2675
|
+
console: {
|
|
2676
|
+
driver: 'console',
|
|
2677
|
+
level: process.env.LOG_LEVEL ?? 'debug',
|
|
2678
|
+
},
|
|
2679
|
+
|
|
2680
|
+
file: {
|
|
2681
|
+
driver: 'file',
|
|
2682
|
+
path: 'storage/logs/app.log',
|
|
2683
|
+
level: process.env.LOG_LEVEL ?? 'debug',
|
|
2684
|
+
},
|
|
2685
|
+
},
|
|
2686
|
+
}
|
|
2687
|
+
`;
|
|
2688
|
+
}
|
|
2689
|
+
// ─────────────────────────────────────────────────────────────
|
|
2690
|
+
// Core File Generators
|
|
2691
|
+
// ─────────────────────────────────────────────────────────────
|
|
2692
|
+
generateHttpKernel() {
|
|
2693
|
+
return `/**
|
|
2694
|
+
* HTTP Kernel
|
|
2695
|
+
*
|
|
2696
|
+
* The HTTP kernel is the central point for managing HTTP middleware.
|
|
2697
|
+
* Global middleware runs on every request, while route middleware
|
|
2698
|
+
* can be assigned to specific routes.
|
|
2699
|
+
*/
|
|
2700
|
+
|
|
2701
|
+
import type { GravitoMiddleware } from 'gravito-core'
|
|
2702
|
+
|
|
2703
|
+
/**
|
|
2704
|
+
* Global middleware stack.
|
|
2705
|
+
* These middleware are run during every request.
|
|
2706
|
+
*/
|
|
2707
|
+
export const globalMiddleware: GravitoMiddleware[] = [
|
|
2708
|
+
// Add global middleware here
|
|
2709
|
+
]
|
|
2710
|
+
|
|
2711
|
+
/**
|
|
2712
|
+
* Route middleware groups.
|
|
2713
|
+
* These can be assigned to routes using the middleware name.
|
|
2714
|
+
*/
|
|
2715
|
+
export const middlewareGroups: Record<string, GravitoMiddleware[]> = {
|
|
2716
|
+
web: [
|
|
2717
|
+
// Session middleware
|
|
2718
|
+
// CSRF middleware
|
|
2719
|
+
],
|
|
2720
|
+
api: [
|
|
2721
|
+
// Rate limiting
|
|
2722
|
+
// API authentication
|
|
2723
|
+
],
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
/**
|
|
2727
|
+
* Route middleware aliases.
|
|
2728
|
+
* Convenient names for middleware that can be assigned to routes.
|
|
2729
|
+
*/
|
|
2730
|
+
export const routeMiddleware: Record<string, GravitoMiddleware> = {
|
|
2731
|
+
// auth: AuthenticateMiddleware,
|
|
2732
|
+
// guest: RedirectIfAuthenticated,
|
|
2733
|
+
// throttle: ThrottleRequests,
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
/**
|
|
2737
|
+
* Middleware priority.
|
|
2738
|
+
* Determines the order in which middleware are executed.
|
|
2739
|
+
*/
|
|
2740
|
+
export const middlewarePriority: string[] = [
|
|
2741
|
+
// StartSession
|
|
2742
|
+
// ShareErrorsFromSession
|
|
2743
|
+
// AuthenticateSession
|
|
2744
|
+
// SubstituteBindings
|
|
2745
|
+
// Authorize
|
|
2746
|
+
]
|
|
2747
|
+
`;
|
|
2748
|
+
}
|
|
2749
|
+
generateBaseController() {
|
|
2750
|
+
return `/**
|
|
2751
|
+
* Base Controller
|
|
2752
|
+
*
|
|
2753
|
+
* All controllers should extend this base class.
|
|
2754
|
+
* Provides common functionality and helper methods.
|
|
2755
|
+
*/
|
|
2756
|
+
|
|
2757
|
+
export abstract class Controller {
|
|
2758
|
+
/**
|
|
2759
|
+
* Create a success response.
|
|
2760
|
+
*/
|
|
2761
|
+
protected success<T>(data: T, message = 'Success') {
|
|
2762
|
+
return {
|
|
2763
|
+
success: true,
|
|
2764
|
+
message,
|
|
2765
|
+
data,
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
/**
|
|
2770
|
+
* Create an error response.
|
|
2771
|
+
*/
|
|
2772
|
+
protected error(message: string, code = 'ERROR', details?: unknown) {
|
|
2773
|
+
return {
|
|
2774
|
+
success: false,
|
|
2775
|
+
error: {
|
|
2776
|
+
code,
|
|
2777
|
+
message,
|
|
2778
|
+
details,
|
|
2779
|
+
},
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
`;
|
|
2784
|
+
}
|
|
2785
|
+
generateHomeController(context) {
|
|
2786
|
+
return `/**
|
|
2787
|
+
* Home Controller
|
|
2788
|
+
*/
|
|
2789
|
+
|
|
2790
|
+
import type { GravitoContext } from 'gravito-core'
|
|
2791
|
+
import { Controller } from './Controller'
|
|
2792
|
+
|
|
2793
|
+
export class HomeController extends Controller {
|
|
2794
|
+
/**
|
|
2795
|
+
* Display the home page.
|
|
2796
|
+
*/
|
|
2797
|
+
async index(c: GravitoContext) {
|
|
2798
|
+
return c.json(this.success({
|
|
2799
|
+
name: '${context.name}',
|
|
2800
|
+
message: 'Welcome to your new Gravito application!',
|
|
2801
|
+
architecture: 'Enterprise MVC',
|
|
2802
|
+
}))
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
/**
|
|
2806
|
+
* Health check endpoint.
|
|
2807
|
+
*/
|
|
2808
|
+
async health(c: GravitoContext) {
|
|
2809
|
+
return c.json({
|
|
2810
|
+
status: 'healthy',
|
|
2811
|
+
timestamp: new Date().toISOString(),
|
|
2812
|
+
})
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
`;
|
|
2816
|
+
}
|
|
2817
|
+
generateAuthMiddleware() {
|
|
2818
|
+
return `/**
|
|
2819
|
+
* Authenticate Middleware
|
|
2820
|
+
*
|
|
2821
|
+
* Protects routes that require authentication.
|
|
2822
|
+
*/
|
|
2823
|
+
|
|
2824
|
+
import type { GravitoContext, GravitoNext } from 'gravito-core'
|
|
2825
|
+
|
|
2826
|
+
export async function Authenticate(c: GravitoContext, next: GravitoNext) {
|
|
2827
|
+
// TODO: Implement authentication check
|
|
2828
|
+
// const session = c.get('session')
|
|
2829
|
+
// if (!session?.user) {
|
|
2830
|
+
// return c.json({ error: 'Authentication required' }, 401)
|
|
2831
|
+
// }
|
|
2832
|
+
|
|
2833
|
+
await next()
|
|
2834
|
+
}
|
|
2835
|
+
`;
|
|
2836
|
+
}
|
|
2837
|
+
generateAppServiceProvider(context) {
|
|
2838
|
+
return `/**
|
|
2839
|
+
* App Service Provider
|
|
2840
|
+
*
|
|
2841
|
+
* This is the main application service provider.
|
|
2842
|
+
* Register and bootstrap application services here.
|
|
2843
|
+
*/
|
|
2844
|
+
|
|
2845
|
+
import { ServiceProvider, type Container, type PlanetCore } from 'gravito-core'
|
|
2846
|
+
|
|
2847
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
2848
|
+
/**
|
|
2849
|
+
* Register any application services.
|
|
2850
|
+
*/
|
|
2851
|
+
register(container: Container): void {
|
|
2852
|
+
// Register services
|
|
2853
|
+
// container.singleton('myService', () => new MyService())
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
/**
|
|
2857
|
+
* Bootstrap any application services.
|
|
2858
|
+
*/
|
|
2859
|
+
boot(core: PlanetCore): void {
|
|
2860
|
+
// Bootstrap services
|
|
2861
|
+
console.log('${context.name} application booted!')
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
`;
|
|
2865
|
+
}
|
|
2866
|
+
generateRouteServiceProvider(_context) {
|
|
2867
|
+
return `/**
|
|
2868
|
+
* Route Service Provider
|
|
2869
|
+
*
|
|
2870
|
+
* Configures and registers application routes.
|
|
2871
|
+
*/
|
|
2872
|
+
|
|
2873
|
+
import { ServiceProvider, type Container, type PlanetCore } from 'gravito-core'
|
|
2874
|
+
import { registerRoutes } from '../routes'
|
|
2875
|
+
|
|
2876
|
+
export class RouteServiceProvider extends ServiceProvider {
|
|
2877
|
+
/**
|
|
2878
|
+
* Register any application services.
|
|
2879
|
+
*/
|
|
2880
|
+
register(_container: Container): void {
|
|
2881
|
+
// Routes are registered in boot
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
/**
|
|
2885
|
+
* Bootstrap any application services.
|
|
2886
|
+
*/
|
|
2887
|
+
boot(core: PlanetCore): void {
|
|
2888
|
+
registerRoutes(core.router)
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
`;
|
|
2892
|
+
}
|
|
2893
|
+
generateExceptionHandler() {
|
|
2894
|
+
return `/**
|
|
2895
|
+
* Exception Handler
|
|
2896
|
+
*
|
|
2897
|
+
* Handles all exceptions thrown by the application.
|
|
2898
|
+
* Customize error responses and logging here.
|
|
2899
|
+
*/
|
|
2900
|
+
|
|
2901
|
+
import type { ErrorHandlerContext } from 'gravito-core'
|
|
2902
|
+
|
|
2903
|
+
/**
|
|
2904
|
+
* Report an exception (logging, monitoring, etc.)
|
|
2905
|
+
*/
|
|
2906
|
+
export function report(error: unknown, context: ErrorHandlerContext): void {
|
|
2907
|
+
// Log to external service (Sentry, etc.)
|
|
2908
|
+
if (!context.isProduction) {
|
|
2909
|
+
console.error('[Exception Handler]', error)
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
/**
|
|
2914
|
+
* Determine if the exception should be reported.
|
|
2915
|
+
*/
|
|
2916
|
+
export function shouldReport(error: unknown): boolean {
|
|
2917
|
+
// Don't report 4xx errors
|
|
2918
|
+
if (error instanceof Error && 'status' in error) {
|
|
2919
|
+
const status = (error as any).status
|
|
2920
|
+
if (status >= 400 && status < 500) {
|
|
2921
|
+
return false
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
return true
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
/**
|
|
2929
|
+
* A list of exception types that should not be reported.
|
|
2930
|
+
*/
|
|
2931
|
+
export const dontReport: string[] = [
|
|
2932
|
+
'ValidationException',
|
|
2933
|
+
'NotFoundException',
|
|
2934
|
+
]
|
|
2935
|
+
`;
|
|
2936
|
+
}
|
|
2937
|
+
generateBootstrap(context) {
|
|
2938
|
+
return `/**
|
|
2939
|
+
* Application Bootstrap
|
|
2940
|
+
*
|
|
2941
|
+
* This is the entry point for your application.
|
|
2942
|
+
* It initializes the core and registers all providers.
|
|
2943
|
+
*/
|
|
2944
|
+
|
|
2945
|
+
import { PlanetCore } from 'gravito-core'
|
|
2946
|
+
import { AppServiceProvider } from './Providers/AppServiceProvider'
|
|
2947
|
+
import { RouteServiceProvider } from './Providers/RouteServiceProvider'
|
|
2948
|
+
|
|
2949
|
+
// Load environment variables
|
|
2950
|
+
// Bun automatically loads .env
|
|
2951
|
+
|
|
2952
|
+
// Create application core
|
|
2953
|
+
const core = new PlanetCore({
|
|
2954
|
+
config: {
|
|
2955
|
+
APP_NAME: '${context.name}',
|
|
2956
|
+
},
|
|
2957
|
+
})
|
|
2958
|
+
|
|
2959
|
+
// Register service providers
|
|
2960
|
+
core.register(new AppServiceProvider())
|
|
2961
|
+
core.register(new RouteServiceProvider())
|
|
2962
|
+
|
|
2963
|
+
// Bootstrap the application
|
|
2964
|
+
await core.bootstrap()
|
|
2965
|
+
|
|
2966
|
+
// Export for Bun.serve()
|
|
2967
|
+
export default core.liftoff()
|
|
2968
|
+
`;
|
|
2969
|
+
}
|
|
2970
|
+
generateRoutes(_context) {
|
|
2971
|
+
return `/**
|
|
2972
|
+
* Application Routes
|
|
2973
|
+
*
|
|
2974
|
+
* Define your application routes here.
|
|
2975
|
+
*/
|
|
2976
|
+
|
|
2977
|
+
import { HomeController } from './Http/Controllers/HomeController'
|
|
2978
|
+
|
|
2979
|
+
export function registerRoutes(router: any): void {
|
|
2980
|
+
const home = new HomeController()
|
|
2981
|
+
|
|
2982
|
+
// API Routes
|
|
2983
|
+
router.get('/api', (c: any) => home.index(c))
|
|
2984
|
+
router.get('/api/health', (c: any) => home.health(c))
|
|
2985
|
+
|
|
2986
|
+
// Web Routes
|
|
2987
|
+
router.get('/', (c: any) => home.index(c))
|
|
2988
|
+
}
|
|
2989
|
+
`;
|
|
2990
|
+
}
|
|
2991
|
+
// ─────────────────────────────────────────────────────────────
|
|
2992
|
+
// Architecture Documentation
|
|
2993
|
+
// ─────────────────────────────────────────────────────────────
|
|
2994
|
+
generateArchitectureDoc(context) {
|
|
2995
|
+
return `# ${context.name} - Architecture Guide
|
|
2996
|
+
|
|
2997
|
+
## Overview
|
|
2998
|
+
|
|
2999
|
+
This project uses **Enterprise MVC Architecture**, inspired by Laravel's conventions.
|
|
3000
|
+
It provides a clear separation of concerns while remaining pragmatic and easy to understand.
|
|
3001
|
+
|
|
3002
|
+
## Directory Structure
|
|
3003
|
+
|
|
3004
|
+
\`\`\`
|
|
3005
|
+
${context.name}/
|
|
3006
|
+
\u251C\u2500\u2500 config/ # Configuration files
|
|
3007
|
+
\u2502 \u251C\u2500\u2500 app.ts # Application settings
|
|
3008
|
+
\u2502 \u251C\u2500\u2500 database.ts # Database connections
|
|
3009
|
+
\u2502 \u251C\u2500\u2500 auth.ts # Authentication settings
|
|
3010
|
+
\u2502 \u251C\u2500\u2500 cache.ts # Cache configuration
|
|
3011
|
+
\u2502 \u2514\u2500\u2500 logging.ts # Logging channels
|
|
3012
|
+
\u251C\u2500\u2500 src/
|
|
3013
|
+
\u2502 \u251C\u2500\u2500 Http/
|
|
3014
|
+
\u2502 \u2502 \u251C\u2500\u2500 Kernel.ts # HTTP middleware management
|
|
3015
|
+
\u2502 \u2502 \u251C\u2500\u2500 Controllers/ # HTTP request handlers
|
|
3016
|
+
\u2502 \u2502 \u2514\u2500\u2500 Middleware/ # Request/Response interceptors
|
|
3017
|
+
\u2502 \u251C\u2500\u2500 Services/ # Business logic layer
|
|
3018
|
+
\u2502 \u251C\u2500\u2500 Repositories/ # Data access layer
|
|
3019
|
+
\u2502 \u251C\u2500\u2500 Models/ # Data models/entities
|
|
3020
|
+
\u2502 \u251C\u2500\u2500 Resources/ # API response transformers
|
|
3021
|
+
\u2502 \u251C\u2500\u2500 Providers/ # Service providers
|
|
3022
|
+
\u2502 \u251C\u2500\u2500 Exceptions/ # Custom exceptions
|
|
3023
|
+
\u2502 \u251C\u2500\u2500 bootstrap.ts # Application entry point
|
|
3024
|
+
\u2502 \u2514\u2500\u2500 routes.ts # Route definitions
|
|
3025
|
+
\u251C\u2500\u2500 tests/
|
|
3026
|
+
\u2502 \u251C\u2500\u2500 Unit/ # Unit tests
|
|
3027
|
+
\u2502 \u2514\u2500\u2500 Feature/ # Integration tests
|
|
3028
|
+
\u2514\u2500\u2500 database/
|
|
3029
|
+
\u251C\u2500\u2500 migrations/ # Database migrations
|
|
3030
|
+
\u2514\u2500\u2500 seeders/ # Database seeders
|
|
3031
|
+
\`\`\`
|
|
3032
|
+
|
|
3033
|
+
## Request Flow
|
|
3034
|
+
|
|
3035
|
+
\`\`\`
|
|
3036
|
+
Request
|
|
3037
|
+
\u2502
|
|
3038
|
+
\u25BC
|
|
3039
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
3040
|
+
\u2502 Http/Kernel \u2502 \u2500\u2500\u25B6 Global Middleware
|
|
3041
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
3042
|
+
\u2502
|
|
3043
|
+
\u25BC
|
|
3044
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
3045
|
+
\u2502 Router \u2502 \u2500\u2500\u25B6 Route Matching
|
|
3046
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
3047
|
+
\u2502
|
|
3048
|
+
\u25BC
|
|
3049
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
3050
|
+
\u2502 Controller \u2502 \u2500\u2500\u25B6 Request Handling
|
|
3051
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
3052
|
+
\u2502
|
|
3053
|
+
\u25BC
|
|
3054
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
3055
|
+
\u2502 Service \u2502 \u2500\u2500\u25B6 Business Logic
|
|
3056
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
3057
|
+
\u2502
|
|
3058
|
+
\u25BC
|
|
3059
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
3060
|
+
\u2502 Repository \u2502 \u2500\u2500\u25B6 Data Access
|
|
3061
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
3062
|
+
\u2502
|
|
3063
|
+
\u25BC
|
|
3064
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
3065
|
+
\u2502 Resource \u2502 \u2500\u2500\u25B6 Response Transform
|
|
3066
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
3067
|
+
\u2502
|
|
3068
|
+
\u25BC
|
|
3069
|
+
Response
|
|
3070
|
+
\`\`\`
|
|
3071
|
+
|
|
3072
|
+
## Layer Responsibilities
|
|
3073
|
+
|
|
3074
|
+
### Controllers
|
|
3075
|
+
- Handle HTTP requests and responses
|
|
3076
|
+
- Validate input (or delegate to Form Requests)
|
|
3077
|
+
- Call services for business logic
|
|
3078
|
+
- Return transformed responses
|
|
3079
|
+
|
|
3080
|
+
### Services
|
|
3081
|
+
- Contain business logic
|
|
3082
|
+
- Orchestrate multiple repositories
|
|
3083
|
+
- Enforce business rules
|
|
3084
|
+
- Should be framework-agnostic
|
|
3085
|
+
|
|
3086
|
+
### Repositories
|
|
3087
|
+
- Abstract data access
|
|
3088
|
+
- Handle database queries
|
|
3089
|
+
- Return domain objects
|
|
3090
|
+
|
|
3091
|
+
### Resources
|
|
3092
|
+
- Transform models for API responses
|
|
3093
|
+
- Control what data is exposed
|
|
3094
|
+
- Handle nested relationships
|
|
3095
|
+
|
|
3096
|
+
## Getting Started
|
|
3097
|
+
|
|
3098
|
+
\`\`\`bash
|
|
3099
|
+
# Install dependencies
|
|
3100
|
+
bun install
|
|
3101
|
+
|
|
3102
|
+
# Run development server
|
|
3103
|
+
bun run dev
|
|
3104
|
+
|
|
3105
|
+
# Run tests
|
|
3106
|
+
bun test
|
|
3107
|
+
\`\`\`
|
|
3108
|
+
|
|
3109
|
+
## Configuration
|
|
3110
|
+
|
|
3111
|
+
All configuration is in the \`config/\` directory.
|
|
3112
|
+
Environment-specific values should use environment variables.
|
|
3113
|
+
|
|
3114
|
+
## Service Providers
|
|
3115
|
+
|
|
3116
|
+
Service providers are the central place to configure your application.
|
|
3117
|
+
They are registered in \`src/bootstrap.ts\`:
|
|
3118
|
+
|
|
3119
|
+
\`\`\`typescript
|
|
3120
|
+
core.register(new AppServiceProvider())
|
|
3121
|
+
core.register(new RouteServiceProvider())
|
|
3122
|
+
\`\`\`
|
|
3123
|
+
|
|
3124
|
+
Created with \u2764\uFE0F using Gravito Framework
|
|
3125
|
+
`;
|
|
3126
|
+
}
|
|
3127
|
+
};
|
|
3128
|
+
|
|
3129
|
+
// src/Scaffold.ts
|
|
3130
|
+
import path3 from "path";
|
|
3131
|
+
var Scaffold = class {
|
|
3132
|
+
templatesDir;
|
|
3133
|
+
verbose;
|
|
3134
|
+
constructor(options = {}) {
|
|
3135
|
+
this.templatesDir = options.templatesDir ?? path3.resolve(__dirname, "../templates");
|
|
3136
|
+
this.verbose = options.verbose ?? false;
|
|
3137
|
+
}
|
|
3138
|
+
/**
|
|
3139
|
+
* Get all available architecture types.
|
|
3140
|
+
*/
|
|
3141
|
+
getArchitectureTypes() {
|
|
3142
|
+
return [
|
|
3143
|
+
{
|
|
3144
|
+
type: "enterprise-mvc",
|
|
3145
|
+
name: "Enterprise MVC",
|
|
3146
|
+
description: "Laravel-inspired MVC with Services and Repositories"
|
|
3147
|
+
},
|
|
3148
|
+
{
|
|
3149
|
+
type: "clean",
|
|
3150
|
+
name: "Clean Architecture",
|
|
3151
|
+
description: "Uncle Bob's Clean Architecture with strict dependency rules"
|
|
3152
|
+
},
|
|
3153
|
+
{
|
|
3154
|
+
type: "ddd",
|
|
3155
|
+
name: "Domain-Driven Design",
|
|
3156
|
+
description: "Full DDD with Bounded Contexts and CQRS"
|
|
3157
|
+
}
|
|
3158
|
+
];
|
|
3159
|
+
}
|
|
3160
|
+
/**
|
|
3161
|
+
* Create a new project scaffold.
|
|
3162
|
+
*/
|
|
3163
|
+
async create(options) {
|
|
3164
|
+
const generator = this.createGenerator(options.architecture);
|
|
3165
|
+
const context = BaseGenerator.createContext(
|
|
3166
|
+
options.name,
|
|
3167
|
+
options.targetDir,
|
|
3168
|
+
options.architecture,
|
|
3169
|
+
options.packageManager ?? "bun",
|
|
3170
|
+
options.context ?? {}
|
|
3171
|
+
);
|
|
3172
|
+
try {
|
|
3173
|
+
const filesCreated = await generator.generate(context);
|
|
3174
|
+
return {
|
|
3175
|
+
success: true,
|
|
3176
|
+
targetDir: options.targetDir,
|
|
3177
|
+
filesCreated
|
|
3178
|
+
};
|
|
3179
|
+
} catch (error) {
|
|
3180
|
+
return {
|
|
3181
|
+
success: false,
|
|
3182
|
+
targetDir: options.targetDir,
|
|
3183
|
+
filesCreated: [],
|
|
3184
|
+
errors: [error instanceof Error ? error.message : String(error)]
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
/**
|
|
3189
|
+
* Create a generator for the specified architecture.
|
|
3190
|
+
*/
|
|
3191
|
+
createGenerator(type) {
|
|
3192
|
+
const config = {
|
|
3193
|
+
templatesDir: this.templatesDir,
|
|
3194
|
+
verbose: this.verbose
|
|
3195
|
+
};
|
|
3196
|
+
switch (type) {
|
|
3197
|
+
case "enterprise-mvc":
|
|
3198
|
+
return new EnterpriseMvcGenerator(config);
|
|
3199
|
+
case "clean":
|
|
3200
|
+
return new CleanArchitectureGenerator(config);
|
|
3201
|
+
case "ddd":
|
|
3202
|
+
return new DddGenerator(config);
|
|
3203
|
+
default:
|
|
3204
|
+
throw new Error(`Unknown architecture type: ${type}`);
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
/**
|
|
3208
|
+
* Generate a single module (for DDD bounded context).
|
|
3209
|
+
*/
|
|
3210
|
+
async generateModule(_targetDir, _moduleName, _options = {}) {
|
|
3211
|
+
throw new Error("Module generation not yet implemented");
|
|
3212
|
+
}
|
|
3213
|
+
/**
|
|
3214
|
+
* Generate a service provider.
|
|
3215
|
+
*/
|
|
3216
|
+
async generateProvider(_targetDir, _providerName) {
|
|
3217
|
+
throw new Error("Provider generation not yet implemented");
|
|
3218
|
+
}
|
|
3219
|
+
};
|
|
3220
|
+
export {
|
|
3221
|
+
BaseGenerator,
|
|
3222
|
+
CleanArchitectureGenerator,
|
|
3223
|
+
DddGenerator,
|
|
3224
|
+
EnterpriseMvcGenerator,
|
|
3225
|
+
Scaffold,
|
|
3226
|
+
StubGenerator
|
|
3227
|
+
};
|