jerkjs 2.5.2 → 2.5.6
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/BENCHMARK_RESULTS.md +60 -0
- package/CHANGELOG.md +87 -137
- package/README.md +108 -454
- package/README_LEGACY.md +513 -0
- package/auditoria_jerkjs.md +91 -0
- package/doc-2.5/SESSION_SECURITY_FLAGS.md +174 -0
- package/doc-2.5/an/303/241lisis-completo-jerk-framework.md +213 -0
- package/doc-2.5/manual-mvc-completo.md +934 -0
- package/docs/MANUAL_CONTROLLER_VIEW_MODEL.md +310 -0
- package/docs/SERVER_OPTIMIZATION_NOTES.md +87 -0
- package/jerk2.5.webp +0 -0
- package/lib/core/server.js +64 -53
- package/lib/middleware/session.js +11 -3
- package/lib/mvc/controllerBase.js +100 -32
- package/lib/router/RouteMatcher.js +45 -10
- package/package.json +13 -5
- package/example-directory-loader.js +0 -46
- package/examples/examples.arj +0 -0
- package/qa/informe_qa_fix_enrutamiento.md +0 -93
- package/utils/find_file_path.sh +0 -36
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
# Manual Completo: Desarrollo de Aplicaciones MVC en JERK Framework v2.5.3
|
|
2
|
+
|
|
3
|
+
## Tabla de Contenidos
|
|
4
|
+
1. [Desarrollo MVC](#desarrollo-mvc)
|
|
5
|
+
- [Crear Controlador](#crear-controlador)
|
|
6
|
+
- [Crear Modelo](#crear-modelo)
|
|
7
|
+
- [Crear Vista](#crear-vista)
|
|
8
|
+
- [Registrar Rutas](#registrar-rutas)
|
|
9
|
+
2. [Sistema de Hooks y Filters](#sistema-de-hooks-y-filters)
|
|
10
|
+
- [Registrar Hooks](#registrar-hooks)
|
|
11
|
+
- [Registrar Filters](#registrar-filters)
|
|
12
|
+
3. [Creación de Extensiones](#creación-de-extensiones)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Desarrollo MVC
|
|
17
|
+
|
|
18
|
+
### Crear Controlador
|
|
19
|
+
|
|
20
|
+
Para crear un controlador en JERK Framework, debes extender la clase `ControllerBase`:
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
// controllers/EjemploController.js
|
|
24
|
+
const { ControllerBase } = require('jerkjs');
|
|
25
|
+
|
|
26
|
+
class EjemploController extends ControllerBase {
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Método para mostrar la lista de elementos
|
|
32
|
+
async index(req, res) {
|
|
33
|
+
try {
|
|
34
|
+
// Cargar el modelo con el adaptador desde la solicitud
|
|
35
|
+
const adapter = req.modelManager?.getAdapter('memory') || null;
|
|
36
|
+
const modelo = await this.loadModel('EjemploModel', { adapter });
|
|
37
|
+
|
|
38
|
+
// Obtener datos del modelo
|
|
39
|
+
const elementos = await modelo.getAll();
|
|
40
|
+
|
|
41
|
+
// Renderizar vista con datos
|
|
42
|
+
res.render('ejemplo/index', {
|
|
43
|
+
title: 'Lista de Elementos',
|
|
44
|
+
elementos: elementos
|
|
45
|
+
});
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Error en el controlador:', error);
|
|
48
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
49
|
+
res.end(JSON.stringify({ error: 'Error interno del servidor' }));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Método para mostrar un elemento específico
|
|
54
|
+
async show(req, res) {
|
|
55
|
+
try {
|
|
56
|
+
const adapter = req.modelManager?.getAdapter('memory') || null;
|
|
57
|
+
const modelo = await this.loadModel('EjemploModel', { adapter });
|
|
58
|
+
|
|
59
|
+
const id = req.params.id;
|
|
60
|
+
const elemento = await modelo.getById(id);
|
|
61
|
+
|
|
62
|
+
if (!elemento) {
|
|
63
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
64
|
+
res.end(JSON.stringify({ error: 'Elemento no encontrado' }));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
res.render('ejemplo/show', {
|
|
69
|
+
title: `Elemento #${id}`,
|
|
70
|
+
elemento: elemento
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('Error en el controlador:', error);
|
|
74
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
75
|
+
res.end(JSON.stringify({ error: 'Error interno del servidor' }));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Método para crear un nuevo elemento
|
|
80
|
+
async create(req, res) {
|
|
81
|
+
try {
|
|
82
|
+
const adapter = req.modelManager?.getAdapter('memory') || null;
|
|
83
|
+
const modelo = await this.loadModel('EjemploModel', { adapter });
|
|
84
|
+
|
|
85
|
+
const datos = req.body;
|
|
86
|
+
const id = await modelo.create(datos);
|
|
87
|
+
|
|
88
|
+
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
89
|
+
res.end(JSON.stringify({
|
|
90
|
+
success: true,
|
|
91
|
+
id: id,
|
|
92
|
+
message: 'Elemento creado exitosamente'
|
|
93
|
+
}));
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('Error en el controlador:', error);
|
|
96
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
97
|
+
res.end(JSON.stringify({ error: 'Error interno del servidor' }));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Crear instancia y preservar contexto
|
|
103
|
+
const ejemploController = new EjemploController();
|
|
104
|
+
ejemploController.index = ejemploController.index.bind(ejemploController);
|
|
105
|
+
ejemploController.show = ejemploController.show.bind(ejemploController);
|
|
106
|
+
ejemploController.create = ejemploController.create.bind(ejemploController);
|
|
107
|
+
|
|
108
|
+
module.exports = ejemploController;
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### Características del Controlador:
|
|
112
|
+
- Extiende `ControllerBase` para acceder a funcionalidades MVC
|
|
113
|
+
- Preserva el contexto `this` usando `bind()`
|
|
114
|
+
- Accede al modelManager a través de `req.modelManager`
|
|
115
|
+
- Utiliza `loadModel()` para cargar modelos con adaptadores
|
|
116
|
+
- Usa `res.render()` para mostrar vistas
|
|
117
|
+
|
|
118
|
+
### Uso de loadModel
|
|
119
|
+
|
|
120
|
+
El método `loadModel()` es fundamental para cargar modelos en los controladores. Su uso correcto es esencial para el funcionamiento del sistema MVC:
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
// Cargar un modelo sin opciones adicionales
|
|
124
|
+
const modelo = await this.loadModel('NombreDelModelo');
|
|
125
|
+
|
|
126
|
+
// Cargar un modelo con un adaptador específico
|
|
127
|
+
const adapter = req.modelManager?.getAdapter('memory') || null;
|
|
128
|
+
const modelo = await this.loadModel('NombreDelModelo', {
|
|
129
|
+
adapter: adapter
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Cargar un modelo con opciones adicionales
|
|
133
|
+
const modelo = await this.loadModel('NombreDelModelo', {
|
|
134
|
+
adapter: adapter,
|
|
135
|
+
tableName: 'nombre_tabla',
|
|
136
|
+
logger: console
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Cargar un modelo desde un directorio personalizado
|
|
140
|
+
const modelo = await this.loadModel('NombreDelModelo', {
|
|
141
|
+
adapter: adapter,
|
|
142
|
+
modelDir: './mi-estructura-especial/models/usuarios'
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### Características de loadModel:
|
|
147
|
+
- **Búsqueda inteligente**: Busca el modelo en múltiples ubicaciones posibles (`./models/`, `../models/`, etc.)
|
|
148
|
+
- **Reutilización de instancias**: Si ya existe una instancia del modelo en el controlador, la reutiliza
|
|
149
|
+
- **Gestión de adaptadores**: Permite asignar un adaptador de base de datos al modelo
|
|
150
|
+
- **Caché interno**: Mantiene una caché de instancias para mejorar el rendimiento
|
|
151
|
+
|
|
152
|
+
#### Ejemplo completo de uso en un controlador:
|
|
153
|
+
```javascript
|
|
154
|
+
async index(req, res) {
|
|
155
|
+
try {
|
|
156
|
+
// Obtener el adaptador desde el modelManager en la solicitud
|
|
157
|
+
const adapter = req.modelManager?.getAdapter('memory') || null;
|
|
158
|
+
|
|
159
|
+
// Cargar el modelo con el adaptador
|
|
160
|
+
const userModel = await this.loadModel('UserModel', {
|
|
161
|
+
adapter: adapter
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Utilizar el modelo para obtener datos
|
|
165
|
+
const users = await userModel.getAll();
|
|
166
|
+
|
|
167
|
+
// Renderizar la vista con los datos
|
|
168
|
+
res.render('users/index', {
|
|
169
|
+
title: 'Lista de Usuarios',
|
|
170
|
+
users: users
|
|
171
|
+
});
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('Error en el controlador:', error);
|
|
174
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
175
|
+
res.end(JSON.stringify({ error: 'Error interno del servidor' }));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### Convenciones de nomenclatura:
|
|
181
|
+
- El nombre del modelo en `loadModel()` debe coincidir con el nombre del archivo del modelo
|
|
182
|
+
- Si el archivo es `UserModel.js`, usar `'UserModel'` como parámetro
|
|
183
|
+
- El sistema buscará en `./models/UserModel.js`, `../models/UserModel.js`, etc.
|
|
184
|
+
|
|
185
|
+
#### Carga de modelos en subdirectorios o ubicaciones personalizadas:
|
|
186
|
+
|
|
187
|
+
Cuando los modelos están en subdirectorios o ubicaciones no estándar, puedes usar las siguientes estrategias:
|
|
188
|
+
|
|
189
|
+
1. **Importar directamente y pasar la clase al controlador**:
|
|
190
|
+
```javascript
|
|
191
|
+
// En el controlador
|
|
192
|
+
const UserModel = require('../path/a/tu/modelo/UserModel');
|
|
193
|
+
const modelo = new UserModel(adapter);
|
|
194
|
+
// Usar el modelo directamente sin loadModel()
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
2. **Modificar el sistema de búsqueda de loadModel** (requiere extensión):
|
|
198
|
+
```javascript
|
|
199
|
+
// Puedes extender ControllerBase para personalizar la búsqueda
|
|
200
|
+
class MiControllerBase extends ControllerBase {
|
|
201
|
+
async loadModel(modelName, options = {}) {
|
|
202
|
+
// Lógica personalizada de búsqueda
|
|
203
|
+
// Buscar en ubicaciones personalizadas
|
|
204
|
+
try {
|
|
205
|
+
// Intentar rutas personalizadas
|
|
206
|
+
const customPath = `./mis-modelos/especializados/${modelName}`;
|
|
207
|
+
const ModelClass = require(customPath);
|
|
208
|
+
return new ModelClass(options);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
// Si falla, usar la lógica estándar
|
|
211
|
+
return super.loadModel(modelName, options);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
3. **Usar rutas absolutas o relativas específicas**:
|
|
218
|
+
```javascript
|
|
219
|
+
// En lugar de usar loadModel, importar directamente
|
|
220
|
+
const { UserModel } = require('../../estructura-compleja/usuarios/UserModel');
|
|
221
|
+
// Crear instancia manualmente
|
|
222
|
+
const userModel = new UserModel({ adapter: req.modelManager?.getAdapter('memory') });
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
5. **Usar la nueva funcionalidad de directorio personalizado**:
|
|
226
|
+
```javascript
|
|
227
|
+
// Cargar un modelo desde un directorio personalizado
|
|
228
|
+
const adapter = req.modelManager?.getAdapter('memory') || null;
|
|
229
|
+
const modelo = await this.loadModel('MiModelo', {
|
|
230
|
+
adapter: adapter,
|
|
231
|
+
modelDir: './estructura-personalizada/modelos/usuarios'
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Esto buscará en: ./estructura-personalizada/modelos/usuarios/MiModelo
|
|
235
|
+
// y en: ./estructura-personalizada/modelos/usuarios/MiModelo.js
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
6. **Estructura recomendada para proyectos complejos**:
|
|
239
|
+
```
|
|
240
|
+
mi-proyecto/
|
|
241
|
+
├── controllers/
|
|
242
|
+
│ └── usuarios/
|
|
243
|
+
│ └── UsuarioController.js
|
|
244
|
+
├── models/
|
|
245
|
+
│ └── usuarios/
|
|
246
|
+
│ └── UsuarioModel.js
|
|
247
|
+
├── views/
|
|
248
|
+
│ └── usuarios/
|
|
249
|
+
│ └── index.html
|
|
250
|
+
└── routes/
|
|
251
|
+
└── usuarios-routes.json
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
En este caso, el sistema de búsqueda de `loadModel` debería encontrar el modelo si está en `./models/usuarios/UsuarioModel.js` cuando se llama desde un controlador en `./controllers/usuarios/`.
|
|
255
|
+
|
|
256
|
+
### Crear Modelo
|
|
257
|
+
|
|
258
|
+
Para crear un modelo en JERK Framework, debes extender la clase `ModelBase`:
|
|
259
|
+
|
|
260
|
+
```javascript
|
|
261
|
+
// models/EjemploModel.js
|
|
262
|
+
const { ModelBase } = require('jerkjs');
|
|
263
|
+
|
|
264
|
+
class EjemploModel extends ModelBase {
|
|
265
|
+
constructor(options = {}) {
|
|
266
|
+
super(options);
|
|
267
|
+
this.tableName = 'ejemplos'; // Nombre de la tabla en la base de datos
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Método para obtener todos los registros
|
|
271
|
+
async getAll() {
|
|
272
|
+
return await this.find({}, { orderBy: 'id', orderDirection: 'ASC' });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Método para obtener un registro por ID
|
|
276
|
+
async getById(id) {
|
|
277
|
+
return await this.findOne({ id: parseInt(id) });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Método para crear un nuevo registro
|
|
281
|
+
async create(datos) {
|
|
282
|
+
return await super.create(datos);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Método para actualizar un registro
|
|
286
|
+
async update(id, datos) {
|
|
287
|
+
return await super.update({ id: parseInt(id) }, datos);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Método para eliminar un registro
|
|
291
|
+
async delete(id) {
|
|
292
|
+
return await super.delete({ id: parseInt(id) });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Método personalizado para búsqueda
|
|
296
|
+
async findByField(fieldName, value) {
|
|
297
|
+
const conditions = {};
|
|
298
|
+
conditions[fieldName] = value;
|
|
299
|
+
return await this.find(conditions);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = EjemploModel;
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### Características del Modelo:
|
|
307
|
+
- Extiende `ModelBase` para acceder a funcionalidades CRUD
|
|
308
|
+
- Define el nombre de la tabla con `this.tableName`
|
|
309
|
+
- Utiliza métodos heredados como `find()`, `findOne()`, `create()`, etc.
|
|
310
|
+
- Puede incluir métodos personalizados para operaciones específicas
|
|
311
|
+
- Se integra con adaptadores de base de datos
|
|
312
|
+
|
|
313
|
+
### Crear Vista
|
|
314
|
+
|
|
315
|
+
Las vistas en JERK Framework utilizan un sistema de plantillas con soporte para variables dinámicas:
|
|
316
|
+
|
|
317
|
+
```html
|
|
318
|
+
<!-- views/ejemplo/index.html -->
|
|
319
|
+
<!DOCTYPE html>
|
|
320
|
+
<html lang="es">
|
|
321
|
+
<head>
|
|
322
|
+
<meta charset="UTF-8">
|
|
323
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
324
|
+
<title>{{title}}</title>
|
|
325
|
+
<style>
|
|
326
|
+
body { font-family: Arial, sans-serif; margin: 40px; }
|
|
327
|
+
.item { border: 1px solid #ccc; padding: 10px; margin: 10px 0; }
|
|
328
|
+
.btn { padding: 8px 16px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; margin: 5px; display: inline-block; }
|
|
329
|
+
</style>
|
|
330
|
+
</head>
|
|
331
|
+
<body>
|
|
332
|
+
<h1>{{title}}</h1>
|
|
333
|
+
|
|
334
|
+
<div class="actions">
|
|
335
|
+
<a href="/api/ejemplos/create" class="btn">Crear Nuevo</a>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
<div class="items-list">
|
|
339
|
+
{{#each elementos}}
|
|
340
|
+
<div class="item">
|
|
341
|
+
<h3>{{this.nombre}}</h3>
|
|
342
|
+
<p>ID: {{this.id}}</p>
|
|
343
|
+
<p>Descripción: {{this.descripcion}}</p>
|
|
344
|
+
<a href="/api/ejemplos/{{this.id}}" class="btn">Ver Detalles</a>
|
|
345
|
+
</div>
|
|
346
|
+
{{/each}}
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
{{#unless elementos}}
|
|
350
|
+
<p>No hay elementos disponibles.</p>
|
|
351
|
+
{{/unless}}
|
|
352
|
+
</body>
|
|
353
|
+
</html>
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
#### Características de las Vistas:
|
|
357
|
+
- Soporte para variables dinámicas con sintaxis `{{variable}}`
|
|
358
|
+
- Soporte para estructuras condicionales y bucles con Handlebars
|
|
359
|
+
- Integración con layouts para mantener consistencia visual
|
|
360
|
+
- Pueden recibir datos desde los controladores
|
|
361
|
+
|
|
362
|
+
### Registrar Rutas
|
|
363
|
+
|
|
364
|
+
Existen tres formas de registrar rutas en JERK Framework:
|
|
365
|
+
|
|
366
|
+
#### 1. Programáticamente
|
|
367
|
+
|
|
368
|
+
```javascript
|
|
369
|
+
// server.js
|
|
370
|
+
const { APIServer } = require('jerkjs');
|
|
371
|
+
|
|
372
|
+
const server = new APIServer({
|
|
373
|
+
port: 3000,
|
|
374
|
+
host: 'localhost'
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Importar controladores
|
|
378
|
+
const ejemploController = require('./controllers/EjemploController');
|
|
379
|
+
|
|
380
|
+
// Registrar rutas programáticamente
|
|
381
|
+
server.addRoute('GET', '/api/ejemplos', ejemploController.index);
|
|
382
|
+
server.addRoute('GET', '/api/ejemplos/:id', ejemploController.show);
|
|
383
|
+
server.addRoute('POST', '/api/ejemplos', ejemploController.create);
|
|
384
|
+
|
|
385
|
+
server.start();
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
#### 2. Usando routes.json
|
|
389
|
+
|
|
390
|
+
```json
|
|
391
|
+
// routes.json
|
|
392
|
+
[
|
|
393
|
+
{
|
|
394
|
+
"path": "/api/ejemplos",
|
|
395
|
+
"method": "GET",
|
|
396
|
+
"controller": "./controllers/EjemploController.js",
|
|
397
|
+
"handler": "index",
|
|
398
|
+
"contentType": "text/html"
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
"path": "/api/ejemplos/:id",
|
|
402
|
+
"method": "GET",
|
|
403
|
+
"controller": "./controllers/EjemploController.js",
|
|
404
|
+
"handler": "show",
|
|
405
|
+
"contentType": "text/html"
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
"path": "/api/ejemplos",
|
|
409
|
+
"method": "POST",
|
|
410
|
+
"controller": "./controllers/EjemploController.js",
|
|
411
|
+
"handler": "create",
|
|
412
|
+
"contentType": "application/json"
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
"path": "/api/ejemplos/static",
|
|
416
|
+
"method": "GET",
|
|
417
|
+
"static": {
|
|
418
|
+
"dir": "./public",
|
|
419
|
+
"index": ["index.html"],
|
|
420
|
+
"cacheControl": "public, max-age=3600"
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
]
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
```javascript
|
|
427
|
+
// Cargar rutas desde archivo JSON
|
|
428
|
+
const { APIServer, RouteLoader } = require('jerkjs');
|
|
429
|
+
|
|
430
|
+
const server = new APIServer({ port: 3000 });
|
|
431
|
+
const routeLoader = new RouteLoader();
|
|
432
|
+
|
|
433
|
+
routeLoader.loadRoutes(server, './routes.json')
|
|
434
|
+
.then(() => server.start())
|
|
435
|
+
.catch(error => console.error('Error:', error.message));
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
#### 3. Usando Múltiples Ficheros de Ruta
|
|
439
|
+
|
|
440
|
+
```javascript
|
|
441
|
+
// Cargar rutas desde directorio
|
|
442
|
+
const { APIServer, RouteDirectoryLoader } = require('jerkjs');
|
|
443
|
+
|
|
444
|
+
const server = new APIServer({ port: 3000 });
|
|
445
|
+
const routeDirectoryLoader = new RouteDirectoryLoader();
|
|
446
|
+
|
|
447
|
+
routeDirectoryLoader.loadRoutesFromDirectory(server, './routes')
|
|
448
|
+
.then(routes => {
|
|
449
|
+
console.log(`${routes.length} rutas cargadas`);
|
|
450
|
+
server.start();
|
|
451
|
+
})
|
|
452
|
+
.catch(error => console.error('Error:', error.message));
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
```json
|
|
456
|
+
// routes/ejemplos-routes.json
|
|
457
|
+
[
|
|
458
|
+
{
|
|
459
|
+
"path": "/api/ejemplos",
|
|
460
|
+
"method": "GET",
|
|
461
|
+
"controller": "./controllers/EjemploController.js",
|
|
462
|
+
"handler": "index",
|
|
463
|
+
"contentType": "text/html"
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
"path": "/api/ejemplos/:id",
|
|
467
|
+
"method": "GET",
|
|
468
|
+
"controller": "./controllers/EjemploController.js",
|
|
469
|
+
"handler": "show",
|
|
470
|
+
"contentType": "text/html"
|
|
471
|
+
}
|
|
472
|
+
]
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
```json
|
|
476
|
+
// routes/otros-routes.json
|
|
477
|
+
[
|
|
478
|
+
{
|
|
479
|
+
"path": "/api/otros",
|
|
480
|
+
"method": "GET",
|
|
481
|
+
"controller": "./controllers/OtroController.js",
|
|
482
|
+
"handler": "index",
|
|
483
|
+
"contentType": "application/json"
|
|
484
|
+
}
|
|
485
|
+
]
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## Sistema de Hooks y Filters
|
|
491
|
+
|
|
492
|
+
### Registrar Hooks
|
|
493
|
+
|
|
494
|
+
Los hooks permiten extender la funcionalidad del framework en puntos específicos del ciclo de vida de la aplicación:
|
|
495
|
+
|
|
496
|
+
```javascript
|
|
497
|
+
// hooks/personalizados.js
|
|
498
|
+
const { hooks } = require('jerkjs');
|
|
499
|
+
|
|
500
|
+
// Registrar un hook para antes de procesar una solicitud
|
|
501
|
+
hooks.addAction('request_received', (req, res) => {
|
|
502
|
+
console.log(`Solicitud recibida: ${req.method} ${req.url}`);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// Registrar un hook para después de encontrar una ruta
|
|
506
|
+
hooks.addAction('route_matched', (matchedRoute, req, res) => {
|
|
507
|
+
console.log(`Ruta encontrada: ${req.method} ${req.url}`);
|
|
508
|
+
console.log(`Handler: ${matchedRoute.route.handler.name}`);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Registrar un hook para antes de ejecutar un handler de ruta
|
|
512
|
+
hooks.addAction('before_route_handler', async (req, res, next) => {
|
|
513
|
+
// Puedes modificar req/res o detener la ejecución
|
|
514
|
+
console.log('Antes de ejecutar handler de ruta');
|
|
515
|
+
next(); // Continuar con la ejecución
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Registrar un hook para después de ejecutar un handler de ruta
|
|
519
|
+
hooks.addAction('after_route_handler', (req, res) => {
|
|
520
|
+
console.log('Después de ejecutar handler de ruta');
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// Registrar un hook personalizado
|
|
524
|
+
hooks.addAction('mi_hook_personalizado', (datos) => {
|
|
525
|
+
console.log('Hook personalizado ejecutado con:', datos);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Disparar el hook personalizado
|
|
529
|
+
hooks.doAction('mi_hook_personalizado', { mensaje: 'Hola Mundo' });
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### Registrar Filters
|
|
533
|
+
|
|
534
|
+
Los filters permiten modificar datos en diferentes puntos del flujo de la aplicación:
|
|
535
|
+
|
|
536
|
+
```javascript
|
|
537
|
+
// filters/personalizados.js
|
|
538
|
+
const { hooks } = require('jerkjs');
|
|
539
|
+
|
|
540
|
+
// Registrar un filter para modificar el cuerpo de la solicitud
|
|
541
|
+
hooks.addFilter('request_body', (body, req) => {
|
|
542
|
+
// Agregar timestamp al cuerpo de la solicitud
|
|
543
|
+
if (typeof body === 'object') {
|
|
544
|
+
body.timestamp = new Date().toISOString();
|
|
545
|
+
}
|
|
546
|
+
return body;
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Registrar un filter para modificar la respuesta
|
|
550
|
+
hooks.addFilter('response_data', (data, res) => {
|
|
551
|
+
// Agregar información de metadata a la respuesta
|
|
552
|
+
if (typeof data === 'object') {
|
|
553
|
+
data.meta = {
|
|
554
|
+
timestamp: new Date().toISOString(),
|
|
555
|
+
version: '2.5.3'
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
return data;
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Registrar un filter para transformar datos del modelo antes de guardar
|
|
562
|
+
hooks.addFilter('model_before_save', (data, modelName) => {
|
|
563
|
+
// Normalizar datos antes de guardar
|
|
564
|
+
if (data.nombre) {
|
|
565
|
+
data.nombre = data.nombre.trim().toUpperCase();
|
|
566
|
+
}
|
|
567
|
+
return data;
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Registrar un filter para transformar datos del modelo después de leer
|
|
571
|
+
hooks.addFilter('model_after_read', (data, modelName) => {
|
|
572
|
+
// Transformar datos después de leer
|
|
573
|
+
if (data.fecha_creacion) {
|
|
574
|
+
data.fecha_formateada = new Date(data.fecha_creacion).toLocaleDateString();
|
|
575
|
+
}
|
|
576
|
+
return data;
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Ejemplo de uso de filter
|
|
580
|
+
const datosOriginales = { nombre: ' juan perez ' };
|
|
581
|
+
const datosFiltrados = hooks.applyFilters('model_before_save', datosOriginales, 'UsuarioModel');
|
|
582
|
+
console.log(datosFiltrados); // { nombre: 'JUAN PEREZ' }
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## Creación de Extensiones
|
|
588
|
+
|
|
589
|
+
Las extensiones permiten extender la funcionalidad del framework con componentes personalizados:
|
|
590
|
+
|
|
591
|
+
### 1. Extensiones de Middleware
|
|
592
|
+
|
|
593
|
+
```javascript
|
|
594
|
+
// extensions/ejemplo-middleware.js
|
|
595
|
+
class EjemploMiddleware {
|
|
596
|
+
/**
|
|
597
|
+
* Middleware para autenticación personalizada
|
|
598
|
+
*/
|
|
599
|
+
static authMiddleware(opciones = {}) {
|
|
600
|
+
return (req, res, next) => {
|
|
601
|
+
// Verificar token de autenticación
|
|
602
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
603
|
+
|
|
604
|
+
if (!token) {
|
|
605
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
606
|
+
res.end(JSON.stringify({ error: 'Token de autenticación requerido' }));
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Validar token (implementación simulada)
|
|
611
|
+
if (token === 'valid-token') {
|
|
612
|
+
req.usuarioAutenticado = { id: 1, nombre: 'Usuario' };
|
|
613
|
+
next();
|
|
614
|
+
} else {
|
|
615
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
616
|
+
res.end(JSON.stringify({ error: 'Token inválido' }));
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Middleware para logging de solicitudes
|
|
623
|
+
*/
|
|
624
|
+
static loggingMiddleware(logger) {
|
|
625
|
+
return (req, res, next) => {
|
|
626
|
+
const startTime = Date.now();
|
|
627
|
+
|
|
628
|
+
// Interceptamos res.end para medir tiempo
|
|
629
|
+
const originalEnd = res.end;
|
|
630
|
+
res.end = function(chunk, encoding) {
|
|
631
|
+
const duration = Date.now() - startTime;
|
|
632
|
+
logger.info(`${req.method} ${req.url} - ${duration}ms`);
|
|
633
|
+
originalEnd.call(this, chunk, encoding);
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
next();
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
module.exports = EjemploMiddleware;
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### 2. Extensiones de Adaptador de Base de Datos
|
|
645
|
+
|
|
646
|
+
```javascript
|
|
647
|
+
// extensions/custom-db-adapter.js
|
|
648
|
+
class CustomDBAdapter {
|
|
649
|
+
constructor(configuracion) {
|
|
650
|
+
this.config = configuracion;
|
|
651
|
+
// Inicializar conexión a base de datos personalizada
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async connect() {
|
|
655
|
+
// Lógica para conectar a la base de datos
|
|
656
|
+
console.log('Conectando a base de datos personalizada...');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async disconnect() {
|
|
660
|
+
// Lógica para desconectar de la base de datos
|
|
661
|
+
console.log('Desconectando de base de datos personalizada...');
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async select(tabla, condiciones = {}, opciones = {}) {
|
|
665
|
+
// Lógica para selección de datos
|
|
666
|
+
console.log(`SELECT * FROM ${tabla}`, condiciones);
|
|
667
|
+
return [];
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async insert(tabla, datos) {
|
|
671
|
+
// Lógica para inserción de datos
|
|
672
|
+
console.log(`INSERT INTO ${tabla}`, datos);
|
|
673
|
+
return { id: 1 };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async update(tabla, condiciones, datos) {
|
|
677
|
+
// Lógica para actualización de datos
|
|
678
|
+
console.log(`UPDATE ${tabla} SET`, datos, 'WHERE', condiciones);
|
|
679
|
+
return 1;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async delete(tabla, condiciones) {
|
|
683
|
+
// Lógica para eliminación de datos
|
|
684
|
+
console.log(`DELETE FROM ${tabla} WHERE`, condiciones);
|
|
685
|
+
return 1;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async query(sql, params = []) {
|
|
689
|
+
// Lógica para consultas personalizadas
|
|
690
|
+
console.log('QUERY:', sql, params);
|
|
691
|
+
return [];
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
module.exports = CustomDBAdapter;
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### 3. Extensiones de Motor de Plantillas
|
|
699
|
+
|
|
700
|
+
```javascript
|
|
701
|
+
// extensions/custom-template-engine.js
|
|
702
|
+
const fs = require('fs');
|
|
703
|
+
const path = require('path');
|
|
704
|
+
|
|
705
|
+
class CustomTemplateEngine {
|
|
706
|
+
constructor(opciones = {}) {
|
|
707
|
+
this.viewsPath = opciones.viewsPath || './views';
|
|
708
|
+
this.cacheEnabled = opciones.cacheEnabled || false;
|
|
709
|
+
this.cache = new Map();
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Renderiza una plantilla con datos
|
|
714
|
+
*/
|
|
715
|
+
render(nombrePlantilla, datos = {}) {
|
|
716
|
+
const rutaPlantilla = path.join(this.viewsPath, nombrePlantilla + '.html');
|
|
717
|
+
|
|
718
|
+
// Verificar cache si está habilitado
|
|
719
|
+
if (this.cacheEnabled && this.cache.has(rutaPlantilla)) {
|
|
720
|
+
const cachedTemplate = this.cache.get(rutaPlantilla);
|
|
721
|
+
return this.procesarPlantilla(cachedTemplate, datos);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Leer archivo de plantilla
|
|
725
|
+
let contenidoPlantilla = fs.readFileSync(rutaPlantilla, 'utf8');
|
|
726
|
+
|
|
727
|
+
// Almacenar en cache si está habilitado
|
|
728
|
+
if (this.cacheEnabled) {
|
|
729
|
+
this.cache.set(rutaPlantilla, contenidoPlantilla);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return this.procesarPlantilla(contenidoPlantilla, datos);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Procesa la plantilla reemplazando variables
|
|
737
|
+
*/
|
|
738
|
+
procesarPlantilla(plantilla, datos) {
|
|
739
|
+
let resultado = plantilla;
|
|
740
|
+
|
|
741
|
+
// Reemplazar variables simples: {{variable}}
|
|
742
|
+
for (const [clave, valor] of Object.entries(datos)) {
|
|
743
|
+
const regex = new RegExp(`{{\\s*${clave}\\s*}}`, 'g');
|
|
744
|
+
resultado = resultado.replace(regex, valor);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Procesar bucles: {{#each arreglo}}...{{/each}}
|
|
748
|
+
const eachRegex = /{{#each\s+(\w+)\s*}}([\s\S]*?){{\/each}}/g;
|
|
749
|
+
resultado = resultado.replace(eachRegex, (match, arregloNombre, contenido) => {
|
|
750
|
+
const arreglo = datos[arregloNombre];
|
|
751
|
+
if (!Array.isArray(arreglo)) return '';
|
|
752
|
+
|
|
753
|
+
let items = '';
|
|
754
|
+
for (const item of arreglo) {
|
|
755
|
+
let itemTemplate = contenido;
|
|
756
|
+
for (const [clave, valor] of Object.entries(item)) {
|
|
757
|
+
const regex = new RegExp(`{{\\s*${clave}\\s*}}`, 'g');
|
|
758
|
+
itemTemplate = itemTemplate.replace(regex, valor);
|
|
759
|
+
}
|
|
760
|
+
items += itemTemplate;
|
|
761
|
+
}
|
|
762
|
+
return items;
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
return resultado;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Limpia el cache de plantillas
|
|
770
|
+
*/
|
|
771
|
+
clearCache() {
|
|
772
|
+
this.cache.clear();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
module.exports = CustomTemplateEngine;
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### 4. Extensiones de Componente del Framework
|
|
780
|
+
|
|
781
|
+
```javascript
|
|
782
|
+
// extensions/custom-component.js
|
|
783
|
+
const { hooks } = require('jerkjs');
|
|
784
|
+
|
|
785
|
+
class CustomComponent {
|
|
786
|
+
constructor(servidor, opciones = {}) {
|
|
787
|
+
this.servidor = servidor;
|
|
788
|
+
this.opciones = opciones;
|
|
789
|
+
this.inicializado = false;
|
|
790
|
+
|
|
791
|
+
// Registrar hooks para este componente
|
|
792
|
+
this.registrarHooks();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Inicializa el componente
|
|
797
|
+
*/
|
|
798
|
+
async inicializar() {
|
|
799
|
+
if (this.inicializado) return;
|
|
800
|
+
|
|
801
|
+
console.log('Inicializando componente personalizado...');
|
|
802
|
+
|
|
803
|
+
// Registrar rutas personalizadas
|
|
804
|
+
this.servidor.addRoute('GET', '/api/custom/status', this.getStatus.bind(this));
|
|
805
|
+
this.servidor.addRoute('POST', '/api/custom/action', this.performAction.bind(this));
|
|
806
|
+
|
|
807
|
+
this.inicializado = true;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Hook para registrar hooks del componente
|
|
812
|
+
*/
|
|
813
|
+
registrarHooks() {
|
|
814
|
+
hooks.addAction('server_started', (servidor) => {
|
|
815
|
+
console.log('Componente personalizado activo');
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
hooks.addAction('request_received', (req, res) => {
|
|
819
|
+
// Agregar encabezado personalizado a todas las respuestas
|
|
820
|
+
res.customHeaderAdded = true;
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Endpoint para obtener estado del componente
|
|
826
|
+
*/
|
|
827
|
+
async getStatus(req, res) {
|
|
828
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
829
|
+
res.end(JSON.stringify({
|
|
830
|
+
status: 'activo',
|
|
831
|
+
componente: 'CustomComponent',
|
|
832
|
+
inicializado: this.inicializado,
|
|
833
|
+
timestamp: new Date().toISOString()
|
|
834
|
+
}));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Endpoint para realizar acciones personalizadas
|
|
839
|
+
*/
|
|
840
|
+
async performAction(req, res) {
|
|
841
|
+
try {
|
|
842
|
+
const datos = req.body;
|
|
843
|
+
|
|
844
|
+
// Realizar acción personalizada
|
|
845
|
+
const resultado = await this.ejecutarAccion(datos);
|
|
846
|
+
|
|
847
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
848
|
+
res.end(JSON.stringify({
|
|
849
|
+
success: true,
|
|
850
|
+
resultado: resultado
|
|
851
|
+
}));
|
|
852
|
+
} catch (error) {
|
|
853
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
854
|
+
res.end(JSON.stringify({
|
|
855
|
+
error: error.message
|
|
856
|
+
}));
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Método para ejecutar acciones personalizadas
|
|
862
|
+
*/
|
|
863
|
+
async ejecutarAccion(datos) {
|
|
864
|
+
// Lógica personalizada aquí
|
|
865
|
+
return {
|
|
866
|
+
mensaje: 'Acción ejecutada exitosamente',
|
|
867
|
+
datos: datos,
|
|
868
|
+
timestamp: new Date().toISOString()
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
module.exports = CustomComponent;
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
### 5. Uso de Extensiones en la Aplicación
|
|
877
|
+
|
|
878
|
+
```javascript
|
|
879
|
+
// app.js - Ejemplo de uso de extensiones
|
|
880
|
+
const { APIServer, RouteLoader, ModelManager, MemoryAdapter } = require('jerkjs');
|
|
881
|
+
const EjemploMiddleware = require('./extensions/ejemplo-middleware');
|
|
882
|
+
const CustomDBAdapter = require('./extensions/custom-db-adapter');
|
|
883
|
+
const CustomTemplateEngine = require('./extensions/custom-template-engine');
|
|
884
|
+
const CustomComponent = require('./extensions/custom-component');
|
|
885
|
+
|
|
886
|
+
// Crear servidor
|
|
887
|
+
const server = new APIServer({
|
|
888
|
+
port: 3000,
|
|
889
|
+
host: 'localhost'
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
// Configurar motor de vistas personalizado
|
|
893
|
+
server.viewEngine = new CustomTemplateEngine({
|
|
894
|
+
viewsPath: './views',
|
|
895
|
+
cacheEnabled: true
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
// Configurar sistema de modelos
|
|
899
|
+
const modelManager = new ModelManager();
|
|
900
|
+
const memoryAdapter = new MemoryAdapter();
|
|
901
|
+
modelManager.registerAdapter('memory', memoryAdapter);
|
|
902
|
+
server.modelManager = modelManager;
|
|
903
|
+
|
|
904
|
+
// Aplicar middleware personalizado
|
|
905
|
+
server.use(EjemploMiddleware.loggingMiddleware(console));
|
|
906
|
+
server.use(EjemploMiddleware.authMiddleware());
|
|
907
|
+
|
|
908
|
+
// Inicializar componente personalizado
|
|
909
|
+
const customComponent = new CustomComponent(server);
|
|
910
|
+
customComponent.inicializar();
|
|
911
|
+
|
|
912
|
+
// Cargar rutas
|
|
913
|
+
const routeLoader = new RouteLoader();
|
|
914
|
+
routeLoader.loadRoutes(server, './routes.json')
|
|
915
|
+
.then(() => {
|
|
916
|
+
server.start();
|
|
917
|
+
console.log('Aplicación iniciada con extensiones personalizadas');
|
|
918
|
+
})
|
|
919
|
+
.catch(error => {
|
|
920
|
+
console.error('Error iniciando aplicación:', error.message);
|
|
921
|
+
});
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
---
|
|
925
|
+
|
|
926
|
+
## Buenas Prácticas
|
|
927
|
+
|
|
928
|
+
1. **Organización de Código**: Mantener una estructura de directorios clara (controllers, models, views, extensions)
|
|
929
|
+
2. **Reutilización**: Aprovechar la herencia de `ControllerBase` y `ModelBase`
|
|
930
|
+
3. **Seguridad**: Validar entradas y usar middleware de autenticación
|
|
931
|
+
4. **Rendimiento**: Usar cacheo de vistas y conexiones a base de datos
|
|
932
|
+
5. **Mantenibilidad**: Documentar extensiones y seguir convenciones de nomenclatura
|
|
933
|
+
|
|
934
|
+
Este manual proporciona una guía completa para desarrollar aplicaciones MVC en JERK Framework, desde la creación de componentes básicos hasta la implementación de extensiones avanzadas.
|