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.
Files changed (65) hide show
  1. package/adapters/jwt.ts +6 -4
  2. package/adapters/mysql.ts +7 -2
  3. package/adapters/postgres.ts +37 -0
  4. package/adapters/sqlite.ts +7 -1
  5. package/adapters/vendor.d.ts +48 -0
  6. package/cli/analyze/checks.ts +333 -0
  7. package/cli/analyze/index.ts +44 -0
  8. package/cli/analyze/report.ts +107 -0
  9. package/cli/analyze/types.ts +46 -0
  10. package/cli/analyze/utils.ts +36 -0
  11. package/cli/analyze.ts +2 -647
  12. package/cli/commands/db-migrate.ts +213 -89
  13. package/cli/commands/db-seed.ts +97 -32
  14. package/cli/commands/db-utils.ts +192 -0
  15. package/cli/commands/new.ts +175 -0
  16. package/cli/commands/routes.ts +94 -0
  17. package/cli/index.ts +57 -404
  18. package/cli/stubs/claude-md-stub.ts +21 -8
  19. package/cli/stubs/module/core.ts +162 -0
  20. package/cli/stubs/module/data.ts +171 -0
  21. package/cli/stubs/module/index.ts +5 -0
  22. package/cli/stubs/module/service.ts +198 -0
  23. package/cli/stubs/module/types.ts +12 -0
  24. package/cli/stubs/module-stub.ts +2 -552
  25. package/kernel/auth.ts +114 -0
  26. package/kernel/cache.ts +37 -0
  27. package/kernel/config.ts +129 -0
  28. package/kernel/container.ts +64 -0
  29. package/kernel/db/orm-migrate.ts +136 -0
  30. package/kernel/db/orm-repository.ts +45 -0
  31. package/kernel/db/orm-utils.ts +93 -0
  32. package/kernel/db/orm.ts +254 -0
  33. package/kernel/db/transactor.ts +17 -0
  34. package/kernel/db/types.ts +72 -0
  35. package/kernel/errors.ts +102 -0
  36. package/kernel/framework.default.ts +41 -0
  37. package/kernel/framework.ts +8 -2144
  38. package/kernel/http/router.ts +131 -0
  39. package/kernel/http/server.ts +303 -0
  40. package/kernel/http/types.ts +56 -0
  41. package/kernel/index.ts +25 -0
  42. package/kernel/logger.ts +50 -0
  43. package/kernel/middlewares.ts +38 -21
  44. package/kernel/modules/create-module.ts +5 -0
  45. package/kernel/modules/system.ts +149 -0
  46. package/kernel/modules/types.ts +46 -0
  47. package/kernel/seeds.ts +48 -0
  48. package/kernel/static.ts +11 -2
  49. package/kernel/testing.ts +8 -3
  50. package/kernel/validator.ts +116 -0
  51. package/modules/events/index.ts +19 -3
  52. package/modules/mail/index.ts +14 -2
  53. package/modules/storage/local-adapter.ts +19 -5
  54. package/modules/ws/index.ts +123 -18
  55. package/package.json +8 -11
  56. package/skills/auth/SKILL.md +36 -220
  57. package/skills/cli/SKILL.md +32 -251
  58. package/skills/config/SKILL.md +30 -239
  59. package/skills/connectors/SKILL.md +32 -295
  60. package/skills/helpers/SKILL.md +26 -195
  61. package/skills/middlewares/SKILL.md +30 -267
  62. package/skills/orm/SKILL.md +42 -349
  63. package/skills/realtime/SKILL.md +22 -297
  64. package/skills/services/SKILL.md +40 -183
  65. 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
+ }