arckode-framework 1.3.2 → 1.4.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.
Files changed (64) hide show
  1. package/adapters/jwt.ts +6 -4
  2. package/adapters/mysql.ts +7 -2
  3. package/adapters/postgres.ts +37 -0
  4. package/adapters/sqlite.ts +7 -1
  5. package/adapters/vendor.d.ts +48 -0
  6. package/cli/analyze/checks.ts +333 -0
  7. package/cli/analyze/index.ts +44 -0
  8. package/cli/analyze/report.ts +107 -0
  9. package/cli/analyze/types.ts +46 -0
  10. package/cli/analyze/utils.ts +36 -0
  11. package/cli/analyze.ts +2 -647
  12. package/cli/commands/db-migrate.ts +213 -89
  13. package/cli/commands/db-seed.ts +97 -32
  14. package/cli/commands/db-utils.ts +192 -0
  15. package/cli/commands/new.ts +175 -0
  16. package/cli/commands/routes.ts +94 -0
  17. package/cli/index.ts +57 -404
  18. package/cli/stubs/module/core.ts +162 -0
  19. package/cli/stubs/module/data.ts +171 -0
  20. package/cli/stubs/module/index.ts +5 -0
  21. package/cli/stubs/module/service.ts +198 -0
  22. package/cli/stubs/module/types.ts +12 -0
  23. package/cli/stubs/module-stub.ts +2 -552
  24. package/kernel/auth.ts +114 -0
  25. package/kernel/cache.ts +37 -0
  26. package/kernel/config.ts +129 -0
  27. package/kernel/container.ts +64 -0
  28. package/kernel/db/orm-migrate.ts +136 -0
  29. package/kernel/db/orm-repository.ts +45 -0
  30. package/kernel/db/orm-utils.ts +93 -0
  31. package/kernel/db/orm.ts +254 -0
  32. package/kernel/db/transactor.ts +17 -0
  33. package/kernel/db/types.ts +72 -0
  34. package/kernel/errors.ts +102 -0
  35. package/kernel/framework.default.ts +41 -0
  36. package/kernel/framework.ts +8 -2144
  37. package/kernel/http/router.ts +131 -0
  38. package/kernel/http/server.ts +303 -0
  39. package/kernel/http/types.ts +56 -0
  40. package/kernel/index.ts +25 -0
  41. package/kernel/logger.ts +50 -0
  42. package/kernel/middlewares.ts +19 -7
  43. package/kernel/modules/create-module.ts +5 -0
  44. package/kernel/modules/system.ts +149 -0
  45. package/kernel/modules/types.ts +46 -0
  46. package/kernel/seeds.ts +48 -0
  47. package/kernel/static.ts +11 -2
  48. package/kernel/testing.ts +8 -3
  49. package/kernel/validator.ts +116 -0
  50. package/modules/events/index.ts +19 -3
  51. package/modules/mail/index.ts +14 -2
  52. package/modules/storage/local-adapter.ts +19 -5
  53. package/modules/ws/index.ts +123 -18
  54. package/package.json +8 -11
  55. package/skills/auth/SKILL.md +36 -220
  56. package/skills/cli/SKILL.md +32 -251
  57. package/skills/config/SKILL.md +30 -239
  58. package/skills/connectors/SKILL.md +32 -295
  59. package/skills/helpers/SKILL.md +26 -195
  60. package/skills/middlewares/SKILL.md +30 -280
  61. package/skills/orm/SKILL.md +42 -349
  62. package/skills/realtime/SKILL.md +22 -297
  63. package/skills/services/SKILL.md +40 -183
  64. package/skills/testing/SKILL.md +34 -266
@@ -1,396 +1,89 @@
1
- # SKILL: Arckode ORM — Modelos, Queries y Migraciones
1
+ # ORM — Modelos, Queries, Migraciones
2
2
 
