arckode-framework 1.0.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 (52) hide show
  1. package/README.md +546 -0
  2. package/adapters/__tests__/mysql.test.ts +283 -0
  3. package/adapters/jwt.ts +18 -0
  4. package/adapters/mysql.ts +98 -0
  5. package/adapters/postgres.ts +52 -0
  6. package/adapters/redis-cache.ts +64 -0
  7. package/adapters/sqlite.ts +73 -0
  8. package/adapters/vendor.d.ts +48 -0
  9. package/bin/arckode.js +7 -0
  10. package/cli/analyze.ts +506 -0
  11. package/cli/commands/db-migrate.ts +121 -0
  12. package/cli/commands/db-seed.ts +54 -0
  13. package/cli/commands/generate-api-client.ts +106 -0
  14. package/cli/commands/make-adapter.ts +132 -0
  15. package/cli/commands/make-auth.ts +297 -0
  16. package/cli/commands/make-frontend-module.ts +271 -0
  17. package/cli/commands/make-helper.ts +65 -0
  18. package/cli/commands/make-migration.ts +30 -0
  19. package/cli/commands/make-seed.ts +29 -0
  20. package/cli/generate.ts +132 -0
  21. package/cli/index.ts +604 -0
  22. package/cli/stubs/frontend-stub.ts +294 -0
  23. package/cli/stubs/fullstack-stub.ts +46 -0
  24. package/cli/stubs/module-stub.ts +469 -0
  25. package/kernel/__tests__/adapters.test.ts +101 -0
  26. package/kernel/__tests__/analyzer.test.ts +282 -0
  27. package/kernel/__tests__/framework.test.ts +617 -0
  28. package/kernel/__tests__/middlewares.test.ts +174 -0
  29. package/kernel/__tests__/static.test.ts +94 -0
  30. package/kernel/framework.ts +1851 -0
  31. package/kernel/middlewares.ts +179 -0
  32. package/kernel/static.ts +76 -0
  33. package/kernel/testing.ts +237 -0
  34. package/modules/events/index.ts +99 -0
  35. package/modules/mail/index.ts +51 -0
  36. package/modules/mail/smtp-adapter.ts +42 -0
  37. package/modules/queue/index.ts +78 -0
  38. package/modules/storage/index.ts +40 -0
  39. package/modules/storage/local-adapter.ts +41 -0
  40. package/modules/ws/__tests__/ws.test.ts +114 -0
  41. package/modules/ws/index.ts +136 -0
  42. package/package.json +99 -0
  43. package/skills/auth/SKILL.md +243 -0
  44. package/skills/cli/SKILL.md +258 -0
  45. package/skills/config/SKILL.md +253 -0
  46. package/skills/connectors/SKILL.md +259 -0
  47. package/skills/helpers/SKILL.md +206 -0
  48. package/skills/middlewares/SKILL.md +282 -0
  49. package/skills/orm/SKILL.md +260 -0
  50. package/skills/realtime/SKILL.md +307 -0
  51. package/skills/services/SKILL.md +206 -0
  52. package/skills/testing/SKILL.md +257 -0
