@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,164 @@
|
|
|
1
|
+
# Profile: Express + PostgreSQL (raw)
|
|
2
|
+
|
|
3
|
+
## Metadata
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
framework: Express
|
|
7
|
+
runtime: Node 20+
|
|
8
|
+
database: PostgreSQL
|
|
9
|
+
orm: node-postgres (pg)
|
|
10
|
+
di_strategy: manual
|
|
11
|
+
architecture: hexagonal-feature
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## DI Strategy
|
|
15
|
+
|
|
16
|
+
Este perfil usa **DI manual** (sin tsyringe). Las dependencias se inyectan por constructor y se instancian manualmente en bootstrap.
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// app.ts — wiring manual
|
|
20
|
+
import { CreditRepository } from "@/features/credit/adapters/out/persistence/CreditRepository.js";
|
|
21
|
+
import { AddCredit } from "@/features/credit/application/use-cases/Add.js";
|
|
22
|
+
import { CreditController } from "@/features/credit/adapters/in/http/CreditController.js";
|
|
23
|
+
|
|
24
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
25
|
+
|
|
26
|
+
const creditRepo = new CreditRepository(pool);
|
|
27
|
+
const addCredit = new AddCredit(creditRepo);
|
|
28
|
+
const creditController = new CreditController(addCredit);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Project Structure
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
├── src/
|
|
35
|
+
│ ├── features/
|
|
36
|
+
│ │ └── <domain>/
|
|
37
|
+
│ │ ├── domain/
|
|
38
|
+
│ │ │ ├── <Domain>.entity.ts
|
|
39
|
+
│ │ │ └── I<Domain>Repository.ts
|
|
40
|
+
│ │ ├── application/
|
|
41
|
+
│ │ │ ├── use-cases/
|
|
42
|
+
│ │ │ └── dto/
|
|
43
|
+
│ │ └── adapters/
|
|
44
|
+
│ │ ├── in/http/
|
|
45
|
+
│ │ └── out/persistence/
|
|
46
|
+
│ ├── shared/
|
|
47
|
+
│ ├── infrastructure/
|
|
48
|
+
│ │ └── pool.ts # pg Pool singleton
|
|
49
|
+
│ └── server.ts
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Persistence (pg)
|
|
53
|
+
|
|
54
|
+
### Pool singleton
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// src/infrastructure/pool.ts
|
|
58
|
+
import { Pool } from "pg";
|
|
59
|
+
|
|
60
|
+
export const pool = new Pool({
|
|
61
|
+
connectionString: process.env.DATABASE_URL,
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Repository pattern
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
export class CreditRepository implements ICreditRepository {
|
|
69
|
+
constructor(private readonly db: Pool) {}
|
|
70
|
+
|
|
71
|
+
async create(data: Partial<Credit>): Promise<Credit> {
|
|
72
|
+
const { rows } = await this.db.query(
|
|
73
|
+
`INSERT INTO credits (amount, status) VALUES ($1, $2) RETURNING *`,
|
|
74
|
+
[data.amount, data.status]
|
|
75
|
+
);
|
|
76
|
+
return rows[0] as Credit;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async findById(id: string): Promise<Credit | null> {
|
|
80
|
+
const { rows } = await this.db.query(
|
|
81
|
+
`SELECT * FROM credits WHERE id = $1`,
|
|
82
|
+
[id]
|
|
83
|
+
);
|
|
84
|
+
return rows[0] ? (rows[0] as Credit) : null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Transactions
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
export class CreateCredit {
|
|
93
|
+
constructor(
|
|
94
|
+
private readonly creditRepo: ICreditRepository,
|
|
95
|
+
private readonly pool: Pool
|
|
96
|
+
) {}
|
|
97
|
+
|
|
98
|
+
async execute(data: CreditInput): Promise<Credit> {
|
|
99
|
+
const client = await this.pool.connect();
|
|
100
|
+
try {
|
|
101
|
+
await client.query("BEGIN");
|
|
102
|
+
const credit = await this.creditRepo.create(data, client);
|
|
103
|
+
await client.query("COMMIT");
|
|
104
|
+
return credit;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
await client.query("ROLLBACK");
|
|
107
|
+
throw error;
|
|
108
|
+
} finally {
|
|
109
|
+
client.release();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Controller pattern
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
export class CreditController {
|
|
119
|
+
constructor(private readonly addCredit: AddCredit) {}
|
|
120
|
+
|
|
121
|
+
add = async (req: Request, res: Response, next: NextFunction) => {
|
|
122
|
+
try {
|
|
123
|
+
const result = await this.addCredit.execute(req.body);
|
|
124
|
+
res.status(201).json({ data: result });
|
|
125
|
+
} catch (error) {
|
|
126
|
+
next(error);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Routes pattern
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { Emitter } from "@adpt/adapters";
|
|
136
|
+
|
|
137
|
+
const router = Router();
|
|
138
|
+
|
|
139
|
+
export function setupCreditRoutes(controller: CreditController) {
|
|
140
|
+
router.post("/", controller.add);
|
|
141
|
+
router.get("/:id", controller.getById);
|
|
142
|
+
return router;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Testing Strategy
|
|
147
|
+
|
|
148
|
+
### Unit test
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const mockRepo = { create: jest.fn() };
|
|
152
|
+
const useCase = new AddCredit(mockRepo);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Integration test
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { Pool } from "pg";
|
|
159
|
+
const testPool = new Pool({ connectionString: process.env.TEST_DATABASE_URL });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Naming Conventions
|
|
163
|
+
|
|
164
|
+
Tables in snake_case, columns in snake_case, JavaScript/TypeScript in camelCase.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Profile: Express + Prisma
|
|
2
|
+
|
|
3
|
+
## Metadata
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
framework: Express
|
|
7
|
+
runtime: Node 20+
|
|
8
|
+
database: Cualquier RDBMS (PostgreSQL, MySQL, SQLite)
|
|
9
|
+
orm: Prisma
|
|
10
|
+
di_strategy: tsyringe
|
|
11
|
+
architecture: hexagonal-feature
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Project Structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
├── prisma/
|
|
18
|
+
│ └── schema.prisma
|
|
19
|
+
├── src/
|
|
20
|
+
│ ├── features/
|
|
21
|
+
│ │ └── <domain>/
|
|
22
|
+
│ │ ├── domain/
|
|
23
|
+
│ │ │ ├── <Domain>.entity.ts
|
|
24
|
+
│ │ │ └── I<Domain>Repository.ts
|
|
25
|
+
│ │ ├── application/
|
|
26
|
+
│ │ │ ├── use-cases/
|
|
27
|
+
│ │ │ ├── dto/
|
|
28
|
+
│ │ │ └── mappers/
|
|
29
|
+
│ │ └── adapters/
|
|
30
|
+
│ │ ├── in/http/
|
|
31
|
+
│ │ └── out/persistence/
|
|
32
|
+
│ ├── shared/
|
|
33
|
+
│ ├── infrastructure/
|
|
34
|
+
│ │ └── prisma.ts # PrismaClient singleton
|
|
35
|
+
│ └── server.ts
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## DI Setup
|
|
39
|
+
|
|
40
|
+
Igual que `express-mongodb` (tsyringe). Las interfaces se registran como singletons.
|
|
41
|
+
|
|
42
|
+
## Persistence (Prisma)
|
|
43
|
+
|
|
44
|
+
### PrismaClient singleton
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// src/infrastructure/prisma.ts
|
|
48
|
+
import { PrismaClient } from "@prisma/client";
|
|
49
|
+
|
|
50
|
+
export const prisma = new PrismaClient();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Repository pattern
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
@injectable()
|
|
57
|
+
export class CreditRepository implements ICreditRepository {
|
|
58
|
+
async create(data: Partial<Credit>): Promise<Credit> {
|
|
59
|
+
const record = await prisma.credit.create({ data });
|
|
60
|
+
return CreditMapper.toDomain(record);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async findById(id: string): Promise<Credit | null> {
|
|
64
|
+
const record = await prisma.credit.findUnique({ where: { id } });
|
|
65
|
+
return record ? CreditMapper.toDomain(record) : null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Transactions
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
@injectable()
|
|
74
|
+
export class CreateCredit {
|
|
75
|
+
async execute(data: CreditInput): Promise<Credit> {
|
|
76
|
+
return prisma.$transaction(async (tx) => {
|
|
77
|
+
const credit = await tx.credit.create({ data });
|
|
78
|
+
await tx.log.create({ data: { action: "credit_created", creditId: credit.id } });
|
|
79
|
+
return credit;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Testing Strategy
|
|
86
|
+
|
|
87
|
+
### Unit test
|
|
88
|
+
|
|
89
|
+
Misma estrategia que express-mongodb: `container.registerInstance()` con mocks.
|
|
90
|
+
|
|
91
|
+
### Integration test
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { prisma } from "@/infrastructure/prisma.js";
|
|
95
|
+
import { CreditRepository } from "./CreditRepository.js";
|
|
96
|
+
|
|
97
|
+
beforeAll(async () => {
|
|
98
|
+
// Prisma usa archivo de test o DB separada
|
|
99
|
+
// process.env.DATABASE_URL = "postgresql://test:test@localhost:5432/test";
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
afterAll(async () => {
|
|
103
|
+
await prisma.$disconnect();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("persists a credit", async () => {
|
|
107
|
+
const repo = new CreditRepository();
|
|
108
|
+
const credit = await repo.create({ amount: 100 });
|
|
109
|
+
expect(credit.id).toBeDefined();
|
|
110
|
+
await prisma.credit.delete({ where: { id: credit.id } }); // cleanup
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Naming Conventions
|
|
115
|
+
|
|
116
|
+
Mismas que express-mongodb. Los nombres de tablas en Prisma siguen snake_case.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Profile: Fastify + PostgreSQL + Prisma
|
|
2
|
+
|
|
3
|
+
## Metadata
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
framework: Fastify
|
|
7
|
+
runtime: Node 20+
|
|
8
|
+
database: PostgreSQL
|
|
9
|
+
orm: Prisma
|
|
10
|
+
di_strategy: manual
|
|
11
|
+
architecture: hexagonal-feature
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## DI Strategy
|
|
15
|
+
|
|
16
|
+
DI manual. Sin contenedor de DI externo. Las dependencias se inyectan por constructor y se registran explícitamente en bootstrap.
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// app.ts
|
|
20
|
+
import Fastify from "fastify";
|
|
21
|
+
import { PrismaClient } from "@prisma/client";
|
|
22
|
+
import { creditRoutes } from "@/features/credit/adapters/in/http/credit.routes.js";
|
|
23
|
+
|
|
24
|
+
const app = Fastify({ logger: true });
|
|
25
|
+
const prisma = new PrismaClient();
|
|
26
|
+
const creditRepo = new CreditRepository(prisma);
|
|
27
|
+
const addCredit = new AddCredit(creditRepo);
|
|
28
|
+
|
|
29
|
+
app.register(creditRoutes(addCredit));
|
|
30
|
+
|
|
31
|
+
await app.listen({ port: 3000 });
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Project Structure
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
├── prisma/
|
|
38
|
+
│ └── schema.prisma
|
|
39
|
+
├── src/
|
|
40
|
+
│ ├── features/
|
|
41
|
+
│ │ └── <domain>/
|
|
42
|
+
│ │ ├── domain/
|
|
43
|
+
│ │ ├── application/
|
|
44
|
+
│ │ └── adapters/
|
|
45
|
+
│ │ ├── in/http/
|
|
46
|
+
│ │ └── out/persistence/
|
|
47
|
+
│ ├── shared/
|
|
48
|
+
│ ├── infrastructure/
|
|
49
|
+
│ │ └── prisma.ts
|
|
50
|
+
│ ├── app.ts
|
|
51
|
+
│ └── server.ts
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Routes & Controllers
|
|
55
|
+
|
|
56
|
+
### Fastify plugin pattern
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// credit.routes.ts
|
|
60
|
+
import type { FastifyInstance } from "fastify";
|
|
61
|
+
import { CreditController } from "./CreditController.js";
|
|
62
|
+
|
|
63
|
+
export async function creditRoutes(app: FastifyInstance, controller: CreditController) {
|
|
64
|
+
app.post("/credits", controller.add);
|
|
65
|
+
app.get("/credits/:id", controller.getById);
|
|
66
|
+
app.get("/credits", controller.list);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Controller
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
export class CreditController {
|
|
74
|
+
constructor(private readonly addCredit: AddCredit) {}
|
|
75
|
+
|
|
76
|
+
add = async (request: FastifyRequest, reply: FastifyReply) => {
|
|
77
|
+
const result = await this.addCredit.execute(request.body as CreditInput);
|
|
78
|
+
return reply.status(201).send({ data: result });
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Persistence
|
|
84
|
+
|
|
85
|
+
Prisma repository pattern, identical to `express-prisma` profile.
|
|
86
|
+
|
|
87
|
+
## Testing Strategy
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import Fastify from "fastify";
|
|
91
|
+
|
|
92
|
+
const app = Fastify();
|
|
93
|
+
const mockRepo = { create: jest.fn() };
|
|
94
|
+
const useCase = new AddCredit(mockRepo);
|
|
95
|
+
const controller = new CreditController(useCase);
|
|
96
|
+
|
|
97
|
+
app.register(creditRoutes(controller));
|
|
98
|
+
|
|
99
|
+
afterAll(() => app.close());
|
|
100
|
+
|
|
101
|
+
it("creates a credit", async () => {
|
|
102
|
+
mockRepo.create.mockResolvedValue({ id: "1" });
|
|
103
|
+
const res = await app.inject({ method: "POST", url: "/credits", payload: { amount: 100 } });
|
|
104
|
+
expect(res.statusCode).toBe(201);
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Naming Conventions
|
|
109
|
+
|
|
110
|
+
Mismas que express-profile profiles. Fastify usa camelCase para rutas.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Profile: NestJS + Prisma
|
|
2
|
+
|
|
3
|
+
## Metadata
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
framework: NestJS
|
|
7
|
+
runtime: Node 20+
|
|
8
|
+
database: Cualquier RDBMS
|
|
9
|
+
orm: Prisma
|
|
10
|
+
di_strategy: framework (NestJS DI)
|
|
11
|
+
architecture: hexagonal-feature
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## DI Strategy
|
|
15
|
+
|
|
16
|
+
NestJS tiene su propio sistema de DI: `@Injectable()` decorator + módulos. No se necesita tsyringe.
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { Injectable } from "@nestjs/common";
|
|
20
|
+
import { InjectRepository } from "@nestjs/typeorm"; // o Prisma
|
|
21
|
+
|
|
22
|
+
@Injectable()
|
|
23
|
+
export class AddCredit {
|
|
24
|
+
constructor(private readonly creditRepo: ICreditRepository) {}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Project Structure
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
├── prisma/
|
|
32
|
+
│ └── schema.prisma
|
|
33
|
+
├── src/
|
|
34
|
+
│ ├── features/
|
|
35
|
+
│ │ └── <domain>/
|
|
36
|
+
│ │ ├── domain/
|
|
37
|
+
│ │ │ ├── <Domain>.entity.ts
|
|
38
|
+
│ │ │ └── I<Domain>Repository.ts
|
|
39
|
+
│ │ ├── application/
|
|
40
|
+
│ │ │ ├── use-cases/
|
|
41
|
+
│ │ │ └── dto/
|
|
42
|
+
│ │ └── adapters/
|
|
43
|
+
│ │ ├── in/http/
|
|
44
|
+
│ │ │ ├── <Domain>Controller.ts
|
|
45
|
+
│ │ │ └── <domain>.module.ts
|
|
46
|
+
│ │ └── out/persistence/
|
|
47
|
+
│ │ ├── <Domain>Repository.ts
|
|
48
|
+
│ │ └── <Domain>PrismaRepository.ts
|
|
49
|
+
│ └── app.module.ts
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Module setup
|
|
53
|
+
|
|
54
|
+
### feature module
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// credit.module.ts
|
|
58
|
+
import { Module } from "@nestjs/common";
|
|
59
|
+
import { CreditController } from "./CreditController.js";
|
|
60
|
+
import { AddCredit } from "../../application/use-cases/Add.js";
|
|
61
|
+
import { CreditRepository } from "../out/persistence/CreditPrismaRepository.js";
|
|
62
|
+
|
|
63
|
+
@Module({
|
|
64
|
+
controllers: [CreditController],
|
|
65
|
+
providers: [
|
|
66
|
+
AddCredit,
|
|
67
|
+
{ provide: "ICreditRepository", useClass: CreditRepository },
|
|
68
|
+
],
|
|
69
|
+
})
|
|
70
|
+
export class CreditModule {}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Controller (NestJS style)
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { Controller, Post, Get, Body, Param } from "@nestjs/common";
|
|
77
|
+
import { AddCredit } from "../../application/use-cases/Add.js";
|
|
78
|
+
|
|
79
|
+
@Controller("credits")
|
|
80
|
+
export class CreditController {
|
|
81
|
+
constructor(private readonly addCredit: AddCredit) {}
|
|
82
|
+
|
|
83
|
+
@Post()
|
|
84
|
+
async create(@Body() data: CreditInput) {
|
|
85
|
+
return this.addCredit.execute(data);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@Get(":id")
|
|
89
|
+
async findById(@Param("id") id: string) {
|
|
90
|
+
return this.addCredit.execute(id);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Persistence (Prisma)
|
|
96
|
+
|
|
97
|
+
NestJS con Prisma: crear un `PrismaModule` global y `PrismaService` que extiende `PrismaClient`.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { Injectable, OnModuleInit, OnModuleDestroy } from "@nestjs/common";
|
|
101
|
+
import { PrismaClient } from "@prisma/client";
|
|
102
|
+
|
|
103
|
+
@Injectable()
|
|
104
|
+
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
|
105
|
+
async onModuleInit() {
|
|
106
|
+
await this.$connect();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async onModuleDestroy() {
|
|
110
|
+
await this.$disconnect();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Repository
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
@Injectable()
|
|
119
|
+
export class CreditRepository implements ICreditRepository {
|
|
120
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
121
|
+
|
|
122
|
+
async create(data: Partial<Credit>): Promise<Credit> {
|
|
123
|
+
const record = await this.prisma.credit.create({ data });
|
|
124
|
+
return CreditMapper.toDomain(record);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Testing Strategy
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { Test, TestingModule } from "@nestjs/testing";
|
|
133
|
+
|
|
134
|
+
const mockRepo = { create: jest.fn() };
|
|
135
|
+
|
|
136
|
+
beforeEach(async () => {
|
|
137
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
138
|
+
providers: [
|
|
139
|
+
AddCredit,
|
|
140
|
+
{ provide: "ICreditRepository", useValue: mockRepo },
|
|
141
|
+
],
|
|
142
|
+
}).compile();
|
|
143
|
+
|
|
144
|
+
useCase = module.get<AddCredit>(AddCredit);
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Naming Conventions
|
|
149
|
+
|
|
150
|
+
NestJS usa PascalCase para clases y camelCase para métodos/propiedades. Decoradores nativos `@Injectable()`, `@Controller()`, etc.
|
|
151
|
+
|
|
152
|
+
## Cross-feature communication
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// En NestJS, los módulos exportan providers para que otros módulos los usen
|
|
156
|
+
@Module({
|
|
157
|
+
exports: [AddCredit], // Disponible para otros módulos
|
|
158
|
+
})
|
|
159
|
+
export class CreditModule {}
|
|
160
|
+
```
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Cast
|
|
2
|
+
|
|
3
|
+
Crea un nuevo feature desde cero siguiendo la arquitectura hexagonal basada en features.
|
|
4
|
+
|
|
5
|
+
## Cuándo usarlo
|
|
6
|
+
|
|
7
|
+
- Agregar un nuevo dominio de negocio al proyecto
|
|
8
|
+
- El dominio no existe previamente en forma legacy
|
|
9
|
+
|
|
10
|
+
## Pre-condiciones
|
|
11
|
+
|
|
12
|
+
Antes de crear un feature, verificar la existencia de los layers arquitectónicos:
|
|
13
|
+
|
|
14
|
+
1. **Platform** — `src/platform/` (config, server, logger, di, etc.)
|
|
15
|
+
2. **Shared** — `src/shared/` (errors, contracts, types, utils)
|
|
16
|
+
3. **Infra** — `src/infra/` (prisma, redis, etc.)
|
|
17
|
+
|
|
18
|
+
Si alguno no existe, ejecutar `bootstrapPlatform()` automáticamente para crearlos.
|
|
19
|
+
|
|
20
|
+
## Flujo
|
|
21
|
+
|
|
22
|
+
1. Verificar que `src/platform/`, `src/shared/`, `src/infra/` existan (crearlos si no)
|
|
23
|
+
2. Determinar el nombre del feature (formato: kebab-case)
|
|
24
|
+
3. Crear estructura de directorios:
|
|
25
|
+
```
|
|
26
|
+
src/features/<name>/
|
|
27
|
+
├── domain/
|
|
28
|
+
├── application/
|
|
29
|
+
│ ├── use-cases/
|
|
30
|
+
│ └── mappers/
|
|
31
|
+
└── adapters/
|
|
32
|
+
├── in/http/
|
|
33
|
+
└── out/persistence/
|
|
34
|
+
```
|
|
35
|
+
4. Crear archivos del feature en este orden (ver `templates/feature/`):
|
|
36
|
+
- `<Name>.entity.ts` — interfaz de dominio
|
|
37
|
+
- `I<Name>Repository.ts` — puerto de repositorio
|
|
38
|
+
- `<Name>.mapper.ts` — mapper dominio ↔ persistencia
|
|
39
|
+
- `<Name>Schema.ts` — schema de BD (según perfil)
|
|
40
|
+
- `<Name>Repository.ts` — implementación del repositorio
|
|
41
|
+
- Use cases (`Create.ts`, `Get.ts`, `List.ts`, `Update.ts`, `Delete.ts`)
|
|
42
|
+
- `<Name>Controller.ts` — controlador HTTP
|
|
43
|
+
- `<name>.routes.ts` — rutas HTTP
|
|
44
|
+
5. Registrar rutas en el enrutador principal
|
|
45
|
+
6. Ejecutar `forge quench` para verificar el feature
|
|
46
|
+
7. Actualizar `ARCHITECTURE.md`
|
|
47
|
+
|
|
48
|
+
## Convenciones
|
|
49
|
+
|
|
50
|
+
Ver `reference/patterns.md` para el patrón completo.
|
|
51
|
+
|
|
52
|
+
| Elemento | Formato | Ejemplo |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Feature directory | `kebab-case/` | `credit-card/` |
|
|
55
|
+
| Entity | `<Name>.entity.ts` | `CreditCard.entity.ts` |
|
|
56
|
+
| Repository interface | `I<Name>.repository.ts` | `ICreditCard.repository.ts` |
|
|
57
|
+
| Repository impl | `<Name>.repository.ts` | `CreditCard.repository.ts` |
|
|
58
|
+
| Use case | `<Action>.uc.ts` | `CreateCreditCard.uc.ts` |
|
|
59
|
+
| Mapper | `<Name>.mapper.ts` | `CreditCard.mapper.ts` |
|
|
60
|
+
| Controller | `<Name>.controller.ts` | `CreditCard.controller.ts` |
|
|
61
|
+
| Routes | `<Name>.routes.ts` | `CreditCard.routes.ts` |
|
|
62
|
+
| Schema | `<Name>.schema.ts` | `CreditCard.schema.ts` |
|
|
63
|
+
|
|
64
|
+
## Con el perfil activo
|
|
65
|
+
|
|
66
|
+
Usar el perfil detectado para determinar:
|
|
67
|
+
- Estrategia de DI (tsyringe/manual/framework)
|
|
68
|
+
- Patrón de controlador (Express/Fastify/NestJS)
|
|
69
|
+
- Patrón de persistencia (Mongoose/Prisma/pg)
|
|
70
|
+
- Convenciones de imports (rutas relativas vs alias)
|
|
71
|
+
- Componentes de platform a usar (config, logger, http, database)
|
|
72
|
+
|
|
73
|
+
## Post-creación
|
|
74
|
+
|
|
75
|
+
- `forge quench` — verificar que no hay violaciones
|
|
76
|
+
- `forge inspect` — confirmar puntuación
|
|
77
|
+
- `ARCHITECTURE.md` actualizado automáticamente
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Chain
|
|
2
|
+
|
|
3
|
+
Gestiona las cadenas de dependencias entre features.
|
|
4
|
+
|
|
5
|
+
## Cuándo usarlo
|
|
6
|
+
|
|
7
|
+
- Antes de migrar un feature (para conocer sus dependencias)
|
|
8
|
+
- Cuando se detectan imports directos entre features
|
|
9
|
+
- Para determinar el orden topológico de migración
|
|
10
|
+
- Para diagnosticar acoplamiento excesivo
|
|
11
|
+
|
|
12
|
+
## Reglas
|
|
13
|
+
|
|
14
|
+
- Nunca imports directos entre features
|
|
15
|
+
- La comunicación entre features siempre via interfaces inyectadas
|
|
16
|
+
- Un feature puede depender de otro, pero nunca al revés (sin ciclos)
|
|
17
|
+
- Las interfaces compartidas se declaran en el feature que las define
|
|
18
|
+
- Dependencias transitivas se inyectan, no se heredan
|
|
19
|
+
|
|
20
|
+
## Orden topológico
|
|
21
|
+
|
|
22
|
+
Migrar features en orden de menor a mayor dependencia:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
1. Features sin dependencias
|
|
26
|
+
2. Features que dependen solo de shared/
|
|
27
|
+
3. Features que dependen de features ya migrados
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Cómo inyectar dependencias entre features
|
|
31
|
+
|
|
32
|
+
### Entre features del mismo proyecto
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
Feature A (credit)
|
|
36
|
+
├── domain/ICreditRepository.ts ← define la interfaz
|
|
37
|
+
└── adapters/out/CreditRepository.ts ← implementa
|
|
38
|
+
|
|
39
|
+
Feature B (payment) ← necesita CreditRepository
|
|
40
|
+
├── application/PaymentUseCase.ts
|
|
41
|
+
└── @inject(ICreditRepository) creditRepo
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Registro en bootstrap
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
container.registerSingleton<ICreditRepository>(
|
|
48
|
+
ICreditRepository as symbol,
|
|
49
|
+
CreditRepository
|
|
50
|
+
);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Detectar dependencias
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Grafo de dependencias entre features (API legacy)
|
|
57
|
+
node .opencode/skills/forge/scripts/chain.mjs
|
|
58
|
+
|
|
59
|
+
# Grafo arquitectónico completo (nodos, edges, violaciones)
|
|
60
|
+
node .opencode/skills/forge/scripts/graph.mjs
|
|
61
|
+
|
|
62
|
+
# ARCHITECTURE.md con grafo incluido
|
|
63
|
+
node .opencode/skills/forge/scripts/architecture.mjs
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`chain.mjs` ahora es un wrapper sobre `graph.mjs`. Produce el mismo formato de salida para compatibilidad (nodos, edges, features, orden topológico). El nuevo `graph.mjs` amplía el análisis a todos los tipos de nodo (core, feature, domain, infra, adapter) y detecta violaciones de reglas arquitectónicas (R1-R6).
|
|
67
|
+
|
|
68
|
+
## Buenas prácticas
|
|
69
|
+
|
|
70
|
+
- Mantener el grafo acíclico
|
|
71
|
+
- Si hay ciclo, extraer la interfaz común a shared/
|
|
72
|
+
- Documentar dependencias en ARCHITECTURE.md
|
|
73
|
+
- Revisar dependencias después de cada migración
|