arckode-framework 1.0.0 → 1.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arckode-framework",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "AI-first TypeScript/Bun framework. Modular, SOLID, zero magic. The AI reads the composition root and knows everything.",
5
5
  "type": "module",
6
6
  "main": "./kernel/framework.ts",
@@ -25,9 +25,20 @@
25
25
  },
26
26
  "files": [
27
27
  "bin/",
28
- "kernel/",
29
- "adapters/",
30
- "modules/",
28
+ "kernel/framework.ts",
29
+ "kernel/middlewares.ts",
30
+ "kernel/static.ts",
31
+ "kernel/testing.ts",
32
+ "adapters/jwt.ts",
33
+ "adapters/mysql.ts",
34
+ "adapters/postgres.ts",
35
+ "adapters/redis-cache.ts",
36
+ "adapters/sqlite.ts",
37
+ "modules/events/index.ts",
38
+ "modules/mail/",
39
+ "modules/queue/index.ts",
40
+ "modules/storage/",
41
+ "modules/ws/index.ts",
31
42
  "cli/",
32
43
  "skills/",
33
44
  "README.md"
@@ -1,283 +0,0 @@
1
- // adapters/__tests__/mysql.test.ts
2
- // Testea MySQLAdapter con mocks de mysql2/promise — sin servidor real.
3
- // Para tests de integración real ver el bloque comentado al final.
4
-
5
- import { describe, it, expect, beforeAll, beforeEach, mock } from 'bun:test'
6
- import type { MySQLAdapter } from '../mysql'
7
-
8
- // ── Mocks de conexión (transacciones) ────────────────────────────────────────
9
- const connMock = {
10
- beginTransaction: mock(async () => {}),
11
- execute: mock(async (): Promise<unknown[]> => [[], []]),
12
- commit: mock(async () => {}),
13
- rollback: mock(async () => {}),
14
- release: mock(() => {}),
15
- }
16
-
17
- // ── Mock del pool ─────────────────────────────────────────────────────────────
18
- const poolMock = {
19
- getConnection: mock(async () => connMock),
20
- execute: mock(async (): Promise<unknown[]> => [[], []]),
21
- end: mock(async () => {}),
22
- }
23
-
24
- // Registrar el mock ANTES de que el adapter importe mysql2/promise.
25
- // El dynamic import en beforeAll garantiza que mysql2/promise ya está mockeado.
26
- mock.module('mysql2/promise', () => ({
27
- default: { createPool: mock(() => poolMock) },
28
- }))
29
-
30
- // ── Carga diferida del adapter ────────────────────────────────────────────────
31
- let Adapter: typeof MySQLAdapter
32
-
33
- beforeAll(async () => {
34
- const mod = await import('../mysql')
35
- Adapter = mod.MySQLAdapter
36
- })
37
-
38
- // ── Helpers ───────────────────────────────────────────────────────────────────
39
- function resetMocks() {
40
- connMock.beginTransaction.mockClear()
41
- connMock.execute.mockClear()
42
- connMock.commit.mockClear()
43
- connMock.rollback.mockClear()
44
- connMock.release.mockClear()
45
- poolMock.getConnection.mockClear()
46
- poolMock.execute.mockClear()
47
- poolMock.end.mockClear()
48
- }
49
-
50
- function makeAdapter() {
51
- return new Adapter({ host: 'localhost', user: 'test', password: 'test', database: 'testdb' })
52
- }
53
-
54
- // ─────────────────────────────────────────────────────────────────────────────
55
-
56
- describe('MySQLAdapter', () => {
57
- beforeEach(resetMocks)
58
-
59
- // ── connect() ───────────────────────────────────────────────────────────────
60
- describe('connect()', () => {
61
- it('crea el pool y verifica la conexión con getConnection+release', async () => {
62
- const adapter = makeAdapter()
63
- await adapter.connect()
64
-
65
- expect(poolMock.getConnection).toHaveBeenCalledTimes(1)
66
- expect(connMock.release).toHaveBeenCalledTimes(1)
67
- })
68
- })
69
-
70
- // ── query() ─────────────────────────────────────────────────────────────────
71
- describe('query()', () => {
72
- let adapter: MySQLAdapter
73
-
74
- beforeEach(async () => {
75
- adapter = makeAdapter()
76
- await adapter.connect()
77
- resetMocks() // limpia las calls de connect()
78
- })
79
-
80
- it('retorna el array de filas', async () => {
81
- const filas = [{ id: '1', nombre: 'Laptop' }, { id: '2', nombre: 'Mouse' }]
82
- poolMock.execute.mockImplementation(async () => [filas, []])
83
-
84
- const result = await adapter.query('SELECT * FROM productos')
85
- expect(result).toEqual(filas)
86
- })
87
-
88
- it('pasa los params al execute del pool', async () => {
89
- poolMock.execute.mockImplementation(async () => [[{ id: '1' }], []])
90
-
91
- await adapter.query('SELECT * FROM productos WHERE id = ?', ['1'])
92
- expect(poolMock.execute).toHaveBeenCalledWith(
93
- 'SELECT * FROM productos WHERE id = ?',
94
- ['1'],
95
- )
96
- })
97
-
98
- it('retorna array vacío cuando no hay resultados', async () => {
99
- poolMock.execute.mockImplementation(async () => [[], []])
100
-
101
- const result = await adapter.query('SELECT * FROM productos WHERE id = ?', ['999'])
102
- expect(result).toEqual([])
103
- })
104
- })
105
-
106
- // ── run() ────────────────────────────────────────────────────────────────────
107
- describe('run()', () => {
108
- let adapter: MySQLAdapter
109
-
110
- beforeEach(async () => {
111
- adapter = makeAdapter()
112
- await adapter.connect()
113
- resetMocks()
114
- })
115
-
116
- it('INSERT retorna changes y lastId como string', async () => {
117
- poolMock.execute.mockImplementation(async () => [{ affectedRows: 1, insertId: 42 }, []])
118
-
119
- const result = await adapter.run(
120
- 'INSERT INTO productos (nombre, precio) VALUES (?, ?)',
121
- ['Laptop', 999],
122
- )
123
-
124
- expect(result.changes).toBe(1)
125
- expect(result.lastId).toBe('42')
126
- })
127
-
128
- it('UPDATE retorna changes y lastId undefined (insertId === 0)', async () => {
129
- poolMock.execute.mockImplementation(async () => [{ affectedRows: 3, insertId: 0 }, []])
130
-
131
- const result = await adapter.run(
132
- 'UPDATE productos SET activo = ? WHERE categoria = ?',
133
- [false, 'perifericos'],
134
- )
135
-
136
- expect(result.changes).toBe(3)
137
- expect(result.lastId).toBeUndefined()
138
- })
139
-
140
- it('DELETE retorna changes y sin lastId', async () => {
141
- poolMock.execute.mockImplementation(async () => [{ affectedRows: 1, insertId: 0 }, []])
142
-
143
- const result = await adapter.run('DELETE FROM productos WHERE id = ?', ['1'])
144
- expect(result.changes).toBe(1)
145
- expect(result.lastId).toBeUndefined()
146
- })
147
- })
148
-
149
- // ── transaction() ────────────────────────────────────────────────────────────
150
- describe('transaction()', () => {
151
- let adapter: MySQLAdapter
152
-
153
- beforeEach(async () => {
154
- adapter = makeAdapter()
155
- await adapter.connect()
156
- resetMocks()
157
- })
158
-
159
- it('adquiere conexión, abre transacción, ejecuta fn y hace commit', async () => {
160
- const filas = [{ id: '1' }]
161
- connMock.execute.mockImplementation(async () => [filas, []])
162
-
163
- const result = await adapter.transaction(async (tx) => {
164
- return tx.query('SELECT * FROM productos')
165
- })
166
-
167
- expect(poolMock.getConnection).toHaveBeenCalledTimes(1)
168
- expect(connMock.beginTransaction).toHaveBeenCalledTimes(1)
169
- expect(connMock.commit).toHaveBeenCalledTimes(1)
170
- expect(connMock.rollback).not.toHaveBeenCalled()
171
- expect(connMock.release).toHaveBeenCalledTimes(1)
172
- expect(result).toEqual(filas)
173
- })
174
-
175
- it('el adapter de transacción usa la conexión, no el pool', async () => {
176
- connMock.execute.mockImplementation(async () => [[{ total: 5 }], []])
177
-
178
- await adapter.transaction(async (tx) => {
179
- await tx.query('SELECT COUNT(*) as total FROM productos')
180
- await tx.run('INSERT INTO auditoria (evento) VALUES (?)', ['sync'])
181
- })
182
-
183
- // Ambas llamadas van a connMock.execute, NO a poolMock.execute
184
- expect(connMock.execute).toHaveBeenCalledTimes(2)
185
- expect(poolMock.execute).not.toHaveBeenCalled()
186
- })
187
-
188
- it('hace rollback y lanza el error si fn falla', async () => {
189
- const error = new Error('error de negocio')
190
-
191
- await expect(
192
- adapter.transaction(async () => {
193
- throw error
194
- }),
195
- ).rejects.toThrow('error de negocio')
196
-
197
- expect(connMock.beginTransaction).toHaveBeenCalledTimes(1)
198
- expect(connMock.rollback).toHaveBeenCalledTimes(1)
199
- expect(connMock.commit).not.toHaveBeenCalled()
200
- expect(connMock.release).toHaveBeenCalledTimes(1)
201
- })
202
-
203
- it('libera la conexión aunque el rollback falle (finally garantizado)', async () => {
204
- connMock.rollback.mockImplementation(async () => {
205
- throw new Error('rollback falló')
206
- })
207
-
208
- await expect(
209
- adapter.transaction(async () => {
210
- throw new Error('error original')
211
- }),
212
- ).rejects.toBeDefined()
213
-
214
- // release() debe ejecutarse siempre — está en finally
215
- expect(connMock.release).toHaveBeenCalledTimes(1)
216
-
217
- // Restaurar implementación para tests siguientes
218
- connMock.rollback.mockImplementation(async () => {})
219
- })
220
- })
221
-
222
- // ── close() ──────────────────────────────────────────────────────────────────
223
- describe('close()', () => {
224
- it('termina el pool', async () => {
225
- const adapter = makeAdapter()
226
- await adapter.connect()
227
- resetMocks()
228
-
229
- await adapter.close()
230
- expect(poolMock.end).toHaveBeenCalledTimes(1)
231
- })
232
- })
233
- })
234
-
235
- // ─────────────────────────────────────────────────────────────────────────────
236
- // Test de integración real (requiere MySQL corriendo):
237
- //
238
- // describe('MySQLAdapter — integración', () => {
239
- // let adapter: MySQLAdapter
240
- //
241
- // beforeAll(async () => {
242
- // const { MySQLAdapter } = await import('../mysql')
243
- // adapter = new MySQLAdapter({
244
- // host: 'localhost',
245
- // user: 'test',
246
- // password: 'test',
247
- // database: 'arckode_test',
248
- // })
249
- // await adapter.connect()
250
- // await adapter.run('CREATE TABLE IF NOT EXISTS test_items (id INT AUTO_INCREMENT PRIMARY KEY, nombre VARCHAR(255))')
251
- // })
252
- //
253
- // afterAll(async () => {
254
- // await adapter.run('DROP TABLE IF EXISTS test_items')
255
- // await adapter.close()
256
- // })
257
- //
258
- // it('INSERT + SELECT funcionan con placeholders ?', async () => {
259
- // await adapter.run('INSERT INTO test_items (nombre) VALUES (?)', ['Laptop'])
260
- // const rows = await adapter.query('SELECT * FROM test_items WHERE nombre = ?', ['Laptop'])
261
- // expect(rows).toHaveLength(1)
262
- // expect((rows[0] as any).nombre).toBe('Laptop')
263
- // })
264
- //
265
- // it('transacción commit persiste los datos', async () => {
266
- // await adapter.transaction(async (tx) => {
267
- // await tx.run('INSERT INTO test_items (nombre) VALUES (?)', ['Mouse'])
268
- // })
269
- // const rows = await adapter.query('SELECT * FROM test_items WHERE nombre = ?', ['Mouse'])
270
- // expect(rows).toHaveLength(1)
271
- // })
272
- //
273
- // it('transacción rollback no persiste los datos', async () => {
274
- // await expect(
275
- // adapter.transaction(async (tx) => {
276
- // await tx.run('INSERT INTO test_items (nombre) VALUES (?)', ['Keyboard'])
277
- // throw new Error('forzar rollback')
278
- // })
279
- // ).rejects.toThrow()
280
- // const rows = await adapter.query('SELECT * FROM test_items WHERE nombre = ?', ['Keyboard'])
281
- // expect(rows).toHaveLength(0)
282
- // })
283
- // })
@@ -1,48 +0,0 @@
1
- // adapters/vendor.d.ts — Minimal type stubs for optional peer dependencies.
2
- // pg and redis are optional — users install them only when using those adapters.
3
- // These stubs let TypeScript compile without the packages installed.
4
-
5
- declare module 'pg' {
6
- export interface PoolConfig {
7
- connectionString?: string
8
- min?: number
9
- max?: number
10
- [key: string]: unknown
11
- }
12
-
13
- export interface QueryResult<R = Record<string, unknown>> {
14
- rows: R[]
15
- rowCount: number | null
16
- }
17
-
18
- export interface PoolClient {
19
- release(): void
20
- }
21
-
22
- export class Pool {
23
- constructor(config?: PoolConfig)
24
- connect(): Promise<PoolClient>
25
- query(sql: string, params?: unknown[]): Promise<QueryResult>
26
- end(): Promise<void>
27
- }
28
-
29
- const pg: {
30
- Pool: typeof Pool
31
- } & typeof import('pg')
32
- export default pg
33
- }
34
-
35
- declare module 'redis' {
36
- export type RedisClientType = {
37
- connect(): Promise<void>
38
- get(key: string): Promise<string | null>
39
- set(key: string, value: string): Promise<unknown>
40
- setEx(key: string, seconds: number, value: string): Promise<unknown>
41
- del(keys: string | string[]): Promise<unknown>
42
- keys(pattern: string): Promise<string[]>
43
- flushAll(): Promise<unknown>
44
- quit(): Promise<unknown>
45
- }
46
-
47
- export function createClient(options?: { url?: string; [key: string]: unknown }): RedisClientType
48
- }
@@ -1,101 +0,0 @@
1
- // kernel/__tests__/adapters.test.ts — Tests de adapters reales
2
- // Bun:test. Requiere: better-sqlite3 (SQLite), pg (Postgres), redis (Redis)
3
-
4
- import { describe, it, expect, beforeAll, afterAll } from 'bun:test'
5
-
6
- // ─── SQLite Adapter ────────────────────────────────────
7
- describe('SQLiteAdapter', () => {
8
- let adapter: any
9
-
10
- beforeAll(async () => {
11
- const { SqliteAdapter } = await import('../../adapters/sqlite')
12
- adapter = new SqliteAdapter({ path: ':memory:' })
13
- await adapter.connect()
14
- })
15
-
16
- afterAll(async () => {
17
- await adapter.close()
18
- })
19
-
20
- it('crea tabla e inserta datos', async () => {
21
- await adapter.run('CREATE TABLE IF NOT EXISTS test (id TEXT PRIMARY KEY, nombre TEXT)')
22
- await adapter.run('INSERT INTO test (id, nombre) VALUES (?, ?)', ['1', 'Test'])
23
- const rows = await adapter.query('SELECT * FROM test WHERE id = ?', ['1'])
24
- expect(rows).toHaveLength(1)
25
- expect((rows[0] as any).nombre).toBe('Test')
26
- })
27
-
28
- it('query con múltiples resultados', async () => {
29
- await adapter.run('INSERT INTO test (id, nombre) VALUES (?, ?)', ['2', 'Otro'])
30
- const rows = await adapter.query('SELECT * FROM test ORDER BY id')
31
- expect(rows).toHaveLength(2)
32
- })
33
-
34
- it('run devuelve changes', async () => {
35
- const result = await adapter.run('DELETE FROM test WHERE id = ?', ['1'])
36
- expect(result.changes).toBeGreaterThan(0)
37
- expect(result.lastId).toBeDefined()
38
- })
39
-
40
- it('query sin resultados devuelve array vacío', async () => {
41
- const rows = await adapter.query('SELECT * FROM test WHERE id = ?', ['999'])
42
- expect(rows).toEqual([])
43
- })
44
- })
45
-
46
- // ─── JWT Adapter ───────────────────────────────────────
47
- describe('JwtAdapter', () => {
48
- it('sign y verify funcionan', async () => {
49
- const { jwtTokenAdapter } = await import('../../adapters/jwt')
50
- const token = jwtTokenAdapter.sign({ id: '1', role: 'admin' }, 'secret', '1h')
51
- expect(token).toBeTruthy()
52
- expect(typeof token).toBe('string')
53
-
54
- const payload = jwtTokenAdapter.verify(token, 'secret')
55
- expect(payload.id).toBe('1')
56
- expect(payload.role).toBe('admin')
57
- })
58
-
59
- it('verify rechaza token con secret incorrecto', async () => {
60
- const { jwtTokenAdapter } = await import('../../adapters/jwt')
61
- const token = jwtTokenAdapter.sign({ id: '1' }, 'correct', '1h')
62
- expect(() => jwtTokenAdapter.verify(token, 'wrong')).toThrow()
63
- })
64
-
65
- it('verify rechaza token expirado', async () => {
66
- const { jwtTokenAdapter } = await import('../../adapters/jwt')
67
- const token = jwtTokenAdapter.sign({ id: '1' }, 'secret', '0s')
68
- expect(() => jwtTokenAdapter.verify(token, 'secret')).toThrow()
69
- })
70
- })
71
-
72
- // ═════════════════════════════════════════════════════════
73
- // Nota: PostgresAdapter y RedisCacheAdapter requieren
74
- // servidores externos. Se testean con mock o con docker.
75
- //
76
- // Test Postgres (requiere pg instalado y servidor postgres):
77
- // describe('PostgresAdapter', () => {
78
- // it('conecta y ejecuta query', async () => {
79
- // const { PostgresAdapter } = await import('../../adapters/postgres')
80
- // const adapter = new PostgresAdapter({
81
- // connectionString: 'postgres://test:test@localhost:5432/test'
82
- // })
83
- // await adapter.connect()
84
- // const rows = await adapter.query('SELECT 1 as num')
85
- // expect((rows[0] as any).num).toBe(1)
86
- // await adapter.close()
87
- // })
88
- // })
89
- //
90
- // Test Redis (requiere redis instalado y servidor redis):
91
- // describe('RedisCacheAdapter', () => {
92
- // it('set y get', async () => {
93
- // const { RedisCacheAdapter } = await import('../../adapters/redis-cache')
94
- // const cache = new RedisCacheAdapter({ url: 'redis://localhost:6379' })
95
- // await cache.connect()
96
- // await cache.set('test', { ok: true })
97
- // const val = await cache.get('test')
98
- // expect(val).toEqual({ ok: true })
99
- // await cache.delete('test')
100
- // })
101
- // })