@gravito/scaffold 1.0.0-beta.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1473 -299
- package/dist/index.d.cts +132 -9
- package/dist/index.d.ts +132 -9
- package/dist/index.js +1468 -299
- package/package.json +8 -5
- package/templates/features/otel/.env.example +2 -0
- package/templates/features/otel/config/telemetry.ts +10 -0
- package/templates/features/otel/docker-compose.yml +15 -0
- package/templates/features/otel/package.json +7 -0
- package/templates/features/redis/.env.example +7 -0
- package/templates/features/redis/config/cache.ts +14 -0
- package/templates/features/redis/config/queue.ts +13 -0
- package/templates/features/redis/docker-compose.yml +17 -0
- package/templates/features/redis/package.json +5 -0
- package/templates/overlays/core/.env.example +14 -0
- package/templates/overlays/core/config/cache.ts +26 -0
- package/templates/overlays/core/config/database.ts +38 -0
- package/templates/overlays/core/config/queue.ts +31 -0
- package/templates/overlays/core/package.json +5 -0
- package/templates/overlays/enterprise/.env.example +26 -0
- package/templates/overlays/enterprise/config/cache.ts +31 -0
- package/templates/overlays/enterprise/config/database.ts +34 -0
- package/templates/overlays/enterprise/config/logging.ts +32 -0
- package/templates/overlays/enterprise/config/queue.ts +31 -0
- package/templates/overlays/enterprise/config/security.ts +20 -0
- package/templates/overlays/enterprise/docker-compose.yml +32 -0
- package/templates/overlays/enterprise/package.json +7 -0
- package/templates/overlays/scale/.env.example +22 -0
- package/templates/overlays/scale/config/cache.ts +31 -0
- package/templates/overlays/scale/config/database.ts +34 -0
- package/templates/overlays/scale/config/queue.ts +34 -0
- package/templates/overlays/scale/docker-compose.yml +32 -0
- package/templates/overlays/scale/package.json +6 -0
package/dist/index.js
CHANGED
|
@@ -1,3 +1,102 @@
|
|
|
1
|
+
// src/EnvironmentDetector.ts
|
|
2
|
+
var EnvironmentDetector = class {
|
|
3
|
+
detect() {
|
|
4
|
+
if (process.env.KUBERNETES_SERVICE_HOST) {
|
|
5
|
+
return {
|
|
6
|
+
platform: "k8s",
|
|
7
|
+
suggestedProfile: "enterprise",
|
|
8
|
+
confidence: "high",
|
|
9
|
+
reason: "Kubernetes environment detected (KUBERNETES_SERVICE_HOST)"
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.AWS_EXECUTION_ENV) {
|
|
13
|
+
return {
|
|
14
|
+
platform: "aws",
|
|
15
|
+
suggestedProfile: "scale",
|
|
16
|
+
confidence: "high",
|
|
17
|
+
reason: "AWS Lambda environment detected"
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (process.env.VERCEL) {
|
|
21
|
+
return {
|
|
22
|
+
platform: "vercel",
|
|
23
|
+
suggestedProfile: "core",
|
|
24
|
+
confidence: "high",
|
|
25
|
+
reason: "Vercel environment detected"
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (process.env.RAILWAY_ENVIRONMENT || process.env.RENDER) {
|
|
29
|
+
return {
|
|
30
|
+
platform: "unknown",
|
|
31
|
+
// Generic cloud
|
|
32
|
+
suggestedProfile: "scale",
|
|
33
|
+
confidence: "medium",
|
|
34
|
+
reason: "Cloud container environment detected"
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
platform: "unknown",
|
|
39
|
+
suggestedProfile: "core",
|
|
40
|
+
confidence: "low",
|
|
41
|
+
reason: "No specific cloud environment detected, defaulting to Core"
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/utils/deepMerge.ts
|
|
47
|
+
function deepMerge(target, source) {
|
|
48
|
+
if (typeof target !== "object" || target === null) {
|
|
49
|
+
return source;
|
|
50
|
+
}
|
|
51
|
+
if (typeof source !== "object" || source === null) {
|
|
52
|
+
return source;
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(target) && Array.isArray(source)) {
|
|
55
|
+
return Array.from(/* @__PURE__ */ new Set([...target, ...source]));
|
|
56
|
+
}
|
|
57
|
+
const output = { ...target };
|
|
58
|
+
for (const key of Object.keys(source)) {
|
|
59
|
+
if (source[key] instanceof Object && key in target) {
|
|
60
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
61
|
+
} else {
|
|
62
|
+
output[key] = source[key];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return output;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/FileMerger.ts
|
|
69
|
+
var FileMerger = class {
|
|
70
|
+
/**
|
|
71
|
+
* Merge content of two files based on their type.
|
|
72
|
+
*/
|
|
73
|
+
merge(fileName, baseContent, overlayContent) {
|
|
74
|
+
if (fileName.endsWith(".json")) {
|
|
75
|
+
return this.mergeJson(baseContent, overlayContent);
|
|
76
|
+
}
|
|
77
|
+
if (fileName.endsWith(".env") || fileName.endsWith(".env.example")) {
|
|
78
|
+
return this.mergeEnv(baseContent, overlayContent);
|
|
79
|
+
}
|
|
80
|
+
return overlayContent;
|
|
81
|
+
}
|
|
82
|
+
mergeJson(base, overlay) {
|
|
83
|
+
try {
|
|
84
|
+
const baseJson = JSON.parse(base);
|
|
85
|
+
const overlayJson = JSON.parse(overlay);
|
|
86
|
+
const merged = deepMerge(baseJson, overlayJson);
|
|
87
|
+
return JSON.stringify(merged, null, 2);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.warn("Failed to merge JSON, returning overlay", e);
|
|
90
|
+
return overlay;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
mergeEnv(base, overlay) {
|
|
94
|
+
return `${base}
|
|
95
|
+
# --- Overlay ---
|
|
96
|
+
${overlay}`;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
1
100
|
// src/generators/BaseGenerator.ts
|
|
2
101
|
import fs2 from "fs/promises";
|
|
3
102
|
import path2 from "path";
|
|
@@ -155,9 +254,24 @@ var StubGenerator = class {
|
|
|
155
254
|
};
|
|
156
255
|
|
|
157
256
|
// src/generators/BaseGenerator.ts
|
|
257
|
+
async function walk(dir) {
|
|
258
|
+
const files = await fs2.readdir(dir);
|
|
259
|
+
const paths = [];
|
|
260
|
+
for (const file of files) {
|
|
261
|
+
const filePath = path2.join(dir, file);
|
|
262
|
+
const stat = await fs2.stat(filePath);
|
|
263
|
+
if (stat.isDirectory()) {
|
|
264
|
+
paths.push(...await walk(filePath));
|
|
265
|
+
} else {
|
|
266
|
+
paths.push(filePath);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return paths;
|
|
270
|
+
}
|
|
158
271
|
var BaseGenerator = class {
|
|
159
272
|
config;
|
|
160
273
|
stubGenerator;
|
|
274
|
+
fileMerger;
|
|
161
275
|
filesCreated = [];
|
|
162
276
|
constructor(config) {
|
|
163
277
|
this.config = config;
|
|
@@ -166,6 +280,7 @@ var BaseGenerator = class {
|
|
|
166
280
|
outputDir: ""
|
|
167
281
|
// Set per-generation
|
|
168
282
|
});
|
|
283
|
+
this.fileMerger = new FileMerger();
|
|
169
284
|
}
|
|
170
285
|
/**
|
|
171
286
|
* Generate the project scaffold.
|
|
@@ -178,6 +293,8 @@ var BaseGenerator = class {
|
|
|
178
293
|
const structure = this.getDirectoryStructure(context);
|
|
179
294
|
await this.createStructure(context.targetDir, structure, context);
|
|
180
295
|
await this.generateCommonFiles(context);
|
|
296
|
+
await this.applyOverlays(context);
|
|
297
|
+
await this.applyFeatureOverlays(context);
|
|
181
298
|
return this.filesCreated;
|
|
182
299
|
}
|
|
183
300
|
/**
|
|
@@ -222,19 +339,69 @@ var BaseGenerator = class {
|
|
|
222
339
|
await this.writeFile(context.targetDir, ".env", this.generateEnvExample(context));
|
|
223
340
|
await this.writeFile(context.targetDir, ".gitignore", this.generateGitignore());
|
|
224
341
|
await this.writeFile(context.targetDir, "tsconfig.json", this.generateTsConfig());
|
|
342
|
+
await this.writeFile(context.targetDir, "Dockerfile", this.generateDockerfile(context));
|
|
343
|
+
await this.writeFile(context.targetDir, ".dockerignore", this.generateDockerIgnore());
|
|
225
344
|
await this.writeFile(
|
|
226
345
|
context.targetDir,
|
|
227
346
|
"ARCHITECTURE.md",
|
|
228
347
|
this.generateArchitectureDoc(context)
|
|
229
348
|
);
|
|
230
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Apply profile-specific overlays
|
|
352
|
+
*/
|
|
353
|
+
async applyOverlays(context) {
|
|
354
|
+
const profile = context.profile;
|
|
355
|
+
if (!profile) return;
|
|
356
|
+
const overlayDir = path2.resolve(this.config.templatesDir, "overlays", profile);
|
|
357
|
+
await this.copyOverlayDirectory(overlayDir, context);
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Apply feature-specific overlays
|
|
361
|
+
*/
|
|
362
|
+
async applyFeatureOverlays(context) {
|
|
363
|
+
const features = context.features || [];
|
|
364
|
+
for (const feature of features) {
|
|
365
|
+
const overlayDir = path2.resolve(this.config.templatesDir, "features", feature);
|
|
366
|
+
await this.copyOverlayDirectory(overlayDir, context);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Helper to copy/merge an overlay directory into the target
|
|
371
|
+
*/
|
|
372
|
+
async copyOverlayDirectory(sourceDir, context) {
|
|
373
|
+
try {
|
|
374
|
+
await fs2.access(sourceDir);
|
|
375
|
+
} catch {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const files = await walk(sourceDir);
|
|
379
|
+
for (const filePath of files) {
|
|
380
|
+
const relativePath = path2.relative(sourceDir, filePath);
|
|
381
|
+
let content = await fs2.readFile(filePath, "utf-8");
|
|
382
|
+
try {
|
|
383
|
+
content = this.stubGenerator.render(content, context);
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
await this.writeFile(context.targetDir, relativePath, content);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
231
389
|
/**
|
|
232
390
|
* Write a file and track it.
|
|
233
391
|
*/
|
|
234
392
|
async writeFile(basePath, relativePath, content) {
|
|
235
393
|
const fullPath = path2.resolve(basePath, relativePath);
|
|
236
394
|
await fs2.mkdir(path2.dirname(fullPath), { recursive: true });
|
|
237
|
-
|
|
395
|
+
let finalContent = content;
|
|
396
|
+
try {
|
|
397
|
+
const existingContent = await fs2.readFile(fullPath, "utf-8");
|
|
398
|
+
finalContent = this.fileMerger.merge(relativePath, existingContent, content);
|
|
399
|
+
if (finalContent !== content) {
|
|
400
|
+
this.log(`\u{1F504} Merged file: ${relativePath}`);
|
|
401
|
+
}
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
await fs2.writeFile(fullPath, finalContent, "utf-8");
|
|
238
405
|
this.filesCreated.push(fullPath);
|
|
239
406
|
this.log(`\u{1F4C4} Created file: ${relativePath}`);
|
|
240
407
|
}
|
|
@@ -242,6 +409,20 @@ var BaseGenerator = class {
|
|
|
242
409
|
* Generate package.json content.
|
|
243
410
|
*/
|
|
244
411
|
generatePackageJson(context) {
|
|
412
|
+
const profile = context.profile || "core";
|
|
413
|
+
const baseDependencies = {
|
|
414
|
+
"@gravito/core": "^1.0.0-beta.5",
|
|
415
|
+
"@gravito/atlas": "^1.0.0-beta.5",
|
|
416
|
+
"@gravito/plasma": "^1.0.0-beta.5",
|
|
417
|
+
"@gravito/stream": "^1.0.0-beta.5"
|
|
418
|
+
};
|
|
419
|
+
if (profile === "enterprise" || profile === "scale") {
|
|
420
|
+
baseDependencies["@gravito/quasar"] = "^1.0.0-beta.5";
|
|
421
|
+
baseDependencies["@gravito/horizon"] = "^1.0.0-beta.5";
|
|
422
|
+
}
|
|
423
|
+
if (context.withSpectrum) {
|
|
424
|
+
baseDependencies["@gravito/spectrum"] = "^1.0.0-beta.1";
|
|
425
|
+
}
|
|
245
426
|
const pkg = {
|
|
246
427
|
name: context.nameKebabCase,
|
|
247
428
|
version: "0.1.0",
|
|
@@ -251,42 +432,193 @@ var BaseGenerator = class {
|
|
|
251
432
|
build: "bun build ./src/bootstrap.ts --outdir ./dist --target bun",
|
|
252
433
|
start: "bun run dist/bootstrap.js",
|
|
253
434
|
test: "bun test",
|
|
254
|
-
typecheck: "tsc --noEmit"
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
"gravito-core": "^1.0.0-beta.5"
|
|
435
|
+
typecheck: "tsc --noEmit",
|
|
436
|
+
"docker:build": `docker build -t ${context.nameKebabCase} .`,
|
|
437
|
+
"docker:run": `docker run -it -p 3000:3000 ${context.nameKebabCase}`
|
|
258
438
|
},
|
|
439
|
+
dependencies: baseDependencies,
|
|
259
440
|
devDependencies: {
|
|
260
|
-
"
|
|
441
|
+
"bun-types": "latest",
|
|
261
442
|
typescript: "^5.0.0"
|
|
262
443
|
}
|
|
263
444
|
};
|
|
264
445
|
return JSON.stringify(pkg, null, 2);
|
|
265
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* Generate Dockerfile content.
|
|
449
|
+
*/
|
|
450
|
+
generateDockerfile(context) {
|
|
451
|
+
const entrypoint = context.architecture === "ddd" ? "dist/main.js" : "dist/bootstrap.js";
|
|
452
|
+
return `FROM oven/bun:1.0 AS base
|
|
453
|
+
WORKDIR /usr/src/app
|
|
454
|
+
|
|
455
|
+
# Install dependencies
|
|
456
|
+
FROM base AS install
|
|
457
|
+
RUN mkdir -p /temp/dev
|
|
458
|
+
COPY package.json bun.lockb /temp/dev/
|
|
459
|
+
RUN cd /temp/dev && bun install --frozen-lockfile
|
|
460
|
+
|
|
461
|
+
# Build application
|
|
462
|
+
FROM base AS build
|
|
463
|
+
COPY --from=install /temp/dev/node_modules node_modules
|
|
464
|
+
COPY . .
|
|
465
|
+
ENV NODE_ENV=production
|
|
466
|
+
RUN bun run build
|
|
467
|
+
|
|
468
|
+
# Final production image
|
|
469
|
+
FROM base AS release
|
|
470
|
+
COPY --from=build /usr/src/app/${entrypoint} index.js
|
|
471
|
+
COPY --from=build /usr/src/app/package.json .
|
|
472
|
+
|
|
473
|
+
# Create a non-root user for security
|
|
474
|
+
USER bun
|
|
475
|
+
EXPOSE 3000/tcp
|
|
476
|
+
ENTRYPOINT [ "bun", "run", "index.js" ]
|
|
477
|
+
`;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Generate .dockerignore content.
|
|
481
|
+
*/
|
|
482
|
+
generateDockerIgnore() {
|
|
483
|
+
return `node_modules
|
|
484
|
+
dist
|
|
485
|
+
.git
|
|
486
|
+
.env
|
|
487
|
+
*.log
|
|
488
|
+
.vscode
|
|
489
|
+
.idea
|
|
490
|
+
tests
|
|
491
|
+
`;
|
|
492
|
+
}
|
|
266
493
|
/**
|
|
267
494
|
* Generate .env.example content.
|
|
268
495
|
*/
|
|
269
496
|
generateEnvExample(context) {
|
|
270
|
-
|
|
271
|
-
|
|
497
|
+
const profile = context.profile || "core";
|
|
498
|
+
let envContent = `# ============================================================================
|
|
499
|
+
# Application Configuration
|
|
500
|
+
# ============================================================================
|
|
501
|
+
|
|
502
|
+
APP_NAME=${context.name}
|
|
272
503
|
APP_ENV=development
|
|
273
|
-
APP_KEY=
|
|
274
504
|
APP_DEBUG=true
|
|
275
505
|
APP_URL=http://localhost:3000
|
|
506
|
+
APP_KEY=
|
|
276
507
|
|
|
277
|
-
#
|
|
278
|
-
|
|
508
|
+
# ============================================================================
|
|
509
|
+
# Database Configuration
|
|
510
|
+
# ============================================================================
|
|
279
511
|
|
|
280
|
-
# Database
|
|
281
|
-
DB_CONNECTION
|
|
512
|
+
# Database Connection (sqlite, postgres, mysql)
|
|
513
|
+
DB_CONNECTION=${profile === "core" ? "sqlite" : "postgres"}
|
|
514
|
+
|
|
515
|
+
# SQLite Configuration (when DB_CONNECTION=sqlite)
|
|
282
516
|
DB_DATABASE=database/database.sqlite
|
|
283
517
|
|
|
284
|
-
#
|
|
285
|
-
|
|
518
|
+
# PostgreSQL Configuration (when DB_CONNECTION=postgres)
|
|
519
|
+
${profile !== "core" ? `DB_HOST=127.0.0.1
|
|
520
|
+
DB_PORT=5432
|
|
521
|
+
DB_DATABASE=${context.name}
|
|
522
|
+
DB_USERNAME=postgres
|
|
523
|
+
DB_PASSWORD=
|
|
524
|
+
DB_SSLMODE=prefer` : `# DB_HOST=127.0.0.1
|
|
525
|
+
# DB_PORT=5432
|
|
526
|
+
# DB_DATABASE=${context.name}
|
|
527
|
+
# DB_USERNAME=postgres
|
|
528
|
+
# DB_PASSWORD=
|
|
529
|
+
# DB_SSLMODE=prefer`}
|
|
530
|
+
|
|
531
|
+
# MySQL Configuration (when DB_CONNECTION=mysql)
|
|
532
|
+
# DB_HOST=127.0.0.1
|
|
533
|
+
# DB_PORT=3306
|
|
534
|
+
# DB_DATABASE=${context.name}
|
|
535
|
+
# DB_USERNAME=root
|
|
536
|
+
# DB_PASSWORD=
|
|
537
|
+
|
|
538
|
+
# ============================================================================
|
|
539
|
+
# Redis Configuration (@gravito/plasma)
|
|
540
|
+
# ============================================================================
|
|
541
|
+
|
|
542
|
+
# Default Redis Connection
|
|
543
|
+
REDIS_CONNECTION=default
|
|
544
|
+
REDIS_HOST=127.0.0.1
|
|
545
|
+
REDIS_PORT=6379
|
|
546
|
+
REDIS_PASSWORD=
|
|
547
|
+
REDIS_DB=0
|
|
548
|
+
|
|
549
|
+
# Redis Connection Options
|
|
550
|
+
REDIS_CONNECT_TIMEOUT=10000
|
|
551
|
+
REDIS_COMMAND_TIMEOUT=5000
|
|
552
|
+
REDIS_KEY_PREFIX=
|
|
553
|
+
REDIS_MAX_RETRIES=3
|
|
554
|
+
REDIS_RETRY_DELAY=1000
|
|
555
|
+
|
|
556
|
+
# Cache-specific Redis Connection (optional, falls back to default)
|
|
557
|
+
# REDIS_CACHE_HOST=127.0.0.1
|
|
558
|
+
# REDIS_CACHE_PORT=6379
|
|
559
|
+
# REDIS_CACHE_PASSWORD=
|
|
560
|
+
REDIS_CACHE_DB=1
|
|
561
|
+
|
|
562
|
+
# Queue-specific Redis Connection (optional, falls back to default)
|
|
563
|
+
# REDIS_QUEUE_HOST=127.0.0.1
|
|
564
|
+
# REDIS_QUEUE_PORT=6379
|
|
565
|
+
# REDIS_QUEUE_PASSWORD=
|
|
566
|
+
REDIS_QUEUE_DB=2
|
|
567
|
+
|
|
568
|
+
# ============================================================================
|
|
569
|
+
# Cache Configuration (@gravito/stasis)
|
|
570
|
+
# ============================================================================
|
|
571
|
+
|
|
572
|
+
# Cache Driver (memory, file, redis)
|
|
573
|
+
CACHE_DRIVER=${profile === "core" ? "memory" : "redis"}
|
|
574
|
+
|
|
575
|
+
# File Cache Path (when CACHE_DRIVER=file)
|
|
576
|
+
CACHE_PATH=storage/framework/cache
|
|
577
|
+
|
|
578
|
+
# Redis Cache Configuration (when CACHE_DRIVER=redis)
|
|
579
|
+
REDIS_CACHE_CONNECTION=cache
|
|
580
|
+
REDIS_CACHE_PREFIX=cache:
|
|
581
|
+
|
|
582
|
+
# ============================================================================
|
|
583
|
+
# Queue Configuration (@gravito/stream)
|
|
584
|
+
# ============================================================================
|
|
585
|
+
|
|
586
|
+
# Queue Connection (sync, memory, database, redis, kafka, sqs, rabbitmq)
|
|
587
|
+
QUEUE_CONNECTION=${profile === "core" ? "sync" : "redis"}
|
|
588
|
+
|
|
589
|
+
# Database Queue Configuration (when QUEUE_CONNECTION=database)
|
|
590
|
+
QUEUE_TABLE=jobs
|
|
591
|
+
|
|
592
|
+
# Redis Queue Configuration (when QUEUE_CONNECTION=redis)
|
|
593
|
+
REDIS_PREFIX=queue:
|
|
594
|
+
|
|
595
|
+
`;
|
|
596
|
+
if (profile === "enterprise" || profile === "scale") {
|
|
597
|
+
envContent += `# Kafka Queue Configuration (when QUEUE_CONNECTION=kafka)
|
|
598
|
+
# KAFKA_BROKERS=localhost:9092
|
|
599
|
+
# KAFKA_CONSUMER_GROUP_ID=gravito-workers
|
|
600
|
+
# KAFKA_CLIENT_ID=${context.name}
|
|
601
|
+
|
|
602
|
+
# AWS SQS Queue Configuration (when QUEUE_CONNECTION=sqs)
|
|
603
|
+
# AWS_REGION=us-east-1
|
|
604
|
+
# SQS_QUEUE_URL_PREFIX=
|
|
605
|
+
# SQS_VISIBILITY_TIMEOUT=30
|
|
606
|
+
# SQS_WAIT_TIME_SECONDS=20
|
|
607
|
+
|
|
608
|
+
# RabbitMQ Queue Configuration (when QUEUE_CONNECTION=rabbitmq)
|
|
609
|
+
# RABBITMQ_URL=amqp://localhost
|
|
610
|
+
# RABBITMQ_EXCHANGE=gravito.events
|
|
611
|
+
# RABBITMQ_EXCHANGE_TYPE=fanout
|
|
612
|
+
|
|
613
|
+
`;
|
|
614
|
+
}
|
|
615
|
+
envContent += `# ============================================================================
|
|
616
|
+
# Logging Configuration
|
|
617
|
+
# ============================================================================
|
|
286
618
|
|
|
287
|
-
# Logging
|
|
288
619
|
LOG_LEVEL=debug
|
|
289
620
|
`;
|
|
621
|
+
return envContent;
|
|
290
622
|
}
|
|
291
623
|
/**
|
|
292
624
|
* Generate .gitignore content.
|
|
@@ -338,6 +670,9 @@ coverage/
|
|
|
338
670
|
strict: true,
|
|
339
671
|
skipLibCheck: true,
|
|
340
672
|
declaration: true,
|
|
673
|
+
experimentalDecorators: true,
|
|
674
|
+
emitDecoratorMetadata: true,
|
|
675
|
+
types: ["bun-types"],
|
|
341
676
|
outDir: "./dist",
|
|
342
677
|
rootDir: "./src",
|
|
343
678
|
baseUrl: ".",
|
|
@@ -407,6 +742,11 @@ var CleanArchitectureGenerator = class extends BaseGenerator {
|
|
|
407
742
|
{ type: "file", name: "logging.ts", content: this.generateLoggingConfig() }
|
|
408
743
|
]
|
|
409
744
|
},
|
|
745
|
+
{
|
|
746
|
+
type: "directory",
|
|
747
|
+
name: "database",
|
|
748
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
749
|
+
},
|
|
410
750
|
{
|
|
411
751
|
type: "directory",
|
|
412
752
|
name: "src",
|
|
@@ -419,16 +759,12 @@ var CleanArchitectureGenerator = class extends BaseGenerator {
|
|
|
419
759
|
{
|
|
420
760
|
type: "directory",
|
|
421
761
|
name: "Entities",
|
|
422
|
-
children: [
|
|
423
|
-
{ type: "file", name: "Entity.ts", content: this.generateBaseEntity() },
|
|
424
|
-
{ type: "file", name: "User.ts", content: this.generateUserEntity() }
|
|
425
|
-
]
|
|
762
|
+
children: [{ type: "file", name: "User.ts", content: this.generateUserEntity() }]
|
|
426
763
|
},
|
|
427
764
|
{
|
|
428
765
|
type: "directory",
|
|
429
766
|
name: "ValueObjects",
|
|
430
767
|
children: [
|
|
431
|
-
{ type: "file", name: "ValueObject.ts", content: this.generateBaseValueObject() },
|
|
432
768
|
{ type: "file", name: "Email.ts", content: this.generateEmailValueObject() }
|
|
433
769
|
]
|
|
434
770
|
},
|
|
@@ -446,13 +782,7 @@ var CleanArchitectureGenerator = class extends BaseGenerator {
|
|
|
446
782
|
{
|
|
447
783
|
type: "directory",
|
|
448
784
|
name: "Exceptions",
|
|
449
|
-
children: [
|
|
450
|
-
{
|
|
451
|
-
type: "file",
|
|
452
|
-
name: "DomainException.ts",
|
|
453
|
-
content: this.generateDomainException()
|
|
454
|
-
}
|
|
455
|
-
]
|
|
785
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
456
786
|
}
|
|
457
787
|
]
|
|
458
788
|
},
|
|
@@ -673,42 +1003,6 @@ var CleanArchitectureGenerator = class extends BaseGenerator {
|
|
|
673
1003
|
console: { driver: 'console', level: process.env.LOG_LEVEL ?? 'debug' },
|
|
674
1004
|
},
|
|
675
1005
|
}
|
|
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
1006
|
`;
|
|
713
1007
|
}
|
|
714
1008
|
generateUserEntity() {
|
|
@@ -719,7 +1013,7 @@ export abstract class Entity<T> {
|
|
|
719
1013
|
* Contains business logic related to users.
|
|
720
1014
|
*/
|
|
721
1015
|
|
|
722
|
-
import { Entity } from '
|
|
1016
|
+
import { Entity } from '@gravito/enterprise'
|
|
723
1017
|
import { Email } from '../ValueObjects/Email'
|
|
724
1018
|
|
|
725
1019
|
export interface UserProps {
|
|
@@ -769,30 +1063,6 @@ export class User extends Entity<string> {
|
|
|
769
1063
|
this.props.updatedAt = new Date()
|
|
770
1064
|
}
|
|
771
1065
|
}
|
|
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
1066
|
`;
|
|
797
1067
|
}
|
|
798
1068
|
generateEmailValueObject() {
|
|
@@ -802,7 +1072,7 @@ export abstract class ValueObject<T> {
|
|
|
802
1072
|
* Encapsulates email validation and comparison.
|
|
803
1073
|
*/
|
|
804
1074
|
|
|
805
|
-
import { ValueObject } from '
|
|
1075
|
+
import { ValueObject } from '@gravito/enterprise'
|
|
806
1076
|
|
|
807
1077
|
interface EmailProps {
|
|
808
1078
|
value: string
|
|
@@ -843,43 +1113,11 @@ export class Email extends ValueObject<EmailProps> {
|
|
|
843
1113
|
* Implementations are in Infrastructure layer.
|
|
844
1114
|
*/
|
|
845
1115
|
|
|
1116
|
+
import type { Repository } from '@gravito/enterprise'
|
|
846
1117
|
import type { User } from '../Entities/User'
|
|
847
1118
|
|
|
848
|
-
export interface IUserRepository {
|
|
849
|
-
findById(id: string): Promise<User | null>
|
|
1119
|
+
export interface IUserRepository extends Repository<User, string> {
|
|
850
1120
|
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
1121
|
}
|
|
884
1122
|
`;
|
|
885
1123
|
}
|
|
@@ -893,6 +1131,7 @@ export class InvalidValueException extends DomainException {
|
|
|
893
1131
|
* Application service for creating new users.
|
|
894
1132
|
*/
|
|
895
1133
|
|
|
1134
|
+
import { UseCase } from '@gravito/enterprise'
|
|
896
1135
|
import { User } from '../../../Domain/Entities/User'
|
|
897
1136
|
import type { IUserRepository } from '../../../Domain/Interfaces/IUserRepository'
|
|
898
1137
|
import type { UserDTO } from '../../DTOs/UserDTO'
|
|
@@ -906,8 +1145,10 @@ export interface CreateUserOutput {
|
|
|
906
1145
|
user: UserDTO
|
|
907
1146
|
}
|
|
908
1147
|
|
|
909
|
-
export class CreateUserUseCase {
|
|
910
|
-
constructor(private userRepository: IUserRepository) {
|
|
1148
|
+
export class CreateUserUseCase extends UseCase<CreateUserInput, CreateUserOutput> {
|
|
1149
|
+
constructor(private userRepository: IUserRepository) {
|
|
1150
|
+
super()
|
|
1151
|
+
}
|
|
911
1152
|
|
|
912
1153
|
async execute(input: CreateUserInput): Promise<CreateUserOutput> {
|
|
913
1154
|
// Check if email already exists
|
|
@@ -944,8 +1185,8 @@ export class CreateUserUseCase {
|
|
|
944
1185
|
* Get User Use Case
|
|
945
1186
|
*/
|
|
946
1187
|
|
|
1188
|
+
import { UseCase } from '@gravito/enterprise'
|
|
947
1189
|
import type { IUserRepository } from '../../../Domain/Interfaces/IUserRepository'
|
|
948
|
-
import { EntityNotFoundException } from '../../../Domain/Exceptions/DomainException'
|
|
949
1190
|
import type { UserDTO } from '../../DTOs/UserDTO'
|
|
950
1191
|
|
|
951
1192
|
export interface GetUserInput {
|
|
@@ -956,14 +1197,16 @@ export interface GetUserOutput {
|
|
|
956
1197
|
user: UserDTO
|
|
957
1198
|
}
|
|
958
1199
|
|
|
959
|
-
export class GetUserUseCase {
|
|
960
|
-
constructor(private userRepository: IUserRepository) {
|
|
1200
|
+
export class GetUserUseCase extends UseCase<GetUserInput, GetUserOutput> {
|
|
1201
|
+
constructor(private userRepository: IUserRepository) {
|
|
1202
|
+
super()
|
|
1203
|
+
}
|
|
961
1204
|
|
|
962
1205
|
async execute(input: GetUserInput): Promise<GetUserOutput> {
|
|
963
1206
|
const user = await this.userRepository.findById(input.id)
|
|
964
1207
|
|
|
965
1208
|
if (!user) {
|
|
966
|
-
throw new
|
|
1209
|
+
throw new Error(\`User with id \${input.id} not found\`)
|
|
967
1210
|
}
|
|
968
1211
|
|
|
969
1212
|
return {
|
|
@@ -1064,6 +1307,10 @@ export class UserRepository implements IUserRepository {
|
|
|
1064
1307
|
async findAll(): Promise<User[]> {
|
|
1065
1308
|
return Array.from(users.values())
|
|
1066
1309
|
}
|
|
1310
|
+
|
|
1311
|
+
async exists(id: string): Promise<boolean> {
|
|
1312
|
+
return users.has(id)
|
|
1313
|
+
}
|
|
1067
1314
|
}
|
|
1068
1315
|
`;
|
|
1069
1316
|
}
|
|
@@ -1087,7 +1334,7 @@ export class MailService implements IMailService {
|
|
|
1087
1334
|
* App Service Provider
|
|
1088
1335
|
*/
|
|
1089
1336
|
|
|
1090
|
-
import { ServiceProvider, type Container, type PlanetCore } from 'gravito
|
|
1337
|
+
import { ServiceProvider, type Container, type PlanetCore } from '@gravito/core'
|
|
1091
1338
|
|
|
1092
1339
|
export class AppServiceProvider extends ServiceProvider {
|
|
1093
1340
|
register(_container: Container): void {
|
|
@@ -1107,7 +1354,7 @@ export class AppServiceProvider extends ServiceProvider {
|
|
|
1107
1354
|
* Binds repository interfaces to implementations.
|
|
1108
1355
|
*/
|
|
1109
1356
|
|
|
1110
|
-
import { ServiceProvider, type Container } from 'gravito
|
|
1357
|
+
import { ServiceProvider, type Container } from '@gravito/core'
|
|
1111
1358
|
import { UserRepository } from '../Persistence/Repositories/UserRepository'
|
|
1112
1359
|
import { MailService } from '../ExternalServices/MailService'
|
|
1113
1360
|
|
|
@@ -1130,7 +1377,7 @@ export class RepositoryServiceProvider extends ServiceProvider {
|
|
|
1130
1377
|
* User Controller
|
|
1131
1378
|
*/
|
|
1132
1379
|
|
|
1133
|
-
import type { GravitoContext } from 'gravito
|
|
1380
|
+
import type { GravitoContext } from '@gravito/core'
|
|
1134
1381
|
import { CreateUserUseCase } from '../../../Application/UseCases/User/CreateUser'
|
|
1135
1382
|
import { GetUserUseCase } from '../../../Application/UseCases/User/GetUser'
|
|
1136
1383
|
import { UserRepository } from '../../../Infrastructure/Persistence/Repositories/UserRepository'
|
|
@@ -1209,15 +1456,51 @@ export class UserPresenter {
|
|
|
1209
1456
|
* Application Bootstrap
|
|
1210
1457
|
*/
|
|
1211
1458
|
|
|
1212
|
-
import { PlanetCore } from 'gravito
|
|
1459
|
+
import { bodySizeLimit, PlanetCore, securityHeaders } from '@gravito/core'
|
|
1460
|
+
import { OrbitAtlas } from '@gravito/atlas'
|
|
1461
|
+
import databaseConfig from '../config/database'
|
|
1213
1462
|
import { AppServiceProvider } from './Infrastructure/Providers/AppServiceProvider'
|
|
1214
1463
|
import { RepositoryServiceProvider } from './Infrastructure/Providers/RepositoryServiceProvider'
|
|
1215
1464
|
import { registerApiRoutes } from './Interface/Http/Routes/api'
|
|
1216
1465
|
|
|
1217
1466
|
const core = new PlanetCore({
|
|
1218
|
-
config: {
|
|
1467
|
+
config: {
|
|
1468
|
+
APP_NAME: '${context.name}',
|
|
1469
|
+
database: databaseConfig,
|
|
1470
|
+
},
|
|
1219
1471
|
})
|
|
1220
1472
|
|
|
1473
|
+
const defaultCsp = [
|
|
1474
|
+
"default-src 'self'",
|
|
1475
|
+
"script-src 'self' 'unsafe-inline'",
|
|
1476
|
+
"style-src 'self' 'unsafe-inline'",
|
|
1477
|
+
"img-src 'self' data:",
|
|
1478
|
+
"object-src 'none'",
|
|
1479
|
+
"base-uri 'self'",
|
|
1480
|
+
"frame-ancestors 'none'",
|
|
1481
|
+
].join('; ')
|
|
1482
|
+
const cspValue = process.env.APP_CSP
|
|
1483
|
+
const csp = cspValue === 'false' ? false : (cspValue ?? defaultCsp)
|
|
1484
|
+
const hstsMaxAge = Number.parseInt(process.env.APP_HSTS_MAX_AGE ?? '15552000', 10)
|
|
1485
|
+
const bodyLimit = Number.parseInt(process.env.APP_BODY_LIMIT ?? '1048576', 10)
|
|
1486
|
+
const requireLength = process.env.APP_BODY_REQUIRE_LENGTH === 'true'
|
|
1487
|
+
|
|
1488
|
+
core.adapter.use(
|
|
1489
|
+
'*',
|
|
1490
|
+
securityHeaders({
|
|
1491
|
+
contentSecurityPolicy: csp,
|
|
1492
|
+
hsts:
|
|
1493
|
+
process.env.NODE_ENV === 'production'
|
|
1494
|
+
? { maxAge: Number.isNaN(hstsMaxAge) ? 15552000 : hstsMaxAge, includeSubDomains: true }
|
|
1495
|
+
: false,
|
|
1496
|
+
})
|
|
1497
|
+
)
|
|
1498
|
+
if (!Number.isNaN(bodyLimit) && bodyLimit > 0) {
|
|
1499
|
+
core.adapter.use('*', bodySizeLimit(bodyLimit, { requireContentLength: requireLength }))
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
await core.orbit(new OrbitAtlas())
|
|
1503
|
+
|
|
1221
1504
|
core.register(new RepositoryServiceProvider())
|
|
1222
1505
|
core.register(new AppServiceProvider())
|
|
1223
1506
|
|
|
@@ -1297,6 +1580,32 @@ src/
|
|
|
1297
1580
|
Created with \u2764\uFE0F using Gravito Framework
|
|
1298
1581
|
`;
|
|
1299
1582
|
}
|
|
1583
|
+
generatePackageJson(context) {
|
|
1584
|
+
const pkg = {
|
|
1585
|
+
name: context.nameKebabCase,
|
|
1586
|
+
version: "0.1.0",
|
|
1587
|
+
type: "module",
|
|
1588
|
+
scripts: {
|
|
1589
|
+
dev: "bun run --watch src/bootstrap.ts",
|
|
1590
|
+
build: "bun build ./src/bootstrap.ts --outdir ./dist --target bun",
|
|
1591
|
+
start: "bun run dist/bootstrap.js",
|
|
1592
|
+
test: "bun test",
|
|
1593
|
+
typecheck: "tsc --noEmit",
|
|
1594
|
+
"docker:build": `docker build -t ${context.nameKebabCase} .`,
|
|
1595
|
+
"docker:run": `docker run -it -p 3000:3000 ${context.nameKebabCase}`
|
|
1596
|
+
},
|
|
1597
|
+
dependencies: {
|
|
1598
|
+
"@gravito/core": "workspace:*",
|
|
1599
|
+
"@gravito/enterprise": "workspace:*",
|
|
1600
|
+
...context.withSpectrum ? { "@gravito/spectrum": "workspace:*" } : {}
|
|
1601
|
+
},
|
|
1602
|
+
devDependencies: {
|
|
1603
|
+
"bun-types": "latest",
|
|
1604
|
+
typescript: "^5.0.0"
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
return JSON.stringify(pkg, null, 2);
|
|
1608
|
+
}
|
|
1300
1609
|
};
|
|
1301
1610
|
|
|
1302
1611
|
// src/generators/DddGenerator.ts
|
|
@@ -1424,22 +1733,6 @@ var DddGenerator = class extends BaseGenerator {
|
|
|
1424
1733
|
{ type: "file", name: "Money.ts", content: this.generateMoneyValueObject() },
|
|
1425
1734
|
{ type: "file", name: "Email.ts", content: this.generateEmailValueObject() }
|
|
1426
1735
|
]
|
|
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
1736
|
}
|
|
1444
1737
|
]
|
|
1445
1738
|
},
|
|
@@ -1628,7 +1921,7 @@ var DddGenerator = class extends BaseGenerator {
|
|
|
1628
1921
|
* Central configuration and initialization of the application.
|
|
1629
1922
|
*/
|
|
1630
1923
|
|
|
1631
|
-
import { PlanetCore } from 'gravito
|
|
1924
|
+
import { bodySizeLimit, PlanetCore, securityHeaders } from '@gravito/core'
|
|
1632
1925
|
import { registerProviders } from './providers'
|
|
1633
1926
|
import { registerRoutes } from './routes'
|
|
1634
1927
|
|
|
@@ -1639,6 +1932,35 @@ export async function createApp(): Promise<PlanetCore> {
|
|
|
1639
1932
|
},
|
|
1640
1933
|
})
|
|
1641
1934
|
|
|
1935
|
+
const defaultCsp = [
|
|
1936
|
+
"default-src 'self'",
|
|
1937
|
+
"script-src 'self' 'unsafe-inline'",
|
|
1938
|
+
"style-src 'self' 'unsafe-inline'",
|
|
1939
|
+
"img-src 'self' data:",
|
|
1940
|
+
"object-src 'none'",
|
|
1941
|
+
"base-uri 'self'",
|
|
1942
|
+
"frame-ancestors 'none'",
|
|
1943
|
+
].join('; ')
|
|
1944
|
+
const cspValue = process.env.APP_CSP
|
|
1945
|
+
const csp = cspValue === 'false' ? false : (cspValue ?? defaultCsp)
|
|
1946
|
+
const hstsMaxAge = Number.parseInt(process.env.APP_HSTS_MAX_AGE ?? '15552000', 10)
|
|
1947
|
+
const bodyLimit = Number.parseInt(process.env.APP_BODY_LIMIT ?? '1048576', 10)
|
|
1948
|
+
const requireLength = process.env.APP_BODY_REQUIRE_LENGTH === 'true'
|
|
1949
|
+
|
|
1950
|
+
core.adapter.use(
|
|
1951
|
+
'*',
|
|
1952
|
+
securityHeaders({
|
|
1953
|
+
contentSecurityPolicy: csp,
|
|
1954
|
+
hsts:
|
|
1955
|
+
process.env.NODE_ENV === 'production'
|
|
1956
|
+
? { maxAge: Number.isNaN(hstsMaxAge) ? 15552000 : hstsMaxAge, includeSubDomains: true }
|
|
1957
|
+
: false,
|
|
1958
|
+
})
|
|
1959
|
+
)
|
|
1960
|
+
if (!Number.isNaN(bodyLimit) && bodyLimit > 0) {
|
|
1961
|
+
core.adapter.use('*', bodySizeLimit(bodyLimit, { requireContentLength: requireLength }))
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1642
1964
|
// Register all service providers
|
|
1643
1965
|
await registerProviders(core)
|
|
1644
1966
|
|
|
@@ -1659,7 +1981,7 @@ export async function createApp(): Promise<PlanetCore> {
|
|
|
1659
1981
|
* Register all module service providers here.
|
|
1660
1982
|
*/
|
|
1661
1983
|
|
|
1662
|
-
import type { PlanetCore } from 'gravito
|
|
1984
|
+
import type { PlanetCore } from '@gravito/core'
|
|
1663
1985
|
import { OrderingServiceProvider } from '../Modules/Ordering/Infrastructure/Providers/OrderingServiceProvider'
|
|
1664
1986
|
import { CatalogServiceProvider } from '../Modules/Catalog/Infrastructure/Providers/CatalogServiceProvider'
|
|
1665
1987
|
|
|
@@ -1754,7 +2076,7 @@ export default {
|
|
|
1754
2076
|
* ${name} Service Provider
|
|
1755
2077
|
*/
|
|
1756
2078
|
|
|
1757
|
-
import { ServiceProvider, type Container, type PlanetCore } from 'gravito
|
|
2079
|
+
import { ServiceProvider, type Container, type PlanetCore } from '@gravito/core'
|
|
1758
2080
|
import { ${name}Repository } from '../Persistence/${name}Repository'
|
|
1759
2081
|
|
|
1760
2082
|
export class ${name}ServiceProvider extends ServiceProvider {
|
|
@@ -1781,13 +2103,16 @@ export class ${name}ServiceProvider extends ServiceProvider {
|
|
|
1781
2103
|
build: "bun build ./src/main.ts --outdir ./dist --target bun",
|
|
1782
2104
|
start: "bun run dist/main.js",
|
|
1783
2105
|
test: "bun test",
|
|
1784
|
-
typecheck: "tsc --noEmit"
|
|
2106
|
+
typecheck: "tsc --noEmit",
|
|
2107
|
+
"docker:build": `docker build -t ${context.nameKebabCase} .`,
|
|
2108
|
+
"docker:run": `docker run -it -p 3000:3000 ${context.nameKebabCase}`
|
|
1785
2109
|
},
|
|
1786
2110
|
dependencies: {
|
|
1787
|
-
"gravito
|
|
2111
|
+
"@gravito/core": "^1.0.0-beta.5",
|
|
2112
|
+
"@gravito/enterprise": "workspace:*"
|
|
1788
2113
|
},
|
|
1789
2114
|
devDependencies: {
|
|
1790
|
-
"
|
|
2115
|
+
"bun-types": "latest",
|
|
1791
2116
|
typescript: "^5.0.0"
|
|
1792
2117
|
}
|
|
1793
2118
|
};
|
|
@@ -1838,11 +2163,15 @@ export class ${name}ServiceProvider extends ServiceProvider {
|
|
|
1838
2163
|
* Shared identifier across all contexts.
|
|
1839
2164
|
*/
|
|
1840
2165
|
|
|
1841
|
-
|
|
1842
|
-
private readonly _value: string
|
|
2166
|
+
import { ValueObject } from '@gravito/enterprise'
|
|
1843
2167
|
|
|
2168
|
+
interface IdProps {
|
|
2169
|
+
value: string
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
export class Id extends ValueObject<IdProps> {
|
|
1844
2173
|
private constructor(value: string) {
|
|
1845
|
-
|
|
2174
|
+
super({ value })
|
|
1846
2175
|
}
|
|
1847
2176
|
|
|
1848
2177
|
static create(): Id {
|
|
@@ -1855,15 +2184,11 @@ export class Id {
|
|
|
1855
2184
|
}
|
|
1856
2185
|
|
|
1857
2186
|
get value(): string {
|
|
1858
|
-
return this.
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
equals(other: Id): boolean {
|
|
1862
|
-
return this._value === other._value
|
|
2187
|
+
return this.props.value
|
|
1863
2188
|
}
|
|
1864
2189
|
|
|
1865
2190
|
toString(): string {
|
|
1866
|
-
return this.
|
|
2191
|
+
return this.props.value
|
|
1867
2192
|
}
|
|
1868
2193
|
}
|
|
1869
2194
|
`;
|
|
@@ -1873,12 +2198,25 @@ export class Id {
|
|
|
1873
2198
|
* Money Value Object
|
|
1874
2199
|
*/
|
|
1875
2200
|
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2201
|
+
import { ValueObject } from '@gravito/enterprise'
|
|
2202
|
+
|
|
2203
|
+
interface MoneyProps {
|
|
2204
|
+
amount: number
|
|
2205
|
+
currency: string
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
export class Money extends ValueObject<MoneyProps> {
|
|
2209
|
+
constructor(amount: number, currency: string = 'USD') {
|
|
1881
2210
|
if (amount < 0) throw new Error('Amount cannot be negative')
|
|
2211
|
+
super({ amount, currency })
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
get amount(): number {
|
|
2215
|
+
return this.props.amount
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
get currency(): string {
|
|
2219
|
+
return this.props.currency
|
|
1882
2220
|
}
|
|
1883
2221
|
|
|
1884
2222
|
add(other: Money): Money {
|
|
@@ -1896,10 +2234,6 @@ export class Money {
|
|
|
1896
2234
|
throw new Error('Cannot operate on different currencies')
|
|
1897
2235
|
}
|
|
1898
2236
|
}
|
|
1899
|
-
|
|
1900
|
-
equals(other: Money): boolean {
|
|
1901
|
-
return this.amount === other.amount && this.currency === other.currency
|
|
1902
|
-
}
|
|
1903
2237
|
}
|
|
1904
2238
|
`;
|
|
1905
2239
|
}
|
|
@@ -1908,11 +2242,15 @@ export class Money {
|
|
|
1908
2242
|
* Email Value Object
|
|
1909
2243
|
*/
|
|
1910
2244
|
|
|
1911
|
-
|
|
1912
|
-
|
|
2245
|
+
import { ValueObject } from '@gravito/enterprise'
|
|
2246
|
+
|
|
2247
|
+
interface EmailProps {
|
|
2248
|
+
value: string
|
|
2249
|
+
}
|
|
1913
2250
|
|
|
2251
|
+
export class Email extends ValueObject<EmailProps> {
|
|
1914
2252
|
private constructor(value: string) {
|
|
1915
|
-
|
|
2253
|
+
super({ value: value.toLowerCase().trim() })
|
|
1916
2254
|
}
|
|
1917
2255
|
|
|
1918
2256
|
static create(email: string): Email {
|
|
@@ -1927,97 +2265,7 @@ export class Email {
|
|
|
1927
2265
|
}
|
|
1928
2266
|
|
|
1929
2267
|
get value(): string {
|
|
1930
|
-
return this.
|
|
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)
|
|
2268
|
+
return this.props.value
|
|
2021
2269
|
}
|
|
2022
2270
|
}
|
|
2023
2271
|
`;
|
|
@@ -2027,7 +2275,7 @@ export abstract class ValueObject<T> {
|
|
|
2027
2275
|
* Event Dispatcher
|
|
2028
2276
|
*/
|
|
2029
2277
|
|
|
2030
|
-
import type { DomainEvent } from '
|
|
2278
|
+
import type { DomainEvent } from '@gravito/enterprise'
|
|
2031
2279
|
|
|
2032
2280
|
type EventHandler = (event: DomainEvent) => void | Promise<void>
|
|
2033
2281
|
|
|
@@ -2063,7 +2311,7 @@ export class EventDispatcher {
|
|
|
2063
2311
|
* ${name} Aggregate Root
|
|
2064
2312
|
*/
|
|
2065
2313
|
|
|
2066
|
-
import { AggregateRoot } from '
|
|
2314
|
+
import { AggregateRoot } from '@gravito/enterprise'
|
|
2067
2315
|
import { Id } from '../../../../../Shared/Domain/ValueObjects/Id'
|
|
2068
2316
|
import { ${name}Created } from '../../Events/${name}Created'
|
|
2069
2317
|
import { ${name}Status } from './${name}Status'
|
|
@@ -2074,7 +2322,7 @@ export interface ${name}Props {
|
|
|
2074
2322
|
createdAt: Date
|
|
2075
2323
|
}
|
|
2076
2324
|
|
|
2077
|
-
export class ${name} extends AggregateRoot {
|
|
2325
|
+
export class ${name} extends AggregateRoot<Id> {
|
|
2078
2326
|
private props: ${name}Props
|
|
2079
2327
|
|
|
2080
2328
|
private constructor(id: Id, props: ${name}Props) {
|
|
@@ -2119,14 +2367,14 @@ export enum ${name}Status {
|
|
|
2119
2367
|
* ${name} Created Event
|
|
2120
2368
|
*/
|
|
2121
2369
|
|
|
2122
|
-
import { DomainEvent } from '
|
|
2370
|
+
import { DomainEvent } from '@gravito/enterprise'
|
|
2123
2371
|
|
|
2124
2372
|
export class ${name}Created extends DomainEvent {
|
|
2125
2373
|
constructor(public readonly ${name.toLowerCase()}Id: string) {
|
|
2126
2374
|
super()
|
|
2127
2375
|
}
|
|
2128
2376
|
|
|
2129
|
-
get eventName(): string {
|
|
2377
|
+
override get eventName(): string {
|
|
2130
2378
|
return '${name.toLowerCase()}.created'
|
|
2131
2379
|
}
|
|
2132
2380
|
|
|
@@ -2141,12 +2389,12 @@ export class ${name}Created extends DomainEvent {
|
|
|
2141
2389
|
* ${name} Repository Interface
|
|
2142
2390
|
*/
|
|
2143
2391
|
|
|
2392
|
+
import { Repository } from '@gravito/enterprise'
|
|
2144
2393
|
import type { ${name} } from '../Aggregates/${name}/${name}'
|
|
2394
|
+
import { Id } from '../../../../../Shared/Domain/ValueObjects/Id'
|
|
2145
2395
|
|
|
2146
|
-
export interface I${name}Repository {
|
|
2147
|
-
|
|
2148
|
-
save(aggregate: ${name}): Promise<void>
|
|
2149
|
-
delete(id: string): Promise<void>
|
|
2396
|
+
export interface I${name}Repository extends Repository<${name}, Id> {
|
|
2397
|
+
// Add specific methods for this repository if needed
|
|
2150
2398
|
}
|
|
2151
2399
|
`;
|
|
2152
2400
|
}
|
|
@@ -2155,11 +2403,15 @@ export interface I${name}Repository {
|
|
|
2155
2403
|
* Create ${name} Command
|
|
2156
2404
|
*/
|
|
2157
2405
|
|
|
2158
|
-
|
|
2406
|
+
import { Command } from '@gravito/enterprise'
|
|
2407
|
+
|
|
2408
|
+
export class Create${name}Command extends Command {
|
|
2159
2409
|
constructor(
|
|
2160
2410
|
// Add command properties
|
|
2161
2411
|
public readonly id?: string
|
|
2162
|
-
) {
|
|
2412
|
+
) {
|
|
2413
|
+
super()
|
|
2414
|
+
}
|
|
2163
2415
|
}
|
|
2164
2416
|
`;
|
|
2165
2417
|
}
|
|
@@ -2168,12 +2420,13 @@ export class Create${name}Command {
|
|
|
2168
2420
|
* Create ${name} Handler
|
|
2169
2421
|
*/
|
|
2170
2422
|
|
|
2423
|
+
import { CommandHandler } from '@gravito/enterprise'
|
|
2171
2424
|
import type { I${name}Repository } from '../../../Domain/Repositories/I${name}Repository'
|
|
2172
2425
|
import { ${name} } from '../../../Domain/Aggregates/${name}/${name}'
|
|
2173
2426
|
import { Id } from '../../../../../Shared/Domain/ValueObjects/Id'
|
|
2174
2427
|
import type { Create${name}Command } from './Create${name}Command'
|
|
2175
2428
|
|
|
2176
|
-
export class Create${name}Handler {
|
|
2429
|
+
export class Create${name}Handler implements CommandHandler<Create${name}Command, string> {
|
|
2177
2430
|
constructor(private repository: I${name}Repository) {}
|
|
2178
2431
|
|
|
2179
2432
|
async handle(command: Create${name}Command): Promise<string> {
|
|
@@ -2192,8 +2445,12 @@ export class Create${name}Handler {
|
|
|
2192
2445
|
* Get ${name} By Id Query
|
|
2193
2446
|
*/
|
|
2194
2447
|
|
|
2195
|
-
|
|
2196
|
-
|
|
2448
|
+
import { Query } from '@gravito/enterprise'
|
|
2449
|
+
|
|
2450
|
+
export class Get${name}ByIdQuery extends Query {
|
|
2451
|
+
constructor(public readonly id: string) {
|
|
2452
|
+
super()
|
|
2453
|
+
}
|
|
2197
2454
|
}
|
|
2198
2455
|
`;
|
|
2199
2456
|
}
|
|
@@ -2202,15 +2459,16 @@ export class Get${name}ByIdQuery {
|
|
|
2202
2459
|
* Get ${name} By Id Handler
|
|
2203
2460
|
*/
|
|
2204
2461
|
|
|
2462
|
+
import { QueryHandler } from '@gravito/enterprise'
|
|
2205
2463
|
import type { I${name}Repository } from '../../../Domain/Repositories/I${name}Repository'
|
|
2206
2464
|
import type { ${name}DTO } from '../../DTOs/${name}DTO'
|
|
2207
2465
|
import type { Get${name}ByIdQuery } from './Get${name}ByIdQuery'
|
|
2208
2466
|
|
|
2209
|
-
export class Get${name}ByIdHandler {
|
|
2467
|
+
export class Get${name}ByIdHandler implements QueryHandler<Get${name}ByIdQuery, ${name}DTO | null> {
|
|
2210
2468
|
constructor(private repository: I${name}Repository) {}
|
|
2211
2469
|
|
|
2212
2470
|
async handle(query: Get${name}ByIdQuery): Promise<${name}DTO | null> {
|
|
2213
|
-
const aggregate = await this.repository.findById(query.id)
|
|
2471
|
+
const aggregate = await this.repository.findById(query.id as any) // Simplified for demo
|
|
2214
2472
|
if (!aggregate) return null
|
|
2215
2473
|
|
|
2216
2474
|
return {
|
|
@@ -2242,20 +2500,29 @@ export interface ${name}DTO {
|
|
|
2242
2500
|
|
|
2243
2501
|
import type { ${name} } from '../../Domain/Aggregates/${name}/${name}'
|
|
2244
2502
|
import type { I${name}Repository } from '../../Domain/Repositories/I${name}Repository'
|
|
2503
|
+
import type { Id } from '../../../../../Shared/Domain/ValueObjects/Id'
|
|
2245
2504
|
|
|
2246
2505
|
const store = new Map<string, ${name}>()
|
|
2247
2506
|
|
|
2248
2507
|
export class ${name}Repository implements I${name}Repository {
|
|
2249
|
-
async findById(id:
|
|
2250
|
-
return store.get(id) ?? null
|
|
2508
|
+
async findById(id: Id): Promise<${name} | null> {
|
|
2509
|
+
return store.get(id.value) ?? null
|
|
2251
2510
|
}
|
|
2252
2511
|
|
|
2253
2512
|
async save(aggregate: ${name}): Promise<void> {
|
|
2254
2513
|
store.set(aggregate.id.value, aggregate)
|
|
2255
2514
|
}
|
|
2256
2515
|
|
|
2257
|
-
async delete(id:
|
|
2258
|
-
store.delete(id)
|
|
2516
|
+
async delete(id: Id): Promise<void> {
|
|
2517
|
+
store.delete(id.value)
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
async findAll(): Promise<${name}[]> {
|
|
2521
|
+
return Array.from(store.values())
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
async exists(id: Id): Promise<boolean> {
|
|
2525
|
+
return store.has(id.value)
|
|
2259
2526
|
}
|
|
2260
2527
|
}
|
|
2261
2528
|
`;
|
|
@@ -2698,7 +2965,7 @@ export default {
|
|
|
2698
2965
|
* can be assigned to specific routes.
|
|
2699
2966
|
*/
|
|
2700
2967
|
|
|
2701
|
-
import type { GravitoMiddleware } from 'gravito
|
|
2968
|
+
import type { GravitoMiddleware } from '@gravito/core'
|
|
2702
2969
|
|
|
2703
2970
|
/**
|
|
2704
2971
|
* Global middleware stack.
|
|
@@ -2787,7 +3054,7 @@ export abstract class Controller {
|
|
|
2787
3054
|
* Home Controller
|
|
2788
3055
|
*/
|
|
2789
3056
|
|
|
2790
|
-
import type { GravitoContext } from 'gravito
|
|
3057
|
+
import type { GravitoContext } from '@gravito/core'
|
|
2791
3058
|
import { Controller } from './Controller'
|
|
2792
3059
|
|
|
2793
3060
|
export class HomeController extends Controller {
|
|
@@ -2821,7 +3088,7 @@ export class HomeController extends Controller {
|
|
|
2821
3088
|
* Protects routes that require authentication.
|
|
2822
3089
|
*/
|
|
2823
3090
|
|
|
2824
|
-
import type { GravitoContext, GravitoNext } from 'gravito
|
|
3091
|
+
import type { GravitoContext, GravitoNext } from '@gravito/core'
|
|
2825
3092
|
|
|
2826
3093
|
export async function Authenticate(c: GravitoContext, next: GravitoNext) {
|
|
2827
3094
|
// TODO: Implement authentication check
|
|
@@ -2831,6 +3098,7 @@ export async function Authenticate(c: GravitoContext, next: GravitoNext) {
|
|
|
2831
3098
|
// }
|
|
2832
3099
|
|
|
2833
3100
|
await next()
|
|
3101
|
+
return undefined
|
|
2834
3102
|
}
|
|
2835
3103
|
`;
|
|
2836
3104
|
}
|
|
@@ -2842,7 +3110,7 @@ export async function Authenticate(c: GravitoContext, next: GravitoNext) {
|
|
|
2842
3110
|
* Register and bootstrap application services here.
|
|
2843
3111
|
*/
|
|
2844
3112
|
|
|
2845
|
-
import { ServiceProvider, type Container, type PlanetCore } from 'gravito
|
|
3113
|
+
import { ServiceProvider, type Container, type PlanetCore } from '@gravito/core'
|
|
2846
3114
|
|
|
2847
3115
|
export class AppServiceProvider extends ServiceProvider {
|
|
2848
3116
|
/**
|
|
@@ -2870,7 +3138,7 @@ export class AppServiceProvider extends ServiceProvider {
|
|
|
2870
3138
|
* Configures and registers application routes.
|
|
2871
3139
|
*/
|
|
2872
3140
|
|
|
2873
|
-
import { ServiceProvider, type Container, type PlanetCore } from 'gravito
|
|
3141
|
+
import { ServiceProvider, type Container, type PlanetCore } from '@gravito/core'
|
|
2874
3142
|
import { registerRoutes } from '../routes'
|
|
2875
3143
|
|
|
2876
3144
|
export class RouteServiceProvider extends ServiceProvider {
|
|
@@ -2898,7 +3166,7 @@ export class RouteServiceProvider extends ServiceProvider {
|
|
|
2898
3166
|
* Customize error responses and logging here.
|
|
2899
3167
|
*/
|
|
2900
3168
|
|
|
2901
|
-
import type { ErrorHandlerContext } from 'gravito
|
|
3169
|
+
import type { ErrorHandlerContext } from '@gravito/core'
|
|
2902
3170
|
|
|
2903
3171
|
/**
|
|
2904
3172
|
* Report an exception (logging, monitoring, etc.)
|
|
@@ -2935,6 +3203,13 @@ export const dontReport: string[] = [
|
|
|
2935
3203
|
`;
|
|
2936
3204
|
}
|
|
2937
3205
|
generateBootstrap(context) {
|
|
3206
|
+
const spectrumImport = context.withSpectrum ? "import { SpectrumOrbit } from '@gravito/spectrum'\n" : "";
|
|
3207
|
+
const spectrumOrbit = context.withSpectrum ? `
|
|
3208
|
+
// Enable Debug Dashboard
|
|
3209
|
+
if (process.env.APP_DEBUG === 'true') {
|
|
3210
|
+
await core.orbit(new SpectrumOrbit())
|
|
3211
|
+
}
|
|
3212
|
+
` : "";
|
|
2938
3213
|
return `/**
|
|
2939
3214
|
* Application Bootstrap
|
|
2940
3215
|
*
|
|
@@ -2942,8 +3217,10 @@ export const dontReport: string[] = [
|
|
|
2942
3217
|
* It initializes the core and registers all providers.
|
|
2943
3218
|
*/
|
|
2944
3219
|
|
|
2945
|
-
import { PlanetCore } from 'gravito
|
|
2946
|
-
import {
|
|
3220
|
+
import { bodySizeLimit, PlanetCore, securityHeaders } from '@gravito/core'
|
|
3221
|
+
import { OrbitAtlas } from '@gravito/atlas'
|
|
3222
|
+
import databaseConfig from '../config/database'
|
|
3223
|
+
${spectrumImport}import { AppServiceProvider } from './Providers/AppServiceProvider'
|
|
2947
3224
|
import { RouteServiceProvider } from './Providers/RouteServiceProvider'
|
|
2948
3225
|
|
|
2949
3226
|
// Load environment variables
|
|
@@ -2952,10 +3229,41 @@ import { RouteServiceProvider } from './Providers/RouteServiceProvider'
|
|
|
2952
3229
|
// Create application core
|
|
2953
3230
|
const core = new PlanetCore({
|
|
2954
3231
|
config: {
|
|
2955
|
-
APP_NAME: '${context.name}',
|
|
3232
|
+
APP_NAME: process.env.APP_NAME ?? '${context.name}',
|
|
3233
|
+
database: databaseConfig,
|
|
2956
3234
|
},
|
|
2957
3235
|
})
|
|
3236
|
+
const defaultCsp = [
|
|
3237
|
+
"default-src 'self'",
|
|
3238
|
+
"script-src 'self' 'unsafe-inline'",
|
|
3239
|
+
"style-src 'self' 'unsafe-inline'",
|
|
3240
|
+
"img-src 'self' data:",
|
|
3241
|
+
"object-src 'none'",
|
|
3242
|
+
"base-uri 'self'",
|
|
3243
|
+
"frame-ancestors 'none'",
|
|
3244
|
+
].join('; ')
|
|
3245
|
+
const cspValue = process.env.APP_CSP
|
|
3246
|
+
const csp = cspValue === 'false' ? false : (cspValue ?? defaultCsp)
|
|
3247
|
+
const hstsMaxAge = Number.parseInt(process.env.APP_HSTS_MAX_AGE ?? '15552000', 10)
|
|
3248
|
+
const bodyLimit = Number.parseInt(process.env.APP_BODY_LIMIT ?? '1048576', 10)
|
|
3249
|
+
const requireLength = process.env.APP_BODY_REQUIRE_LENGTH === 'true'
|
|
3250
|
+
|
|
3251
|
+
core.adapter.use(
|
|
3252
|
+
'*',
|
|
3253
|
+
securityHeaders({
|
|
3254
|
+
contentSecurityPolicy: csp,
|
|
3255
|
+
hsts:
|
|
3256
|
+
process.env.NODE_ENV === 'production'
|
|
3257
|
+
? { maxAge: Number.isNaN(hstsMaxAge) ? 15552000 : hstsMaxAge, includeSubDomains: true }
|
|
3258
|
+
: false,
|
|
3259
|
+
})
|
|
3260
|
+
)
|
|
3261
|
+
if (!Number.isNaN(bodyLimit) && bodyLimit > 0) {
|
|
3262
|
+
core.adapter.use('*', bodySizeLimit(bodyLimit, { requireContentLength: requireLength }))
|
|
3263
|
+
}
|
|
2958
3264
|
|
|
3265
|
+
await core.orbit(new OrbitAtlas())
|
|
3266
|
+
${spectrumOrbit}
|
|
2959
3267
|
// Register service providers
|
|
2960
3268
|
core.register(new AppServiceProvider())
|
|
2961
3269
|
core.register(new RouteServiceProvider())
|
|
@@ -3126,8 +3434,829 @@ Created with \u2764\uFE0F using Gravito Framework
|
|
|
3126
3434
|
}
|
|
3127
3435
|
};
|
|
3128
3436
|
|
|
3437
|
+
// src/generators/SatelliteGenerator.ts
|
|
3438
|
+
var SatelliteGenerator = class extends BaseGenerator {
|
|
3439
|
+
get architectureType() {
|
|
3440
|
+
return "satellite";
|
|
3441
|
+
}
|
|
3442
|
+
get displayName() {
|
|
3443
|
+
return "Gravito Satellite";
|
|
3444
|
+
}
|
|
3445
|
+
get description() {
|
|
3446
|
+
return "A modular plugin for Gravito following DDD and Clean Architecture";
|
|
3447
|
+
}
|
|
3448
|
+
getDirectoryStructure(context) {
|
|
3449
|
+
const name = context.namePascalCase;
|
|
3450
|
+
return [
|
|
3451
|
+
{
|
|
3452
|
+
type: "directory",
|
|
3453
|
+
name: "src",
|
|
3454
|
+
children: [
|
|
3455
|
+
// Domain Layer
|
|
3456
|
+
{
|
|
3457
|
+
type: "directory",
|
|
3458
|
+
name: "Domain",
|
|
3459
|
+
children: [
|
|
3460
|
+
{
|
|
3461
|
+
type: "directory",
|
|
3462
|
+
name: "Entities",
|
|
3463
|
+
children: [
|
|
3464
|
+
{ type: "file", name: `${name}.ts`, content: this.generateEntity(name) }
|
|
3465
|
+
]
|
|
3466
|
+
},
|
|
3467
|
+
{
|
|
3468
|
+
type: "directory",
|
|
3469
|
+
name: "Contracts",
|
|
3470
|
+
children: [
|
|
3471
|
+
{
|
|
3472
|
+
type: "file",
|
|
3473
|
+
name: `I${name}Repository.ts`,
|
|
3474
|
+
content: this.generateRepositoryInterface(name)
|
|
3475
|
+
}
|
|
3476
|
+
]
|
|
3477
|
+
},
|
|
3478
|
+
{ type: "directory", name: "ValueObjects", children: [] },
|
|
3479
|
+
{ type: "directory", name: "Events", children: [] }
|
|
3480
|
+
]
|
|
3481
|
+
},
|
|
3482
|
+
// Application Layer
|
|
3483
|
+
{
|
|
3484
|
+
type: "directory",
|
|
3485
|
+
name: "Application",
|
|
3486
|
+
children: [
|
|
3487
|
+
{
|
|
3488
|
+
type: "directory",
|
|
3489
|
+
name: "UseCases",
|
|
3490
|
+
children: [
|
|
3491
|
+
{
|
|
3492
|
+
type: "file",
|
|
3493
|
+
name: `Create${name}.ts`,
|
|
3494
|
+
content: this.generateUseCase(name)
|
|
3495
|
+
}
|
|
3496
|
+
]
|
|
3497
|
+
},
|
|
3498
|
+
{ type: "directory", name: "DTOs", children: [] }
|
|
3499
|
+
]
|
|
3500
|
+
},
|
|
3501
|
+
// Infrastructure Layer
|
|
3502
|
+
{
|
|
3503
|
+
type: "directory",
|
|
3504
|
+
name: "Infrastructure",
|
|
3505
|
+
children: [
|
|
3506
|
+
{
|
|
3507
|
+
type: "directory",
|
|
3508
|
+
name: "Persistence",
|
|
3509
|
+
children: [
|
|
3510
|
+
{
|
|
3511
|
+
type: "file",
|
|
3512
|
+
name: `Atlas${name}Repository.ts`,
|
|
3513
|
+
content: this.generateAtlasRepository(name)
|
|
3514
|
+
},
|
|
3515
|
+
{ type: "directory", name: "Migrations", children: [] }
|
|
3516
|
+
]
|
|
3517
|
+
}
|
|
3518
|
+
]
|
|
3519
|
+
},
|
|
3520
|
+
// Entry Point
|
|
3521
|
+
{ type: "file", name: "index.ts", content: this.generateEntryPoint(name) },
|
|
3522
|
+
{
|
|
3523
|
+
type: "file",
|
|
3524
|
+
name: "env.d.ts",
|
|
3525
|
+
content: "interface ImportMeta {\n readonly dir: string\n readonly path: string\n}\n"
|
|
3526
|
+
},
|
|
3527
|
+
{ type: "file", name: "manifest.json", content: this.generateManifest(context) }
|
|
3528
|
+
]
|
|
3529
|
+
},
|
|
3530
|
+
{
|
|
3531
|
+
type: "directory",
|
|
3532
|
+
name: "tests",
|
|
3533
|
+
children: [
|
|
3534
|
+
{
|
|
3535
|
+
type: "file",
|
|
3536
|
+
name: "unit.test.ts",
|
|
3537
|
+
content: `import { describe, it, expect } from "bun:test";
|
|
3538
|
+
|
|
3539
|
+
describe("${name}", () => {
|
|
3540
|
+
it("should work", () => {
|
|
3541
|
+
expect(true).toBe(true);
|
|
3542
|
+
});
|
|
3543
|
+
});`
|
|
3544
|
+
}
|
|
3545
|
+
]
|
|
3546
|
+
}
|
|
3547
|
+
];
|
|
3548
|
+
}
|
|
3549
|
+
// ─────────────────────────────────────────────────────────────
|
|
3550
|
+
// Domain Templates
|
|
3551
|
+
// ─────────────────────────────────────────────────────────────
|
|
3552
|
+
generateEntity(name) {
|
|
3553
|
+
return `import { Entity } from '@gravito/enterprise'
|
|
3554
|
+
|
|
3555
|
+
export interface ${name}Props {
|
|
3556
|
+
name: string
|
|
3557
|
+
createdAt: Date
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
export class ${name} extends Entity<string> {
|
|
3561
|
+
constructor(id: string, private props: ${name}Props) {
|
|
3562
|
+
super(id)
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
static create(id: string, name: string): ${name} {
|
|
3566
|
+
return new ${name}(id, {
|
|
3567
|
+
name,
|
|
3568
|
+
createdAt: new Date()
|
|
3569
|
+
})
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
get name() { return this.props.name }
|
|
3573
|
+
}
|
|
3574
|
+
`;
|
|
3575
|
+
}
|
|
3576
|
+
generateRepositoryInterface(name) {
|
|
3577
|
+
return `import { Repository } from '@gravito/enterprise'
|
|
3578
|
+
import { ${name} } from '../Entities/${name}'
|
|
3579
|
+
|
|
3580
|
+
export interface I${name}Repository extends Repository<${name}, string> {
|
|
3581
|
+
// Add custom methods here
|
|
3582
|
+
}
|
|
3583
|
+
`;
|
|
3584
|
+
}
|
|
3585
|
+
// ─────────────────────────────────────────────────────────────
|
|
3586
|
+
// Application Templates
|
|
3587
|
+
// ─────────────────────────────────────────────────────────────
|
|
3588
|
+
generateUseCase(name) {
|
|
3589
|
+
return `import { UseCase } from '@gravito/enterprise'
|
|
3590
|
+
import { I${name}Repository } from '../../Domain/Contracts/I${name}Repository'
|
|
3591
|
+
import { ${name} } from '../../Domain/Entities/${name}'
|
|
3592
|
+
|
|
3593
|
+
export interface Create${name}Input {
|
|
3594
|
+
name: string
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
export class Create${name} extends UseCase<Create${name}Input, string> {
|
|
3598
|
+
constructor(private repository: I${name}Repository) {
|
|
3599
|
+
super()
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
async execute(input: Create${name}Input): Promise<string> {
|
|
3603
|
+
const id = crypto.randomUUID()
|
|
3604
|
+
const entity = ${name}.create(id, input.name)
|
|
3605
|
+
|
|
3606
|
+
await this.repository.save(entity)
|
|
3607
|
+
|
|
3608
|
+
return id
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
`;
|
|
3612
|
+
}
|
|
3613
|
+
// ─────────────────────────────────────────────────────────────
|
|
3614
|
+
// Infrastructure Templates (Dogfooding Atlas)
|
|
3615
|
+
// ─────────────────────────────────────────────────────────────
|
|
3616
|
+
generateAtlasRepository(name) {
|
|
3617
|
+
return `import { I${name}Repository } from '../../Domain/Contracts/I${name}Repository'
|
|
3618
|
+
import { ${name} } from '../../Domain/Entities/${name}'
|
|
3619
|
+
import { DB } from '@gravito/atlas'
|
|
3620
|
+
|
|
3621
|
+
export class Atlas${name}Repository implements I${name}Repository {
|
|
3622
|
+
async save(entity: ${name}): Promise<void> {
|
|
3623
|
+
// Dogfooding: Use @gravito/atlas for persistence
|
|
3624
|
+
console.log('[Atlas] Saving entity:', entity.id)
|
|
3625
|
+
// await DB.table('${name.toLowerCase()}s').insert({ ... })
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
async findById(id: string): Promise<${name} | null> {
|
|
3629
|
+
return null
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
async findAll(): Promise<${name}[]> {
|
|
3633
|
+
return []
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
async delete(id: string): Promise<void> {}
|
|
3637
|
+
|
|
3638
|
+
async exists(id: string): Promise<boolean> {
|
|
3639
|
+
return false
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
`;
|
|
3643
|
+
}
|
|
3644
|
+
// ─────────────────────────────────────────────────────────────
|
|
3645
|
+
// Entry Point & Manifest
|
|
3646
|
+
// ─────────────────────────────────────────────────────────────
|
|
3647
|
+
generateEntryPoint(name) {
|
|
3648
|
+
return `import { ServiceProvider, type Container } from '@gravito/core'
|
|
3649
|
+
import { Atlas${name}Repository } from './Infrastructure/Persistence/Atlas${name}Repository'
|
|
3650
|
+
|
|
3651
|
+
export class ${name}ServiceProvider extends ServiceProvider {
|
|
3652
|
+
register(container: Container): void {
|
|
3653
|
+
// Bind Repository
|
|
3654
|
+
container.singleton('${name.toLowerCase()}.repo', () => new Atlas${name}Repository())
|
|
3655
|
+
|
|
3656
|
+
// Bind UseCases
|
|
3657
|
+
container.singleton('${name.toLowerCase()}.create', () => {
|
|
3658
|
+
return new (require('./Application/UseCases/Create${name}').Create${name})(
|
|
3659
|
+
container.make('${name.toLowerCase()}.repo')
|
|
3660
|
+
)
|
|
3661
|
+
})
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
boot(): void {
|
|
3665
|
+
this.core?.logger.info('\u{1F6F0}\uFE0F Satellite ${name} is operational')
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
`;
|
|
3669
|
+
}
|
|
3670
|
+
generateManifest(context) {
|
|
3671
|
+
return JSON.stringify(
|
|
3672
|
+
{
|
|
3673
|
+
name: context.name,
|
|
3674
|
+
id: context.nameKebabCase,
|
|
3675
|
+
version: "0.1.0",
|
|
3676
|
+
description: context.description || "A Gravito Satellite",
|
|
3677
|
+
capabilities: [`create-${context.nameKebabCase}`],
|
|
3678
|
+
requirements: [
|
|
3679
|
+
"cache"
|
|
3680
|
+
// Example requirement
|
|
3681
|
+
],
|
|
3682
|
+
hooks: [`${context.nameKebabCase}:created`]
|
|
3683
|
+
},
|
|
3684
|
+
null,
|
|
3685
|
+
2
|
|
3686
|
+
);
|
|
3687
|
+
}
|
|
3688
|
+
generatePackageJson(context) {
|
|
3689
|
+
const isInternal = context.isInternal || false;
|
|
3690
|
+
const depVersion = isInternal ? "workspace:*" : "^1.0.0-beta.1";
|
|
3691
|
+
const pkg = {
|
|
3692
|
+
name: isInternal ? `@gravito/satellite-${context.nameKebabCase}` : `gravito-satellite-${context.nameKebabCase}`,
|
|
3693
|
+
version: "0.1.0",
|
|
3694
|
+
type: "module",
|
|
3695
|
+
main: "dist/index.js",
|
|
3696
|
+
module: "dist/index.mjs",
|
|
3697
|
+
types: "dist/index.d.ts",
|
|
3698
|
+
scripts: {
|
|
3699
|
+
build: "tsup src/index.ts --format cjs,esm --dts",
|
|
3700
|
+
test: "bun test",
|
|
3701
|
+
typecheck: "tsc --noEmit"
|
|
3702
|
+
},
|
|
3703
|
+
dependencies: {
|
|
3704
|
+
"@gravito/core": depVersion,
|
|
3705
|
+
"@gravito/enterprise": depVersion,
|
|
3706
|
+
"@gravito/atlas": depVersion,
|
|
3707
|
+
"@gravito/stasis": depVersion
|
|
3708
|
+
},
|
|
3709
|
+
devDependencies: {
|
|
3710
|
+
tsup: "^8.0.0",
|
|
3711
|
+
typescript: "^5.0.0"
|
|
3712
|
+
}
|
|
3713
|
+
};
|
|
3714
|
+
return JSON.stringify(pkg, null, 2);
|
|
3715
|
+
}
|
|
3716
|
+
generateArchitectureDoc(context) {
|
|
3717
|
+
return `# ${context.name} Satellite Architecture
|
|
3718
|
+
|
|
3719
|
+
This satellite follows the Gravito Satellite Specification v1.0.
|
|
3720
|
+
|
|
3721
|
+
## Design
|
|
3722
|
+
- **DDD**: Domain logic is separated from framework concerns.
|
|
3723
|
+
- **Dogfooding**: Uses official Gravito modules (@gravito/atlas, @gravito/stasis).
|
|
3724
|
+
- **Decoupled**: Inter-satellite communication happens via Contracts and Events.
|
|
3725
|
+
|
|
3726
|
+
## Layers
|
|
3727
|
+
- **Domain**: Pure business rules.
|
|
3728
|
+
- **Application**: Orchestration of domain tasks.
|
|
3729
|
+
- **Infrastructure**: Implementation of persistence and external services.
|
|
3730
|
+
- **Interface**: HTTP and Event entry points.
|
|
3731
|
+
`;
|
|
3732
|
+
}
|
|
3733
|
+
};
|
|
3734
|
+
|
|
3735
|
+
// src/LockGenerator.ts
|
|
3736
|
+
var LockGenerator = class {
|
|
3737
|
+
generate(profileName, config, templateName = "basic", templateVersion = "1.0.0") {
|
|
3738
|
+
const lock = {
|
|
3739
|
+
profile: {
|
|
3740
|
+
name: profileName,
|
|
3741
|
+
version: "1.0.0"
|
|
3742
|
+
// This could be dynamic based on profile system version
|
|
3743
|
+
},
|
|
3744
|
+
features: config.features,
|
|
3745
|
+
drivers: config.drivers,
|
|
3746
|
+
manifest: {
|
|
3747
|
+
template: templateName,
|
|
3748
|
+
version: templateVersion
|
|
3749
|
+
},
|
|
3750
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3751
|
+
};
|
|
3752
|
+
return JSON.stringify(lock, null, 2);
|
|
3753
|
+
}
|
|
3754
|
+
};
|
|
3755
|
+
|
|
3756
|
+
// src/ProfileResolver.ts
|
|
3757
|
+
var ProfileResolver = class _ProfileResolver {
|
|
3758
|
+
static DEFAULTS = {
|
|
3759
|
+
core: {
|
|
3760
|
+
drivers: {
|
|
3761
|
+
database: "sqlite",
|
|
3762
|
+
cache: "memory",
|
|
3763
|
+
queue: "sync",
|
|
3764
|
+
storage: "local",
|
|
3765
|
+
session: "file"
|
|
3766
|
+
},
|
|
3767
|
+
features: []
|
|
3768
|
+
},
|
|
3769
|
+
scale: {
|
|
3770
|
+
drivers: {
|
|
3771
|
+
database: "postgresql",
|
|
3772
|
+
cache: "redis",
|
|
3773
|
+
queue: "redis",
|
|
3774
|
+
storage: "s3",
|
|
3775
|
+
session: "redis"
|
|
3776
|
+
},
|
|
3777
|
+
features: ["stream", "nebula"]
|
|
3778
|
+
},
|
|
3779
|
+
enterprise: {
|
|
3780
|
+
drivers: {
|
|
3781
|
+
database: "postgresql",
|
|
3782
|
+
cache: "redis",
|
|
3783
|
+
queue: "redis",
|
|
3784
|
+
storage: "s3",
|
|
3785
|
+
session: "redis"
|
|
3786
|
+
},
|
|
3787
|
+
features: ["stream", "nebula", "monitor", "sentinel", "fortify"]
|
|
3788
|
+
}
|
|
3789
|
+
};
|
|
3790
|
+
resolve(profile = "core", withFeatures = []) {
|
|
3791
|
+
const base = _ProfileResolver.DEFAULTS[profile] || _ProfileResolver.DEFAULTS.core;
|
|
3792
|
+
const config = {
|
|
3793
|
+
drivers: { ...base.drivers },
|
|
3794
|
+
features: [...base.features]
|
|
3795
|
+
};
|
|
3796
|
+
for (const feature of withFeatures) {
|
|
3797
|
+
this.applyFeature(config, feature);
|
|
3798
|
+
}
|
|
3799
|
+
return config;
|
|
3800
|
+
}
|
|
3801
|
+
applyFeature(config, feature) {
|
|
3802
|
+
switch (feature) {
|
|
3803
|
+
case "redis":
|
|
3804
|
+
config.drivers.cache = "redis";
|
|
3805
|
+
config.drivers.queue = "redis";
|
|
3806
|
+
config.drivers.session = "redis";
|
|
3807
|
+
break;
|
|
3808
|
+
case "postgres":
|
|
3809
|
+
case "postgresql":
|
|
3810
|
+
config.drivers.database = "postgresql";
|
|
3811
|
+
break;
|
|
3812
|
+
case "mysql":
|
|
3813
|
+
config.drivers.database = "mysql";
|
|
3814
|
+
break;
|
|
3815
|
+
case "s3":
|
|
3816
|
+
config.drivers.storage = "s3";
|
|
3817
|
+
break;
|
|
3818
|
+
case "r2":
|
|
3819
|
+
config.drivers.storage = "r2";
|
|
3820
|
+
break;
|
|
3821
|
+
case "queue":
|
|
3822
|
+
if (config.drivers.queue === "sync") {
|
|
3823
|
+
config.drivers.queue = "redis";
|
|
3824
|
+
}
|
|
3825
|
+
break;
|
|
3826
|
+
default:
|
|
3827
|
+
if (!config.features.includes(feature)) {
|
|
3828
|
+
config.features.push(feature);
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
/**
|
|
3833
|
+
* Validator for profile names
|
|
3834
|
+
*/
|
|
3835
|
+
isValidProfile(profile) {
|
|
3836
|
+
return profile in _ProfileResolver.DEFAULTS;
|
|
3837
|
+
}
|
|
3838
|
+
/**
|
|
3839
|
+
* Validator for feature names
|
|
3840
|
+
*/
|
|
3841
|
+
isValidFeature(feature) {
|
|
3842
|
+
const validFeatures = [
|
|
3843
|
+
"redis",
|
|
3844
|
+
"postgres",
|
|
3845
|
+
"postgresql",
|
|
3846
|
+
"mysql",
|
|
3847
|
+
"s3",
|
|
3848
|
+
"r2",
|
|
3849
|
+
"queue",
|
|
3850
|
+
"stream",
|
|
3851
|
+
"nebula",
|
|
3852
|
+
"monitor",
|
|
3853
|
+
"sentinel",
|
|
3854
|
+
"fortify"
|
|
3855
|
+
];
|
|
3856
|
+
return validFeatures.includes(feature);
|
|
3857
|
+
}
|
|
3858
|
+
};
|
|
3859
|
+
|
|
3129
3860
|
// src/Scaffold.ts
|
|
3130
3861
|
import path3 from "path";
|
|
3862
|
+
|
|
3863
|
+
// src/generators/ActionDomainGenerator.ts
|
|
3864
|
+
var ActionDomainGenerator = class extends BaseGenerator {
|
|
3865
|
+
get architectureType() {
|
|
3866
|
+
return "action-domain";
|
|
3867
|
+
}
|
|
3868
|
+
get displayName() {
|
|
3869
|
+
return "Action Domain";
|
|
3870
|
+
}
|
|
3871
|
+
get description() {
|
|
3872
|
+
return "Single-responsibility Action pattern for clear business logic separation";
|
|
3873
|
+
}
|
|
3874
|
+
getDirectoryStructure(context) {
|
|
3875
|
+
return [
|
|
3876
|
+
{
|
|
3877
|
+
type: "directory",
|
|
3878
|
+
name: "config",
|
|
3879
|
+
children: [
|
|
3880
|
+
{ type: "file", name: "app.ts", content: this.generateAppConfig(context) },
|
|
3881
|
+
{ type: "file", name: "database.ts", content: this.generateDatabaseConfig() }
|
|
3882
|
+
]
|
|
3883
|
+
},
|
|
3884
|
+
{
|
|
3885
|
+
type: "directory",
|
|
3886
|
+
name: "database",
|
|
3887
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
3888
|
+
},
|
|
3889
|
+
{
|
|
3890
|
+
type: "directory",
|
|
3891
|
+
name: "src",
|
|
3892
|
+
children: [
|
|
3893
|
+
{
|
|
3894
|
+
type: "directory",
|
|
3895
|
+
name: "actions",
|
|
3896
|
+
children: [
|
|
3897
|
+
{ type: "file", name: "Action.ts", content: this.generateActionBase() },
|
|
3898
|
+
{
|
|
3899
|
+
type: "directory",
|
|
3900
|
+
name: "server",
|
|
3901
|
+
children: [
|
|
3902
|
+
{
|
|
3903
|
+
type: "file",
|
|
3904
|
+
name: "GetServerStatusAction.ts",
|
|
3905
|
+
content: this.generateGetServerStatusAction()
|
|
3906
|
+
}
|
|
3907
|
+
]
|
|
3908
|
+
}
|
|
3909
|
+
]
|
|
3910
|
+
},
|
|
3911
|
+
{
|
|
3912
|
+
type: "directory",
|
|
3913
|
+
name: "controllers",
|
|
3914
|
+
children: [
|
|
3915
|
+
{
|
|
3916
|
+
type: "directory",
|
|
3917
|
+
name: "api",
|
|
3918
|
+
children: [
|
|
3919
|
+
{
|
|
3920
|
+
type: "directory",
|
|
3921
|
+
name: "v1",
|
|
3922
|
+
children: [
|
|
3923
|
+
{
|
|
3924
|
+
type: "file",
|
|
3925
|
+
name: "ServerController.ts",
|
|
3926
|
+
content: this.generateServerController()
|
|
3927
|
+
}
|
|
3928
|
+
]
|
|
3929
|
+
}
|
|
3930
|
+
]
|
|
3931
|
+
}
|
|
3932
|
+
]
|
|
3933
|
+
},
|
|
3934
|
+
{
|
|
3935
|
+
type: "directory",
|
|
3936
|
+
name: "types",
|
|
3937
|
+
children: [
|
|
3938
|
+
{
|
|
3939
|
+
type: "directory",
|
|
3940
|
+
name: "requests",
|
|
3941
|
+
children: [
|
|
3942
|
+
{
|
|
3943
|
+
type: "directory",
|
|
3944
|
+
name: "api",
|
|
3945
|
+
children: [
|
|
3946
|
+
{
|
|
3947
|
+
type: "directory",
|
|
3948
|
+
name: "v1",
|
|
3949
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
3950
|
+
}
|
|
3951
|
+
]
|
|
3952
|
+
}
|
|
3953
|
+
]
|
|
3954
|
+
},
|
|
3955
|
+
{
|
|
3956
|
+
type: "directory",
|
|
3957
|
+
name: "responses",
|
|
3958
|
+
children: [
|
|
3959
|
+
{
|
|
3960
|
+
type: "directory",
|
|
3961
|
+
name: "api",
|
|
3962
|
+
children: [
|
|
3963
|
+
{
|
|
3964
|
+
type: "directory",
|
|
3965
|
+
name: "v1",
|
|
3966
|
+
children: [
|
|
3967
|
+
{
|
|
3968
|
+
type: "file",
|
|
3969
|
+
name: "ServerStatusResponse.ts",
|
|
3970
|
+
content: this.generateServerStatusResponse()
|
|
3971
|
+
}
|
|
3972
|
+
]
|
|
3973
|
+
}
|
|
3974
|
+
]
|
|
3975
|
+
}
|
|
3976
|
+
]
|
|
3977
|
+
}
|
|
3978
|
+
]
|
|
3979
|
+
},
|
|
3980
|
+
{
|
|
3981
|
+
type: "directory",
|
|
3982
|
+
name: "models",
|
|
3983
|
+
children: [{ type: "file", name: "User.ts", content: this.generateUserModel() }]
|
|
3984
|
+
},
|
|
3985
|
+
{
|
|
3986
|
+
type: "directory",
|
|
3987
|
+
name: "repositories",
|
|
3988
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
3989
|
+
},
|
|
3990
|
+
{
|
|
3991
|
+
type: "directory",
|
|
3992
|
+
name: "routes",
|
|
3993
|
+
children: [{ type: "file", name: "api.ts", content: this.generateApiRoutes() }]
|
|
3994
|
+
},
|
|
3995
|
+
{
|
|
3996
|
+
type: "directory",
|
|
3997
|
+
name: "providers",
|
|
3998
|
+
children: [
|
|
3999
|
+
{
|
|
4000
|
+
type: "file",
|
|
4001
|
+
name: "AppServiceProvider.ts",
|
|
4002
|
+
content: this.generateAppServiceProvider(context)
|
|
4003
|
+
}
|
|
4004
|
+
]
|
|
4005
|
+
},
|
|
4006
|
+
{ type: "file", name: "bootstrap.ts", content: this.generateBootstrap(context) }
|
|
4007
|
+
]
|
|
4008
|
+
},
|
|
4009
|
+
{
|
|
4010
|
+
type: "directory",
|
|
4011
|
+
name: "tests",
|
|
4012
|
+
children: [{ type: "file", name: ".gitkeep", content: "" }]
|
|
4013
|
+
}
|
|
4014
|
+
];
|
|
4015
|
+
}
|
|
4016
|
+
// ─────────────────────────────────────────────────────────────
|
|
4017
|
+
// Config Generators
|
|
4018
|
+
// ─────────────────────────────────────────────────────────────
|
|
4019
|
+
generateAppConfig(context) {
|
|
4020
|
+
return `export default {
|
|
4021
|
+
name: process.env.APP_NAME ?? '${context.name}',
|
|
4022
|
+
env: process.env.APP_ENV ?? 'development',
|
|
4023
|
+
debug: process.env.APP_DEBUG === 'true',
|
|
4024
|
+
url: process.env.APP_URL ?? 'http://localhost:3000',
|
|
4025
|
+
key: process.env.APP_KEY,
|
|
4026
|
+
}
|
|
4027
|
+
`;
|
|
4028
|
+
}
|
|
4029
|
+
generateDatabaseConfig() {
|
|
4030
|
+
return `export default {
|
|
4031
|
+
default: process.env.DB_CONNECTION ?? 'sqlite',
|
|
4032
|
+
connections: {
|
|
4033
|
+
sqlite: {
|
|
4034
|
+
driver: 'sqlite',
|
|
4035
|
+
database: process.env.DB_DATABASE ?? 'database/database.sqlite',
|
|
4036
|
+
},
|
|
4037
|
+
},
|
|
4038
|
+
}
|
|
4039
|
+
`;
|
|
4040
|
+
}
|
|
4041
|
+
// ─────────────────────────────────────────────────────────────
|
|
4042
|
+
// Model Generators
|
|
4043
|
+
// ─────────────────────────────────────────────────────────────
|
|
4044
|
+
generateUserModel() {
|
|
4045
|
+
return `/**
|
|
4046
|
+
* User Model
|
|
4047
|
+
*/
|
|
4048
|
+
|
|
4049
|
+
import { Model, column } from '@gravito/atlas'
|
|
4050
|
+
|
|
4051
|
+
export class User extends Model {
|
|
4052
|
+
static table = 'users'
|
|
4053
|
+
|
|
4054
|
+
@column({ isPrimary: true })
|
|
4055
|
+
id!: number
|
|
4056
|
+
|
|
4057
|
+
@column()
|
|
4058
|
+
name!: string
|
|
4059
|
+
|
|
4060
|
+
@column()
|
|
4061
|
+
email!: string
|
|
4062
|
+
|
|
4063
|
+
@column()
|
|
4064
|
+
created_at!: Date
|
|
4065
|
+
|
|
4066
|
+
@column()
|
|
4067
|
+
updated_at!: Date
|
|
4068
|
+
}
|
|
4069
|
+
`;
|
|
4070
|
+
}
|
|
4071
|
+
// ─────────────────────────────────────────────────────────────
|
|
4072
|
+
// Action Generators
|
|
4073
|
+
// ─────────────────────────────────────────────────────────────
|
|
4074
|
+
generateActionBase() {
|
|
4075
|
+
return `/**
|
|
4076
|
+
* Action Base Class
|
|
4077
|
+
*
|
|
4078
|
+
* All business logic actions should extend this class.
|
|
4079
|
+
* It enforces a consistent execution method.
|
|
4080
|
+
*/
|
|
4081
|
+
|
|
4082
|
+
export abstract class Action<TInput = unknown, TOutput = unknown> {
|
|
4083
|
+
/**
|
|
4084
|
+
* Execute the business logic.
|
|
4085
|
+
*/
|
|
4086
|
+
abstract execute(input: TInput): Promise<TOutput> | TOutput
|
|
4087
|
+
}
|
|
4088
|
+
`;
|
|
4089
|
+
}
|
|
4090
|
+
generateGetServerStatusAction() {
|
|
4091
|
+
return `/**
|
|
4092
|
+
* Get Server Status Action
|
|
4093
|
+
*/
|
|
4094
|
+
|
|
4095
|
+
import { Action } from '../Action'
|
|
4096
|
+
import type { ServerStatusResponse } from '../../types/responses/api/v1/ServerStatusResponse'
|
|
4097
|
+
|
|
4098
|
+
export class GetServerStatusAction extends Action<void, ServerStatusResponse> {
|
|
4099
|
+
execute(): Promise<ServerStatusResponse> {
|
|
4100
|
+
return Promise.resolve({
|
|
4101
|
+
status: 'active',
|
|
4102
|
+
timestamp: new Date().toISOString(),
|
|
4103
|
+
service: 'Gravito Hub'
|
|
4104
|
+
})
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
`;
|
|
4108
|
+
}
|
|
4109
|
+
// ─────────────────────────────────────────────────────────────
|
|
4110
|
+
// Controller Generators
|
|
4111
|
+
// ─────────────────────────────────────────────────────────────
|
|
4112
|
+
generateServerController() {
|
|
4113
|
+
return `/**
|
|
4114
|
+
* Server Controller
|
|
4115
|
+
*/
|
|
4116
|
+
|
|
4117
|
+
import type { GravitoContext } from '@gravito/core'
|
|
4118
|
+
import { GetServerStatusAction } from '../../../actions/server/GetServerStatusAction'
|
|
4119
|
+
|
|
4120
|
+
export class ServerController {
|
|
4121
|
+
/**
|
|
4122
|
+
* GET /v1/server/status
|
|
4123
|
+
*/
|
|
4124
|
+
async status(c: GravitoContext) {
|
|
4125
|
+
const action = new GetServerStatusAction()
|
|
4126
|
+
const result = await action.execute()
|
|
4127
|
+
|
|
4128
|
+
return c.json({
|
|
4129
|
+
success: true,
|
|
4130
|
+
data: result
|
|
4131
|
+
})
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
`;
|
|
4135
|
+
}
|
|
4136
|
+
// ─────────────────────────────────────────────────────────────
|
|
4137
|
+
// Type Generators
|
|
4138
|
+
// ─────────────────────────────────────────────────────────────
|
|
4139
|
+
generateServerStatusResponse() {
|
|
4140
|
+
return `/**
|
|
4141
|
+
* Server Status Response Type
|
|
4142
|
+
*/
|
|
4143
|
+
|
|
4144
|
+
export interface ServerStatusResponse {
|
|
4145
|
+
status: string
|
|
4146
|
+
timestamp: string
|
|
4147
|
+
service: string
|
|
4148
|
+
}
|
|
4149
|
+
`;
|
|
4150
|
+
}
|
|
4151
|
+
// ─────────────────────────────────────────────────────────────
|
|
4152
|
+
// Routes & Bootstrap
|
|
4153
|
+
// ─────────────────────────────────────────────────────────────
|
|
4154
|
+
generateApiRoutes() {
|
|
4155
|
+
return `/**
|
|
4156
|
+
* API Routes Registration
|
|
4157
|
+
*/
|
|
4158
|
+
|
|
4159
|
+
import type { Router } from '@gravito/core'
|
|
4160
|
+
import { ServerController } from '../controllers/api/v1/ServerController'
|
|
4161
|
+
|
|
4162
|
+
export function registerApiRoutes(router: Router) {
|
|
4163
|
+
const server = new ServerController()
|
|
4164
|
+
|
|
4165
|
+
router.prefix('/v1').group((group) => {
|
|
4166
|
+
// Server Domain
|
|
4167
|
+
group.get('/server/status', (c) => server.status(c))
|
|
4168
|
+
})
|
|
4169
|
+
}
|
|
4170
|
+
`;
|
|
4171
|
+
}
|
|
4172
|
+
generateAppServiceProvider(context) {
|
|
4173
|
+
return `/**
|
|
4174
|
+
* App Service Provider
|
|
4175
|
+
*/
|
|
4176
|
+
|
|
4177
|
+
import { ServiceProvider, type Container, type PlanetCore } from '@gravito/core'
|
|
4178
|
+
|
|
4179
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
4180
|
+
register(container: Container): void {
|
|
4181
|
+
// Register global services here
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
boot(core: PlanetCore): void {
|
|
4185
|
+
core.logger.info('${context.name} (Action Domain) booted!')
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
`;
|
|
4189
|
+
}
|
|
4190
|
+
generateBootstrap(context) {
|
|
4191
|
+
return `/**
|
|
4192
|
+
* Application Entry Point
|
|
4193
|
+
*/
|
|
4194
|
+
|
|
4195
|
+
import { PlanetCore, securityHeaders, bodySizeLimit } from '@gravito/core'
|
|
4196
|
+
import { OrbitAtlas } from '@gravito/atlas'
|
|
4197
|
+
import databaseConfig from '../config/database'
|
|
4198
|
+
import { AppServiceProvider } from './providers/AppServiceProvider'
|
|
4199
|
+
import { registerApiRoutes } from './routes/api'
|
|
4200
|
+
|
|
4201
|
+
// Initialize Core
|
|
4202
|
+
const core = new PlanetCore({
|
|
4203
|
+
config: {
|
|
4204
|
+
APP_NAME: '${context.name}',
|
|
4205
|
+
database: databaseConfig
|
|
4206
|
+
},
|
|
4207
|
+
})
|
|
4208
|
+
|
|
4209
|
+
// Middleware
|
|
4210
|
+
core.adapter.use('*', securityHeaders())
|
|
4211
|
+
core.adapter.use('*', bodySizeLimit(1024 * 1024)) // 1MB
|
|
4212
|
+
|
|
4213
|
+
// Install Orbits
|
|
4214
|
+
await core.orbit(new OrbitAtlas())
|
|
4215
|
+
|
|
4216
|
+
// Service Providers
|
|
4217
|
+
core.register(new AppServiceProvider())
|
|
4218
|
+
|
|
4219
|
+
// Bootstrap
|
|
4220
|
+
await core.bootstrap()
|
|
4221
|
+
|
|
4222
|
+
// Routes
|
|
4223
|
+
registerApiRoutes(core.router)
|
|
4224
|
+
|
|
4225
|
+
// Liftoff
|
|
4226
|
+
export default core.liftoff()
|
|
4227
|
+
`;
|
|
4228
|
+
}
|
|
4229
|
+
generateArchitectureDoc(context) {
|
|
4230
|
+
return "# " + context.name + ' - Action Domain Architecture\n\n## Overview\n\nThis project uses the **Action Domain** pattern, designed for high-clarity API implementations.\n\n## Directory Structure\n\n```\nsrc/\n\u251C\u2500\u2500 actions/ # Single Responsibility Business Logic\n\u2502 \u251C\u2500\u2500 Action.ts # Base Action class\n\u2502 \u2514\u2500\u2500 [Domain]/ # Domain-specific actions\n\u251C\u2500\u2500 controllers/ # HTTP Request Handlers\n\u2502 \u2514\u2500\u2500 api/v1/ # API Controllers\n\u251C\u2500\u2500 types/ # TypeScript Definitions\n\u2502 \u251C\u2500\u2500 requests/ # Request Payloads\n\u2502 \u2514\u2500\u2500 responses/ # Response Structures\n\u251C\u2500\u2500 repositories/ # Data Access Layer\n\u251C\u2500\u2500 routes/ # Route Definitions\n\u2514\u2500\u2500 config/ # Configuration\n```\n\n## Core Concepts\n\n### Actions\nEvery business operation is an "Action". An action:\n- Does ONE thing.\n- Takes specific input.\n- Returns specific output.\n- Is framework-agnostic (ideally).\n\n### Controllers\nControllers are thin. They:\n1. Parse the request.\n2. Instantiate/Call the Action.\n3. Return the response.\n\nCreated with \u2764\uFE0F using Gravito Framework\n';
|
|
4231
|
+
}
|
|
4232
|
+
generatePackageJson(context) {
|
|
4233
|
+
const pkg = {
|
|
4234
|
+
name: context.nameKebabCase,
|
|
4235
|
+
version: "0.1.0",
|
|
4236
|
+
type: "module",
|
|
4237
|
+
scripts: {
|
|
4238
|
+
dev: "bun run --watch src/bootstrap.ts",
|
|
4239
|
+
build: "bun build ./src/bootstrap.ts --outdir ./dist --target bun",
|
|
4240
|
+
start: "bun run dist/bootstrap.js",
|
|
4241
|
+
test: "bun test",
|
|
4242
|
+
typecheck: "tsc --noEmit"
|
|
4243
|
+
},
|
|
4244
|
+
dependencies: {
|
|
4245
|
+
"@gravito/core": "workspace:*",
|
|
4246
|
+
"@gravito/enterprise": "workspace:*",
|
|
4247
|
+
"@gravito/atlas": "workspace:*"
|
|
4248
|
+
// Usually needed for repositories
|
|
4249
|
+
},
|
|
4250
|
+
devDependencies: {
|
|
4251
|
+
"bun-types": "latest",
|
|
4252
|
+
typescript: "^5.0.0"
|
|
4253
|
+
}
|
|
4254
|
+
};
|
|
4255
|
+
return JSON.stringify(pkg, null, 2);
|
|
4256
|
+
}
|
|
4257
|
+
};
|
|
4258
|
+
|
|
4259
|
+
// src/Scaffold.ts
|
|
3131
4260
|
var Scaffold = class {
|
|
3132
4261
|
templatesDir;
|
|
3133
4262
|
verbose;
|
|
@@ -3154,6 +4283,16 @@ var Scaffold = class {
|
|
|
3154
4283
|
type: "ddd",
|
|
3155
4284
|
name: "Domain-Driven Design",
|
|
3156
4285
|
description: "Full DDD with Bounded Contexts and CQRS"
|
|
4286
|
+
},
|
|
4287
|
+
{
|
|
4288
|
+
type: "action-domain",
|
|
4289
|
+
name: "Action Domain",
|
|
4290
|
+
description: "Single Action Controllers pattern for high-clarity APIs"
|
|
4291
|
+
},
|
|
4292
|
+
{
|
|
4293
|
+
type: "satellite",
|
|
4294
|
+
name: "Gravito Satellite",
|
|
4295
|
+
description: "Plug-and-play module for the Gravito ecosystem"
|
|
3157
4296
|
}
|
|
3158
4297
|
];
|
|
3159
4298
|
}
|
|
@@ -3162,15 +4301,36 @@ var Scaffold = class {
|
|
|
3162
4301
|
*/
|
|
3163
4302
|
async create(options) {
|
|
3164
4303
|
const generator = this.createGenerator(options.architecture);
|
|
4304
|
+
const fs3 = await import("fs/promises");
|
|
4305
|
+
const profileResolver = new ProfileResolver();
|
|
4306
|
+
const profileConfig = profileResolver.resolve(options.profile, options.features);
|
|
3165
4307
|
const context = BaseGenerator.createContext(
|
|
3166
4308
|
options.name,
|
|
3167
4309
|
options.targetDir,
|
|
3168
4310
|
options.architecture,
|
|
3169
4311
|
options.packageManager ?? "bun",
|
|
3170
|
-
|
|
4312
|
+
{
|
|
4313
|
+
...options.context,
|
|
4314
|
+
withSpectrum: options.withSpectrum ?? false,
|
|
4315
|
+
isInternal: options.isInternal ?? false,
|
|
4316
|
+
profile: options.profile ?? "core",
|
|
4317
|
+
features: options.features ?? [],
|
|
4318
|
+
profileConfig
|
|
4319
|
+
}
|
|
3171
4320
|
);
|
|
3172
4321
|
try {
|
|
3173
4322
|
const filesCreated = await generator.generate(context);
|
|
4323
|
+
const lockGenerator = new LockGenerator();
|
|
4324
|
+
const lockContent = lockGenerator.generate(
|
|
4325
|
+
options.profile ?? "core",
|
|
4326
|
+
profileConfig,
|
|
4327
|
+
"basic",
|
|
4328
|
+
// Default template for now, should come from options if applicable
|
|
4329
|
+
"1.0.0"
|
|
4330
|
+
);
|
|
4331
|
+
const lockPath = path3.resolve(options.targetDir, "gravito.lock.json");
|
|
4332
|
+
await fs3.writeFile(lockPath, lockContent, "utf-8");
|
|
4333
|
+
filesCreated.push(lockPath);
|
|
3174
4334
|
return {
|
|
3175
4335
|
success: true,
|
|
3176
4336
|
targetDir: options.targetDir,
|
|
@@ -3200,6 +4360,10 @@ var Scaffold = class {
|
|
|
3200
4360
|
return new CleanArchitectureGenerator(config);
|
|
3201
4361
|
case "ddd":
|
|
3202
4362
|
return new DddGenerator(config);
|
|
4363
|
+
case "action-domain":
|
|
4364
|
+
return new ActionDomainGenerator(config);
|
|
4365
|
+
case "satellite":
|
|
4366
|
+
return new SatelliteGenerator(config);
|
|
3203
4367
|
default:
|
|
3204
4368
|
throw new Error(`Unknown architecture type: ${type}`);
|
|
3205
4369
|
}
|
|
@@ -3222,6 +4386,11 @@ export {
|
|
|
3222
4386
|
CleanArchitectureGenerator,
|
|
3223
4387
|
DddGenerator,
|
|
3224
4388
|
EnterpriseMvcGenerator,
|
|
4389
|
+
EnvironmentDetector,
|
|
4390
|
+
FileMerger,
|
|
4391
|
+
LockGenerator,
|
|
4392
|
+
ProfileResolver,
|
|
4393
|
+
SatelliteGenerator,
|
|
3225
4394
|
Scaffold,
|
|
3226
4395
|
StubGenerator
|
|
3227
4396
|
};
|