@ronaldjdevfs/forge 1.0.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/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +338 -0
- package/logo.png +0 -0
- package/package.json +47 -0
- package/skills/forge/SKILL.md +207 -0
- package/skills/forge/command/forge.md +110 -0
- package/skills/forge/profiles/express-mongodb.md +289 -0
- package/skills/forge/profiles/express-postgres.md +164 -0
- package/skills/forge/profiles/express-prisma.md +116 -0
- package/skills/forge/profiles/fastify-postgres.md +110 -0
- package/skills/forge/profiles/nestjs-prisma.md +160 -0
- package/skills/forge/reference/cast.md +77 -0
- package/skills/forge/reference/chain.md +73 -0
- package/skills/forge/reference/forge.md +44 -0
- package/skills/forge/reference/inscribe.md +129 -0
- package/skills/forge/reference/inspect.md +58 -0
- package/skills/forge/reference/patterns.md +108 -0
- package/skills/forge/reference/principles.md +29 -0
- package/skills/forge/reference/quench.md +74 -0
- package/skills/forge/reference/reforge.md +40 -0
- package/skills/forge/reference/relocate.md +38 -0
- package/skills/forge/reference/smelt.md +31 -0
- package/skills/forge/reference/temper.md +49 -0
- package/skills/forge/scripts/architecture.mjs +176 -0
- package/skills/forge/scripts/armorer.mjs +421 -0
- package/skills/forge/scripts/bootstrap.mjs +187 -0
- package/skills/forge/scripts/chain.mjs +258 -0
- package/skills/forge/scripts/context.mjs +237 -0
- package/skills/forge/scripts/detect.mjs +843 -0
- package/skills/forge/scripts/graph.mjs +594 -0
- package/skills/forge/scripts/inspect.mjs +193 -0
- package/skills/forge/scripts/profile.mjs +92 -0
- package/skills/forge/templates/feature/controller.ts.md +66 -0
- package/skills/forge/templates/feature/entity.ts.md +11 -0
- package/skills/forge/templates/feature/mapper.ts.md +18 -0
- package/skills/forge/templates/feature/repository-impl.ts.md +59 -0
- package/skills/forge/templates/feature/repository-interface.ts.md +12 -0
- package/skills/forge/templates/feature/routes.ts.md +17 -0
- package/skills/forge/templates/feature/schema.ts.md +16 -0
- package/skills/forge/templates/feature/use-case.ts.md +19 -0
- package/skills/forge/templates/infra/mail.ts.md +31 -0
- package/skills/forge/templates/infra/mongodb.ts.md +12 -0
- package/skills/forge/templates/infra/prisma.ts.md +21 -0
- package/skills/forge/templates/infra/redis.ts.md +30 -0
- package/skills/forge/templates/platform/config.ts.md +37 -0
- package/skills/forge/templates/platform/database.ts.md +40 -0
- package/skills/forge/templates/platform/di.ts.md +18 -0
- package/skills/forge/templates/platform/http.ts.md +29 -0
- package/skills/forge/templates/platform/logger.ts.md +38 -0
- package/skills/forge/templates/platform/server.ts.md +25 -0
- package/skills/forge/templates/shared/contract.ts.md +20 -0
- package/skills/forge/templates/shared/error.ts.md +27 -0
- package/skills/forge/templates/shared/type.ts.md +18 -0
- package/skills/forge/templates/shared/util.ts.md +23 -0
- package/src/cli.js +179 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { join, basename } from "path";
|
|
4
|
+
import { buildContext } from "./context.mjs";
|
|
5
|
+
import { detectProfile, detectProfileExtended } from "./profile.mjs";
|
|
6
|
+
import { buildDependencyGraph } from "./chain.mjs";
|
|
7
|
+
import { allChecks } from "./detect.mjs";
|
|
8
|
+
|
|
9
|
+
const ROOT = process.cwd();
|
|
10
|
+
|
|
11
|
+
const CYAN = "\x1b[36m";
|
|
12
|
+
const GREEN = "\x1b[32m";
|
|
13
|
+
const RED = "\x1b[31m";
|
|
14
|
+
const YELLOW = "\x1b[33m";
|
|
15
|
+
const BOLD = "\x1b[1m";
|
|
16
|
+
const RESET = "\x1b[0m";
|
|
17
|
+
const DIM = "\x1b[2m";
|
|
18
|
+
const GRAY = "\x1b[90m";
|
|
19
|
+
|
|
20
|
+
const SEVERITY_COLORS = {
|
|
21
|
+
CRITICAL: RED,
|
|
22
|
+
ERROR: RED,
|
|
23
|
+
WARNING: YELLOW,
|
|
24
|
+
INFO: CYAN,
|
|
25
|
+
SUGGESTION: GRAY,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const CAT_NAMES = {
|
|
29
|
+
structure: "Estructura",
|
|
30
|
+
layers: "Capas",
|
|
31
|
+
ownership: "Ownership",
|
|
32
|
+
platform: "Platform",
|
|
33
|
+
dependencies: "Dependencias",
|
|
34
|
+
graph: "Grafo",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const CAT_MAX = {
|
|
38
|
+
structure: 20,
|
|
39
|
+
layers: 20,
|
|
40
|
+
ownership: 20,
|
|
41
|
+
platform: 15,
|
|
42
|
+
dependencies: 15,
|
|
43
|
+
graph: 20,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function countBySeverity(checks) {
|
|
47
|
+
const counts = {};
|
|
48
|
+
for (const c of checks) {
|
|
49
|
+
if (!c.pass) {
|
|
50
|
+
counts[c.severity] = (counts[c.severity] || 0) + 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return counts;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildReport(result) {
|
|
57
|
+
let total = 0;
|
|
58
|
+
let maxTotal = 0;
|
|
59
|
+
const violations = [];
|
|
60
|
+
const recommendations = [];
|
|
61
|
+
|
|
62
|
+
for (const [name, cat] of Object.entries(result.categories)) {
|
|
63
|
+
total += cat.score;
|
|
64
|
+
maxTotal += CAT_MAX[name] || 10;
|
|
65
|
+
for (const check of cat.checks) {
|
|
66
|
+
if (!check.pass) {
|
|
67
|
+
violations.push({ category: name, ...check });
|
|
68
|
+
if (check.fix) recommendations.push(check.fix);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { total, max: maxTotal, categories: result.categories, violations, recommendations, severityCounts: countBySeverity(violations) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function printReport(report, ctx, profile, graph, archGraph) {
|
|
77
|
+
const barLen = 40;
|
|
78
|
+
const pct = report.max > 0 ? Math.round((report.total / report.max) * 100) : 0;
|
|
79
|
+
|
|
80
|
+
function scoreBar(score, max) {
|
|
81
|
+
const p = max > 0 ? Math.round((score / max) * barLen) : 0;
|
|
82
|
+
const filled = "█".repeat(p);
|
|
83
|
+
const empty = "░".repeat(barLen - p);
|
|
84
|
+
const color = score >= max * 0.8 ? GREEN : score >= max * 0.5 ? YELLOW : RED;
|
|
85
|
+
return `${color}${filled}${RESET}${DIM}${empty}${RESET}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log("\n" + "═".repeat(58));
|
|
89
|
+
console.log(`${BOLD}${CYAN} FORGE AUDIT — Reporte Hexagonal${RESET}`);
|
|
90
|
+
console.log(` ${DIM}Proyecto: ${basename(ROOT)}${RESET}`);
|
|
91
|
+
console.log(` ${DIM}Perfil: ${profile}${RESET}`);
|
|
92
|
+
console.log(` ${DIM}Framework: ${ctx.framework} | DB: ${ctx.database} | ORM: ${ctx.orm} | DI: ${ctx.diStrategy}${RESET}`);
|
|
93
|
+
console.log(` ${DIM}Fecha: ${new Date().toISOString().slice(0, 10)}${RESET}`);
|
|
94
|
+
console.log("═".repeat(58) + "\n");
|
|
95
|
+
|
|
96
|
+
const gradeColor = pct >= 80 ? GREEN : pct >= 50 ? YELLOW : RED;
|
|
97
|
+
const grade = pct >= 90 ? "A" : pct >= 80 ? "B" : pct >= 65 ? "C" : pct >= 50 ? "D" : "F";
|
|
98
|
+
console.log(` ${BOLD}Puntaje total: ${gradeColor}${report.total}/${report.max} (${pct}%) — ${grade}${RESET}\n`);
|
|
99
|
+
|
|
100
|
+
/* Severity summary */
|
|
101
|
+
if (Object.keys(report.severityCounts).length > 0) {
|
|
102
|
+
console.log(` ${BOLD}Resumen por severidad${RESET}`);
|
|
103
|
+
for (const [sev, count] of Object.entries(report.severityCounts)) {
|
|
104
|
+
const color = SEVERITY_COLORS[sev] || GRAY;
|
|
105
|
+
console.log(` ${color}${sev}${RESET}: ${count}`);
|
|
106
|
+
}
|
|
107
|
+
console.log();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Context summary */
|
|
111
|
+
console.log(` ${BOLD}Contexto del proyecto${RESET}`);
|
|
112
|
+
console.log(` Features migrados: ${ctx.features.migrated.length}`);
|
|
113
|
+
console.log(` Features legacy: ${ctx.features.legacy.length}`);
|
|
114
|
+
console.log(` Platform: ${ctx.platform.exists ? ctx.platform.components.join(", ") : "(no detectado)"}`);
|
|
115
|
+
console.log(` Shared: ${ctx.shared.exists ? ctx.shared.components.join(", ") : "(no detectado)"}`);
|
|
116
|
+
console.log(` Infra: ${ctx.infra.exists ? ctx.infra.components.join(", ") : "(no detectado)"}`);
|
|
117
|
+
console.log(` Dependencias: ${graph.edges.length} edges entre ${graph.nodes.length} features`);
|
|
118
|
+
if (graph.hasCycles) console.log(` ${RED}Ciclos detectados en el grafo de dependencias${RESET}`);
|
|
119
|
+
if (graph.edges.length > 0) {
|
|
120
|
+
for (const edge of graph.edges) {
|
|
121
|
+
console.log(` ${GRAY}→ ${edge.source} depende de ${edge.target} (${edge.file})${RESET}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Architecture Graph summary */
|
|
126
|
+
if (archGraph) {
|
|
127
|
+
const ag = archGraph.stats;
|
|
128
|
+
console.log(`\n ${BOLD}Grafo Arquitectónico${RESET}`);
|
|
129
|
+
console.log(` Nodos: ${ag.totalNodes} | Edges: ${ag.totalEdges} | Violaciones: ${ag.violations}`);
|
|
130
|
+
console.log(` Risk Score: ${ag.riskScore}/100 | Health: ${ag.health}`);
|
|
131
|
+
console.log(` Dependency Health: ${ag.dependencyHealth}%`);
|
|
132
|
+
if (ag.layers) {
|
|
133
|
+
console.log(` Capas: Platform=${ag.layers.platform}, Feature=${ag.layers.feature}, Shared=${ag.layers.shared}, Infra=${ag.layers.infra}`);
|
|
134
|
+
}
|
|
135
|
+
if (ag.health === "critical") console.log(` ${RED}⚠ Salud crítica — se requieren acciones correctivas${RESET}`);
|
|
136
|
+
}
|
|
137
|
+
console.log();
|
|
138
|
+
|
|
139
|
+
/* Categories */
|
|
140
|
+
for (const [key, cat] of Object.entries(report.categories)) {
|
|
141
|
+
const name = CAT_NAMES[key] || key;
|
|
142
|
+
const cmax = CAT_MAX[key] || cat.max || 10;
|
|
143
|
+
console.log(` ${BOLD}${name} (${cat.score}/${cmax})${RESET}`);
|
|
144
|
+
console.log(` ${scoreBar(cat.score, cmax)}`);
|
|
145
|
+
|
|
146
|
+
for (const check of cat.checks) {
|
|
147
|
+
const icon = check.pass ? `${GREEN}✔${RESET}` : `${RED}✘${RESET}`;
|
|
148
|
+
const sev = check.pass ? "" : ` ${SEVERITY_COLORS[check.severity]}[${check.severity}]${RESET}`;
|
|
149
|
+
const detail = check.detail ? ` ${GRAY}— ${check.detail}${RESET}` : "";
|
|
150
|
+
console.log(` ${icon}${sev} ${check.label}${detail}`);
|
|
151
|
+
if (!check.pass && check.fix) {
|
|
152
|
+
console.log(` ${DIM}→ Fix: ${check.fix}${RESET}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
console.log();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (report.recommendations.length > 0) {
|
|
159
|
+
console.log(` ${BOLD}${YELLOW}Recomendaciones${RESET}`);
|
|
160
|
+
const unique = [...new Set(report.recommendations)];
|
|
161
|
+
unique.forEach((r, i) => console.log(` ${i + 1}. ${r}`));
|
|
162
|
+
console.log();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log("═".repeat(58) + "\n");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function printJson(report, ctx, profile, graph, archGraph) {
|
|
169
|
+
console.log(JSON.stringify({ ...report, profile, context: ctx, dependencies: graph, architectureGraph: archGraph }, null, 2));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function main() {
|
|
173
|
+
const args = process.argv.slice(2);
|
|
174
|
+
const isJson = args.includes("--json");
|
|
175
|
+
const filterSeverity = args.includes("--severity") ? args[args.indexOf("--severity") + 1] : null;
|
|
176
|
+
|
|
177
|
+
const ctx = await buildContext();
|
|
178
|
+
const profile = detectProfile(ctx);
|
|
179
|
+
const chainGraph = buildDependencyGraph();
|
|
180
|
+
const archGraph = ctx.graph;
|
|
181
|
+
const features = ctx.features.migrated;
|
|
182
|
+
const result = allChecks(features, archGraph, ctx);
|
|
183
|
+
|
|
184
|
+
const report = buildReport({ categories: result });
|
|
185
|
+
|
|
186
|
+
if (isJson) {
|
|
187
|
+
printJson(report, ctx, profile, chainGraph, archGraph);
|
|
188
|
+
} else {
|
|
189
|
+
printReport(report, ctx, profile, chainGraph, archGraph);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { buildContext } from "./context.mjs";
|
|
4
|
+
|
|
5
|
+
const PROFILES = [
|
|
6
|
+
{
|
|
7
|
+
name: "express-mongodb",
|
|
8
|
+
match: (ctx) =>
|
|
9
|
+
ctx.framework === "Express" &&
|
|
10
|
+
ctx.database === "MongoDB" &&
|
|
11
|
+
ctx.orm === "Mongoose",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "express-prisma",
|
|
15
|
+
match: (ctx) =>
|
|
16
|
+
ctx.framework === "Express" &&
|
|
17
|
+
ctx.orm === "Prisma",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "express-postgres",
|
|
21
|
+
match: (ctx) =>
|
|
22
|
+
ctx.framework === "Express" &&
|
|
23
|
+
ctx.database === "PostgreSQL" &&
|
|
24
|
+
ctx.orm === "native",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "fastify-postgres",
|
|
28
|
+
match: (ctx) =>
|
|
29
|
+
ctx.framework === "Fastify" &&
|
|
30
|
+
ctx.database === "PostgreSQL" &&
|
|
31
|
+
ctx.orm === "Prisma",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "nestjs-prisma",
|
|
35
|
+
match: (ctx) =>
|
|
36
|
+
ctx.framework === "NestJS" &&
|
|
37
|
+
ctx.orm === "Prisma",
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export function detectProfile(ctx) {
|
|
42
|
+
for (const profile of PROFILES) {
|
|
43
|
+
if (profile.match(ctx)) return profile.name;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (ctx.framework !== "unknown") {
|
|
47
|
+
return `${ctx.framework.toLowerCase()}-${ctx.database.toLowerCase()}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return "unknown";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function detectProfileExtended(ctx) {
|
|
54
|
+
const profile = detectProfile(ctx);
|
|
55
|
+
|
|
56
|
+
const diStrategy = ctx.diStrategy || "manual";
|
|
57
|
+
const hasPlatform = ctx.platform?.exists || false;
|
|
58
|
+
const hasShared = ctx.shared?.exists || false;
|
|
59
|
+
const hasInfra = ctx.infra?.exists || false;
|
|
60
|
+
const platformComponents = ctx.platform?.components || [];
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
profile,
|
|
64
|
+
hasPlatform,
|
|
65
|
+
hasShared,
|
|
66
|
+
hasInfra,
|
|
67
|
+
platformComponents,
|
|
68
|
+
diStrategy,
|
|
69
|
+
layers: {
|
|
70
|
+
platform: hasPlatform,
|
|
71
|
+
features: ctx.hasFeaturesDir || false,
|
|
72
|
+
shared: hasShared,
|
|
73
|
+
infra: hasInfra,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function main() {
|
|
79
|
+
const ctx = await buildContext();
|
|
80
|
+
const profile = detectProfile(ctx);
|
|
81
|
+
const extended = detectProfileExtended(ctx);
|
|
82
|
+
const args = process.argv.slice(2);
|
|
83
|
+
if (args.includes("--extended")) {
|
|
84
|
+
console.log(JSON.stringify(extended, null, 2));
|
|
85
|
+
} else {
|
|
86
|
+
console.log(profile);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (process.argv[1] && (process.argv[1].endsWith("profile.mjs") || process.argv[1].endsWith("profile.js"))) {
|
|
91
|
+
main().catch(console.error);
|
|
92
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/features/<domain>/adapters/in/http/<Domain>.controller.ts
|
|
3
|
+
import { injectable, inject } from "tsyringe";
|
|
4
|
+
import type { Request, Response, NextFunction } from "express";
|
|
5
|
+
import { Create<Domain> } from "../../../application/use-cases/Create<Domain>.uc.js";
|
|
6
|
+
import { Get<Domain> } from "../../../application/use-cases/Get<Domain>.uc.js";
|
|
7
|
+
import { List<Domain> } from "../../../application/use-cases/List<Domain>.uc.js";
|
|
8
|
+
import { Update<Domain> } from "../../../application/use-cases/Update<Domain>.uc.js";
|
|
9
|
+
import { Delete<Domain> } from "../../../application/use-cases/Delete<Domain>.uc.js";
|
|
10
|
+
|
|
11
|
+
@injectable()
|
|
12
|
+
export class <Domain>Controller {
|
|
13
|
+
constructor(
|
|
14
|
+
@inject(Create<Domain>) private readonly create: Create<Domain>,
|
|
15
|
+
@inject(Get<Domain>) private readonly get: Get<Domain>,
|
|
16
|
+
@inject(List<Domain>) private readonly list: List<Domain>,
|
|
17
|
+
@inject(Update<Domain>) private readonly update: Update<Domain>,
|
|
18
|
+
@inject(Delete<Domain>) private readonly delete: Delete<Domain>
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
createHandler = async (req: Request, res: Response, next: NextFunction) => {
|
|
22
|
+
try {
|
|
23
|
+
const result = await this.create.execute(req.body);
|
|
24
|
+
res.status(201).json({ data: result });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
next(error);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
getById = async (req: Request, res: Response, next: NextFunction) => {
|
|
31
|
+
try {
|
|
32
|
+
const result = await this.get.execute(req.params.id);
|
|
33
|
+
res.status(200).json({ data: result });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
next(error);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
list = async (req: Request, res: Response, next: NextFunction) => {
|
|
40
|
+
try {
|
|
41
|
+
const result = await this.list.execute(req.query);
|
|
42
|
+
res.status(200).json({ data: result });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
next(error);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
update = async (req: Request, res: Response, next: NextFunction) => {
|
|
49
|
+
try {
|
|
50
|
+
const result = await this.update.execute(req.params.id, req.body);
|
|
51
|
+
res.status(200).json({ data: result });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
next(error);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
delete = async (req: Request, res: Response, next: NextFunction) => {
|
|
58
|
+
try {
|
|
59
|
+
await this.delete.execute(req.params.id);
|
|
60
|
+
res.status(204).send();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
next(error);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/features/<domain>/domain/<Domain>.entity.ts
|
|
3
|
+
import type { SomeGlobalType } from "@/shared/types/types.js";
|
|
4
|
+
|
|
5
|
+
export interface <Domain> {
|
|
6
|
+
id: string;
|
|
7
|
+
// propiedades específicas del dominio
|
|
8
|
+
createdAt?: Date;
|
|
9
|
+
updatedAt?: Date;
|
|
10
|
+
}
|
|
11
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/features/<domain>/application/mappers/<Domain>.mapper.ts
|
|
3
|
+
import type { <Domain> } from "../domain/<Domain>.entity.js";
|
|
4
|
+
|
|
5
|
+
export class <Domain>Mapper {
|
|
6
|
+
static toDomain(doc: Record<string, any>): <Domain> {
|
|
7
|
+
return {
|
|
8
|
+
...doc,
|
|
9
|
+
} as <Domain>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static toPersistence(domain: <Domain>): Record<string, any> {
|
|
13
|
+
return {
|
|
14
|
+
...domain,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/features/<domain>/adapters/out/persistence/<Domain>.repository.ts
|
|
3
|
+
import { injectable } from "tsyringe";
|
|
4
|
+
import type { <Domain> } from "../../../domain/<Domain>.entity.js";
|
|
5
|
+
import type { I<Domain>Repository } from "../../../domain/I<Domain>.repository.js";
|
|
6
|
+
import { <Domain>Mapper } from "../../../application/mappers/<Domain>.mapper.js";
|
|
7
|
+
import <Domain>Model from "./<Domain>.schema.js";
|
|
8
|
+
import { RepositoryError } from "@/shared/errors/RepositoryError.js";
|
|
9
|
+
|
|
10
|
+
@injectable()
|
|
11
|
+
export class <Domain>Repository implements I<Domain>Repository {
|
|
12
|
+
async create(data: Partial<<Domain>>, session?: unknown): Promise<<Domain>> {
|
|
13
|
+
try {
|
|
14
|
+
const doc = await <Domain>Model.create(data);
|
|
15
|
+
return <Domain>Mapper.toDomain(doc.toObject());
|
|
16
|
+
} catch (error: any) {
|
|
17
|
+
throw new RepositoryError("Error al crear", error);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async findById(id: string, session?: unknown): Promise<<Domain> | null> {
|
|
22
|
+
try {
|
|
23
|
+
const doc = await <Domain>Model.findById(id).lean();
|
|
24
|
+
return doc ? <Domain>Mapper.toDomain(doc) : null;
|
|
25
|
+
} catch (error: any) {
|
|
26
|
+
throw new RepositoryError("Error al buscar por ID", error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async findAll(options?: Record<string, unknown>): Promise<{ data: <Domain>[]; total: number }> {
|
|
31
|
+
try {
|
|
32
|
+
const docs = await <Domain>Model.find().lean();
|
|
33
|
+
return {
|
|
34
|
+
data: docs.map((d: any) => <Domain>Mapper.toDomain(d)),
|
|
35
|
+
total: docs.length,
|
|
36
|
+
};
|
|
37
|
+
} catch (error: any) {
|
|
38
|
+
throw new RepositoryError("Error al listar", error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async update(id: string, data: Partial<<Domain>>, session?: unknown): Promise<<Domain> | null> {
|
|
43
|
+
try {
|
|
44
|
+
const doc = await <Domain>Model.findByIdAndUpdate(id, data, { new: true }).lean();
|
|
45
|
+
return doc ? <Domain>Mapper.toDomain(doc) : null;
|
|
46
|
+
} catch (error: any) {
|
|
47
|
+
throw new RepositoryError("Error al actualizar", error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async delete(id: string, session?: unknown): Promise<void> {
|
|
52
|
+
try {
|
|
53
|
+
await <Domain>Model.findByIdAndDelete(id);
|
|
54
|
+
} catch (error: any) {
|
|
55
|
+
throw new RepositoryError("Error al eliminar", error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/features/<domain>/domain/I<Domain>.repository.ts
|
|
3
|
+
import type { <Domain> } from "./<Domain>.entity.js";
|
|
4
|
+
|
|
5
|
+
export interface I<Domain>Repository {
|
|
6
|
+
create(data: Partial<<Domain>>, session?: unknown): Promise<<Domain>>;
|
|
7
|
+
findById(id: string, session?: unknown): Promise<<Domain> | null>;
|
|
8
|
+
findAll(options?: Record<string, unknown>): Promise<{ data: <Domain>[]; total: number }>;
|
|
9
|
+
update(id: string, data: Partial<<Domain>>, session?: unknown): Promise<<Domain> | null>;
|
|
10
|
+
delete(id: string, session?: unknown): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/features/<domain>/adapters/in/http/<Domain>.routes.ts
|
|
3
|
+
import { Router } from "express";
|
|
4
|
+
import { container } from "tsyringe";
|
|
5
|
+
import { <Domain>Controller } from "./<Domain>.controller.js";
|
|
6
|
+
|
|
7
|
+
const router = Router();
|
|
8
|
+
const controller = container.resolve(<Domain>Controller);
|
|
9
|
+
|
|
10
|
+
router.post("/", controller.createHandler);
|
|
11
|
+
router.get("/:id", controller.getById);
|
|
12
|
+
router.get("/", controller.list);
|
|
13
|
+
router.patch("/:id", controller.update);
|
|
14
|
+
router.delete("/:id", controller.delete);
|
|
15
|
+
|
|
16
|
+
export default router;
|
|
17
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/features/<domain>/adapters/out/persistence/<Domain>.schema.ts
|
|
3
|
+
import { model, Schema } from "mongoose";
|
|
4
|
+
import type { <Domain> } from "../../../domain/<Domain>.entity.js";
|
|
5
|
+
|
|
6
|
+
const <Domain>Schema = new Schema<<Domain>>(
|
|
7
|
+
{
|
|
8
|
+
// campos específicos
|
|
9
|
+
},
|
|
10
|
+
{ timestamps: true }
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
<Domain>Schema.index({ /* índices */ });
|
|
14
|
+
|
|
15
|
+
export default model<<Domain>>("<Domain>", <Domain>Schema);
|
|
16
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/features/<domain>/application/use-cases/Create<Domain>.uc.ts
|
|
3
|
+
import { injectable, inject } from "tsyringe";
|
|
4
|
+
import type { <Domain> } from "../../domain/<Domain>.entity.js";
|
|
5
|
+
import type { I<Domain>Repository } from "../../domain/I<Domain>.repository.js";
|
|
6
|
+
import { UseCaseError } from "@/shared/errors/UseCaseError.js";
|
|
7
|
+
|
|
8
|
+
@injectable()
|
|
9
|
+
export class Create<Domain> {
|
|
10
|
+
constructor(
|
|
11
|
+
@inject(I<Domain>Repository) private readonly repo: I<Domain>Repository
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
async execute(data: Partial<<Domain>>): Promise<<Domain>> {
|
|
15
|
+
if (!data) throw new UseCaseError("Datos requeridos");
|
|
16
|
+
return this.repo.create(data);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/infra/mail/Mail.config.ts
|
|
3
|
+
|
|
4
|
+
export interface MailConfig {
|
|
5
|
+
host: string;
|
|
6
|
+
port: number;
|
|
7
|
+
user: string;
|
|
8
|
+
pass: string;
|
|
9
|
+
from: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function loadMailConfig(): MailConfig {
|
|
13
|
+
return {
|
|
14
|
+
host: process.env.MAIL_HOST || "smtp.example.com",
|
|
15
|
+
port: parseInt(process.env.MAIL_PORT || "587", 10),
|
|
16
|
+
user: process.env.MAIL_USER || "",
|
|
17
|
+
pass: process.env.MAIL_PASS || "",
|
|
18
|
+
from: process.env.MAIL_FROM || "noreply@example.com",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// src/infra/mail/Mail.service.ts
|
|
25
|
+
import { loadMailConfig } from "./Mail.config.js";
|
|
26
|
+
|
|
27
|
+
export async function sendMail(to: string, subject: string, body: string): Promise<void> {
|
|
28
|
+
const config = loadMailConfig();
|
|
29
|
+
// implementar con nodemailer, sendgrid, etc.
|
|
30
|
+
}
|
|
31
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/infra/mongodb/Mongo.config.ts
|
|
3
|
+
import mongoose from "mongoose";
|
|
4
|
+
|
|
5
|
+
export async function connectMongo(uri: string): Promise<void> {
|
|
6
|
+
await mongoose.connect(uri);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function disconnectMongo(): Promise<void> {
|
|
10
|
+
await mongoose.disconnect();
|
|
11
|
+
}
|
|
12
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/infra/prisma/Prisma.client.ts
|
|
3
|
+
import { PrismaClient } from "@prisma/client";
|
|
4
|
+
|
|
5
|
+
const prisma = new PrismaClient();
|
|
6
|
+
|
|
7
|
+
export { prisma };
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
// src/infra/prisma/Prisma.service.ts
|
|
12
|
+
import { prisma } from "./Prisma.client.js";
|
|
13
|
+
|
|
14
|
+
export async function connectPrisma(): Promise<void> {
|
|
15
|
+
await prisma.$connect();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function disconnectPrisma(): Promise<void> {
|
|
19
|
+
await prisma.$disconnect();
|
|
20
|
+
}
|
|
21
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/infra/redis/Redis.config.ts
|
|
3
|
+
|
|
4
|
+
export interface RedisConfig {
|
|
5
|
+
url: string;
|
|
6
|
+
prefix: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function loadRedisConfig(): RedisConfig {
|
|
10
|
+
return {
|
|
11
|
+
url: process.env.REDIS_URL || "redis://localhost:6379",
|
|
12
|
+
prefix: process.env.REDIS_PREFIX || "app:",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// src/infra/redis/Redis.service.ts
|
|
19
|
+
import { loadRedisConfig } from "./Redis.config.js";
|
|
20
|
+
|
|
21
|
+
let client: unknown = null;
|
|
22
|
+
|
|
23
|
+
export async function connectRedis(): Promise<void> {
|
|
24
|
+
// conectar según librería (ioredis, node-redis, etc.)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function disconnectRedis(): Promise<void> {
|
|
28
|
+
// desconectar
|
|
29
|
+
}
|
|
30
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/platform/config/App.config.ts
|
|
3
|
+
|
|
4
|
+
export interface AppConfig {
|
|
5
|
+
nodeEnv: string;
|
|
6
|
+
port: number;
|
|
7
|
+
apiPrefix: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function loadAppConfig(): AppConfig {
|
|
11
|
+
return {
|
|
12
|
+
nodeEnv: process.env.NODE_ENV || "development",
|
|
13
|
+
port: parseInt(process.env.PORT || "3000", 10),
|
|
14
|
+
apiPrefix: process.env.API_PREFIX || "/api/v1",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// src/platform/config/Env.config.ts
|
|
21
|
+
|
|
22
|
+
export interface EnvConfig {
|
|
23
|
+
databaseUrl: string;
|
|
24
|
+
redisUrl: string;
|
|
25
|
+
jwtSecret: string;
|
|
26
|
+
jwtExpiresIn: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadEnvConfig(): EnvConfig {
|
|
30
|
+
return {
|
|
31
|
+
databaseUrl: process.env.DATABASE_URL || "",
|
|
32
|
+
redisUrl: process.env.REDIS_URL || "",
|
|
33
|
+
jwtSecret: process.env.JWT_SECRET || "change-me",
|
|
34
|
+
jwtExpiresIn: process.env.JWT_EXPIRES_IN || "1d",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/platform/database/Database.config.ts
|
|
3
|
+
import { loadEnvConfig } from "../config/Env.config.js";
|
|
4
|
+
|
|
5
|
+
export interface DatabaseConfig {
|
|
6
|
+
url: string;
|
|
7
|
+
maxRetries: number;
|
|
8
|
+
retryDelay: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function loadDatabaseConfig(): DatabaseConfig {
|
|
12
|
+
const env = loadEnvConfig();
|
|
13
|
+
return {
|
|
14
|
+
url: env.databaseUrl,
|
|
15
|
+
maxRetries: 3,
|
|
16
|
+
retryDelay: 1000,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/platform/database/Connection.ts
|
|
23
|
+
import { loadDatabaseConfig } from "./Database.config.js";
|
|
24
|
+
|
|
25
|
+
let connection: unknown = null;
|
|
26
|
+
|
|
27
|
+
export async function connect(): Promise<void> {
|
|
28
|
+
const config = loadDatabaseConfig();
|
|
29
|
+
// conectar según ORM (prisma, mongoose, pg, etc.)
|
|
30
|
+
console.log(`Connecting to database...`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function disconnect(): Promise<void> {
|
|
34
|
+
// desconectar
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getConnection(): unknown {
|
|
38
|
+
return connection;
|
|
39
|
+
}
|
|
40
|
+
```
|