arckode-framework 1.0.8 → 1.1.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/cli/analyze.ts +162 -47
- package/cli/generate.ts +30 -23
- package/cli/index.ts +29 -92
- package/cli/stubs/claude-md-stub.ts +174 -0
- package/cli/stubs/module-stub.ts +90 -21
- package/kernel/framework.ts +112 -7
- package/package.json +3 -3
- package/skills/auth/SKILL.md +1 -1
- package/skills/cli/SKILL.md +21 -12
- package/skills/connectors/SKILL.md +4 -4
- package/skills/helpers/SKILL.md +1 -1
- package/skills/orm/SKILL.md +154 -18
- package/skills/realtime/SKILL.md +1 -1
- package/skills/services/SKILL.md +1 -1
- package/skills/testing/SKILL.md +1 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// cli/stubs/claude-md-stub.ts — Plantilla del CLAUDE.md generado por `arckode new`
|
|
2
|
+
//
|
|
3
|
+
// IMPORTANTE: Este archivo debe mantenerse SINCRONIZADO con CLAUDE.md del framework.
|
|
4
|
+
// Si agregás/modificás una regla en /CLAUDE.md → actualizá también este stub.
|
|
5
|
+
// Los proyectos NUEVOS lo reciben hardcoded en su raíz al ejecutar `arckode new`.
|
|
6
|
+
|
|
7
|
+
export interface ClaudeMdStubParams {
|
|
8
|
+
projectName: string
|
|
9
|
+
db: 'sqlite' | 'mysql' | 'postgres'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function claudeMdStub(p: ClaudeMdStubParams): string {
|
|
13
|
+
return `# CLAUDE.md — ${p.projectName}
|
|
14
|
+
|
|
15
|
+
## Lee esto ANTES de escribir cualquier línea de código.
|
|
16
|
+
|
|
17
|
+
### PASO 0 — Cargar contexto
|
|
18
|
+
1. Si existe \`arckode.json\` → leelo (snapshot del sistema: módulos, conectores, violaciones).
|
|
19
|
+
Si no existe (proyecto recién creado), generalo con: \`bun run analyze:json\`
|
|
20
|
+
2. Leer \`src/composition-root.ts\` → es la única fuente de verdad del sistema.
|
|
21
|
+
3. Leer este CLAUDE.md → reglas inmutables y estructura obligatoria.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Estructura obligatoria de un módulo
|
|
26
|
+
|
|
27
|
+
\`\`\`
|
|
28
|
+
src/modules/mi-modulo/
|
|
29
|
+
index.ts ← OBLIGATORIO — factory: registra modelo via registerXxxModels, wirea rutas
|
|
30
|
+
model.ts ← OBLIGATORIO — ModelDefinition (schema DB) + registerXxxModels(orm)
|
|
31
|
+
types.ts ← OBLIGATORIO — DTOs y tipos de queries (sin schema DB)
|
|
32
|
+
sockets.ts ← OBLIGATORIO — interfaz de eventos hacia otros módulos
|
|
33
|
+
service.ts ← OBLIGATORIO — facade del módulo (lógica de negocio)
|
|
34
|
+
controller.ts ← OBLIGATORIO — adaptador HTTP
|
|
35
|
+
repository.ts ← OPCIONAL — queries con nombres del dominio (ver Regla #21)
|
|
36
|
+
usecases/ ← OPCIONAL — solo si hay 3+ flujos DIFERENTES (no CRUD)
|
|
37
|
+
validators/schema.ts ← OBLIGATORIO — esquemas de validateSchema()
|
|
38
|
+
tests/service.test.ts ← OBLIGATORIO — mínimo 2 casos (happy path + error)
|
|
39
|
+
\`\`\`
|
|
40
|
+
|
|
41
|
+
**\`service.ts\` y \`controller.ts\` van al ROOT del módulo, NO en \`actions/\`.**
|
|
42
|
+
**\`model.ts\` separado de \`types.ts\`** porque schema DB y contrato TS son conceptos distintos.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Reglas INMUTABLES (no se negocian, no se omiten)
|
|
47
|
+
|
|
48
|
+
| # | Regla | Detección |
|
|
49
|
+
|---|-------|-----------|
|
|
50
|
+
| 1 | Módulos NO importan de otros módulos → usar conectores | \`arckode analyze\` |
|
|
51
|
+
| 2 | index.ts es APPEND-ONLY → no eliminar exports existentes | manual |
|
|
52
|
+
| 3 | Conectores NO tienen lógica de negocio → solo delegan | \`arckode analyze\` |
|
|
53
|
+
| 4 | Cada tabla pertenece a UN módulo | manual |
|
|
54
|
+
| 5 | Si no está en composition-root.ts, no existe | \`arckode analyze\` |
|
|
55
|
+
| 6 | Todo POST/PUT/PATCH requiere validateSchema() | \`arckode analyze\` |
|
|
56
|
+
| 7 | Controller NO llama al ORM → llama al service | \`arckode analyze\` |
|
|
57
|
+
| 8 | Toda operación atómica multi-tabla usa Transactor (no inyectar ORM) | manual |
|
|
58
|
+
| 9 | findById → verificar ownership inmediatamente (IDOR) | \`arckode analyze\` |
|
|
59
|
+
| 10 | ORM dentro de un loop → N+1 prohibido | \`arckode analyze\` |
|
|
60
|
+
| 11 | Service recibe RepositoryAdapter<T> o Repository del dominio, NO ORM directo | \`arckode analyze\` |
|
|
61
|
+
| 15 | Extraer a usecases/ solo si hay 3+ flujos DIFERENTES (CRUD nunca se fragmenta) | manual |
|
|
62
|
+
| 19 | Transactor para atomicidad multi-tabla — \`transactor.run(async (repos) => ...)\` | manual |
|
|
63
|
+
| 20 | Conectores limpios: kebab-case, con prefijo \`connect\`, registrados en composition-root | \`arckode analyze\` |
|
|
64
|
+
| 21 | Si el service repite filtros 3+ veces o necesita JOIN/IN/LIKE → crear \`repository.ts\` | manual |
|
|
65
|
+
| 22 | \`model.ts\` separado de \`types.ts\` (schema DB ≠ contrato TS) | manual |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Protocolo de la IA (4 pasos obligatorios)
|
|
70
|
+
|
|
71
|
+
\`\`\`
|
|
72
|
+
PASO 1 — IDENTIFICAR ALCANCE
|
|
73
|
+
¿Toca 1 módulo? → Seguir
|
|
74
|
+
¿Toca 1 conector? → Seguir
|
|
75
|
+
¿Toca 2+ módulos? → DETENERSE. Dividir la tarea.
|
|
76
|
+
|
|
77
|
+
PASO 2 — VERIFICAR REGLAS
|
|
78
|
+
¿Importo de otro módulo? → NO (usar conector)
|
|
79
|
+
¿Modifico index.ts? → Solo append
|
|
80
|
+
¿Pongo lógica en el conector? → NO
|
|
81
|
+
¿Comparto una tabla con otro módulo? → NO
|
|
82
|
+
¿Inyecto ORM al service? → NO (RepositoryAdapter<T> o Repository del dominio)
|
|
83
|
+
|
|
84
|
+
PASO 3 — GENERAR (estructura nueva)
|
|
85
|
+
Obligatorios: index.ts, model.ts, types.ts, sockets.ts,
|
|
86
|
+
service.ts, controller.ts (al root),
|
|
87
|
+
validators/schema.ts, tests/service.test.ts
|
|
88
|
+
Opcionales: repository.ts (si hay queries del dominio repetidas)
|
|
89
|
+
usecases/ (si hay 3+ flujos DIFERENTES, no CRUD)
|
|
90
|
+
|
|
91
|
+
PASO 4 — VERIFICAR
|
|
92
|
+
bun run analyze --json → violations: 0
|
|
93
|
+
bun test → 0 fallos
|
|
94
|
+
\`\`\`
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Patrones de código
|
|
99
|
+
|
|
100
|
+
### Atomicidad multi-tabla — usar Transactor
|
|
101
|
+
\`\`\`ts
|
|
102
|
+
import type { Transactor, RepositoryAdapter } from 'arckode-framework'
|
|
103
|
+
|
|
104
|
+
class WalletsService {
|
|
105
|
+
constructor(
|
|
106
|
+
private walletRepo: RepositoryAdapter<WalletDTO>,
|
|
107
|
+
private transactor: Transactor,
|
|
108
|
+
) {}
|
|
109
|
+
|
|
110
|
+
async credit(userId: string, amount: number) {
|
|
111
|
+
await this.transactor.run(async (repos) => {
|
|
112
|
+
const wallets = repos.for<WalletDTO>('Wallet')
|
|
113
|
+
const txs = repos.for<TransactionDTO>('Transaction')
|
|
114
|
+
await wallets.update(id, { balance })
|
|
115
|
+
await txs.create({ ... })
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// composition-root:
|
|
121
|
+
const transactor = new OrmTransactor(orm)
|
|
122
|
+
\`\`\`
|
|
123
|
+
|
|
124
|
+
### Repository del dominio (cuando aplique)
|
|
125
|
+
\`\`\`ts
|
|
126
|
+
// repository.ts — opcional, encapsula queries con nombres del dominio
|
|
127
|
+
export class WalletsRepository {
|
|
128
|
+
constructor(private base: RepositoryAdapter<WalletDTO>) {}
|
|
129
|
+
|
|
130
|
+
findByUserAndCurrency(userId: string, currency: WalletCurrency) {
|
|
131
|
+
return this.base.findOne({ user_id: userId, currency })
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// service.ts — habla en lenguaje del dominio, no de columnas
|
|
136
|
+
const wallet = await this.wallets.findByUserAndCurrency(userId, currency)
|
|
137
|
+
\`\`\`
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## DB Adapter activo: \`${p.db}\`
|
|
142
|
+
|
|
143
|
+
## Estructura del proyecto
|
|
144
|
+
\`\`\`
|
|
145
|
+
src/
|
|
146
|
+
composition-root.ts ← TODO el sistema (módulos, conectores, infra)
|
|
147
|
+
modules/ ← módulos independientes (NO se importan entre sí)
|
|
148
|
+
connectors/ ← puentes entre módulos (sin lógica de negocio)
|
|
149
|
+
shared/helpers/ ← funciones puras sin estado
|
|
150
|
+
shared/services/ ← servicios con estado compartidos
|
|
151
|
+
seeds/ ← datos de prueba
|
|
152
|
+
migrations/ ← migraciones explícitas de DB
|
|
153
|
+
\`\`\`
|
|
154
|
+
|
|
155
|
+
## Comandos útiles
|
|
156
|
+
\`\`\`bash
|
|
157
|
+
bun run dev # desarrollo con hot reload
|
|
158
|
+
bun run analyze # verificar arquitectura (consola)
|
|
159
|
+
bun run analyze:json # generar/actualizar arckode.json
|
|
160
|
+
bun arckode make:module Nombre # generar módulo completo
|
|
161
|
+
bun arckode make:connector nombre-x mod1 mod2 # conectar dos módulos (kebab-case)
|
|
162
|
+
bun test # correr todos los tests
|
|
163
|
+
\`\`\`
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Notas para la IA
|
|
168
|
+
|
|
169
|
+
- Si el módulo es CRUD simple: NO crees \`repository.ts\` ni \`usecases/\`. Pasás \`OrmRepository<T>\` directo al service.
|
|
170
|
+
- Si el módulo tiene 3+ tablas: definí TODOS los modelos en \`model.ts\` y registralos en \`registerXxxModels(orm)\` (helper único). El \`index.ts\` queda limpio.
|
|
171
|
+
- Si necesitás atomicidad: inyectá \`Transactor\` al service, no el ORM. Mantiene la dependency inversion.
|
|
172
|
+
- Si el analyzer muestra \`LEGACY_ACTIONS_FOLDER\`, \`DUPLICATE_CONNECTOR\` o \`UNREGISTERED_CONNECTOR\`: arreglarlos ANTES de pushear código nuevo.
|
|
173
|
+
`
|
|
174
|
+
}
|
package/cli/stubs/module-stub.ts
CHANGED
|
@@ -18,8 +18,9 @@ export function indexStub(p: ModuleStubParams): string {
|
|
|
18
18
|
// ⚠ REGLA: Append-only. No sacar ni modificar exports existentes.
|
|
19
19
|
|
|
20
20
|
import { createModule, OrmRepository } from 'arckode-framework'
|
|
21
|
-
import { ${p.name}
|
|
22
|
-
import { ${p.name}
|
|
21
|
+
import { register${p.name}Models } from './model'
|
|
22
|
+
import { ${p.name}Service } from './service'
|
|
23
|
+
import { ${p.name}Controller } from './controller'
|
|
23
24
|
import type { ${p.name}DTO } from './types'
|
|
24
25
|
|
|
25
26
|
export { ${p.name}Service }
|
|
@@ -45,6 +46,9 @@ export function ${p.name}Module() {
|
|
|
45
46
|
},
|
|
46
47
|
|
|
47
48
|
create({ logger, orm, cache, router, auth }) {
|
|
49
|
+
// Registrar modelo(s) — delegado a model.ts
|
|
50
|
+
register${p.name}Models(orm)
|
|
51
|
+
|
|
48
52
|
const repo = new OrmRepository<${p.name}DTO>(orm, '${p.name}')
|
|
49
53
|
const log = logger.child('${p.module}')
|
|
50
54
|
const service = new ${p.name}Service(repo, log, cache)
|
|
@@ -65,10 +69,10 @@ export function ${p.name}Module() {
|
|
|
65
69
|
`
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
// ─── model.ts — Definición de DB (schema) ───
|
|
73
|
+
// Separado de types.ts porque conceptualmente describe la PERSISTENCIA (la DB),
|
|
74
|
+
// no el contrato de TypeScript (DTOs). Cambia con migraciones, no con la API.
|
|
75
|
+
export function modelStub(p: ModuleStubParams): string {
|
|
72
76
|
const modelFields = p.fields
|
|
73
77
|
.filter(f => !['id', 'createdAt', 'updatedAt', 'deletedAt'].includes(f.name))
|
|
74
78
|
.map(f => {
|
|
@@ -80,13 +84,14 @@ export function typesStub(p: ModuleStubParams): string {
|
|
|
80
84
|
})
|
|
81
85
|
.join(',\n')
|
|
82
86
|
|
|
83
|
-
return `// ${p.module}/
|
|
84
|
-
// Responsabilidad ÚNICA:
|
|
85
|
-
//
|
|
87
|
+
return `// ${p.module}/model.ts — Schema de base de datos
|
|
88
|
+
// Responsabilidad ÚNICA: describir la estructura física de la tabla.
|
|
89
|
+
// Importado por index.ts → orm.define() para registrar el modelo.
|
|
90
|
+
//
|
|
91
|
+
// Si el módulo tiene 3+ tablas, agregá registerModels(orm) acá y delegá desde index.ts.
|
|
86
92
|
|
|
87
|
-
import type { ModelDefinition } from 'arckode-framework'
|
|
93
|
+
import type { ModelDefinition, ORM } from 'arckode-framework'
|
|
88
94
|
|
|
89
|
-
// Definición del modelo para ORM.define() — importar en composition-root.ts
|
|
90
95
|
export const ${p.name}Model: ModelDefinition = {
|
|
91
96
|
table: '${p.module}',
|
|
92
97
|
fields: {
|
|
@@ -96,6 +101,22 @@ ${modelFields}
|
|
|
96
101
|
${p.softDelete ? ' softDelete: true,' : ''}
|
|
97
102
|
}
|
|
98
103
|
|
|
104
|
+
// Helper opcional — usalo si el módulo registra 3+ tablas
|
|
105
|
+
// (mantiene index.ts limpio, enfocado en wiring)
|
|
106
|
+
export function register${p.name}Models(orm: ORM): void {
|
|
107
|
+
orm.define('${p.name}', ${p.name}Model)
|
|
108
|
+
}
|
|
109
|
+
`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function typesStub(p: ModuleStubParams): string {
|
|
113
|
+
const fields = p.fields.map(f => ` ${f.name}${f.required ? '' : '?'}: ${mapTS(f.type)}`).join('\n')
|
|
114
|
+
const filterFields = p.fields.map(f => ` ${f.name}?: ${mapTS(f.type)}`).join('\n')
|
|
115
|
+
|
|
116
|
+
return `// ${p.module}/types.ts — DTOs y tipos de queries
|
|
117
|
+
// Responsabilidad ÚNICA: contrato TypeScript del módulo (cómo se ven los datos).
|
|
118
|
+
// El schema de DB vive en ./model.ts — son conceptos distintos.
|
|
119
|
+
|
|
99
120
|
export interface ${p.name}DTO {
|
|
100
121
|
id: string
|
|
101
122
|
${fields}
|
|
@@ -154,18 +175,21 @@ export interface ${p.name}Sockets {
|
|
|
154
175
|
export function serviceStub(p: ModuleStubParams): string {
|
|
155
176
|
const hasSearch = p.fields.some(f => f.type === 'string')
|
|
156
177
|
|
|
157
|
-
return `// ${p.module}/
|
|
178
|
+
return `// ${p.module}/service.ts — Facade pública del módulo
|
|
158
179
|
// Responsabilidad ÚNICA: casos de uso del módulo.
|
|
159
180
|
// NO sabe de HTTP. NO importa de otros módulos.
|
|
160
181
|
// Recibe dependencias por constructor (Dependency Inversion).
|
|
161
182
|
//
|
|
183
|
+
// Si este archivo supera 200 líneas → extraer casos de uso a ./usecases/{caso}.ts
|
|
184
|
+
// y dejar acá solo el orquestador que delega.
|
|
185
|
+
//
|
|
162
186
|
// IMPORTANTE: depende de RepositoryAdapter<${p.name}DTO>, no del ORM directamente.
|
|
163
187
|
// Esto permite swapear SQL → MongoDB → Prisma en composition-root.ts sin tocar este archivo.
|
|
164
188
|
|
|
165
189
|
import type { RepositoryAdapter, Logger, CacheAdapter } from 'arckode-framework'
|
|
166
190
|
import { NotFoundError } from 'arckode-framework'
|
|
167
|
-
import type { ${p.name}DTO, Create${p.name}DTO, Update${p.name}DTO, ${p.name}Query, ${p.name}Paginated } from '
|
|
168
|
-
import type { ${p.name}Sockets } from '
|
|
191
|
+
import type { ${p.name}DTO, Create${p.name}DTO, Update${p.name}DTO, ${p.name}Query, ${p.name}Paginated } from './types'
|
|
192
|
+
import type { ${p.name}Sockets } from './sockets'
|
|
169
193
|
|
|
170
194
|
export class ${p.name}Service {
|
|
171
195
|
private sockets: ${p.name}Sockets = {}
|
|
@@ -209,6 +233,8 @@ export class ${p.name}Service {
|
|
|
209
233
|
this.logger.info('Obteniendo ${p.module}', { id })
|
|
210
234
|
const item = await this.repo.findById(id)
|
|
211
235
|
if (!item) throw new NotFoundError('${p.name} no encontrado')
|
|
236
|
+
// IDOR: si el recurso tiene userId u ownerId, descomentar y adaptar:
|
|
237
|
+
// auth.assertOwnership(item.userId as string, currentUser.id, currentUser.role)
|
|
212
238
|
return item
|
|
213
239
|
}
|
|
214
240
|
|
|
@@ -241,14 +267,15 @@ export class ${p.name}Service {
|
|
|
241
267
|
}
|
|
242
268
|
|
|
243
269
|
export function controllerStub(p: ModuleStubParams): string {
|
|
244
|
-
return `// ${p.module}/
|
|
245
|
-
// Responsabilidad ÚNICA: traducir request
|
|
246
|
-
// SIN lógica de negocio. SIN
|
|
270
|
+
return `// ${p.module}/controller.ts — Adaptador HTTP del módulo
|
|
271
|
+
// Responsabilidad ÚNICA: traducir request → service → response.
|
|
272
|
+
// SIN lógica de negocio. SIN llamadas directas al ORM. (REGLA #12)
|
|
273
|
+
// Toda mutación (POST/PUT/PATCH) DEBE pasar por validateSchema(). (REGLA #11)
|
|
247
274
|
|
|
248
275
|
import type { HttpRequest, Logger } from 'arckode-framework'
|
|
249
276
|
import { validateSchema } from 'arckode-framework'
|
|
250
277
|
import type { ${p.name}Service } from './service'
|
|
251
|
-
import { Create${p.name}Schema, Update${p.name}Schema } from '
|
|
278
|
+
import { Create${p.name}Schema, Update${p.name}Schema } from './validators/schema'
|
|
252
279
|
|
|
253
280
|
export class ${p.name}Controller {
|
|
254
281
|
constructor(
|
|
@@ -301,7 +328,7 @@ export function validatorStub(p: ModuleStubParams): string {
|
|
|
301
328
|
parts.push(' }')
|
|
302
329
|
return parts.join('')
|
|
303
330
|
})
|
|
304
|
-
.join('
|
|
331
|
+
.join(',\n')
|
|
305
332
|
|
|
306
333
|
const updateRules = p.fields
|
|
307
334
|
.filter(f => f.name !== 'id' && f.name !== 'createdAt')
|
|
@@ -311,7 +338,7 @@ export function validatorStub(p: ModuleStubParams): string {
|
|
|
311
338
|
parts.push(' }')
|
|
312
339
|
return parts.join('')
|
|
313
340
|
})
|
|
314
|
-
.join('
|
|
341
|
+
.join(',\n')
|
|
315
342
|
|
|
316
343
|
return `// ${p.module}/validators/schema.ts — Validación de entrada
|
|
317
344
|
// Schemas planos, sin dependencias externas.
|
|
@@ -341,7 +368,7 @@ export function testStub(p: ModuleStubParams): string {
|
|
|
341
368
|
import { describe, it, expect } from 'bun:test'
|
|
342
369
|
import type { RepositoryAdapter, CacheAdapter } from 'arckode-framework'
|
|
343
370
|
import { silentLogger } from 'arckode-framework/testing'
|
|
344
|
-
import { ${p.name}Service } from '../
|
|
371
|
+
import { ${p.name}Service } from '../service'
|
|
345
372
|
import type { ${p.name}DTO } from '../types'
|
|
346
373
|
|
|
347
374
|
// silentLogger es una factory function — SIEMPRE llamarla con ()
|
|
@@ -469,6 +496,48 @@ ${sampleData.replace(`'${p.name} de ejemplo'`, `'${p.name} de ejemplo 2'`).repla
|
|
|
469
496
|
`
|
|
470
497
|
}
|
|
471
498
|
|
|
499
|
+
// ─── repository.ts — OPCIONAL ────────────────────────────
|
|
500
|
+
// NO se genera por default. Crealo a mano cuando:
|
|
501
|
+
// 1. El service repite los mismos filtros en varios métodos
|
|
502
|
+
// (ej: findOne({ user_id, currency }) en 4 lugares)
|
|
503
|
+
// 2. Necesitás queries con JOIN, IN, LIKE — el ORM builtin no las soporta
|
|
504
|
+
// 3. Querés expresar la query en lenguaje del DOMINIO
|
|
505
|
+
// (ej: findActiveByUser vs findMany({ user_id, status: 'active' }))
|
|
506
|
+
//
|
|
507
|
+
// Si solo hacés CRUD pelado → NO necesitás repository.ts.
|
|
508
|
+
// Pasás el OrmRepository<T> directo al service.
|
|
509
|
+
export function repositoryStub(p: ModuleStubParams): string {
|
|
510
|
+
return `// ${p.module}/repository.ts — Repository de dominio (OPCIONAL)
|
|
511
|
+
// Encapsula queries con NOMBRES DEL DOMINIO en lugar de nombres de columna.
|
|
512
|
+
//
|
|
513
|
+
// Antes: service.findOne({ user_id: userId, currency }) ← service conoce la DB
|
|
514
|
+
// Ahora: service.findByUserAndCurrency(userId, currency) ← service habla el dominio
|
|
515
|
+
//
|
|
516
|
+
// Implementa RepositoryAdapter<${p.name}DTO> si necesitás queries complejas (JOIN, IN).
|
|
517
|
+
|
|
518
|
+
import type { RepositoryAdapter } from 'arckode-framework'
|
|
519
|
+
import type { ${p.name}DTO } from './types'
|
|
520
|
+
|
|
521
|
+
export class ${p.name}Repository {
|
|
522
|
+
constructor(private readonly base: RepositoryAdapter<${p.name}DTO>) {}
|
|
523
|
+
|
|
524
|
+
// Métodos genéricos delegados al adapter base
|
|
525
|
+
findById(id: string) { return this.base.findById(id) }
|
|
526
|
+
findMany(filters?: Record<string, unknown>) { return this.base.findMany(filters) }
|
|
527
|
+
create(data: Omit<${p.name}DTO, 'id'>) { return this.base.create(data) }
|
|
528
|
+
update(id: string, data: Partial<Omit<${p.name}DTO, 'id'>>) { return this.base.update(id, data) }
|
|
529
|
+
delete(id: string) { return this.base.delete(id) }
|
|
530
|
+
|
|
531
|
+
// ─── Métodos específicos del dominio ───
|
|
532
|
+
// Agregá acá las queries que se repiten en el service
|
|
533
|
+
//
|
|
534
|
+
// async findActiveByUser(userId: string): Promise<${p.name}DTO[]> {
|
|
535
|
+
// return this.base.findMany({ user_id: userId, active: true })
|
|
536
|
+
// }
|
|
537
|
+
}
|
|
538
|
+
`
|
|
539
|
+
}
|
|
540
|
+
|
|
472
541
|
function mapTS(type: string): string {
|
|
473
542
|
const map: Record<string, string> = { string: 'string', number: 'number', boolean: 'boolean', date: 'string' }
|
|
474
543
|
return map[type] ?? 'unknown'
|
package/kernel/framework.ts
CHANGED
|
@@ -463,6 +463,35 @@ export interface RepositoryAdapter<T extends object = Record<string, unknown>> {
|
|
|
463
463
|
): Promise<PageResult<T>>
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
+
// ── Transactor — atomicidad cross-modelo SIN exponer ORM al service ──
|
|
467
|
+
//
|
|
468
|
+
// Los services NO reciben ORM. Para operaciones atómicas multi-tabla
|
|
469
|
+
// (ledger doble entrada, transferencias, etc.), reciben Transactor.
|
|
470
|
+
//
|
|
471
|
+
// El service obtiene repos transaccionales por modelo dentro del run():
|
|
472
|
+
//
|
|
473
|
+
// await this.transactor.run(async (repos) => {
|
|
474
|
+
// const walletRepo = repos.for<WalletDTO>('Wallet')
|
|
475
|
+
// const txRepo = repos.for<TransactionDTO>('Transaction')
|
|
476
|
+
// await walletRepo.update(id, { balance })
|
|
477
|
+
// await txRepo.create({ ... })
|
|
478
|
+
// })
|
|
479
|
+
//
|
|
480
|
+
// Si una operación dentro de run() falla, todo se rolleabackea automáticamente.
|
|
481
|
+
//
|
|
482
|
+
// Implementaciones:
|
|
483
|
+
// OrmTransactor → built-in, delega a ORM.transaction()
|
|
484
|
+
// Custom → implementar para Prisma, Drizzle, MongoDB, etc.
|
|
485
|
+
|
|
486
|
+
export interface TransactionalRepos {
|
|
487
|
+
/** Crea un RepositoryAdapter<T> ligado a la transacción activa para el modelo dado. */
|
|
488
|
+
for<T extends object = Record<string, unknown>>(modelName: string): RepositoryAdapter<T>
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export interface Transactor {
|
|
492
|
+
run<R>(fn: (repos: TransactionalRepos) => Promise<R>): Promise<R>
|
|
493
|
+
}
|
|
494
|
+
|
|
466
495
|
// ── ORM — SOLID: S = solo ORM, D = depende de DbAdapter ──
|
|
467
496
|
export class ORM {
|
|
468
497
|
private models = new Map<string, ModelDefinition>()
|
|
@@ -990,6 +1019,31 @@ export class OrmRepository<T extends object = Record<string, unknown>>
|
|
|
990
1019
|
}
|
|
991
1020
|
}
|
|
992
1021
|
|
|
1022
|
+
// ── OrmTransactor — implementación SQL del Transactor ──
|
|
1023
|
+
//
|
|
1024
|
+
// Composition root:
|
|
1025
|
+
// const transactor = new OrmTransactor(orm)
|
|
1026
|
+
// const service = new WalletsService(walletRepo, txRepo, transactor, logger, cache)
|
|
1027
|
+
//
|
|
1028
|
+
// El service recibe Transactor (interfaz). Para swapear a Prisma:
|
|
1029
|
+
// class PrismaTransactor implements Transactor { ... }
|
|
1030
|
+
// const transactor = new PrismaTransactor(prisma)
|
|
1031
|
+
// El service NO cambia.
|
|
1032
|
+
|
|
1033
|
+
export class OrmTransactor implements Transactor {
|
|
1034
|
+
constructor(private readonly orm: ORM) {}
|
|
1035
|
+
|
|
1036
|
+
run<R>(fn: (repos: TransactionalRepos) => Promise<R>): Promise<R> {
|
|
1037
|
+
return this.orm.transaction(async (txOrm) => {
|
|
1038
|
+
const repos: TransactionalRepos = {
|
|
1039
|
+
for: <T extends object = Record<string, unknown>>(modelName: string) =>
|
|
1040
|
+
new OrmRepository<T>(txOrm, modelName) as RepositoryAdapter<T>,
|
|
1041
|
+
}
|
|
1042
|
+
return fn(repos)
|
|
1043
|
+
})
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
993
1047
|
// ═══════════════════════════════════════════════════════════════
|
|
994
1048
|
// 6. ROUTER + HTTP — Enrutamiento con middlewares y error handler
|
|
995
1049
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1198,6 +1252,55 @@ export interface ServerAdapter {
|
|
|
1198
1252
|
stop(): Promise<void>
|
|
1199
1253
|
}
|
|
1200
1254
|
|
|
1255
|
+
// ── Envelope estándar de respuesta API ──────────────────────────
|
|
1256
|
+
// Todas las respuestas siguen este contrato. Un solo lugar para cambiar el formato.
|
|
1257
|
+
// Inspirado en JSend + convenciones de GitHub/Laravel:
|
|
1258
|
+
// success → { success: true, data: T, meta: { pagination? } | null, error: null }
|
|
1259
|
+
// error → { success: false, data: null, meta: null, error: { code, message, details } }
|
|
1260
|
+
export interface ApiResponse<T = unknown> {
|
|
1261
|
+
success: boolean
|
|
1262
|
+
data: T | null
|
|
1263
|
+
meta: { pagination?: unknown } | null
|
|
1264
|
+
error: { code: string; message: string; details?: unknown } | null
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function buildEnvelope(status: number, body: unknown): string {
|
|
1268
|
+
// Errores 4xx / 5xx
|
|
1269
|
+
if (status >= 400) {
|
|
1270
|
+
const b = (body ?? {}) as Record<string, unknown>
|
|
1271
|
+
return JSON.stringify({
|
|
1272
|
+
success: false,
|
|
1273
|
+
data: null,
|
|
1274
|
+
meta: null,
|
|
1275
|
+
error: {
|
|
1276
|
+
code: b.code ?? 'ERROR',
|
|
1277
|
+
message: b.error ?? 'Error',
|
|
1278
|
+
details: b.details ?? null,
|
|
1279
|
+
},
|
|
1280
|
+
} satisfies ApiResponse)
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Sin contenido (DELETE 204)
|
|
1284
|
+
if (body === null || body === undefined) {
|
|
1285
|
+
return JSON.stringify({ success: true, data: null, meta: null, error: null } satisfies ApiResponse)
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const b = body as Record<string, unknown>
|
|
1289
|
+
|
|
1290
|
+
// Lista paginada — el service retorna { data: [], pagination: {} }
|
|
1291
|
+
if (Array.isArray(b.data) && b.pagination !== undefined) {
|
|
1292
|
+
return JSON.stringify({
|
|
1293
|
+
success: true,
|
|
1294
|
+
data: b.data,
|
|
1295
|
+
meta: { pagination: b.pagination },
|
|
1296
|
+
error: null,
|
|
1297
|
+
} satisfies ApiResponse)
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// Recurso único o cualquier otro payload exitoso
|
|
1301
|
+
return JSON.stringify({ success: true, data: body, meta: null, error: null } satisfies ApiResponse)
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1201
1304
|
export class NodeServer implements ServerAdapter {
|
|
1202
1305
|
private server?: ReturnType<typeof createNodeServer>
|
|
1203
1306
|
private maxBodyBytes: number
|
|
@@ -1260,8 +1363,10 @@ export class NodeServer implements ServerAdapter {
|
|
|
1260
1363
|
|
|
1261
1364
|
// ── Respuesta normal (JSON o binario comprimido) ──
|
|
1262
1365
|
const isBuffer = Buffer.isBuffer(res.body)
|
|
1263
|
-
|
|
1264
|
-
|
|
1366
|
+
// 204 prohíbe body por spec HTTP — con envelope usamos 200 para "no content"
|
|
1367
|
+
const effectiveStatus = res.status === 204 ? 200 : res.status
|
|
1368
|
+
const responseBody = isBuffer ? res.body : buildEnvelope(effectiveStatus, res.body)
|
|
1369
|
+
nodeRes.writeHead(effectiveStatus, {
|
|
1265
1370
|
'Content-Type': 'application/json',
|
|
1266
1371
|
'X-Request-Id': req.id,
|
|
1267
1372
|
...res.headers,
|
|
@@ -1271,13 +1376,13 @@ export class NodeServer implements ServerAdapter {
|
|
|
1271
1376
|
const httpStatus = (error as any)?.httpStatus
|
|
1272
1377
|
if (httpStatus) {
|
|
1273
1378
|
nodeRes.writeHead(httpStatus, { 'Content-Type': 'application/json' })
|
|
1274
|
-
nodeRes.end(
|
|
1379
|
+
nodeRes.end(buildEnvelope(httpStatus, { error: (error as Error).message, code: 'REQUEST_ERROR' }))
|
|
1275
1380
|
return
|
|
1276
1381
|
}
|
|
1277
1382
|
const stack = error instanceof Error ? error.stack : String(error)
|
|
1278
1383
|
this.logger.error('Error no manejado en HTTP', { error: String(error), stack })
|
|
1279
1384
|
nodeRes.writeHead(500, { 'Content-Type': 'application/json' })
|
|
1280
|
-
nodeRes.end(
|
|
1385
|
+
nodeRes.end(buildEnvelope(500, { error: 'Error interno', code: 'INTERNAL_ERROR' }))
|
|
1281
1386
|
} finally {
|
|
1282
1387
|
this.activeRequests--
|
|
1283
1388
|
}
|
|
@@ -1903,7 +2008,7 @@ export default {
|
|
|
1903
2008
|
Logger, ConsoleTransport,
|
|
1904
2009
|
ConfigStore, loadEnv,
|
|
1905
2010
|
Container,
|
|
1906
|
-
ORM, OrmRepository,
|
|
2011
|
+
ORM, OrmRepository, OrmTransactor,
|
|
1907
2012
|
Router, NodeServer,
|
|
1908
2013
|
validateSchema,
|
|
1909
2014
|
Auth, MemoryCache,
|
|
@@ -1914,6 +2019,6 @@ export default {
|
|
|
1914
2019
|
// Seeds
|
|
1915
2020
|
SeedRunner,
|
|
1916
2021
|
|
|
1917
|
-
//
|
|
1918
|
-
//
|
|
2022
|
+
// Response envelope
|
|
2023
|
+
// ApiResponse interface is exported as named export above
|
|
1919
2024
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arckode-framework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "AI-first TypeScript/Bun framework. Modular, SOLID, zero magic. The AI reads the composition root and knows everything.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./kernel/framework.ts",
|
|
@@ -55,13 +55,12 @@
|
|
|
55
55
|
"analyze:framework": "bun run cli/index.ts analyze"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"arckode-framework": "^1.0.3",
|
|
59
58
|
"jsonwebtoken": "^9.0.0"
|
|
60
59
|
},
|
|
61
60
|
"peerDependencies": {
|
|
62
61
|
"better-sqlite3": ">=11.0.0",
|
|
63
62
|
"mysql2": ">=3.0.0",
|
|
64
|
-
"pg": "
|
|
63
|
+
"pg": "^8.0.0",
|
|
65
64
|
"redis": ">=4.0.0",
|
|
66
65
|
"nodemailer": ">=6.0.0"
|
|
67
66
|
},
|
|
@@ -88,6 +87,7 @@
|
|
|
88
87
|
"@types/nodemailer": "^6.4.0",
|
|
89
88
|
"@types/pg": "^8.11.0",
|
|
90
89
|
"bun-types": "^1.3.14",
|
|
90
|
+
"pg": "^8.0.0",
|
|
91
91
|
"typescript": "^5.6.0"
|
|
92
92
|
},
|
|
93
93
|
"keywords": [
|
package/skills/auth/SKILL.md
CHANGED
|
@@ -36,7 +36,7 @@ JWT_REFRESH_SECRET=otra-cadena-distinta-para-refresh-tokens
|
|
|
36
36
|
## 2. REGISTRO Y LOGIN (patrón completo)
|
|
37
37
|
|
|
38
38
|
```ts
|
|
39
|
-
// En el módulo de autenticación —
|
|
39
|
+
// En el módulo de autenticación — service.ts (al root del módulo)
|
|
40
40
|
export class AuthService {
|
|
41
41
|
constructor(
|
|
42
42
|
private repo: RepositoryAdapter<UserDTO>,
|
package/skills/cli/SKILL.md
CHANGED
|
@@ -28,20 +28,26 @@ arckode db:migrate --rollback # Revertir última migración
|
|
|
28
28
|
arckode make:module Producto
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Genera en `src/modules/producto
|
|
31
|
+
Genera en `src/modules/producto/` (nueva estructura):
|
|
32
32
|
```
|
|
33
|
-
index.ts ← ProductoModule()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
index.ts ← ProductoModule() — registra modelos, crea repos, wirea rutas
|
|
34
|
+
model.ts ← ProductoModel (ModelDefinition) + registerProductoModels(orm)
|
|
35
|
+
types.ts ← ProductoDTO + CreateProductoDTO + UpdateProductoDTO + queries
|
|
36
|
+
sockets.ts ← ProductosSockets interface
|
|
37
|
+
service.ts ← ProductosService con RepositoryAdapter<ProductoDTO> (al root)
|
|
38
|
+
controller.ts ← ProductosController con validateSchema en store/update (al root)
|
|
39
39
|
validators/
|
|
40
40
|
schema.ts ← crearProductoSchema + actualizarProductoSchema
|
|
41
41
|
tests/
|
|
42
42
|
service.test.ts ← 2 test cases: listar + crear con error
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
**Opcionales (no generados — agregar a mano cuando aplique):**
|
|
46
|
+
- `repository.ts` → queries con nombres del dominio (si el service repite filtros)
|
|
47
|
+
- `usecases/{caso}.ts` → solo si hay 3+ flujos DIFERENTES (no CRUD)
|
|
48
|
+
|
|
49
|
+
`service.ts` y `controller.ts` van al ROOT del módulo. `model.ts` está separado de `types.ts` por convención: schema DB vs contrato TypeScript.
|
|
50
|
+
|
|
45
51
|
**Importante:** El nombre se usa en PascalCase para clases, camelCase para variables, kebab-case para rutas:
|
|
46
52
|
- `arckode make:module LineaPedido` → clase `LineaPedidoService`, tabla `linea_pedidos`, ruta `/linea-pedidos`
|
|
47
53
|
|
|
@@ -102,11 +108,11 @@ Output ejemplo:
|
|
|
102
108
|
```
|
|
103
109
|
🔍 Analizando proyecto...
|
|
104
110
|
|
|
105
|
-
❌ DIRECT_MODULE_IMPORT modules/pedidos/
|
|
106
|
-
"import { ProductosService } from '../productos/
|
|
111
|
+
❌ DIRECT_MODULE_IMPORT modules/pedidos/service.ts:5
|
|
112
|
+
"import { ProductosService } from '../productos/service'"
|
|
107
113
|
→ Los módulos no pueden importar de otros módulos directamente.
|
|
108
114
|
|
|
109
|
-
❌ CONTROLLER_MISSING_VALIDATION modules/productos/
|
|
115
|
+
❌ CONTROLLER_MISSING_VALIDATION modules/productos/controller.ts:23
|
|
110
116
|
"router.post sin validateSchema()"
|
|
111
117
|
→ Todo POST debe validar el body antes de llamar al service.
|
|
112
118
|
|
|
@@ -126,8 +132,11 @@ Resumen: 2 errores, 1 advertencia
|
|
|
126
132
|
| Código | Severidad | Descripción |
|
|
127
133
|
|--------|-----------|-------------|
|
|
128
134
|
| `MISSING_INDEX` | ❌ | Módulo sin index.ts |
|
|
129
|
-
| `MISSING_SERVICE` | ❌ | Sin
|
|
130
|
-
| `MISSING_CONTROLLER` | ❌ | Sin
|
|
135
|
+
| `MISSING_SERVICE` | ❌ | Sin service.ts (al root del módulo) |
|
|
136
|
+
| `MISSING_CONTROLLER` | ❌ | Sin controller.ts (al root del módulo) |
|
|
137
|
+
| `LEGACY_ACTIONS_FOLDER` | ⚠️ | Módulo usa estructura legacy actions/ — mover al root |
|
|
138
|
+
| `DUPLICATE_CONNECTOR` | ❌ | Conectores con mismo nombre lógico (kebab + camelCase) |
|
|
139
|
+
| `UNREGISTERED_CONNECTOR` | ❌ | Archivo en connectors/ no importado en composition-root |
|
|
131
140
|
| `MISSING_TYPES` | ❌ | Sin types.ts |
|
|
132
141
|
| `MISSING_VALIDATORS` | ❌ | Sin validators/schema.ts |
|
|
133
142
|
| `MISSING_TESTS` | ❌ | Sin directorio tests/ |
|