jerkjs 2.1.6 → 2.2.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 (54) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +202 -5
  3. package/index.js +29 -4
  4. package/lib/core/server.js +328 -27
  5. package/lib/loader/routeLoader.js +148 -117
  6. package/lib/middleware/compressor.js +87 -18
  7. package/lib/mvc/GenericAdapter.js +136 -0
  8. package/lib/mvc/MariaDBAdapter.js +315 -0
  9. package/lib/mvc/MemoryAdapter.js +269 -0
  10. package/lib/mvc/ModelControllerExample.js +285 -0
  11. package/lib/mvc/controllerBase.js +60 -0
  12. package/lib/mvc/modelBase.js +383 -0
  13. package/lib/mvc/modelManager.js +284 -0
  14. package/lib/mvc/userModel.js +265 -0
  15. package/lib/mvc/viewEngine.js +32 -1
  16. package/lib/utils/mimeType.js +62 -0
  17. package/package.json +5 -3
  18. package/JERK_FRAMEWORK_DIAGRAM.txt +0 -492
  19. package/JERK_FRAMEWORK_DIAGRAM_MERMAID.mmd +0 -124
  20. package/JERK_FRAMEWORK_DOCUMENTATION.md +0 -527
  21. package/LICENSE +0 -201
  22. package/README_EN.md +0 -230
  23. package/README_PT.md +0 -230
  24. package/docs/ARQUITECTURA_ROUTES.md +0 -140
  25. package/docs/EXTENSION_MANUAL.md +0 -955
  26. package/docs/FIREWALL_MANUAL.md +0 -416
  27. package/docs/HOOK-2.0.md +0 -512
  28. package/docs/HOOKS_REFERENCE_IMPROVED.md +0 -596
  29. package/docs/MANUAL_API_SDK.md +0 -536
  30. package/docs/MARIADB_TOKENS_IMPLEMENTATION.md +0 -110
  31. package/docs/MIDDLEWARE_MANUAL.md +0 -518
  32. package/docs/OAUTH2_GOOGLE_MANUAL.md +0 -405
  33. package/docs/ROUTING_WITHOUT_JSON_GUIDE.md +0 -454
  34. package/docs/frontend-and-sessions.md +0 -353
  35. package/docs/guia_inicio_rapido_jerkjs.md +0 -113
  36. package/examples/examples.arj +0 -0
  37. package/standard/CompressionTestController.js +0 -56
  38. package/standard/HealthController.js +0 -16
  39. package/standard/HomeController.js +0 -12
  40. package/standard/ProductController.js +0 -18
  41. package/standard/README.md +0 -47
  42. package/standard/UserController.js +0 -23
  43. package/standard/package.json +0 -22
  44. package/standard/routes.json +0 -65
  45. package/standard/server.js +0 -140
  46. package/standardA/controllers/AuthController.js +0 -82
  47. package/standardA/controllers/HomeController.js +0 -19
  48. package/standardA/controllers/UserController.js +0 -41
  49. package/standardA/server.js +0 -311
  50. package/standardA/views/auth/dashboard.html +0 -51
  51. package/standardA/views/auth/login.html +0 -47
  52. package/standardA/views/index.html +0 -32
  53. package/standardA/views/users/detail.html +0 -28
  54. package/standardA/views/users/list.html +0 -36
@@ -1,15 +1,17 @@
1
1
  /**
2
2
  * Servidor HTTP básico para el framework JERK
3
3
  * Implementación del componente core/server.js
4
- * JERK Framework v2.1 - Con optimizaciones de rendimiento
4
+ * JERK Framework v2.1 - Con optimizaciones de rendimiento y soporte para archivos estáticos
5
5
  */
6
6
 
7
7
  const http = require('http');
8
8
  const https = require('https');
9
9
  const url = require('url');
10
10
  const fs = require('fs');
11
+ const path = require('path');
11
12
  const { Logger } = require('../utils/logger');
12
13
  const { ErrorHandler } = require('../utils/errorHandler');
14
+ const { getMimeType } = require('../utils/mimeType');
13
15
 