3
- > Activar cuando: definir un modelo, escribir queries, crear migración, agregar campo, cambiar schema.
4
-
5
- ---
6
-
7
- ## 0. NAMING FIJO (no negociable)
8
-
9
- ```
10
- Métodos de service: list() | getById() | create() | update() | delete()
11
- Controller actions: index | show | store | update | destroy
12
- Tablas DB: snake_case plural → pedidos, lineas_pedido
13
- Campos del modelo: camelCase → usuarioId, fechaEntrega
14
- ```
15
-
16
- ---
17
-
18
- ## 1. DEFINIR UN MODELO — vive en `model.ts` (no en types.ts)
3
+ ## Modelo (`model.ts`, no `types.ts`)
19
4
 
20
5
  ```ts
21
- // modules/pedidos/model.ts
22
- import type { ModelDefinition, ORM } from 'arckode-framework'
23
-
24
6
  export const PedidoModel: ModelDefinition = {
25
- table: 'pedidos',
7
+ table: 'pedidos', // snake_case plural
26
8
  fields: {
27
- // Tipos: 'string' | 'number' | 'boolean' | 'date' | 'text' | 'json'
28
- usuarioId: { type: 'string', required: true },
29
- productoId: { type: 'string', required: true },
30
- cantidad: { type: 'number', required: true },
31
- total: { type: 'number', required: true },
32
- estado: { type: 'string', default: 'pendiente' },
33
- notas: { type: 'text', required: false },
34
- metadata: { type: 'json', required: false },
9
+ usuarioId: { type: 'string', required: true },
10
+ total: { type: 'number', required: true },
11
+ estado: { type: 'string', default: 'pendiente' },
12
+ metadata: { type: 'json' },
35
13
  },
36
- timestamps: true, // agrega createdAt, updatedAt (RECOMENDADO)
37
- softDelete: false, // true → agrega deletedAt, delete() solo marca, no borra
14
+ timestamps: true, // createdAt, updatedAt
15
+ softDelete: false, // deletedAt
38
16
  }
39
-
40
- // Helper para registrar TODOS los modelos del módulo desde index.ts
41
- // Si tu módulo tiene 3+ tablas (ej: wallets tiene Wallet, Transaction, Ledger...),
42
- // definí todos los modelos arriba y registralos todos acá.
43
17
  export function registerPedidosModels(orm: ORM): void {
44
18
  orm.define('Pedido', PedidoModel)
45
19
  }
46
20
  ```
47
21
 
48
- **Reglas de naming:**
49
- - Tabla: snake_case plural (`pedidos`, `lineas_pedido`)
50
- - Campos: camelCase (`usuarioId`, `fechaEntrega`)
51
- - Solo alfanuméricos y guión bajo — `assertSafeIdentifier()` valida esto en ORM
52
- - No usar palabras reservadas SQL: `order`, `table`, `select`, `where`
53
-
54
- **Por qué `model.ts` separado de `types.ts`:**
55
- - `model.ts` describe la persistencia (schema DB) — cambia con migraciones
56
- - `types.ts` describe el contrato TS (DTOs, queries) — cambia con la API
57
- - Conceptos distintos → archivos distintos
58
-
59
- ---
60
-
61
- ## 2. REGISTRAR EL MODELO — desde `index.ts` del módulo (NO composition-root)
62
-
63
- El módulo es dueño de su modelo. Lo registra en su `create()`, no afuera.
22
+ ## Registrar (en `index.ts` del módulo, NO composition-root)
64
23
 
65
24
  ```ts
66
- // modules/pedidos/index.ts
67
- import { registerPedidosModels } from './model'
68
-
69
- export function PedidosModule() {
70
- return createModule({
71
- create({ orm, ... }) {
72
- registerPedidosModels(orm) // ← acá adentro
73
- const repo = new OrmRepository<PedidoDTO>(orm, 'Pedido')
74
- // ...
75
- }
76
- })
25
+ create({ orm }) {
26
+ registerPedidosModels(orm)
27
+ const repo = new OrmRepository<PedidoDTO>(orm, 'Pedido')
77
28
  }
78
29
  ```
