arckode-framework 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +546 -0
- package/adapters/__tests__/mysql.test.ts +283 -0
- package/adapters/jwt.ts +18 -0
- package/adapters/mysql.ts +98 -0
- package/adapters/postgres.ts +52 -0
- package/adapters/redis-cache.ts +64 -0
- package/adapters/sqlite.ts +73 -0
- package/adapters/vendor.d.ts +48 -0
- package/bin/arckode.js +7 -0
- package/cli/analyze.ts +506 -0
- package/cli/commands/db-migrate.ts +121 -0
- package/cli/commands/db-seed.ts +54 -0
- package/cli/commands/generate-api-client.ts +106 -0
- package/cli/commands/make-adapter.ts +132 -0
- package/cli/commands/make-auth.ts +297 -0
- package/cli/commands/make-frontend-module.ts +271 -0
- package/cli/commands/make-helper.ts +65 -0
- package/cli/commands/make-migration.ts +30 -0
- package/cli/commands/make-seed.ts +29 -0
- package/cli/generate.ts +132 -0
- package/cli/index.ts +604 -0
- package/cli/stubs/frontend-stub.ts +294 -0
- package/cli/stubs/fullstack-stub.ts +46 -0
- package/cli/stubs/module-stub.ts +469 -0
- package/kernel/__tests__/adapters.test.ts +101 -0
- package/kernel/__tests__/analyzer.test.ts +282 -0
- package/kernel/__tests__/framework.test.ts +617 -0
- package/kernel/__tests__/middlewares.test.ts +174 -0
- package/kernel/__tests__/static.test.ts +94 -0
- package/kernel/framework.ts +1851 -0
- package/kernel/middlewares.ts +179 -0
- package/kernel/static.ts +76 -0
- package/kernel/testing.ts +237 -0
- package/modules/events/index.ts +99 -0
- package/modules/mail/index.ts +51 -0
- package/modules/mail/smtp-adapter.ts +42 -0
- package/modules/queue/index.ts +78 -0
- package/modules/storage/index.ts +40 -0
- package/modules/storage/local-adapter.ts +41 -0
- package/modules/ws/__tests__/ws.test.ts +114 -0
- package/modules/ws/index.ts +136 -0
- package/package.json +99 -0
- package/skills/auth/SKILL.md +243 -0
- package/skills/cli/SKILL.md +258 -0
- package/skills/config/SKILL.md +253 -0
- package/skills/connectors/SKILL.md +259 -0
- package/skills/helpers/SKILL.md +206 -0
- package/skills/middlewares/SKILL.md +282 -0
- package/skills/orm/SKILL.md +260 -0
- package/skills/realtime/SKILL.md +307 -0
- package/skills/services/SKILL.md +206 -0
- package/skills/testing/SKILL.md +257 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# SKILL: Arckode Auth — JWT, Roles, Passwords y IDOR
|
|
2
|
+
|
|
3
|
+
> Activar cuando: implementar login, registro, proteger rutas, verificar permisos, manejar passwords.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. SETUP EN COMPOSITION-ROOT
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Auth } from 'arckode-framework'
|
|
11
|
+
import { jwtTokenAdapter } from 'arckode-framework/adapters/jwt'
|
|
12
|
+
|
|
13
|
+
const auth = new Auth(
|
|
14
|
+
jwtTokenAdapter,
|
|
15
|
+
config.get('JWT_SECRET'), // REQUERIDO — string largo y aleatorio
|
|
16
|
+
logger,
|
|
17
|
+
{
|
|
18
|
+
accessTokenExpiry: '15m', // default: '15m'
|
|
19
|
+
refreshTokenExpiry: '7d', // default: '7d'
|
|
20
|
+
adminRole: 'admin', // default: 'admin'
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
// Pasar al System y a los módulos que lo necesiten
|
|
25
|
+
const system = new System({ config, logger, orm, router, http, cache, auth })
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Config requerida en .env:**
|
|
29
|
+
```env
|
|
30
|
+
JWT_SECRET=una-cadena-larga-y-aleatoria-de-al-menos-32-chars
|
|
31
|
+
JWT_REFRESH_SECRET=otra-cadena-distinta-para-refresh-tokens
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 2. REGISTRO Y LOGIN (patrón completo)
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// En el módulo de autenticación — actions/service.ts
|
|
40
|
+
export class AuthService {
|
|
41
|
+
constructor(
|
|
42
|
+
private repo: RepositoryAdapter<UserDTO>,
|
|
43
|
+
private auth: Auth,
|
|
44
|
+
private logger: Logger,
|
|
45
|
+
) {}
|
|
46
|
+
|
|
47
|
+
async registrar(dto: RegisterDTO): Promise<{ accessToken: string; refreshToken: string }> {
|
|
48
|
+
const existente = await this.repo.findOne({ email: dto.email })
|
|
49
|
+
if (existente) throw new ConflictError('El email ya está registrado')
|
|
50
|
+
|
|
51
|
+
const hashedPassword = await this.auth.hashPassword(dto.password)
|
|
52
|
+
const user = await this.repo.create({
|
|
53
|
+
email: dto.email,
|
|
54
|
+
password: hashedPassword,
|
|
55
|
+
role: 'user',
|
|
56
|
+
} as Omit<UserDTO, 'id'>)
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
accessToken: this.auth.createToken({ id: user.id, role: user.role }),
|
|
60
|
+
refreshToken: this.auth.createRefreshToken({ id: user.id, role: user.role }),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async login(dto: LoginDTO): Promise<{ accessToken: string; refreshToken: string }> {
|
|
65
|
+
const user = await this.repo.findOne({ email: dto.email })
|
|
66
|
+
if (!user) throw new AuthError('Credenciales inválidas')
|
|
67
|
+
|
|
68
|
+
const ok = await this.auth.comparePassword(dto.password, user.password as string)
|
|
69
|
+
if (!ok) throw new AuthError('Credenciales inválidas')
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
accessToken: this.auth.createToken({ id: user.id, role: user.role }),
|
|
73
|
+
refreshToken: this.auth.createRefreshToken({ id: user.id, role: user.role }),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async refresh(refreshToken: string): Promise<{ accessToken: string; refreshToken: string }> {
|
|
78
|
+
return this.auth.refresh(refreshToken)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Crítico:** Siempre usar el mismo mensaje de error para usuario no encontrado y contraseña incorrecta — previene user enumeration.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 3. PROTEGER RUTAS
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// En index.ts del módulo — al registrar rutas:
|
|
91
|
+
|
|
92
|
+
// Requiere cualquier usuario autenticado
|
|
93
|
+
router.get('/perfil', [auth.authenticate()], req => controller.perfil(req))
|
|
94
|
+
|
|
95
|
+
// Requiere rol específico
|
|
96
|
+
router.post('/admin/config', [auth.authenticate('admin')], req => controller.setConfig(req))
|
|
97
|
+
|
|
98
|
+
// Múltiples roles permitidos
|
|
99
|
+
router.get('/reportes', [auth.authenticate('admin', 'supervisor')], req => controller.reportes(req))
|
|
100
|
+
|
|
101
|
+
// Ruta pública (sin middleware)
|
|
102
|
+
router.post('/auth/login', req => controller.login(req))
|
|
103
|
+
router.post('/auth/registro', req => controller.registrar(req))
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**El middleware agrega `req.user` con `{ id: string, role: string }` para handlers autenticados.**
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 4. ACCESO AL USUARIO ACTUAL
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// En el controller — req.user está disponible en rutas protegidas
|
|
114
|
+
async perfil(req: HttpRequest): Promise<HttpResponse> {
|
|
115
|
+
const user = req.user! // { id: string, role: string }
|
|
116
|
+
const data = await this.service.getPerfil(user.id, user)
|
|
117
|
+
return { status: 200, body: data }
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 5. PREVENCIÓN DE IDOR (obligatorio)
|
|
124
|
+
|
|
125
|
+
Todo `findById` que devuelve datos de un usuario específico **DEBE** verificar ownership:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// ❌ PROHIBIDO — cualquier usuario autenticado puede ver datos de otro
|
|
129
|
+
async getMiPedido(id: string): Promise<PedidoDTO> {
|
|
130
|
+
const pedido = await this.repo.findById(id)
|
|
131
|
+
if (!pedido) throw new NotFoundError('Pedido no encontrado')
|
|
132
|
+
return pedido // IDOR: usuario B puede ver pedidos de usuario A con ID correcto
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ✅ OBLIGATORIO — verificar ownership
|
|
136
|
+
async getMiPedido(id: string, currentUser: { id: string; role: string }): Promise<PedidoDTO> {
|
|
137
|
+
const pedido = await this.repo.findById(id)
|
|
138
|
+
if (!pedido) throw new NotFoundError('Pedido no encontrado')
|
|
139
|
+
|
|
140
|
+
// Lanza ForbiddenError si currentUser.id !== pedido.usuarioId
|
|
141
|
+
// EXCEPTO si currentUser.role === adminRole (admin bypassa el check)
|
|
142
|
+
this.auth.assertOwnership(pedido.usuarioId as string, currentUser.id, currentUser.role)
|
|
143
|
+
|
|
144
|
+
return pedido
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**`arckode analyze` detecta:** `IDOR_RISK` — findById sin assertOwnership.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 6. ROLES Y AUTORIZACIÓN
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// Verificar rol manualmente en service (cuando la lógica es más compleja)
|
|
156
|
+
async cancelarPedido(id: string, currentUser: { id: string; role: string }): Promise<void> {
|
|
157
|
+
const pedido = await this.repo.findById(id)
|
|
158
|
+
if (!pedido) throw new NotFoundError('Pedido no encontrado')
|
|
159
|
+
|
|
160
|
+
// Solo el dueño o admin puede cancelar
|
|
161
|
+
this.auth.assertOwnership(pedido.usuarioId as string, currentUser.id, currentUser.role)
|
|
162
|
+
|
|
163
|
+
// Solo se puede cancelar si está pendiente (lógica de negocio)
|
|
164
|
+
if (pedido.estado !== 'pendiente') {
|
|
165
|
+
throw new ConflictError('Solo se pueden cancelar pedidos pendientes')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await this.repo.update(id, { estado: 'cancelado' })
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 7. HASHING DE PASSWORDS
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
// Hashear al registrar o cambiar contraseña
|
|
178
|
+
const hash = await auth.hashPassword(plainPassword)
|
|
179
|
+
|
|
180
|
+
// Verificar al hacer login
|
|
181
|
+
const matches = await auth.comparePassword(plainPassword, storedHash)
|
|
182
|
+
|
|
183
|
+
// Usar timing-safe comparison internamente (previene timing attacks)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Algoritmo:** scrypt + salt aleatorio. No implementar hashing propio.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 8. REFRESH DE TOKEN
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
// En el controller de auth
|
|
194
|
+
async refreshToken(req: HttpRequest): Promise<HttpResponse> {
|
|
195
|
+
const { refreshToken } = req.body as { refreshToken: string }
|
|
196
|
+
if (!refreshToken) throw new ValidationError('refreshToken requerido')
|
|
197
|
+
|
|
198
|
+
const tokens = await this.service.refresh(refreshToken)
|
|
199
|
+
return { status: 200, body: tokens }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// En el service (delegar a auth)
|
|
203
|
+
async refresh(refreshToken: string) {
|
|
204
|
+
return this.auth.refresh(refreshToken) // lanza AuthError si expiró o es inválido
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Importante:** El refresh token tiene `type: 'refresh'` en el payload. `auth.verifyToken()` (para access tokens) rechaza tokens con `type: 'refresh'` — son tokens distintos por diseño.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## 9. NUNCA EXPONER AL CLIENTE
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// ❌ PROHIBIDO en responses HTTP
|
|
216
|
+
{
|
|
217
|
+
password: user.password, // hash de contraseña
|
|
218
|
+
jwtSecret: 'el-secreto', // secreto JWT
|
|
219
|
+
internalId: 'uuid-interno', // IDs internos innecesarios
|
|
220
|
+
stackTrace: error.stack, // stack trace de error
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ✅ Proyectar solo lo necesario en el DTO de respuesta
|
|
224
|
+
export interface UserPublicDTO {
|
|
225
|
+
id: string
|
|
226
|
+
email: string
|
|
227
|
+
role: string
|
|
228
|
+
createdAt: string
|
|
229
|
+
// SIN password, SIN tokens internos
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 10. CHECKLIST AUTH
|
|
236
|
+
|
|
237
|
+
- [ ] `JWT_SECRET` en .env, no hardcodeado
|
|
238
|
+
- [ ] Login usa mismo mensaje para "usuario no existe" y "contraseña incorrecta"
|
|
239
|
+
- [ ] Passwords hasheados con `auth.hashPassword()` al guardar
|
|
240
|
+
- [ ] `auth.authenticate()` en array de middlewares: `[auth.authenticate('admin')]`
|
|
241
|
+
- [ ] Todo `findById` de recurso de usuario tiene `assertOwnership()`
|
|
242
|
+
- [ ] El DTO de respuesta nunca incluye password, tokens ni stack traces
|
|
243
|
+
- [ ] Refresh token endpoint separado del login
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# SKILL: Arckode CLI — Generadores y Comandos
|
|
2
|
+
|
|
3
|
+
> Activar cuando: crear módulo nuevo, generar código, correr migraciones, analizar arquitectura, listar rutas.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. COMANDOS DISPONIBLES
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
arckode new <nombre> # Crear proyecto nuevo
|
|
11
|
+
arckode make:module <Nombre> # Generar módulo completo (7 archivos)
|
|
12
|
+
arckode make:connector <nombre> <m1> <m2> # Generar conector entre módulos
|
|
13
|
+
arckode make:migration <descripcion> # Crear archivo de migración SQL
|
|
14
|
+
arckode make:seed <nombre> # Crear archivo de seed
|
|
15
|
+
arckode make:auth # Generar módulo de autenticación completo
|
|
16
|
+
arckode analyze # Detectar violaciones de arquitectura
|
|
17
|
+
arckode analyze --strict # Idem, falla con exit 1 si hay violaciones
|
|
18
|
+
arckode routes # Listar todas las rutas del proyecto
|
|
19
|
+
arckode db:migrate # Correr migraciones pendientes
|
|
20
|
+
arckode db:migrate --rollback # Revertir última migración
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 2. `arckode make:module` — Lo que genera
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
arckode make:module Producto
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Genera en `src/modules/producto/`:
|
|
32
|
+
```
|
|
33
|
+
index.ts ← ProductoModule() con createModule + OrmRepository + rutas
|
|
34
|
+
types.ts ← ProductoModel (ModelDefinition) + ProductoDTO + CreateProductoDTO
|
|
35
|
+
sockets.ts ← ProductosSockets interface + SocketsAware
|
|
36
|
+
actions/
|
|
37
|
+
service.ts ← ProductosService con RepositoryAdapter<ProductoDTO>
|
|
38
|
+
controller.ts ← ProductosController con validateSchema en store/update
|
|
39
|
+
validators/
|
|
40
|
+
schema.ts ← crearProductoSchema + actualizarProductoSchema
|
|
41
|
+
tests/
|
|
42
|
+
service.test.ts ← 2 test cases: listar + crear con error
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Importante:** El nombre se usa en PascalCase para clases, camelCase para variables, kebab-case para rutas:
|
|
46
|
+
- `arckode make:module LineaPedido` → clase `LineaPedidoService`, tabla `linea_pedidos`, ruta `/linea-pedidos`
|
|
47
|
+
|
|
48
|
+
**Después de generar:**
|
|
49
|
+
1. Revisar `types.ts` → ajustar campos al dominio real
|
|
50
|
+
2. Revisar `validators/schema.ts` → ajustar validaciones
|
|
51
|
+
3. Agregar a `composition-root.ts`:
|
|
52
|
+
```ts
|
|
53
|
+
import { ProductoModel } from './modules/producto/types'
|
|
54
|
+
import { ProductoModule } from './modules/producto'
|
|
55
|
+
|
|
56
|
+
orm.define('Producto', ProductoModel)
|
|
57
|
+
system.addModule(ProductoModule())
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 3. `arckode make:connector` — Lo que genera
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
arckode make:connector pedido-inventario pedidos inventario
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Genera `src/connectors/pedido-inventario.ts`:
|
|
69
|
+
```ts
|
|
70
|
+
import type { ConnectorContext } from 'arckode-framework'
|
|
71
|
+
import type { PedidosService } from '../modules/pedidos'
|
|
72
|
+
import type { InventarioService } from '../modules/inventario'
|
|
73
|
+
|
|
74
|
+
export function conectarPedidoConInventario(ctx: ConnectorContext): void {
|
|
75
|
+
const pedidos = ctx.resolveModule<PedidosService>('pedidos')
|
|
76
|
+
const inventario = ctx.resolveModule<InventarioService>('inventario')
|
|
77
|
+
|
|
78
|
+
// TODO: inyectar sockets
|
|
79
|
+
pedidos.setSockets({
|
|
80
|
+
// onEventoEjemplo: async (data) => { await inventario.accion(data) }
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Después de generar:**
|
|
86
|
+
1. Implementar los sockets necesarios
|
|
87
|
+
2. Agregar a `composition-root.ts`:
|
|
88
|
+
```ts
|
|
89
|
+
import { conectarPedidoConInventario } from './connectors/pedido-inventario'
|
|
90
|
+
system.addConnector('pedido-inventario', conectarPedidoConInventario)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 4. `arckode analyze` — Violaciones detectadas
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
arckode analyze
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Output ejemplo:
|
|
102
|
+
```
|
|
103
|
+
🔍 Analizando proyecto...
|
|
104
|
+
|
|
105
|
+
❌ DIRECT_MODULE_IMPORT modules/pedidos/actions/service.ts:5
|
|
106
|
+
"import { ProductosService } from '../productos/actions/service'"
|
|
107
|
+
→ Los módulos no pueden importar de otros módulos directamente.
|
|
108
|
+
|
|
109
|
+
❌ CONTROLLER_MISSING_VALIDATION modules/productos/actions/controller.ts:23
|
|
110
|
+
"router.post sin validateSchema()"
|
|
111
|
+
→ Todo POST debe validar el body antes de llamar al service.
|
|
112
|
+
|
|
113
|
+
⚠️ MISSING_SOCKETS modules/usuarios/
|
|
114
|
+
→ El módulo no tiene sockets.ts
|
|
115
|
+
|
|
116
|
+
✅ Sin violaciones en: pedidos, inventario, auth
|
|
117
|
+
|
|
118
|
+
Resumen: 2 errores, 1 advertencia
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Violaciones críticas (❌):** Deben corregirse antes del merge.
|
|
122
|
+
**Advertencias (⚠️):** Recomendadas pero no bloqueantes.
|
|
123
|
+
|
|
124
|
+
### Tabla completa de checks
|
|
125
|
+
|
|
126
|
+
| Código | Severidad | Descripción |
|
|
127
|
+
|--------|-----------|-------------|
|
|
128
|
+
| `MISSING_INDEX` | ❌ | Módulo sin index.ts |
|
|
129
|
+
| `MISSING_SERVICE` | ❌ | Sin actions/service.ts |
|
|
130
|
+
| `MISSING_CONTROLLER` | ❌ | Sin actions/controller.ts |
|
|
131
|
+
| `MISSING_TYPES` | ❌ | Sin types.ts |
|
|
132
|
+
| `MISSING_VALIDATORS` | ❌ | Sin validators/schema.ts |
|
|
133
|
+
| `MISSING_TESTS` | ❌ | Sin directorio tests/ |
|
|
134
|
+
| `MISSING_SOCKETS` | ⚠️ | Sin sockets.ts |
|
|
135
|
+
| `MISSING_CONNECTORS` | ⚠️ | 2+ módulos sin conectores definidos |
|
|
136
|
+
| `DIRECT_MODULE_IMPORT` | ❌ | Un módulo importa de otro |
|
|
137
|
+
| `CONNECTOR_BUSINESS_LOGIC` | ❌ | Conector con if/for/lógica |
|
|
138
|
+
| `CONTROLLER_MISSING_VALIDATION` | ❌ | POST/PUT/PATCH sin validateSchema |
|
|
139
|
+
| `BUSINESS_LOGIC_IN_CONTROLLER` | ❌ | ORM directo en controller |
|
|
140
|
+
| `EMPTY_MODULE_DESCRIPTION` | ❌ | description: '' en createModule |
|
|
141
|
+
| `TESTS_WITHOUT_CASES` | ❌ | Archivo test sin test() |
|
|
142
|
+
| `SERVICE_IMPORTS_OTHER_MODULE` | ❌ | Service importa de otro módulo |
|
|
143
|
+
| `GOD_SERVICE` | ⚠️ | Service > 200 líneas |
|
|
144
|
+
| `N_PLUS_ONE_RISK` | ⚠️ | ORM dentro de loop |
|
|
145
|
+
| `IDOR_RISK` | ❌ | findById sin assertOwnership |
|
|
146
|
+
| `SERVICE_DEPENDS_ON_ORM` | ❌ | Service recibe ORM, no RepositoryAdapter |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 5. `arckode routes` — Ver rutas registradas
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
arckode routes
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Output:
|
|
157
|
+
```
|
|
158
|
+
GET /productos
|
|
159
|
+
POST /productos [validate: crearProductoSchema]
|
|
160
|
+
GET /productos/:id
|
|
161
|
+
PUT /productos/:id [auth: admin] [validate: actualizarProductoSchema]
|
|
162
|
+
DELETE /productos/:id [auth: admin]
|
|
163
|
+
POST /auth/login
|
|
164
|
+
POST /auth/registro
|
|
165
|
+
POST /auth/refresh
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 6. `arckode db:migrate` — Migraciones SQL
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# Ver pendientes y correrlos
|
|
174
|
+
arckode db:migrate
|
|
175
|
+
|
|
176
|
+
# Revertir último
|
|
177
|
+
arckode db:migrate --rollback
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Tabla de control:** `_arckode_migrations` (columnas: name, runAt, direction)
|
|
181
|
+
|
|
182
|
+
**Los archivos de migración van en `src/migrations/`:**
|
|
183
|
+
```
|
|
184
|
+
src/migrations/
|
|
185
|
+
1716000000_create_productos.ts
|
|
186
|
+
1716100000_add_stock_to_productos.ts
|
|
187
|
+
1716200000_add_index_pedidos_usuario.ts
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
El timestamp en el nombre determina el orden de ejecución.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 7. `arckode new` — Proyecto desde cero
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
arckode new mi-api
|
|
198
|
+
cd mi-api
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Genera:
|
|
202
|
+
```
|
|
203
|
+
mi-api/
|
|
204
|
+
├── src/
|
|
205
|
+
│ ├── composition-root.ts ← con loadEnv(), ORM, Router, System
|
|
206
|
+
│ └── modules/
|
|
207
|
+
│ └── .gitkeep
|
|
208
|
+
├── .env.example
|
|
209
|
+
├── .gitignore
|
|
210
|
+
├── CLAUDE.md ← apunta a este skill
|
|
211
|
+
├── package.json ← arckode-framework como dependencia
|
|
212
|
+
└── tsconfig.json ← strict mode
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 8. INTEGRAR CON CI/CD
|
|
218
|
+
|
|
219
|
+
```yaml
|
|
220
|
+
# .github/workflows/ci.yml
|
|
221
|
+
- name: Analyze architecture
|
|
222
|
+
run: arckode analyze --strict # falla el CI si hay violaciones
|
|
223
|
+
|
|
224
|
+
- name: Run tests
|
|
225
|
+
run: bun test
|
|
226
|
+
|
|
227
|
+
- name: Type check
|
|
228
|
+
run: bun run --bun tsc --noEmit
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 9. WORKFLOW COMPLETO — Agregar feature
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
# 1. Generar el módulo
|
|
237
|
+
arckode make:module Pago
|
|
238
|
+
|
|
239
|
+
# 2. Ajustar types.ts con los campos reales
|
|
240
|
+
|
|
241
|
+
# 3. Ajustar validators/schema.ts
|
|
242
|
+
|
|
243
|
+
# 4. Implementar service.ts (lógica de negocio)
|
|
244
|
+
|
|
245
|
+
# 5. Si se conecta con otro módulo
|
|
246
|
+
arckode make:connector pago-pedido pagos pedidos
|
|
247
|
+
|
|
248
|
+
# 6. Agregar a composition-root.ts
|
|
249
|
+
|
|
250
|
+
# 7. Si necesita migración SQL
|
|
251
|
+
arckode make:migration add_pagos_table
|
|
252
|
+
|
|
253
|
+
# 8. Verificar todo
|
|
254
|
+
bun run src/composition-root.ts # sin errores TypeScript
|
|
255
|
+
arckode analyze # 0 violaciones críticas
|
|
256
|
+
bun test modules/pago # tests pasan
|
|
257
|
+
arckode routes # rutas visibles
|
|
258
|
+
```
|