14
16
  class APIServer {
15
17
  /**
@@ -49,6 +51,198 @@ class APIServer {
49
51
  this.routeRegexCache = new Map();
50
52
  }
51
53
 
54
+ /**
55
+ * Método para crear un handler para servir archivos estáticos
56
+ * @param {Object} staticConfig - Configuración de la ruta estática
57
+ * @returns {Function} - Handler para servir archivos estáticos
58
+ */
59
+ createStaticFileHandler(staticConfig) {
60
+ return async (req, res) => {
61
+ try {
62
+ // Disparar hook antes de procesar archivo estático
63
+ const hooks = require('../../index.js').hooks;
64
+ if (hooks) {
65
+ const hookResult = hooks.applyFilters('pre_static_file_serve', {
66
+ req,
67
+ res,
68
+ staticConfig,
69
+ shouldContinue: true
70
+ });
71
+
72
+ if (!hookResult.shouldContinue) {
73
+ return; // Cancelar proceso si el filtro lo indica
74
+ }
75
+
76
+ // Actualizar configuración si el filtro la modificó
77
+ staticConfig = hookResult.staticConfig || staticConfig;
78
+ }
79
+
80
+ // Extraer la subruta de la solicitud
81
+ // Si la ruta base es /static y la solicitud es /static/assets/hola.js
82
+ // entonces la subruta sería assets/hola.js
83
+ const parsedUrl = require('url').parse(req.url, true);
84
+ const pathname = parsedUrl.pathname;
85
+ let requestedPath = '';
86
+
87
+ // Encontrar la ruta estática que coincide con la solicitud actual
88
+ for (const route of this.routes) {
89
+ if (route.isStatic && pathname.startsWith(route.path)) {
90
+ requestedPath = pathname.substring(route.path.length);
91
+ if (requestedPath.startsWith('/')) {
92
+ requestedPath = requestedPath.substring(1);
93
+ }
94
+ break;
95
+ }
96
+ }
97
+
98
+ // Prevenir path traversal
99
+ const normalizedPath = path.normalize(requestedPath);
100
+ if (normalizedPath.includes('..')) {
101
+ res.writeHead(403, { 'Content-Type': 'application/json' });
102
+ res.end(JSON.stringify({ error: 'Acceso denegado' }));
103
+
104
+ if (hooks) {
105
+ hooks.doAction('static_file_access_denied', req, res, normalizedPath);
106
+ }
107
+ return;
108
+ }
109
+
110
+ // Asegurarse de que el directorio base sea absoluto
111
+ let baseDir = staticConfig.dir;
112
+ if (!path.isAbsolute(baseDir)) {
113
+ baseDir = path.join(process.cwd(), baseDir);
114
+ }
115
+
116
+ // Construir la ruta física
117
+ const physicalPath = path.join(baseDir, normalizedPath);
118
+
119
+ // Verificar si el archivo existe
120
+ try {
121
+ await fs.promises.access(physicalPath);
122
+ } catch (accessErr) {
123
+ // Si no existe el archivo solicitado, verificar si es un directorio y buscar archivo índice
124
+ const dirPath = path.join(baseDir, normalizedPath);
125
+ try {
126
+ const stats = await fs.promises.stat(dirPath);
127
+
128
+ if (stats.isDirectory()) {
129
+ // Buscar archivo índice
130
+ for (const indexFile of staticConfig.index || ['index.html']) {
131
+ const indexPath = path.join(dirPath, indexFile);
132
+ try {
133
+ await fs.promises.access(indexPath);
134
+ // Disparar hook antes de servir archivo índice
135
+ if (hooks) {
136
+ hooks.doAction('serving_index_file', indexPath, req, res);
137
+ }
138
+
139
+ const fileContent = await fs.promises.readFile(indexPath);
140
+ const mimeType = getMimeType(indexPath);
141
+
142
+ res.setHeader('Content-Type', mimeType);
143
+
144
+ if (staticConfig.cacheControl) {
145
+ res.setHeader('Cache-Control', staticConfig.cacheControl);
146
+ }
147
+
148
+ res.writeHead(200);
149
+ res.end(fileContent);
150
+
151
+ // Disparar hook después de servir archivo
152
+ if (hooks) {
153
+ hooks.doAction('static_file_served', indexPath, req, res);
154
+ }
155
+ return;
156
+ } catch (e) {
157
+ continue; // Probar siguiente archivo índice
158
+ }
159
+ }
160
+
161
+ // Si es directorio pero no hay archivo índice
162
+ res.writeHead(404, { 'Content-Type': 'application/json' });
163
+ res.end(JSON.stringify({ error: 'Directorio no encontrado' }));
164
+
165
+ if (hooks) {
166
+ hooks.doAction('static_directory_no_index', dirPath, req, res);
167
+ }
168
+ return;
169
+ } else {
170
+ // No es directorio ni archivo existente
171
+ res.writeHead(404, { 'Content-Type': 'application/json' });
172
+ res.end(JSON.stringify({ error: 'Archivo no encontrado' }));
173
+
174
+ if (hooks) {
175
+ hooks.doAction('static_file_not_found', physicalPath, req, res);
176
+ }
177
+ return;
178
+ }
179
+ } catch (statErr) {
180
+ // No es directorio ni archivo
181
+ res.writeHead(404, { 'Content-Type': 'application/json' });
182
+ res.end(JSON.stringify({ error: 'Archivo no encontrado' }));
183
+
184
+ if (hooks) {
185
+ hooks.doAction('static_file_not_found', physicalPath, req, res);
186
+ }
187
+ return;
188
+ }
189
+ }
190
+
191
+ // Disparar hook antes de leer archivo
192
+ if (hooks) {
193
+ hooks.doAction('before_reading_static_file', physicalPath, req, res);
194
+ }
195
+
196
+ // Leer y servir el archivo
197
+ const fileContent = await fs.promises.readFile(physicalPath);
198
+
199
+ // Determinar el tipo MIME
200
+ const mimeType = getMimeType(physicalPath);
201
+
202
+ // Configurar headers
203
+ res.setHeader('Content-Type', mimeType);
204
+
205
+ if (staticConfig.cacheControl) {
206
+ res.setHeader('Cache-Control', staticConfig.cacheControl);
207
+ }
208
+
209
+ res.writeHead(200);
210
+ res.end(fileContent);
211
+
212
+ // Disparar hook después de servir archivo
213
+ if (hooks) {
214
+ hooks.doAction('static_file_served', physicalPath, req, res);
215
+ }
216
+ } catch (error) {
217
+ if (error.code === 'ENOENT') {
218
+ res.writeHead(404, { 'Content-Type': 'application/json' });
219
+ res.end(JSON.stringify({ error: 'Archivo no encontrado' }));
220
+
221
+ const hooks = require('../../index.js').hooks;
222
+ if (hooks) {
223
+ hooks.doAction('static_file_not_found', error.path, req, res);
224
+ }
225
+ } else if (error.code === 'EACCES') {
226
+ res.writeHead(403, { 'Content-Type': 'application/json' });
227
+ res.end(JSON.stringify({ error: 'Acceso denegado' }));
228
+
229
+ const hooks = require('../../index.js').hooks;
230
+ if (hooks) {
231
+ hooks.doAction('static_file_access_denied', error.path, req, res);
232
+ }
233
+ } else {
234
+ res.writeHead(500, { 'Content-Type': 'application/json' });
235
+ res.end(JSON.stringify({ error: 'Error interno del servidor' }));
236
+
237
+ const hooks = require('../../index.js').hooks;
238
+ if (hooks) {
239
+ hooks.doAction('static_file_error', error, req, res);
240
+ }
241
+ }
242
+ }
243
+ };
244
+ }
245
+
52
246
  /**
53
247
  * Método para agregar una ruta al servidor
54
248
  * @param {string|Object} method - Método HTTP (GET, POST, PUT, DELETE, etc.) o un objeto de configuración de ruta
@@ -91,6 +285,31 @@ class APIServer {
91
285
  throw new Error('La ruta debe tener una propiedad "method"');
92
286
  }
93
287
 
288
+ // Verificar si es una ruta estática
289
+ if (routeConfig.static) {
290
+ // Validar configuración estática
291
+ if (!routeConfig.static.dir) {
292
+ throw new Error('Las rutas estáticas deben tener un directorio especificado');
293
+ }
294
+
295
+ // El método debe ser GET para rutas estáticas
296
+ if (routeConfig.method.toUpperCase() !== 'GET') {
297
+ throw new Error('Las rutas estáticas deben usar el método GET, no ' + routeConfig.method);
298
+ }
299
+
300
+ // Crear handler para archivos estáticos
301
+ const staticHandler = this.createStaticFileHandler(routeConfig.static);
302
+
303
+ this.routes.push({
304
+ method: routeConfig.method.toUpperCase(),
305
+ path: routeConfig.path,
306
+ handler: staticHandler,
307
+ isStatic: true // Marcar como ruta estática para posible procesamiento especial
308
+ });
309
+
310
+ return; // Salir después de procesar ruta estática
311
+ }
312
+
94
313
  if (!routeConfig.handler && !routeConfig.controller) {
95
314
  throw new Error('La ruta debe tener un "handler" o un "controller"');
96
315
  }
@@ -256,7 +475,7 @@ class APIServer {
256
475
  */
257
476
  findRoute(method, pathname) {
258
477
  // Buscar ruta exacta primero
259
- const exactMatch = this.routes.find(route =>
478
+ const exactMatch = this.routes.find(route =>
260
479
  route.method === method && route.path === pathname
261
480
  );
262
481
 
@@ -267,6 +486,23 @@ class APIServer {
267
486
  };
268
487
  }
269
488
 
489
+ // Buscar rutas estáticas (prefijos)
490
+ for (const route of this.routes) {
491
+ if (route.method !== method) continue;
492
+
493
+ // Para rutas estáticas, verificar si la ruta solicitada comienza con el prefijo de la ruta estática
494
+ if (route.isStatic && pathname.startsWith(route.path)) {
495
+ // Verificar que sea exactamente el prefijo o que haya una barra después del prefijo
496
+ const remainingPath = pathname.substring(route.path.length);
497
+ if (route.path === '/' || remainingPath === '' || remainingPath.startsWith('/')) {
498
+ return {
499
+ route: route,
500
+ params: {}
501
+ };
502
+ }
503
+ }
504
+ }
505
+
270
506
  // Buscar rutas parametrizadas
271
507
  for (const route of this.routes) {
272
508
  if (route.method !== method) continue;
@@ -426,9 +662,16 @@ class APIServer {
426
662
  const parsedUrl = url.parse(req.url, true);
427
663
  const { pathname, query } = parsedUrl;
428
664
 
665
+ // Disparar hook antes de procesar la solicitud
666
+ const hooks = require('../../index.js').hooks;
667
+ if (hooks) {
668
+ hooks.doAction('request_received', req, res);
669
+ }
670
+
429
671
  // Agregar propiedades útiles a la solicitud
430
672
  req.query = query;
431
673
  req.params = {};
674
+ req.originalUrl = req.url; // Guardar la URL original para rutas estáticas
432
675
 
433
676
  // Inicializar array para acumular chunks del body
434
677
  const bodyChunks = [];
@@ -443,6 +686,10 @@ class APIServer {
443
686
  res.writeHead(413, { 'Content-Type': 'application/json' });
444
687
  res.end(JSON.stringify({ error: 'Solicitud demasiado grande', details: `El cuerpo de la solicitud excede el límite permitido de ${this.maxBodySize} bytes` }));
445
688
  req.destroy(); // Terminar la conexión
689
+
690
+ if (hooks) {
691
+ hooks.doAction('request_body_too_large', req, res, bodySize, this.maxBodySize);
692
+ }
446
693
  return;
447
694
  }
448
695
  bodyChunks.push(chunk);
@@ -459,6 +706,10 @@ class APIServer {
459
706
  req.body = JSON.parse(req.body);
460
707
  } catch (e) {
461
708
  req.body = {};
709
+
710
+ if (hooks) {
711
+ hooks.doAction('request_body_parse_error', req, res, e);
712
+ }
462
713
  }
463
714
  }
464
715
 
@@ -469,10 +720,18 @@ class APIServer {
469
720
  const renderedHtml = this.viewEngine.render(viewName, data);
470
721
  res.writeHead(200, { 'Content-Type': 'text/html' });
471
722
  res.end(renderedHtml);
723
+
724
+ if (hooks) {
725
+ hooks.doAction('view_rendered', viewName, data, req, res);
726
+ }
472
727
  } catch (error) {
473
728
  console.error('Error renderizando vista:', error);
474
729
  res.writeHead(500, { 'Content-Type': 'application/json' });
475
730
  res.end(JSON.stringify({ error: 'Error interno del servidor', details: error.message }));
731
+
732
+ if (hooks) {
733
+ hooks.doAction('view_render_error', viewName, data, req, res, error);
734
+ }
476
735
  }
477
736
  };
478
737
  }
@@ -506,7 +765,12 @@ class APIServer {
506
765
  await middleware(req, res);
507
766
  }
508
767
 
509
- if (res.finished) return; // Si el middleware respondió, salir
768
+ if (res.finished) {
769
+ if (hooks) {
770
+ hooks.doAction('middleware_response_finished', req, res);
771
+ }
772
+ return; // Si el middleware respondió, salir
773
+ }
510
774
  }