79
30
 
80
- **Composition-root solo coordina** no toca `orm.define()` por módulo:
31
+ ## RepositoryAdapter<T> (interface genérica CRUD)
81
32
 
82
33
  ```ts
83
- // composition-root.ts
84
- system.addModule(PedidosModule()) // el módulo se autorregistra
85
- system.init()
86
- await orm.migrate() // CREATE TABLE IF NOT EXISTS + drift detection
87
- await system.start()
88
- ```
89
-
90
- **Orden importa:** `system.init()` (corre todos los `create()`) → `orm.migrate()` → `system.start()`
91
-
92
- ---
93
-
94
- ## 3. REPOSITORYADAPTER<T> (la interfaz genérica)
95
-
96
- El service recibe `RepositoryAdapter<T>`, nunca `ORM` directo:
97
-
98
- ```ts
99
- import type { RepositoryAdapter } from 'arckode-framework'
100
- import { OrmRepository } from 'arckode-framework'
101
-
102
- // En el módulo (index.ts):
103
- const repo = new OrmRepository<PedidoDTO>(orm, 'Pedido')
104
- const service = new PedidosService(repo)
105
-
106
- // En el service:
107
34
  class PedidosService {
108
35
  constructor(private repo: RepositoryAdapter<PedidoDTO>) {}
109
36
  }
110
37
  ```
111
38
 
112
- `RepositoryAdapter<T>` te da CRUD básico: `findById`, `findOne`, `findMany`,
113
- `create`, `update`, `delete`, `count`, `paginate`. Para CRUD simple, esto basta.
39
+ | Método | Retorno |
40
+ |--------|---------|
41
+ | `findMany(filters?, options?)` | `T[]` |
42
+ | `findById(id, select?)` | `T \| null` |
43
+ | `findOne(filters)` | `T \| null` |
44
+ | `create(data)` | `T` |
45
+ | `update(id, data)` | `T \| null` |
46
+ | `delete(id)` | `boolean` |
47
+ | `count(filters?)` | `number` |
48
+ | `paginate(filters?, options)` | `PageResult<T>` |
114
49
 
115
- ---
50
+ ## Repository del dominio (`repository.ts` — opcional)
116
51
 
117
- ## 3b. REPOSITORY DEL DOMINIO (`repository.ts` OPCIONAL)
118
-
119
- `RepositoryAdapter<T>` es genérico. Cuando necesitás expresar queries con
120
- **nombres del dominio**, creás un `repository.ts` por módulo:
52
+ Crear cuando: filtros repetidos 3+ veces || JOIN/IN/LIKE || nombres del dominio.
121
53
 
122
54
  ```ts
123
- // modules/wallets/repository.ts
124
- import type { RepositoryAdapter } from 'arckode-framework'
125
- import type { WalletDTO, WalletCurrency } from './types'
126
-
127
55
  export class WalletsRepository {
128
56
  constructor(private base: RepositoryAdapter<WalletDTO>) {}
129
-
130
- // Delegamos los métodos genéricos
131
- findById(id: string) { return this.base.findById(id) }
132
- create(data: Omit<WalletDTO, 'id'>) { return this.base.create(data) }
133
- update(id: string, data: Partial<Omit<WalletDTO, 'id'>>) { return this.base.update(id, data) }
134
-
135
- // ─── Métodos del dominio (lo realmente importante) ───
57
+ findById(id: string) { return this.base.findById(id) }
136
58
  findByUserAndCurrency(userId: string, currency: WalletCurrency) {
137
59
  return this.base.findOne({ user_id: userId, currency })
138
60
  }
139
-
140
- findActiveByUser(userId: string) {
141
- return this.base.findMany({ user_id: userId, status: 'active' })
142
- }
143
61
  }
144
62
  ```
145
63
 
