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.
@@ -15,11 +15,11 @@ Campos del modelo: camelCase → usuarioId, fechaEntrega
15
15
 
16
16
  ---
17
17
 
18
- ## 1. DEFINIR UN MODELO
18
+ ## 1. DEFINIR UN MODELO — vive en `model.ts` (no en types.ts)
19
19
 
20
20
  ```ts
21
- // En types.ts del módulo
22
- import type { ModelDefinition } from 'arckode-framework'
21
+ // modules/pedidos/model.ts
22
+ import type { ModelDefinition, ORM } from 'arckode-framework'
23
23
 
24
24
  export const PedidoModel: ModelDefinition = {
25
25
  table: 'pedidos',
@@ -36,6 +36,13 @@ export const PedidoModel: ModelDefinition = {
36
36
  timestamps: true, // agrega createdAt, updatedAt (RECOMENDADO)
37
37
  softDelete: false, // true → agrega deletedAt, delete() solo marca, no borra
38
38
  }
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
+ export function registerPedidosModels(orm: ORM): void {
44
+ orm.define('Pedido', PedidoModel)
45
+ }
39
46
  ```
40
47
 
41
48
  **Reglas de naming:**
@@ -44,23 +51,47 @@ export const PedidoModel: ModelDefinition = {
44
51
  - Solo alfanuméricos y guión bajo — `assertSafeIdentifier()` valida esto en ORM
45
52
  - No usar palabras reservadas SQL: `order`, `table`, `select`, `where`
46
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
+
47
59
  ---
48
60
 
49
- ## 2. REGISTRAR EL MODELO (composition-root.ts)
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.
50
64
 
51
65
  ```ts
52
- import { PedidoModel } from './modules/pedidos/types'
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
+ })
77
+ }
78
+ ```
79
+
80
+ **Composition-root solo coordina** — no toca `orm.define()` por módulo:
53
81
 
54
- // ANTES de llamar system.start()
55
- orm.define('Pedido', PedidoModel)
82
+ ```ts
83
+ // composition-root.ts
84
+ system.addModule(PedidosModule()) // el módulo se autorregistra
85
+ system.init()
56
86
  await orm.migrate() // CREATE TABLE IF NOT EXISTS + drift detection
87
+ await system.start()
57
88
  ```
58
89
 
59
- **Orden importa:** `orm.define()` → `orm.migrate()` → `system.start()`
90
+ **Orden importa:** `system.init()` (corre todos los `create()`) → `orm.migrate()` → `system.start()`
60
91
 
61
92
  ---
62
93
 
63
- ## 3. REPOSITORYADAPTER<T> (la interfaz correcta)
94
+ ## 3. REPOSITORYADAPTER<T> (la interfaz genérica)
64
95
 
65
96
  El service recibe `RepositoryAdapter<T>`, nunca `ORM` directo:
66
97
 
@@ -78,6 +109,61 @@ class PedidosService {
78
109
  }
79
110
  ```
80
111
 
112
+ `RepositoryAdapter<T>` te da CRUD básico: `findById`, `findOne`, `findMany`,
113
+ `create`, `update`, `delete`, `count`, `paginate`. Para CRUD simple, esto basta.
114
+
115
+ ---
116
+
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:
121
+
122
+ ```ts
123
+ // modules/wallets/repository.ts
124
+ import type { RepositoryAdapter } from 'arckode-framework'
125
+ import type { WalletDTO, WalletCurrency } from './types'
126
+
127
+ export class WalletsRepository {
128
+ 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) ───
136
+ findByUserAndCurrency(userId: string, currency: WalletCurrency) {
137
+ return this.base.findOne({ user_id: userId, currency })
138
+ }
139
+
140
+ findActiveByUser(userId: string) {
141
+ return this.base.findMany({ user_id: userId, status: 'active' })
142
+ }
143
+ }
144
+ ```
145
+
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
+
81
167
  ---
82
168
 
83
169
  ## 4. QUERIES DISPONIBLES
@@ -134,20 +220,70 @@ Los filtros son **igualdad exacta** (AND implícito). Para queries complejas (LI
134
220
 
135
221
  ---
136
222
 
137
- ## 5. TRANSACCIONES
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.
138
227
 
139
228
  ```ts
140
- // En el service si necesitás atomicidad entre operaciones
141
- await orm.transaction(async (tx) => {
142
- const txRepo = new OrmRepository<PedidoDTO>(tx, 'Pedido')
143
- const txInventarioRepo = new OrmRepository<InventarioDTO>(tx, 'Inventario')
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> {
239
+ return this.transactor.run(async (repos) => {
240
+ // Obtener repos transaccionales tipados para cualquier modelo
241
+ 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
+ ```
144
260
 
145
- await txRepo.create(datosPedido)
146
- await txInventarioRepo.update(inventarioId, { stock: stockNuevo })
147
- })
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)
280
+ })
281
+ }
282
+ }
148
283
  ```
149
284
 
150
- **Importante:** Si el adapter no soporta transacciones (MemoryDb, RecordingDb), el ORM ejecuta sin transacción — no falla, solo no es atómico.
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.
151
287
 
152
288
  ---
153
289
 
@@ -220,7 +220,7 @@ create({ logger, orm, router, ws }: ModuleDependencies) {
220
220
  ...
221
221
  }
222
222
 
223
- // En actions/service.ts
223
+ // En service.ts (al root del módulo)
224
224
  class PedidosService {
225
225
  constructor(
226
226
  private repo: RepositoryAdapter<PedidoDTO>,
@@ -82,7 +82,7 @@ create({ logger, orm, cache, router, auth, mail }: ModuleDependencies) {
82
82
  ...
83
83
  }
84
84
 
85
- // En actions/service.ts
85
+ // En service.ts (al root del módulo)
86
86
  class AuthService {
87
87
  constructor(
88
88
  private repo: RepositoryAdapter<UserDTO>,
@@ -12,7 +12,7 @@ import { describe, test, expect, beforeEach, mock } from 'bun:test'
12
12
  // createRecordingOrm / createSilentLogger / createNullCache NO existen — nunca usar esos nombres
13
13
  import { silentLogger, createRecordingDb } from 'arckode-framework/testing'
14
14
  import type { RepositoryAdapter, CacheAdapter } from 'arckode-framework'
15
- import { ProductosService } from '../actions/service'
15
+ import { ProductosService } from '../service'
16
16
  import type { ProductoDTO } from '../types'
17
17
 
18
18
  // silentLogger es factory function — SIEMPRE llamar con ()