arckode-framework 1.3.1 → 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.
- package/adapters/jwt.ts +6 -4
- package/adapters/mysql.ts +7 -2
- package/adapters/postgres.ts +37 -0
- package/adapters/sqlite.ts +7 -1
- package/adapters/vendor.d.ts +48 -0
- package/cli/analyze/checks.ts +333 -0
- package/cli/analyze/index.ts +44 -0
- package/cli/analyze/report.ts +107 -0
- package/cli/analyze/types.ts +46 -0
- package/cli/analyze/utils.ts +36 -0
- package/cli/analyze.ts +2 -647
- package/cli/commands/db-migrate.ts +213 -89
- package/cli/commands/db-seed.ts +97 -32
- package/cli/commands/db-utils.ts +192 -0
- package/cli/commands/new.ts +175 -0
- package/cli/commands/routes.ts +94 -0
- package/cli/index.ts +57 -404
- package/cli/stubs/claude-md-stub.ts +21 -8
- package/cli/stubs/module/core.ts +162 -0
- package/cli/stubs/module/data.ts +171 -0
- package/cli/stubs/module/index.ts +5 -0
- package/cli/stubs/module/service.ts +198 -0
- package/cli/stubs/module/types.ts +12 -0
- package/cli/stubs/module-stub.ts +2 -552
- package/kernel/auth.ts +114 -0
- package/kernel/cache.ts +37 -0
- package/kernel/config.ts +129 -0
- package/kernel/container.ts +64 -0
- package/kernel/db/orm-migrate.ts +136 -0
- package/kernel/db/orm-repository.ts +45 -0
- package/kernel/db/orm-utils.ts +93 -0
- package/kernel/db/orm.ts +254 -0
- package/kernel/db/transactor.ts +17 -0
- package/kernel/db/types.ts +72 -0
- package/kernel/errors.ts +102 -0
- package/kernel/framework.default.ts +41 -0
- package/kernel/framework.ts +8 -2144
- package/kernel/http/router.ts +131 -0
- package/kernel/http/server.ts +303 -0
- package/kernel/http/types.ts +56 -0
- package/kernel/index.ts +25 -0
- package/kernel/logger.ts +50 -0
- package/kernel/middlewares.ts +38 -21
- package/kernel/modules/create-module.ts +5 -0
- package/kernel/modules/system.ts +149 -0
- package/kernel/modules/types.ts +46 -0
- package/kernel/seeds.ts +48 -0
- package/kernel/static.ts +11 -2
- package/kernel/testing.ts +8 -3
- package/kernel/validator.ts +116 -0
- package/modules/events/index.ts +19 -3
- package/modules/mail/index.ts +14 -2
- package/modules/storage/local-adapter.ts +19 -5
- package/modules/ws/index.ts +123 -18
- package/package.json +8 -11
- package/skills/auth/SKILL.md +36 -220
- package/skills/cli/SKILL.md +32 -251
- package/skills/config/SKILL.md +30 -239
- package/skills/connectors/SKILL.md +32 -295
- package/skills/helpers/SKILL.md +26 -195
- package/skills/middlewares/SKILL.md +30 -267
- package/skills/orm/SKILL.md +42 -349
- package/skills/realtime/SKILL.md +22 -297
- package/skills/services/SKILL.md +40 -183
- package/skills/testing/SKILL.md +34 -266
package/skills/orm/SKILL.md
CHANGED
|
@@ -1,396 +1,89 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ORM — Modelos, Queries, Migraciones
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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,
|
|
37
|
-
softDelete: false,
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
80
|
+
## Errores silenciosos
|
|
389
81
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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 |
|