146
- **¿Por qué?** El service ahora habla en lenguaje del dominio:
147
- ```ts
148
- // ❌ Service conoce nombres de columna (acoplado a la DB)
149
- const wallet = await this.repo.findOne({ user_id: userId, currency })
150
-
151
- // ✅ Service habla el dominio (desacoplado)
152
- const wallet = await this.wallets.findByUserAndCurrency(userId, currency)
153
- ```
154
-
155
- Si mañana renombrás la columna `user_id → owner_id`, cambiás SOLO `repository.ts`.
156
- El service no se entera.
157
-
158
- **¿Cuándo crear repository.ts?**
159
- 1. El service repite los mismos filtros en 3+ métodos
160
- 2. Necesitás JOIN, IN, LIKE (el ORM builtin no los soporta)
161
- 3. Querés nombres del dominio
162
-
163
- **¿Cuándo NO?**
164
- - CRUD pelado — `OrmRepository<T>` directo, sin agregar archivo
165
- - Cada filtro se usa una sola vez
166
-
167
- ---
168
-
169
- ## 4. QUERIES DISPONIBLES
170
-
171
- ```ts
172
- // Buscar muchos con filtros
173
- await repo.findMany(
174
- { estado: 'pendiente', usuarioId: userId }, // filtros (AND implícito)
175
- { limit: 20, offset: 0, orderBy: 'createdAt', orderDir: 'desc' }
176
- )
177
-
178
- // Buscar por ID
179
- const pedido = await repo.findById(id) // null si no existe
180
-
181
- // Buscar uno con filtro
182
- const pedido = await repo.findOne({ usuarioId, estado: 'activo' })
183
-
184
- // Crear
185
- const nuevo = await repo.create({
186
- usuarioId: 'u123',
187
- productoId: 'p456',
188
- cantidad: 2,
189
- total: 200,
190
- })
191
-
192
- // Actualizar (solo los campos pasados)
193
- const actualizado = await repo.update(id, { estado: 'confirmado' })
194
-
195
- // Eliminar
196
- const ok = await repo.delete(id)
197
-
198
- // Contar
199
- const total = await repo.count({ estado: 'pendiente' })
200
-
201
- // Paginación
202
- const resultado = await repo.paginate(
203
- { estado: 'pendiente' },
204
- { page: 1, limit: 10 }
205
- )
206
- // → { data: PedidoDTO[], total: number, page: 1, limit: 10, totalPages: number }
207
- ```
208
-
209
- ### Filtros disponibles
210
- Los filtros son **igualdad exacta** (AND implícito). Para queries complejas (LIKE, IN, JOIN) → implementar `RepositoryAdapter<T>` custom con el driver directo.
211
-
212
- ```ts
213
- // ✅ Soportado
214
- { estado: 'activo', usuarioId: 'u123' }
215
-
216
- // ❌ No soportado directamente — necesita adapter custom
217
- { total: { gte: 1000 } }
218
- { nombre: { like: '%zapato%' } }
219
- ```
220
-
221
- ---
222
-
223
- ## 5. TRANSACCIONES MULTI-TABLA → usar `Transactor`
224
-
225
- **El service NO recibe `ORM` directamente** (viola Regla #18). Recibe `Transactor`,
226
- una interfaz que el framework provee para atomicidad cross-modelo.
64
+ ## Transactor (solo multi-tabla atómica)
227
65
 
228
66
  ```ts
229
- import type { Transactor, RepositoryAdapter } from 'arckode-framework'
230
-
231
- // service.ts (al root del módulo)
232
- export class WalletsService {
233
- constructor(
234
- private walletRepo: RepositoryAdapter<WalletDTO>,
235
- private transactor: Transactor, // ← interfaz, no ORM concreto
236
- ) {}
237
-
238
- async creditDeposit(userId: string, amount: number): Promise<TransactionDTO> {
67
+ class WalletsService {
68
+ constructor(private transactor: Transactor) {}
69
+ async credit(userId: string, amount: number) {
239
70
  return this.transactor.run(async (repos) => {
240
- // Obtener repos transaccionales tipados para cualquier modelo
241
71
  const wallets = repos.for<WalletDTO>('Wallet')
242
- const txs = repos.for<TransactionDTO>('Transaction')
243
- const ledger = repos.for<LedgerEntryDTO>('LedgerEntry')
244
-
245
- const wallet = await wallets.findOne({ user_id: userId })
246
- if (!wallet) throw new NotFoundError('Wallet no encontrada')
247
-
248
- const newBalance = Number(wallet.balance) + amount
249
-
250
- const tx = await txs.create({ user_id: userId, type: 'deposit', amount, ... })
251
- await ledger.create({ transaction_id: tx.id, entry_type: 'credit', amount, ... })
252
- await wallets.update(wallet.id, { balance: newBalance })
253
-
254
- return tx
255
- })
256
- // Si CUALQUIER operación dentro de run() falla → rollback automático completo
257
- }
258
- }
259
- ```
260
-
261
- **Composition root:**
262
- ```ts
263
- import { OrmTransactor } from 'arckode-framework'
264
-
265
- const transactor = new OrmTransactor(orm) // implementación SQL del Transactor
266
- const service = new WalletsService(walletRepo, transactor)
267
- ```
268
-
269
- **Para Prisma/Drizzle/MongoDB:** implementar `Transactor` custom. El service NO cambia.
270
-
271
- ```ts
272
- class PrismaTransactor implements Transactor {
273
- constructor(private prisma: PrismaClient) {}
274
- async run<R>(fn: (repos: TransactionalRepos) => Promise<R>): Promise<R> {
275
- return this.prisma.$transaction(async (tx) => {
276
- const repos: TransactionalRepos = {
277
- for: (modelName) => new PrismaRepo(tx[modelName]),
278
- }
279
- return fn(repos)
72
+ const txs = repos.for<TransactionDTO>('Transaction')
280
73
  })
281
74
  }
282
75
  }
283
76
  ```
284
77
 
285
- **Importante:** Si el adapter no soporta transacciones (RecordingDb en tests), el `Transactor`
286
- ejecuta sin atomicidad — los tests siguen funcionando, solo no son atómicos.
287
-
288
- ---
289
-
290
- ## 6. MIGRACIONES MANUALES (SQL explícito)
291
-
292
- Para cambios que el auto-migrate no puede hacer (renombrar columna, cambiar tipo, agregar índice):
293
-
294
- ```ts
295
- // Generar archivo de migración
296
- // arckode make:migration add_index_to_pedidos
297
-
298
- // src/migrations/1716000000_add_index_to_pedidos.ts
299
- export async function up(adapter: DbAdapter): Promise<void> {
300
- await adapter.run('CREATE INDEX IF NOT EXISTS idx_pedidos_usuario ON pedidos(usuario_id)')
301
- }
302
-
303
- export async function down(adapter: DbAdapter): Promise<void> {
304
- await adapter.run('DROP INDEX IF EXISTS idx_pedidos_usuario')
305
- }
306
- ```
307
-
308
- ```bash
309
- arckode db:migrate # corre up() de migraciones pendientes
310
- arckode db:migrate --rollback # corre down() de la última migración
311
- ```
312
-
313
- **Tabla de control:** `_arckode_migrations` (no tocar manualmente)
314
- **Tabla de schema drift:** `_arckode_schema` (usada por `orm.migrate()`, separada)
315
-
316
- ---
317
-
318
- ## 7. DRIFT DETECTION (automático)
319
-
320
- `orm.migrate()` detecta:
321
- - **Columnas nuevas en el modelo** → ejecuta `ALTER TABLE ADD COLUMN` automáticamente
322
- - **Columnas que dejaron de existir en el modelo** → emite WARNING, NO las elimina (datos en producción)
323
-
324
- Si necesitás eliminar una columna → hacerlo con una migración manual explícita.
325
-
326
- ---
327
-
328
- ## 8. ADAPTERS DE BASE DE DATOS
329
-
330
- ```ts
331
- // SQLite (desarrollo)
332
- import { SqliteAdapter } from 'arckode-framework/adapters/sqlite'
333
- const db = new SqliteAdapter({ path: './data/db.sqlite' })
334
- await db.connect()
335
-
336
- // MySQL (producción)
337
- import { MySqlAdapter } from 'arckode-framework/adapters/mysql'
338
- const db = new MySqlAdapter({
339
- host: config.get('DB_HOST'),
340
- port: config.get<number>('DB_PORT'),
341
- user: config.get('DB_USER'),
342
- password: config.get('DB_PASSWORD'),
343
- database: config.get('DB_NAME'),
344
- })
345
- await db.connect()
346
-
347
- // PostgreSQL (producción)
348
- import { PostgresAdapter } from 'arckode-framework/adapters/postgres'
349
- const db = new PostgresAdapter({ connectionString: config.get('DATABASE_URL') })
350
- await db.connect()
351
- ```
352
-
353
- **El ORM y todos los services son iguales independientemente del adapter.** Solo cambia el adapter en composition-root.
354
-
355
- ---
356
-
357
- ## 9. ANTI-PATRONES DE ORM
358
-
359
- ```ts
360
- // ❌ ORM directo en controller
361
- router.get('/pedidos', async (req) => {
362
- return orm.findMany('Pedido') // NO — viola REGLA #12
363
- })
364
-
365
- // ❌ ORM directo en service (debe ser RepositoryAdapter)
366
- class PedidosService {
367
- constructor(private orm: ORM) {} // NO — viola REGLA #18
368
- }
369
-
370
- // ❌ N+1: ORM dentro de un loop (viola REGLA #17)
371
- const pedidos = await repo.findMany()
372
- for (const p of pedidos) {
373
- const user = await orm.findById('Usuario', p.usuarioId) // N queries
374
- }
375
-
376
- // ❌ Dos módulos escribiendo en la misma tabla (viola REGLA #4)
377
- // módulo-pedidos: orm.update('Inventario', ...) → NO
378
- // módulo-inventario es el dueño de la tabla inventario
379
-
380
- // ✅ Para queries complejas: adapter custom o dos queries separadas
381
- const pedidos = await repo.findMany()
382
- const userIds = [...new Set(pedidos.map(p => p.usuarioId))]
383
- // luego pasar userIds al módulo de usuarios por acción pública
384
- ```
385
-
386
- ---
78
+ Composition root: `const transactor = new OrmTransactor(orm)`
387
79
 
388
- ## 10. CHECKLIST ORM
80
+ ## Errores silenciosos
389
81
 
390
- - [ ] El modelo tiene `table` en snake_case plural
391
- - [ ] Los campos usan camelCase y tipos válidos
392
- - [ ] `orm.define()` en composition-root, NO en el módulo
393
- - [ ] `orm.migrate()` después de todos los `define()`
394
- - [ ] Service recibe `RepositoryAdapter<T>`, no `ORM`
395
- - [ ] No hay ORM dentro de loops
396
- - [ ] Migraciones manuales en `src/migrations/` para cambios de schema complejos
82
+ | Error | Señal | Fix |
83
+ |-------|-------|-----|
84
+ | Transactor sin atomicidad | Adapter sin `transaction?()` | No hay fix automático — el ORM ejecuta sin transacción |
85
+ | `findMany` solo igualdad exacta | Filtrar con `{ total: { gte: 1000 } }` no funciona | Adapter custom con driver directo |
86
+ | `model.ts` en `types.ts` | `arckode analyze` `MODEL_IN_TYPES_FILE` | Mover a `model.ts` |
87
+ | Service recibe ORM directo | `arckode analyze` → `SERVICE_DEPENDS_ON_ORM` | Inyectar `RepositoryAdapter<T>` |
88
+ | N+1 en loop | `arckode analyze` `N_PLUS_ONE_RISK` | Dos queries separadas: `findMany` + segundo query con ids |
89
+ | Migración manual sin timestamp | Archivos no ordenados | Usar `arckode make:migration` que genera timestamp |