arckode-framework 1.0.7 → 1.1.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/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 +72 -1
- 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>()
|
|
@@ -518,11 +547,28 @@ export class ORM {
|
|
|
518
547
|
|
|
519
548
|
private deserializeFromDb(def: ModelDefinition, row: ModelResult): ModelResult {
|
|
520
549
|
const result = { ...row } as Record<string, unknown>
|
|
550
|
+
|
|
551
|
+
// Deserializar campos JSON almacenados como string
|
|
521
552
|
for (const [k, field] of Object.entries(def.fields)) {
|
|
522
553
|
if (field.type === 'json' && typeof result[k] === 'string') {
|
|
523
554
|
try { result[k] = JSON.parse(result[k] as string) } catch { /* no es JSON válido */ }
|
|
524
555
|
}
|
|
525
556
|
}
|
|
557
|
+
|
|
558
|
+
// Normalizar timestamps que PostgreSQL devuelve en minúscula
|
|
559
|
+
// PostgreSQL baja el case de identificadores no-quoted: createdAt → createdat
|
|
560
|
+
const tsMap: Record<string, string> = {
|
|
561
|
+
createdat: 'createdAt',
|
|
562
|
+
updatedat: 'updatedAt',
|
|
563
|
+
deletedat: 'deletedAt',
|
|
564
|
+
}
|
|
565
|
+
for (const [lower, camel] of Object.entries(tsMap)) {
|
|
566
|
+
if (lower in result && !(camel in result)) {
|
|
567
|
+
result[camel] = result[lower]
|
|
568
|
+
delete result[lower]
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
526
572
|
return result as ModelResult
|
|
527
573
|
}
|
|
528
574
|
|
|
@@ -973,6 +1019,31 @@ export class OrmRepository<T extends object = Record<string, unknown>>
|
|
|
973
1019
|
}
|
|
974
1020
|
}
|
|
975
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
|
+
|
|
976
1047
|
// ═══════════════════════════════════════════════════════════════
|
|
977
1048
|
// 6. ROUTER + HTTP — Enrutamiento con middlewares y error handler
|
|
978
1049
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1886,7 +1957,7 @@ export default {
|
|
|
1886
1957
|
Logger, ConsoleTransport,
|
|
1887
1958
|
ConfigStore, loadEnv,
|
|
1888
1959
|
Container,
|
|
1889
|
-
ORM, OrmRepository,
|
|
1960
|
+
ORM, OrmRepository, OrmTransactor,
|
|
1890
1961
|
Router, NodeServer,
|
|
1891
1962
|
validateSchema,
|
|
1892
1963
|
Auth, MemoryCache,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arckode-framework",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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/ |
|
|
@@ -60,8 +60,8 @@ export interface PedidosSockets {
|
|
|
60
60
|
onPedidoCancelado?: (pedido: PedidoDTO) => Promise<void>
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
// En
|
|
64
|
-
import type { PedidosSockets } from '
|
|
63
|
+
// En service.ts (al root del módulo)
|
|
64
|
+
import type { PedidosSockets } from './sockets'
|
|
65
65
|
|
|
66
66
|
export class PedidosService {
|
|
67
67
|
private sockets: PedidosSockets = {}
|
|
@@ -184,8 +184,8 @@ events.on('pedido.confirmado', async (event) => {
|
|
|
184
184
|
## 8. ANTI-PATRONES DE CONECTORES
|
|
185
185
|
|
|
186
186
|
```ts
|
|
187
|
-
// ❌ Importar desde
|
|
188
|
-
import { PedidosService } from '../modules/pedidos/
|
|
187
|
+
// ❌ Importar desde ./service directamente (viola REGLA #1)
|
|
188
|
+
import { PedidosService } from '../modules/pedidos/service' // NO
|
|
189
189
|
|
|
190
190
|
// ✅ Solo desde index.ts (puerta pública)
|
|
191
191
|
import type { PedidosService } from '../modules/pedidos'
|
package/skills/helpers/SKILL.md
CHANGED
|
@@ -63,7 +63,7 @@ class PedidosService {
|
|
|
63
63
|
| Tipo | Dónde va | Ejemplo |
|
|
64
64
|
|------|---------|---------|
|
|
65
65
|
| Función pura de transformación | `shared/helpers/` | `formatearFecha`, `slugify`, `calcularIVA` |
|
|
66
|
-
| Lógica de negocio | `modules/{modulo}/
|
|
66
|
+
| Lógica de negocio | `modules/{modulo}/service.ts` | `calcularDescuento(pedido)` |
|
|
67
67
|
| Validación de dominio | `modules/{modulo}/validators/schema.ts` | Schema de validación |
|
|
68
68
|
| Constante compartida | `shared/constants.ts` | `IVA_RATE = 0.18` |
|
|
69
69
|
| Tipo/interfaz compartida | `shared/types.ts` | `Paginado<T>`, `ApiResponse<T>` |
|