511
775
  }
512
776
 
@@ -516,6 +780,11 @@ class APIServer {
516
780
  if (matchedRoute) {
517
781
  // Agregar parámetros a la solicitud
518
782
  req.params = matchedRoute.params;
783
+
784
+ if (hooks) {
785
+ hooks.doAction('route_matched', matchedRoute, req, res);
786
+ }
787
+
519
788
  // Ejecutar handler de la ruta
520
789
  await matchedRoute.route.handler(req, res);
521
790
  } else {
@@ -526,6 +795,10 @@ class APIServer {
526
795
  // devolver 204 para cumplir con CORS preflight
527
796
  res.writeHead(204);
528
797
  res.end();
798
+
799
+ if (hooks) {
800
+ hooks.doAction('options_response_sent', req, res);
801
+ }
529
802
  }
530
803
  }
531
804
  } else {
@@ -536,45 +809,73 @@ class APIServer {
536
809
  // Agregar parámetros a la solicitud
537
810
  req.params = matchedRoute.params;
538
811
 
539
- // Ejecutar middlewares
540
- for (const middleware of this.middlewares) {
541
- // Verificar si el middleware es una función antes de ejecutarla
542
- if (typeof middleware === 'function') {
543
- // Verificar si el middleware tiene firma (req, res, next)
544
- if (middleware.length === 3) {
545
- // Middleware con next
546
- await new Promise((resolve, reject) => {
547
- const next = (err) => {
548
- if (err) {
549
- reject(err);
550
- } else {
551
- resolve();
812
+ if (hooks) {
813
+ hooks.doAction('route_matched', matchedRoute, req, res);
814
+ }
815
+
816
+ // Ejecutar middlewares (excepto para rutas estáticas)
817
+ if (!matchedRoute.route.isStatic) {
818
+ for (const middleware of this.middlewares) {
819
+ // Verificar si el middleware es una función antes de ejecutarla
820
+ if (typeof middleware === 'function') {
821
+ // Verificar si el middleware tiene firma (req, res, next)
822
+ if (middleware.length === 3) {
823
+ // Middleware con next
824
+ await new Promise((resolve, reject) => {
825
+ const next = (err) => {
826
+ if (err) {
827
+ reject(err);
828
+ } else {
829
+ resolve();
830
+ }
831
+ };
832
+ const result = middleware(req, res, next);
833
+ // Si el middleware devuelve una promesa, esperarla
834
+ if (result && typeof result.then === 'function') {
835
+ result.then(resolve).catch(reject);
552
836
  }
553
- };
554
- const result = middleware(req, res, next);
555
- // Si el middleware devuelve una promesa, esperarla
556
- if (result && typeof result.then === 'function') {
557
- result.then(resolve).catch(reject);
837
+ });
838
+ } else {
839
+ // Middleware sin next
840
+ await middleware(req, res);
841
+ }
842
+
843
+ if (res.finished) {
844
+ if (hooks) {
845
+ hooks.doAction('middleware_response_finished', req, res);
558
846
  }
559
- });
560
- } else {
561
- // Middleware sin next
562
- await middleware(req, res);
847
+ return; // Si el middleware respondió, salir
848
+ }
563
849
  }
564
-
565
- if (res.finished) return; // Si el middleware respondió, salir
566
850
  }
567
851
  }
568
852
 
569
853
  // Ejecutar handler de la ruta
570
854
  await matchedRoute.route.handler(req, res);
855
+
856
+ if (hooks) {
857
+ hooks.doAction('route_handler_executed', matchedRoute, req, res);
858
+ }
571
859
  } else {
572
860
  // Ruta no encontrada
861
+ if (hooks) {
862
+ hooks.doAction('route_not_found', pathname, req, res);
863
+ }
573
864
  res.writeHead(404, { 'Content-Type': 'application/json' });
574
865
  res.end(JSON.stringify({ error: 'Ruta no encontrada', path: pathname }));
866
+
867
+
575
868
  }
576
869
  }
870
+
871
+ if (hooks) {
872
+ hooks.doAction('request_processed', req, res);
873
+ }
577
874
  } catch (error) {
875
+ if (hooks) {
876
+ hooks.doAction('request_error', req, res, error);
877
+ }
878
+
578
879
  ErrorHandler.handle(error, req, res, this.logger);
579
880
  }
580
881
  });