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
package/skills/orm/SKILL.md
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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:** `
|
|
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
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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 (
|
|
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
|
|
package/skills/realtime/SKILL.md
CHANGED
package/skills/services/SKILL.md
CHANGED
package/skills/testing/SKILL.md
CHANGED
|
@@ -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 '../
|
|
15
|
+
import { ProductosService } from '../service'
|
|
16
16
|
import type { ProductoDTO } from '../types'
|
|
17
17
|
|
|
18
18
|
// silentLogger es factory function — SIEMPRE llamar con ()
|