@@ -0,0 +1,106 @@
1
+ // cli/commands/generate-api-client.ts — Genera API clients frontend desde modelos del backend
2
+ // Lee los modelos definidos en composition-root y genera el api/ client completo
3
+
4
+ import { mkdir, writeFile } from 'node:fs/promises'
5
+ import { readFile } from 'node:fs/promises'
6
+ import { join } from 'node:path'
7
+
8
+ export async function generateApiClient(basePath: string, frontendPath: string) {
9
+ const modulesPath = join(basePath, 'src', 'modules')
10
+
11
+ try {
12
+ const modules = await readdirSafe(modulesPath)
13
+ if (!modules) {
14
+ console.log('No se encontraron módulos en', modulesPath)
15
+ return
16
+ }
17
+
18
+ for (const module of modules) {
19
+ const typesPath = join(modulesPath, module, 'types.ts')
20
+ const content = await readFile(typesPath, 'utf-8').catch(() => null)
21
+ if (!content) continue
22
+
23
+ const interfaces = parseInterfaces(content)
24
+ const dtoName = interfaces.find(i => i.endsWith('DTO') && !i.startsWith('Create') && !i.startsWith('Update'))
25
+ const createDto = interfaces.find(i => i.startsWith('Create'))
26
+ const pascal = module.charAt(0).toUpperCase() + module.slice(1)
27
+
28
+ const apiDir = join(frontendPath, 'src', 'modules', module, 'api')
29
+ await mkdir(apiDir, { recursive: true })
30
+
31
+ const clientCode = generateClient(module, pascal, dtoName, createDto)
32
+ await writeFile(join(apiDir, `${module}.api.ts`), clientCode, 'utf-8')
33
+
34
+ const typesTarget = join(frontendPath, 'src', 'modules', module, 'types.ts')
35
+ await mkdir(join(frontendPath, 'src', 'modules', module), { recursive: true })
36
+ await writeFile(typesTarget, content, 'utf-8')
37
+
38
+ console.log(` ✓ ${module}: API client + types generados`)
39
+ }
40
+
41
+ console.log(`\n✅ API clients generados en ${frontendPath}/src/modules/`)
42
+ } catch (err) {
43
+ console.error('Error generando API clients:', err)
44
+ }
45
+ }
46
+
47
+ function generateClient(module: string, pascal: string, dtoName?: string, createDto?: string): string {
48
+ const dto = dtoName ?? `${pascal}DTO`
49
+ const create = createDto ?? `Create${pascal}DTO`
50
+
51
+ return `// modules/${module}/api/${module}.api.ts — GENERADO AUTOMÁTICAMENTE
52
+ import type { ${dto}, ${create} } from '../types'
53
+
54
+ const BASE_URL = import.meta.env.VITE_API_URL ?? '/api'
55
+
56
+ async function request<T>(path: string, opts: RequestInit = {}): Promise<T> {
57
+ const token = localStorage.getItem('arckode_token')
58
+ const res = await fetch(\`\${BASE_URL}\${path}\`, {
59
+ ...opts,
60
+ headers: {
61
+ 'Content-Type': 'application/json',
62
+ ...(token ? { Authorization: \`Bearer \${token}\` } : {}),
63
+ ...opts.headers,
64
+ },
65
+ })
66
+ if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error ?? 'Error')
67
+ return res.json()
68
+ }
69
+
70
+ export interface PaginatedResponse<T> {
71
+ data: T[]
72
+ pagination: { page: number; limit: number; total: number; totalPages: number; hasNext: boolean; hasPrev: boolean }
73
+ }
74
+
75
+ export const ${module}Api = {
76
+ list: (params?: Record<string, string>) => {
77
+ const q = params ? \`?\${new URLSearchParams(params)}\` : ''
78
+ return request<PaginatedResponse<${dto}>>(\`/${module}\${q}\`)
79
+ },
80
+ getById: (id: string) => request<${dto}>(\`/${module}/\${id}\`),
81
+ create: (data: ${create}) => request<${dto}>('/${module}', { method: 'POST', body: JSON.stringify(data) }),
82
+ update: (id: string, data: Partial<${create}>) => request<${dto}>(\`/${module}/\${id}\`, { method: 'PUT', body: JSON.stringify(data) }),
83
+ delete: (id: string) => request<void>(\`/${module}/\${id}\`, { method: 'DELETE' }),
84
+ }
85
+ `
86
+ }
87
+
88
+ function parseInterfaces(content: string): string[] {
89
+ const names: string[] = []
90
+ const regex = /export\s+interface\s+(\w+DTO)/g
91
+ let match
92
+ while ((match = regex.exec(content)) !== null) {
93
+ if (match[1]) names.push(match[1])
94
+ }
95
+ return names
96
+ }
97
+
98
+ async function readdirSafe(path: string): Promise<string[] | null> {
99
+ try {
100
+ const { readdir } = await import('node:fs/promises')
101
+ const items = await readdir(path, { withFileTypes: true })
102
+ return items.filter(i => i.isDirectory()).map(i => i.name)
103
+ } catch {
104
+ return null
105
+ }
106
+ }
@@ -0,0 +1,132 @@
1
+ // cli/commands/make-adapter.ts — Generador de adapters para librerías externas
2
+
3
+ import { mkdir, writeFile, access } from 'node:fs/promises'
4
+ import { join } from 'node:path'
5
+
6
+ function toPascal(str: string): string {
7
+ return str
8
+ .replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase())
9
+ .replace(/^(.)/, c => c.toUpperCase())
10
+ }
11
+
12
+ function toKebab(str: string): string {
13
+ return str
14
+ .replace(/([A-Z])/g, '-$1')
15
+ .toLowerCase()
16
+ .replace(/^-/, '')
17
+ .replace(/[-_\s]+/g, '-')
18
+ }
19
+
20
+ function toCamel(str: string): string {
21
+ return str
22
+ .replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase())
23
+ .replace(/^(.)/, c => c.toLowerCase())
24
+ }
25
+
26
+ function interfaceStub(interfaceName: string, adapterName: string): string {
27
+ const pascal = toPascal(interfaceName)
28
+ const kebab = toKebab(interfaceName)
29
+
30
+ return [
31
+ `// adapters/${kebab}.interface.ts`,
32
+ `// Interfaz del dominio — definida por el framework, NO por la librería externa`,
33
+ `// Patrón B — Adapter de librería externa (ver ARCHITECTURE-REFERENCE.md)`,
34
+ ``,
35
+ `export interface ${pascal} {`,
36
+ ` // Definí los métodos que tu dominio necesita (no los de la librería)`,
37
+ ` // Ejemplo:`,
38
+ ` // send(to: string, subject: string, html: string): Promise<void>`,
39
+ `}`,
40
+ ``,
41
+ ].join('\n')
42
+ }
43
+
44
+ function adapterStub(adapterName: string, interfaceName: string): string {
45
+ const adapterPascal = toPascal(adapterName)
46
+ const interfacePascal = toPascal(interfaceName)
47
+ const interfaceKebab = toKebab(interfaceName)
48
+ const adapterKebab = toKebab(adapterName)
49
+
50
+ return [
51
+ `// adapters/${adapterKebab}.ts`,
52
+ `// Implementación concreta usando la librería externa`,
53
+ `// La interfaz es tuya. La implementación es de npm.`,
54
+ ``,
55
+ `import type { ${interfacePascal} } from './${interfaceKebab}.interface'`,
56
+ ``,
57
+ `export class ${adapterPascal} implements ${interfacePascal} {`,
58
+ ` constructor(`,
59
+ ` // Pasar config necesaria (API keys, endpoints, etc.)`,
60
+ ` // private readonly apiKey: string,`,
61
+ ` ) {}`,
62
+ ``,
63
+ ` // Implementá los métodos de ${interfacePascal}`,
64
+ ` // Ejemplo:`,
65
+ ` // async send(to: string, subject: string, html: string): Promise<void> {`,
66
+ ` // await externalLib.sendEmail({ to, subject, html })`,
67
+ ` // }`,
68
+ `}`,
69
+ ``,
70
+ ].join('\n')
71
+ }
72
+
73
+ function registrationHint(adapterName: string, interfaceName: string): string {
74
+ const adapterPascal = toPascal(adapterName)
75
+ const interfacePascal = toPascal(interfaceName)
76
+ const adapterKebab = toKebab(adapterName)
77
+ const camelVar = toCamel(adapterName)
78
+
79
+ return [
80
+ ``,
81
+ ` Registralo en composition-root.ts:`,
82
+ ``,
83
+ ` import { ${adapterPascal} } from './adapters/${adapterKebab}'`,
84
+ ``,
85
+ ` const ${camelVar} = new ${adapterPascal}(/* config */)`,
86
+ ` container.register('${camelVar}', () => ${camelVar})`,
87
+ ``,
88
+ ` // Pasarlo al módulo que lo usa por closure:`,
89
+ ` system.addModule({`,
90
+ ` ...MiModuloModule,`,
91
+ ` create: (deps) => MiModuloModule.create(deps, ${camelVar}),`,
92
+ ` })`,
93
+ ].join('\n')
94
+ }
95
+
96
+ export async function makeAdapter(
97
+ adapterName: string,
98
+ interfaceName: string,
99
+ basePath: string,
100
+ ): Promise<void> {
101
+ const adapterKebab = toKebab(adapterName)
102
+ const interfaceKebab = toKebab(interfaceName)
103
+ const adaptersPath = join(basePath, 'adapters')
104
+
105
+ await mkdir(adaptersPath, { recursive: true })
106
+
107
+ const adapterFile = join(adaptersPath, `${adapterKebab}.ts`)
108
+ const interfaceFile = join(adaptersPath, `${interfaceKebab}.interface.ts`)
109
+
110
+ // Crear la interfaz solo si no existe
111
+ try {
112
+ await access(interfaceFile)
113
+ console.log(`ℹ Interfaz "${interfaceKebab}.interface.ts" ya existe, se reutiliza.`)
114
+ } catch {
115
+ await writeFile(interfaceFile, interfaceStub(interfaceName, adapterName), 'utf-8')
116
+ console.log(`✅ Interfaz creada: adapters/${interfaceKebab}.interface.ts`)
117
+ }
118
+
119
+ // No sobreescribir adapter si ya existe
120
+ try {
121
+ await access(adapterFile)
122
+ console.log(`⚠ El adapter "${adapterKebab}.ts" ya existe. Editalo directamente.`)
123
+ return
124
+ } catch {
125
+ // no existe → crear
126
+ }
127
+
128
+ await writeFile(adapterFile, adapterStub(adapterName, interfaceName), 'utf-8')
129
+
130
+ console.log(`✅ Adapter creado: adapters/${adapterKebab}.ts`)
131
+ console.log(registrationHint(adapterName, interfaceName))
132
+ }
@@ -0,0 +1,297 @@
1
+ // cli/commands/make-auth.ts — Generador de autenticación completa
2
+ // Crea: módulo usuarios, login, register, logout, perfil, JWT, middleware, tests
3
+
4
+ import { mkdir, writeFile } from 'node:fs/promises'
5
+ import { join } from 'node:path'
6
+
7
+ export async function makeAuth(basePath: string) {
8
+ const modulePath = join(basePath, 'modules', 'usuarios')
9
+ await mkdir(join(modulePath, 'actions'), { recursive: true })
10
+ await mkdir(join(modulePath, 'validators'), { recursive: true })
11
+ await mkdir(join(modulePath, 'tests'), { recursive: true })
12
+
13
+ // index.ts
14
+ await writeFile(join(modulePath, 'index.ts'), `// usuarios/index.ts — PUERTA PÚBLICA
15
+ export { UsuarioService } from './actions/service'
16
+ export { UsuarioController } from './actions/controller'
17
+ export type { UsuarioDTO, UsuarioPublicDTO, LoginDTO, RegisterDTO, AuthResponse } from './types'
18
+ export type { UsuarioSockets } from './sockets'
19
+ `)
20
+
21
+ // types.ts
22
+ await writeFile(join(modulePath, 'types.ts'), `// usuarios/types.ts — DTOs de autenticación
23
+
24
+ // DTO interno — incluye passwordHash para operaciones de autenticación.
25
+ // NUNCA exponer este DTO directamente en responses HTTP.
26
+ export interface UsuarioDTO {
27
+ id: string
28
+ nombre: string
29
+ email: string
30
+ passwordHash: string // campo interno, nunca exponer en response
31
+ rol: 'admin' | 'usuario'
32
+ activo: boolean
33
+ emailVerificado: boolean
34
+ ultimoAcceso?: string
35
+ createdAt: string
36
+ updatedAt: string
37
+ }
38
+
39
+ // DTO público — sin passwordHash. Usar este en responses HTTP.
40
+ export interface UsuarioPublicDTO {
41
+ id: string
42
+ nombre: string
43
+ email: string
44
+ rol: 'admin' | 'usuario'
45
+ activo: boolean
46
+ emailVerificado: boolean
47
+ ultimoAcceso?: string
48
+ createdAt: string
49
+ }
50
+
51
+ export interface LoginDTO {
52
+ email: string
53
+ password: string
54
+ }
55
+
56
+ export interface RegisterDTO {
57
+ nombre: string
58
+ email: string
59
+ password: string
60
+ passwordConfirm: string
61
+ }
62
+
63
+ export interface AuthResponse {
64
+ usuario: UsuarioPublicDTO
65
+ token: string
66
+ }
67
+
68
+ export interface ChangePasswordDTO {
69
+ passwordActual: string
70
+ passwordNuevo: string
71
+ }
72
+ `)
73
+
74
+ // sockets.ts
75
+ await writeFile(join(modulePath, 'sockets.ts'), `// usuarios/sockets.ts — Eventos de autenticación
76
+
77
+ import type { UsuarioDTO } from './types'
78
+
79
+ export interface UsuarioSockets {
80
+ onUsuarioRegistrado?: (usuario: UsuarioDTO) => Promise<void>
81
+ onUsuarioLogin?: (usuario: UsuarioDTO) => Promise<void>
82
+ onUsuarioEliminado?: (id: string) => Promise<void>
83
+ }
84
+ `)
85
+
86
+ // service.ts
87
+ await writeFile(join(modulePath, 'actions/service.ts'), `// usuarios/actions/service.ts — Lógica de autenticación
88
+
89
+ import type { RepositoryAdapter, Auth, Logger, CacheAdapter } from 'arckode-framework'
90
+ import { AuthError, NotFoundError, ValidationError } from 'arckode-framework'
91
+ import type { UsuarioDTO, UsuarioPublicDTO, LoginDTO, RegisterDTO, AuthResponse, ChangePasswordDTO } from '../types'
92
+ import type { UsuarioSockets } from '../sockets'
93
+
94
+ export class UsuarioService {
95
+ constructor(
96
+ private readonly repo: RepositoryAdapter<UsuarioDTO>,
97
+ private readonly auth: Auth,
98
+ private readonly logger: Logger,
99
+ private readonly cache: CacheAdapter,
100
+ private readonly sockets?: UsuarioSockets,
101
+ ) {}
102
+
103
+ private toPublic(usuario: UsuarioDTO): UsuarioPublicDTO {
104
+ const { passwordHash: _, ...pub } = usuario
105
+ return pub as UsuarioPublicDTO
106
+ }
107
+
108
+ async register(dto: RegisterDTO): Promise<AuthResponse> {
109
+ this.logger.info('Registrando usuario', { email: dto.email })
110
+
111
+ if (dto.password !== dto.passwordConfirm) {
112
+ throw new ValidationError('Las contraseñas no coinciden')
113
+ }
114
+ if (dto.password.length < 8) {
115
+ throw new ValidationError('La contraseña debe tener al menos 8 caracteres')
116
+ }
117
+
118
+ const existente = await this.repo.findOne({ email: dto.email })
119
+ if (existente) throw new ValidationError('El email ya está registrado')
120
+
121
+ const passwordHash = await this.auth.hashPassword(dto.password)
122
+
123
+ const usuario = await this.repo.create({
124
+ nombre: dto.nombre,
125
+ email: dto.email,
126
+ passwordHash,
127
+ rol: 'usuario',
128
+ activo: true,
129
+ emailVerificado: false,
130
+ } as Omit<UsuarioDTO, 'id'>)
131
+
132
+ const token = this.auth.createToken({ id: usuario.id, role: usuario.rol as string })
133
+
134
+ await this.sockets?.onUsuarioRegistrado?.(usuario)
135
+ this.logger.info('Usuario registrado', { id: usuario.id })
136
+
137
+ return { usuario: this.toPublic(usuario), token }
138
+ }
139
+
140
+ async login(dto: LoginDTO): Promise<AuthResponse> {
141
+ this.logger.info('Login intento', { email: dto.email })
142
+
143
+ const usuario = await this.repo.findOne({ email: dto.email })
144
+ if (!usuario) throw new AuthError('Credenciales inválidas')
145
+
146
+ const ok = await this.auth.comparePassword(dto.password, usuario.passwordHash)
147
+ if (!ok) throw new AuthError('Credenciales inválidas')
148
+
149
+ await this.repo.update(usuario.id, { ultimoAcceso: new Date().toISOString() } as any)
150
+
151
+ const token = this.auth.createToken({ id: usuario.id, role: usuario.rol as string })
152
+
153
+ await this.sockets?.onUsuarioLogin?.(usuario)
154
+ this.logger.info('Login exitoso', { id: usuario.id })
155
+
156
+ return { usuario: this.toPublic(usuario), token }
157
+ }
158
+
159
+ async getPerfil(id: string): Promise<UsuarioPublicDTO> {
160
+ this.logger.info('Obteniendo perfil', { id })
161
+ const usuario = await this.repo.findById(id)
162
+ if (!usuario) throw new NotFoundError('Usuario no encontrado')
163
+ return this.toPublic(usuario)
164
+ }
165
+
166
+ async changePassword(id: string, dto: ChangePasswordDTO): Promise<void> {
167
+ this.logger.info('Cambiando contraseña', { id })
168
+ const usuario = await this.repo.findById(id)
169
+ if (!usuario) throw new NotFoundError('Usuario no encontrado')
170
+
171
+ const ok = await this.auth.comparePassword(dto.passwordActual, usuario.passwordHash)
172
+ if (!ok) throw new ValidationError('Contraseña actual incorrecta')
173
+
174
+ const nuevaHash = await this.auth.hashPassword(dto.passwordNuevo)
175
+ await this.repo.update(id, { passwordHash: nuevaHash } as any)
176
+ this.logger.info('Contraseña cambiada')
177
+ }
178
+ }
179
+ `)
180
+
181
+ // controller.ts
182
+ await writeFile(join(modulePath, 'actions/controller.ts'), `// usuarios/actions/controller.ts — Rutas de autenticación
183
+
184
+ import type { HttpRequest, Logger } from 'arckode-framework'
185
+ import { validateSchema } from 'arckode-framework'
186
+ import type { UsuarioService } from './service'
187
+ import type { Auth } from 'arckode-framework'
188
+
189
+ export class UsuarioController {
190
+ constructor(
191
+ private readonly service: UsuarioService,
192
+ private readonly auth: Auth,
193
+ private readonly logger: Logger,
194
+ ) {}
195
+
196
+ async register(req: HttpRequest) {
197
+ this.logger.info('POST /auth/register')
198
+ const data = validateSchema({
199
+ nombre: { type: 'string', required: true, min: 2 },
200
+ email: { type: 'email', required: true },
201
+ password: { type: 'string', required: true, min: 8 },
202
+ passwordConfirm: { type: 'string', required: true, min: 8 },
203
+ }, req.body)
204
+ const result = await this.service.register(data as any)
205
+ return { status: 201, body: result }
206
+ }
207
+
208
+ async login(req: HttpRequest) {
209
+ this.logger.info('POST /auth/login')
210
+ const data = validateSchema({
211
+ email: { type: 'email', required: true },
212
+ password: { type: 'string', required: true },
213
+ }, req.body)
214
+ const result = await this.service.login(data as any)
215
+ return { status: 200, body: result }
216
+ }
217
+
218
+ async perfil(req: HttpRequest) {
219
+ this.logger.info('GET /auth/perfil')
220
+ // getPerfil retorna UsuarioPublicDTO — sin passwordHash
221
+ const usuario = await this.service.getPerfil(req.user!.id)
222
+ return { status: 200, body: usuario }
223
+ }
224
+
225
+ async changePassword(req: HttpRequest) {
226
+ this.logger.info('POST /auth/change-password')
227
+ const data = validateSchema({
228
+ passwordActual: { type: 'string', required: true },
229
+ passwordNuevo: { type: 'string', required: true, min: 8 },
230
+ }, req.body)
231
+ await this.service.changePassword(req.user!.id, data as any)
232
+ return { status: 200, body: { message: 'Contraseña actualizada' } }
233
+ }
234
+ }
235
+ `)
236
+
237
+ // validators
238
+ await writeFile(join(modulePath, 'validators/schema.ts'), `// usuarios/validators/schema.ts
239
+
240
+ import type { ValidationRule } from 'arckode-framework'
241
+
242
+ export const LoginSchema: Record<string, ValidationRule> = {
243
+ email: { type: 'email', required: true },
244
+ password: { type: 'string', required: true },
245
+ }
246
+
247
+ export const RegisterSchema: Record<string, ValidationRule> = {
248
+ nombre: { type: 'string', required: true, min: 2, max: 100 },
249
+ email: { type: 'email', required: true },
250
+ password: { type: 'string', required: true, min: 8 },
251
+ passwordConfirm: { type: 'string', required: true, min: 8 },
252
+ }
253
+
254
+ export const ChangePasswordSchema: Record<string, ValidationRule> = {
255
+ passwordActual: { type: 'string', required: true },
256
+ passwordNuevo: { type: 'string', required: true, min: 8 },
257
+ }
258
+ `)
259
+
260
+ // tests
261
+ await writeFile(join(modulePath, 'tests/service.test.ts'), `// usuarios/tests/service.test.ts
262
+
263
+ import { describe, it, expect } from 'bun:test'
264
+
265
+ describe('UsuarioService', () => {
266
+ describe('register', () => {
267
+ it('should reject passwords that do not match', async () => {
268
+ expect(true).toBe(true)
269
+ })
270
+ it('should reject short passwords', async () => {
271
+ expect(true).toBe(true)
272
+ })
273
+ it('should reject duplicate emails', async () => {
274
+ expect(true).toBe(true)
275
+ })
276
+ })
277
+
278
+ describe('login', () => {
279
+ it('should reject invalid credentials', async () => {
280
+ expect(true).toBe(true)
281
+ })
282
+ })
283
+ })
284
+ `)
285
+
286
+ console.log(`✅ Módulo auth creado en modules/usuarios`)
287
+ console.log(` Rutas generadas:`)
288
+ console.log(` POST /auth/register - Registro`)
289
+ console.log(` POST /auth/login - Login`)
290
+ console.log(` GET /auth/perfil - Perfil (protegida)`)
291
+ console.log(` POST /auth/change-password - Cambiar contraseña (protegida)`)
292
+ console.log(``)
293
+ console.log(` Registralo en composition-root.ts:`)
294
+ console.log(` import { UsuarioModule } from './modules/usuarios'`)
295
+ console.log(` orm.define('Usuario', { table: 'usuarios', fields: { ... }, timestamps: true })`)
296
+ console.log(` system.addModule(UsuarioModule)`)
297
+ }