arckode-framework 1.3.2 → 1.4.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/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/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 +19 -7
- 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 -280
- 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
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { ModuleStubParams } from './types'
|
|
2
|
+
import { mapTS } from './types'
|
|
3
|
+
|
|
4
|
+
export function indexStub(p: ModuleStubParams): string {
|
|
5
|
+
const actions = ['list', 'getById', 'create', 'update', 'delete']
|
|
6
|
+
const events = [`on${p.name}Created`, `on${p.name}Updated`, `on${p.name}Deleted`]
|
|
7
|
+
|
|
8
|
+
return `// ${p.module}/index.ts — PUERTA PÚBLICA
|
|
9
|
+
// Solo esto es visible para otros módulos y conectores.
|
|
10
|
+
// ⚠ REGLA: Append-only. No sacar ni modificar exports existentes.
|
|
11
|
+
|
|
12
|
+
import { createModule, OrmRepository } from 'arckode-framework'
|
|
13
|
+
import { register${p.name}Models } from './model'
|
|
14
|
+
import { ${p.name}Service } from './service'
|
|
15
|
+
import { ${p.name}Controller } from './controller'
|
|
16
|
+
import type { ${p.name}DTO } from './types'
|
|
17
|
+
|
|
18
|
+
export { ${p.name}Service }
|
|
19
|
+
export type { ${p.name}DTO, Create${p.name}DTO, Update${p.name}DTO, ${p.name}Query, ${p.name}Paginated } from './types'
|
|
20
|
+
export type { ${p.name}Sockets } from './sockets'
|
|
21
|
+
export { ${p.name}Validator, Create${p.name}Schema, Update${p.name}Schema } from './validators/schema'
|
|
22
|
+
|
|
23
|
+
export function ${p.name}Module() {
|
|
24
|
+
return createModule({
|
|
25
|
+
name: '${p.module}',
|
|
26
|
+
version: '1.0.0',
|
|
27
|
+
description: 'Módulo de ${p.module}',
|
|
28
|
+
|
|
29
|
+
contract: {
|
|
30
|
+
name: '${p.module}',
|
|
31
|
+
version: '1.0.0',
|
|
32
|
+
description: 'Módulo de ${p.module}',
|
|
33
|
+
actions: ${JSON.stringify(actions)},
|
|
34
|
+
events: ${JSON.stringify(events)},
|
|
35
|
+
tables: ['${p.module}'],
|
|
36
|
+
dependencies: [],
|
|
37
|
+
rules: ['No importar de otros módulos'],
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
create({ logger, orm, cache, router, auth }) {
|
|
41
|
+
// Registrar modelo(s) — delegado a model.ts
|
|
42
|
+
register${p.name}Models(orm)
|
|
43
|
+
|
|
44
|
+
const repo = new OrmRepository<${p.name}DTO>(orm, '${p.name}')
|
|
45
|
+
const log = logger.child('${p.module}')
|
|
46
|
+
const service = new ${p.name}Service(repo, log, cache)
|
|
47
|
+
const controller = new ${p.name}Controller(service, log)
|
|
48
|
+
|
|
49
|
+
// Rutas públicas por defecto — agregar [auth.authenticate()] para proteger
|
|
50
|
+
router.get('/${p.module}', (req) => controller.index(req))
|
|
51
|
+
router.get('/${p.module}/:id', (req) => controller.show(req))
|
|
52
|
+
router.post('/${p.module}', (req) => controller.store(req))
|
|
53
|
+
router.put('/${p.module}/:id', (req) => controller.update(req))
|
|
54
|
+
router.delete('/${p.module}/:id', (req) => controller.destroy(req))
|
|
55
|
+
|
|
56
|
+
log.info('Módulo ${p.module} listo')
|
|
57
|
+
return service
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function modelStub(p: ModuleStubParams): string {
|
|
65
|
+
const modelFields = p.fields
|
|
66
|
+
.filter(f => !['id', 'createdAt', 'updatedAt', 'deletedAt'].includes(f.name))
|
|
67
|
+
.map(f => {
|
|
68
|
+
const parts = [` ${f.name}: { type: '${f.type}'`]
|
|
69
|
+
if (f.required) parts.push(`, required: true`)
|
|
70
|
+
if (f.default !== undefined) parts.push(`, default: ${JSON.stringify(f.default)}`)
|
|
71
|
+
parts.push(` }`)
|
|
72
|
+
return parts.join('')
|
|
73
|
+
})
|
|
74
|
+
.join(',\n')
|
|
75
|
+
|
|
76
|
+
return `// ${p.module}/model.ts — Schema de base de datos
|
|
77
|
+
// Responsabilidad ÚNICA: describir la estructura física de la tabla.
|
|
78
|
+
// Importado por index.ts → orm.define() para registrar el modelo.
|
|
79
|
+
//
|
|
80
|
+
// Si el módulo tiene 3+ tablas, agregá registerModels(orm) acá y delegá desde index.ts.
|
|
81
|
+
|
|
82
|
+
import type { ModelDefinition, ORM } from 'arckode-framework'
|
|
83
|
+
|
|
84
|
+
export const ${p.name}Model: ModelDefinition = {
|
|
85
|
+
table: '${p.module}',
|
|
86
|
+
fields: {
|
|
87
|
+
${modelFields}
|
|
88
|
+
},
|
|
89
|
+
timestamps: true,
|
|
90
|
+
${p.softDelete ? ' softDelete: true,' : ''}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Helper opcional — usalo si el módulo registra 3+ tablas
|
|
94
|
+
// (mantiene index.ts limpio, enfocado en wiring)
|
|
95
|
+
export function register${p.name}Models(orm: ORM): void {
|
|
96
|
+
orm.define('${p.name}', ${p.name}Model)
|
|
97
|
+
}
|
|
98
|
+
`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function typesStub(p: ModuleStubParams): string {
|
|
102
|
+
const fields = p.fields.map(f => ` ${f.name}${f.required ? '' : '?'}: ${mapTS(f.type)}`).join('\n')
|
|
103
|
+
const filterFields = p.fields.map(f => ` ${f.name}?: ${mapTS(f.type)}`).join('\n')
|
|
104
|
+
|
|
105
|
+
return `// ${p.module}/types.ts — DTOs y tipos de queries
|
|
106
|
+
// Responsabilidad ÚNICA: contrato TypeScript del módulo (cómo se ven los datos).
|
|
107
|
+
// El schema de DB vive en ./model.ts — son conceptos distintos.
|
|
108
|
+
|
|
109
|
+
export interface ${p.name}DTO {
|
|
110
|
+
id: string
|
|
111
|
+
${fields}
|
|
112
|
+
createdAt: string
|
|
113
|
+
updatedAt: string
|
|
114
|
+
${p.softDelete ? ' deletedAt: string | null' : ''}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface Create${p.name}DTO {
|
|
118
|
+
${p.fields.filter(f => !f.name.match(/^(id|createdAt|updatedAt|deletedAt)$/)).map(f => ` ${f.name}${f.required ? '' : '?'}: ${mapTS(f.type)}`).join('\n')}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface Update${p.name}DTO {
|
|
122
|
+
${p.fields.filter(f => !f.name.match(/^(id|createdAt|updatedAt|deletedAt)$/)).map(f => ` ${f.name}?: ${mapTS(f.type)}`).join('\n')}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── Consultas y paginación ────────────────────────────
|
|
126
|
+
export interface ${p.name}Query {
|
|
127
|
+
${filterFields}
|
|
128
|
+
page?: number
|
|
129
|
+
limit?: number
|
|
130
|
+
sortBy?: string
|
|
131
|
+
sortOrder?: 'asc' | 'desc'
|
|
132
|
+
search?: string
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ${p.name}Paginated {
|
|
136
|
+
data: ${p.name}DTO[]
|
|
137
|
+
pagination: {
|
|
138
|
+
page: number
|
|
139
|
+
limit: number
|
|
140
|
+
total: number
|
|
141
|
+
totalPages: number
|
|
142
|
+
hasNext: boolean
|
|
143
|
+
hasPrev: boolean
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function socketsStub(p: ModuleStubParams): string {
|
|
150
|
+
return `// ${p.module}/sockets.ts — Hooks OPCIONALES hacia otros módulos
|
|
151
|
+
// Los sockets son opcionales. El módulo funciona sin ellos.
|
|
152
|
+
// Un conector puede pasar sockets para reaccionar a eventos del módulo.
|
|
153
|
+
|
|
154
|
+
import type { ${p.name}DTO } from './types'
|
|
155
|
+
|
|
156
|
+
export interface ${p.name}Sockets {
|
|
157
|
+
on${p.name}Created?: (data: ${p.name}DTO) => Promise<void>
|
|
158
|
+
on${p.name}Updated?: (data: ${p.name}DTO) => Promise<void>
|
|
159
|
+
on${p.name}Deleted?: (id: string) => Promise<void>
|
|
160
|
+
}
|
|
161
|
+
`
|
|
162
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { ModuleStubParams } from './types'
|
|
2
|
+
|
|
3
|
+
export function testStub(p: ModuleStubParams): string {
|
|
4
|
+
return `// ${p.module}/tests/service.test.ts — Tests del servicio
|
|
5
|
+
// Usa RepositoryAdapter mock — sin dependencia de SQLite ni Postgres.
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'bun:test'
|
|
8
|
+
import type { RepositoryAdapter, CacheAdapter } from 'arckode-framework'
|
|
9
|
+
import { silentLogger } from 'arckode-framework/testing'
|
|
10
|
+
import { ${p.name}Service } from '../service'
|
|
11
|
+
import type { ${p.name}DTO } from '../types'
|
|
12
|
+
|
|
13
|
+
// silentLogger es una factory function — SIEMPRE llamarla con ()
|
|
14
|
+
const log = silentLogger()
|
|
15
|
+
const silentCache: CacheAdapter = { get: async () => null, set: async () => {}, delete: async () => {}, clear: async () => {}, flush: async () => {} }
|
|
16
|
+
|
|
17
|
+
function makeRepo(overrides: Partial<RepositoryAdapter<${p.name}DTO>> = {}): RepositoryAdapter<${p.name}DTO> {
|
|
18
|
+
return {
|
|
19
|
+
findMany: async () => [],
|
|
20
|
+
findById: async () => null,
|
|
21
|
+
findOne: async () => null,
|
|
22
|
+
create: async (data) => ({ id: 'test-id', ...data } as ${p.name}DTO),
|
|
23
|
+
update: async (id, data) => ({ id, ...data } as ${p.name}DTO),
|
|
24
|
+
delete: async () => true,
|
|
25
|
+
count: async () => 0,
|
|
26
|
+
paginate: async () => ({ data: [], total: 0, limit: 20, offset: 0, pages: 0 }),
|
|
27
|
+
...overrides,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('${p.name}Service', () => {
|
|
32
|
+
describe('getById', () => {
|
|
33
|
+
it('lanza NotFound si el item no existe', async () => {
|
|
34
|
+
const service = new ${p.name}Service(makeRepo(), log, silentCache)
|
|
35
|
+
await expect(service.getById('no-existe')).rejects.toThrow('${p.name} no encontrado')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('retorna el item si existe', async () => {
|
|
39
|
+
const item = { id: '1' } as ${p.name}DTO
|
|
40
|
+
const service = new ${p.name}Service(makeRepo({ findById: async () => item }), log, silentCache)
|
|
41
|
+
expect(await service.getById('1')).toEqual(item)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('create', () => {
|
|
46
|
+
it('crea y retorna el item', async () => {
|
|
47
|
+
const service = new ${p.name}Service(makeRepo(), log, silentCache)
|
|
48
|
+
const result = await service.create({} as any)
|
|
49
|
+
expect(result.id).toBe('test-id')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('delete', () => {
|
|
54
|
+
it('lanza NotFound si el item no existe', async () => {
|
|
55
|
+
const service = new ${p.name}Service(makeRepo({ delete: async () => false }), log, silentCache)
|
|
56
|
+
await expect(service.delete('no-existe')).rejects.toThrow('${p.name} no encontrado')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function migrationStub(p: ModuleStubParams): string {
|
|
64
|
+
const sqlType = (f: ModuleStubParams['fields'][0]): string => {
|
|
65
|
+
if (f.name === 'id') return 'VARCHAR(36)'
|
|
66
|
+
const map: Record<string, string> = {
|
|
67
|
+
string: 'VARCHAR(255)',
|
|
68
|
+
number: 'DECIMAL(15,4)',
|
|
69
|
+
boolean: 'BOOLEAN',
|
|
70
|
+
date: 'TIMESTAMP',
|
|
71
|
+
json: 'TEXT',
|
|
72
|
+
}
|
|
73
|
+
return map[f.type] ?? 'TEXT'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const cols = p.fields.map(f => {
|
|
77
|
+
const pk = f.name === 'id' ? ' PRIMARY KEY' : ''
|
|
78
|
+
const req = f.required && f.name !== 'id' ? ' NOT NULL' : ''
|
|
79
|
+
const def = f.default !== undefined ? ` DEFAULT ${typeof f.default === 'string' ? `'${f.default}'` : f.default}` : ''
|
|
80
|
+
return ` ${f.name} ${sqlType(f)}${pk}${req}${def}`
|
|
81
|
+
}).join('\n')
|
|
82
|
+
|
|
83
|
+
return `// migrations/${Date.now()}_create_${p.module}.ts
|
|
84
|
+
// Migración generada por arckode make:module
|
|
85
|
+
// SQL ANSI — compatible con SQLite, MySQL y Postgres sin modificaciones.
|
|
86
|
+
import type { MigrationRunner } from 'arckode-framework/cli/commands/db-migrate'
|
|
87
|
+
|
|
88
|
+
export async function up(db: MigrationRunner): Promise<void> {
|
|
89
|
+
await db.run(\`
|
|
90
|
+
CREATE TABLE IF NOT EXISTS ${p.module} (
|
|
91
|
+
${cols},
|
|
92
|
+
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
93
|
+
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP${p.softDelete ? ',\n deletedAt TIMESTAMP NULL' : ''}
|
|
94
|
+
)
|
|
95
|
+
\`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function down(db: MigrationRunner): Promise<void> {
|
|
99
|
+
await db.run(\`DROP TABLE IF EXISTS ${p.module}\`)
|
|
100
|
+
}
|
|
101
|
+
`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function seedStub(p: ModuleStubParams): string {
|
|
105
|
+
const sampleData = p.fields
|
|
106
|
+
.filter(f => f.name !== 'id' && f.name !== 'createdAt' && f.name !== 'updatedAt')
|
|
107
|
+
.map(f => {
|
|
108
|
+
if (f.name === 'nombre' || f.name === 'name') return ` ${f.name}: '${p.name} de ejemplo'`
|
|
109
|
+
if (f.name === 'email') return ` ${f.name}: 'ejemplo@correo.com'`
|
|
110
|
+
if (f.name === 'activo' || f.name === 'active') return ` ${f.name}: true`
|
|
111
|
+
if (f.type === 'number') return ` ${f.name}: 100`
|
|
112
|
+
if (f.type === 'boolean') return ` ${f.name}: false`
|
|
113
|
+
return ` ${f.name}: 'valor'`
|
|
114
|
+
})
|
|
115
|
+
.join('\n')
|
|
116
|
+
|
|
117
|
+
return `// seeds/${p.module}.ts — Datos de prueba
|
|
118
|
+
import type { SeedOrm } from 'arckode-framework/cli/commands/db-seed'
|
|
119
|
+
|
|
120
|
+
export async function seed${p.name}(orm: SeedOrm): Promise<void> {
|
|
121
|
+
const items = [
|
|
122
|
+
{
|
|
123
|
+
${sampleData}
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
${sampleData.replace(`'${p.name} de ejemplo'`, `'${p.name} de ejemplo 2'`).replace(`'ejemplo@correo.com'`, `'ejemplo2@correo.com'`)}
|
|
127
|
+
},
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
await Promise.all(items.map(item => orm.create('${p.name}', item)))
|
|
131
|
+
|
|
132
|
+
console.log(' ✓ ${p.name} seeded: ' + items.length + ' items')
|
|
133
|
+
}
|
|
134
|
+
`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// NO se genera por default. Crealo a mano cuando:
|
|
138
|
+
// 1. El service repite los mismos filtros en varios métodos
|
|
139
|
+
// 2. Necesitás queries con JOIN, IN, LIKE — el ORM builtin no las soporta
|
|
140
|
+
// 3. Querés expresar la query en lenguaje del DOMINIO
|
|
141
|
+
export function repositoryStub(p: ModuleStubParams): string {
|
|
142
|
+
return `// ${p.module}/repository.ts — Repository de dominio (OPCIONAL)
|
|
143
|
+
// Encapsula queries con NOMBRES DEL DOMINIO en lugar de nombres de columna.
|
|
144
|
+
//
|
|
145
|
+
// Antes: service.findOne({ user_id: userId, currency }) ← service conoce la DB
|
|
146
|
+
// Ahora: service.findByUserAndCurrency(userId, currency) ← service habla el dominio
|
|
147
|
+
//
|
|
148
|
+
// Implementa RepositoryAdapter<${p.name}DTO> si necesitás queries complejas (JOIN, IN).
|
|
149
|
+
|
|
150
|
+
import type { RepositoryAdapter } from 'arckode-framework'
|
|
151
|
+
import type { ${p.name}DTO } from './types'
|
|
152
|
+
|
|
153
|
+
export class ${p.name}Repository {
|
|
154
|
+
constructor(private readonly base: RepositoryAdapter<${p.name}DTO>) {}
|
|
155
|
+
|
|
156
|
+
// Métodos genéricos delegados al adapter base
|
|
157
|
+
findById(id: string) { return this.base.findById(id) }
|
|
158
|
+
findMany(filters?: Record<string, unknown>) { return this.base.findMany(filters) }
|
|
159
|
+
create(data: Omit<${p.name}DTO, 'id'>) { return this.base.create(data) }
|
|
160
|
+
update(id: string, data: Partial<Omit<${p.name}DTO, 'id'>>) { return this.base.update(id, data) }
|
|
161
|
+
delete(id: string) { return this.base.delete(id) }
|
|
162
|
+
|
|
163
|
+
// ─── Métodos específicos del dominio ───
|
|
164
|
+
// Agregá acá las queries que se repiten en el service
|
|
165
|
+
//
|
|
166
|
+
// async findActiveByUser(userId: string): Promise<${p.name}DTO[]> {
|
|
167
|
+
// return this.base.findMany({ user_id: userId, active: true })
|
|
168
|
+
// }
|
|
169
|
+
}
|
|
170
|
+
`
|
|
171
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { ModuleStubParams } from './types'
|
|
2
|
+
export { mapTS } from './types'
|
|
3
|
+
export { indexStub, modelStub, typesStub, socketsStub } from './core'
|
|
4
|
+
export { serviceStub, controllerStub, validatorStub } from './service'
|
|
5
|
+
export { testStub, migrationStub, seedStub, repositoryStub } from './data'
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { ModuleStubParams } from './types'
|
|
2
|
+
|
|
3
|
+
export function serviceStub(p: ModuleStubParams): string {
|
|
4
|
+
return `// ${p.module}/service.ts — Facade pública del módulo
|
|
5
|
+
// Responsabilidad ÚNICA: casos de uso del módulo.
|
|
6
|
+
// NO sabe de HTTP. NO importa de otros módulos.
|
|
7
|
+
// Recibe dependencias por constructor (Dependency Inversion).
|
|
8
|
+
//
|
|
9
|
+
// Si este archivo supera 200 líneas → extraer casos de uso a ./usecases/{caso}.ts
|
|
10
|
+
// y dejar acá solo el orquestador que delega.
|
|
11
|
+
//
|
|
12
|
+
// IMPORTANTE: depende de RepositoryAdapter<${p.name}DTO>, no del ORM directamente.
|
|
13
|
+
// Esto permite swapear SQL → MongoDB → Prisma en composition-root.ts sin tocar este archivo.
|
|
14
|
+
|
|
15
|
+
import type { RepositoryAdapter, Logger, CacheAdapter } from 'arckode-framework'
|
|
16
|
+
import { NotFoundError } from 'arckode-framework'
|
|
17
|
+
import type { ${p.name}DTO, Create${p.name}DTO, Update${p.name}DTO, ${p.name}Query, ${p.name}Paginated } from './types'
|
|
18
|
+
import type { ${p.name}Sockets } from './sockets'
|
|
19
|
+
|
|
20
|
+
export class ${p.name}Service {
|
|
21
|
+
private sockets: ${p.name}Sockets = {}
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
private readonly repo: RepositoryAdapter<${p.name}DTO>,
|
|
25
|
+
private readonly logger: Logger,
|
|
26
|
+
private readonly cache: CacheAdapter,
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
// ACUMULA handlers — nunca pisa el anterior.
|
|
30
|
+
// Si dos conectores registran el mismo evento, ambos corren en cadena (secuencial).
|
|
31
|
+
// Para ejecución paralela independiente → usar EventBus en composition-root.ts.
|
|
32
|
+
setSockets(s: Partial<${p.name}Sockets>): void {
|
|
33
|
+
const next = s as Record<string, any>
|
|
34
|
+
const cur = this.sockets as Record<string, any>
|
|
35
|
+
for (const key of Object.keys(next)) {
|
|
36
|
+
const h = next[key]
|
|
37
|
+
if (!h) continue
|
|
38
|
+
const prev = cur[key]
|
|
39
|
+
cur[key] = prev ? async (...a: any[]) => { await prev(...a); await h(...a) } : h
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async list(query?: ${p.name}Query): Promise<${p.name}Paginated> {
|
|
44
|
+
this.logger.info('Listando ${p.module}', { query })
|
|
45
|
+
|
|
46
|
+
const page = query?.page ?? 1
|
|
47
|
+
const limit = query?.limit ?? 20
|
|
48
|
+
const offset = (page - 1) * limit
|
|
49
|
+
const filters: Record<string, unknown> = {}
|
|
50
|
+
|
|
51
|
+
${p.fields.filter(f => f.name !== 'id').slice(0, 3).map(f => `if (query?.${f.name} !== undefined) filters.${f.name} = query.${f.name}`).join('\n ')}
|
|
52
|
+
|
|
53
|
+
const result = await this.repo.paginate(filters, { limit, offset })
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
data: result.data,
|
|
57
|
+
pagination: {
|
|
58
|
+
page,
|
|
59
|
+
limit,
|
|
60
|
+
total: result.total,
|
|
61
|
+
totalPages: result.pages,
|
|
62
|
+
hasNext: offset + limit < result.total,
|
|
63
|
+
hasPrev: page > 1,
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async getById(id: string): Promise<${p.name}DTO> {
|
|
69
|
+
this.logger.info('Obteniendo ${p.module}', { id })
|
|
70
|
+
const item = await this.repo.findById(id)
|
|
71
|
+
if (!item) throw new NotFoundError('${p.name} no encontrado')
|
|
72
|
+
// IDOR: si el recurso tiene userId u ownerId, descomentar y adaptar:
|
|
73
|
+
// auth.assertOwnership(item.userId as string, currentUser.id, currentUser.role)
|
|
74
|
+
return item
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async create(dto: Create${p.name}DTO): Promise<${p.name}DTO> {
|
|
78
|
+
this.logger.info('Creando ${p.module}')
|
|
79
|
+
const item = await this.repo.create(dto as Omit<${p.name}DTO, 'id'>)
|
|
80
|
+
await this.sockets.on${p.name}Created?.(item)
|
|
81
|
+
await this.cache.delete('${p.module}:list')
|
|
82
|
+
return item
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async update(id: string, dto: Update${p.name}DTO): Promise<${p.name}DTO> {
|
|
86
|
+
this.logger.info('Actualizando ${p.module}', { id })
|
|
87
|
+
const item = await this.repo.update(id, dto as Partial<Omit<${p.name}DTO, 'id'>>)
|
|
88
|
+
if (!item) throw new NotFoundError('${p.name} no encontrado')
|
|
89
|
+
await this.sockets.on${p.name}Updated?.(item)
|
|
90
|
+
await this.cache.delete('${p.module}:list')
|
|
91
|
+
return item
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async delete(id: string): Promise<void> {
|
|
95
|
+
this.logger.info('Eliminando ${p.module}', { id })
|
|
96
|
+
const deleted = await this.repo.delete(id)
|
|
97
|
+
if (!deleted) throw new NotFoundError('${p.name} no encontrado')
|
|
98
|
+
await this.sockets.on${p.name}Deleted?.(id)
|
|
99
|
+
await this.cache.delete('${p.module}:list')
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function controllerStub(p: ModuleStubParams): string {
|
|
106
|
+
return `// ${p.module}/controller.ts — Adaptador HTTP del módulo
|
|
107
|
+
// Responsabilidad ÚNICA: traducir request → service → response.
|
|
108
|
+
// SIN lógica de negocio. SIN llamadas directas al ORM. (REGLA #12)
|
|
109
|
+
// Toda mutación (POST/PUT/PATCH) DEBE pasar por validateSchema(). (REGLA #11)
|
|
110
|
+
|
|
111
|
+
import type { HttpRequest, Logger } from 'arckode-framework'
|
|
112
|
+
import { validateSchema } from 'arckode-framework'
|
|
113
|
+
import type { ${p.name}Service } from './service'
|
|
114
|
+
import { Create${p.name}Schema, Update${p.name}Schema } from './validators/schema'
|
|
115
|
+
|
|
116
|
+
export class ${p.name}Controller {
|
|
117
|
+
constructor(
|
|
118
|
+
private readonly service: ${p.name}Service,
|
|
119
|
+
private readonly logger: Logger,
|
|
120
|
+
) {}
|
|
121
|
+
|
|
122
|
+
async index(req: HttpRequest) {
|
|
123
|
+
this.logger.info('GET /${p.module}')
|
|
124
|
+
const result = await this.service.list(req.query as any)
|
|
125
|
+
return { status: 200, body: result }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async show(req: HttpRequest) {
|
|
129
|
+
this.logger.info('GET /${p.module}/:id', { id: req.params.id })
|
|
130
|
+
const item = await this.service.getById(req.params.id)
|
|
131
|
+
return { status: 200, body: item }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async store(req: HttpRequest) {
|
|
135
|
+
this.logger.info('POST /${p.module}')
|
|
136
|
+
const data = validateSchema(Create${p.name}Schema, req.body)
|
|
137
|
+
const item = await this.service.create(data as any)
|
|
138
|
+
return { status: 201, body: item }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async update(req: HttpRequest) {
|
|
142
|
+
this.logger.info('PUT /${p.module}/:id', { id: req.params.id })
|
|
143
|
+
const data = validateSchema(Update${p.name}Schema, req.body)
|
|
144
|
+
const item = await this.service.update(req.params.id, data as any)
|
|
145
|
+
return { status: 200, body: item }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async destroy(req: HttpRequest) {
|
|
149
|
+
this.logger.info('DELETE /${p.module}/:id', { id: req.params.id })
|
|
150
|
+
await this.service.delete(req.params.id)
|
|
151
|
+
return { status: 204, body: null }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function validatorStub(p: ModuleStubParams): string {
|
|
158
|
+
const createRules = p.fields
|
|
159
|
+
.filter(f => f.name !== 'id')
|
|
160
|
+
.map(f => {
|
|
161
|
+
const parts = [` ${f.name}: { type: '${f.type}' as const`]
|
|
162
|
+
if (f.required && f.name !== 'createdAt' && f.name !== 'updatedAt') parts.push(', required: true')
|
|
163
|
+
if (f.type === 'string' && f.required) parts.push(', min: 2, max: 200')
|
|
164
|
+
parts.push(' }')
|
|
165
|
+
return parts.join('')
|
|
166
|
+
})
|
|
167
|
+
.join(',\n')
|
|
168
|
+
|
|
169
|
+
const updateRules = p.fields
|
|
170
|
+
.filter(f => f.name !== 'id' && f.name !== 'createdAt')
|
|
171
|
+
.map(f => {
|
|
172
|
+
const parts = [` ${f.name}: { type: '${f.type}' as const`]
|
|
173
|
+
if (f.type === 'string') parts.push(', min: 2, max: 200')
|
|
174
|
+
parts.push(' }')
|
|
175
|
+
return parts.join('')
|
|
176
|
+
})
|
|
177
|
+
.join(',\n')
|
|
178
|
+
|
|
179
|
+
return `// ${p.module}/validators/schema.ts — Validación de entrada
|
|
180
|
+
// Schemas planos, sin dependencias externas.
|
|
181
|
+
|
|
182
|
+
import type { ValidationRule } from 'arckode-framework'
|
|
183
|
+
|
|
184
|
+
export const Create${p.name}Schema: Record<string, ValidationRule> = {
|
|
185
|
+
${createRules}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const Update${p.name}Schema: Record<string, ValidationRule> = {
|
|
189
|
+
${updateRules}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Schema compuesto para usar en validación directa
|
|
193
|
+
export const ${p.name}Validator = {
|
|
194
|
+
create: Create${p.name}Schema,
|
|
195
|
+
update: Update${p.name}Schema,
|
|
196
|
+
}
|
|
197
|
+
`
|
|
198
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ModuleStubParams {
|
|
2
|
+
name: string
|
|
3
|
+
module: string
|
|
4
|
+
fields: { name: string; type: 'string' | 'number' | 'boolean' | 'date'; required: boolean; default?: unknown }[]
|
|
5
|
+
relations?: { type: 'hasMany' | 'belongsTo'; model: string; foreignKey: string }[]
|
|
6
|
+
softDelete?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function mapTS(type: string): string {
|
|
10
|
+
const map: Record<string, string> = { string: 'string', number: 'number', boolean: 'boolean', date: 'string' }
|
|
11
|
+
return map[type] ?? 'unknown'
|
|
12
|
+
}
|