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.
@@ -207,57 +207,125 @@ class ControllerBase {
207
207
  * Carga un modelo para su uso en el controlador
208
208
  * @param {string} modelName - Nombre del modelo a cargar
209
209
  * @param {Object} options - Opciones para la creación del modelo
210
+ * @param {Object} options.adapter - Adaptador de base de datos opcional
211
+ * @param {string} options.modelDir - Directorio personalizado donde buscar el modelo
210
212
  * @returns {ModelBase} - Instancia del modelo cargado
211
213
  */
212
214
  loadModel(modelName, options = {}) {
213
215
  try {
214
- // Construir la ruta del modelo
215
- // Buscar en diferentes ubicaciones posibles
216
+ // Obtener la ruta del archivo que llama a este método para calcular rutas relativas correctamente
217
+ const callerDir = process.cwd();
216
218
  let ModelClass;
217
219
 
218
- // Array de rutas posibles para buscar el modelo
219
- // Usamos rutas absolutas basadas en el directorio actual de trabajo
220
- const possiblePaths = [
221
- `./models/${modelName}`, // En el subdirectorio models del directorio actual
222
- `./models/${modelName}.js`, // Con extensión explícita
223
- `../models/${modelName}`, // En el directorio models del directorio padre
224
- `../models/${modelName}.js`, // Con extensión explícita
225
- `../../models/${modelName}`, // En el directorio models del directorio abuelo
226
- `../../models/${modelName}.js`, // Con extensión explícita
227
- `../../../models/${modelName}`, // En el directorio models del directorio bisabuelo
228
- `../../../models/${modelName}.js`, // Con extensión explícita
229
- `./${modelName}`, // En el directorio actual sin subdirectorio
230
- `./${modelName}.js` // En el directorio actual con extensión
231
- ];
232
-
233
- let lastError = null;
234
-
235
- for (const path of possiblePaths) {
220
+ // Si se especifica un directorio personalizado, usarlo como prioridad
221
+ if (options.modelDir) {
222
+ const customPath = `${options.modelDir}/${modelName}`;
223
+ const customPathWithJs = `${options.modelDir}/${modelName}.js`;
224
+
225
+ // Intentar cargar desde el directorio personalizado
236
226
  try {
237
- // Usar require con la ruta absoluta desde el directorio actual
238
- ModelClass = require(path);
239
- // Si la importación fue exitosa, salir del bucle
240
- break;
227
+ const fullPath = require.resolve(customPath, { paths: [callerDir] });
228
+ ModelClass = require(fullPath);
241
229
  } catch (error) {
242
- // Guardar el último error para propósitos de depuración
243
- lastError = error;
244
- continue;
230
+ try {
231
+ const fullPath = require.resolve(customPathWithJs, { paths: [callerDir] });
232
+ ModelClass = require(fullPath);
233
+ } catch (error2) {
234
+ // Si falla en el directorio personalizado, continuar con rutas estándar
235
+ }
245
236
  }
246
237
  }
247
238
 
248
- // Si no se encontró el modelo en ninguna ubicación
239
+ // Si no se encontró el modelo en el directorio personalizado, buscar en rutas estándar
249
240
  if (!ModelClass) {
250
- throw new Error(`Modelo '${modelName}' no encontrado en ubicaciones estándar. Ultimo error: ${lastError?.message || 'desconocido'}`);
241
+ // Array de rutas posibles para buscar el modelo
242
+ // Usamos rutas relativas al directorio actual de trabajo
243
+ const possiblePaths = [
244
+ // Rutas relativas al directorio actual
245
+ `./models/${modelName}`, // En el subdirectorio models del directorio actual
246
+ `./models/${modelName}.js`, // Con extensión explícita
247
+ `./${modelName}`, // En el directorio actual sin subdirectorio
248
+ `./${modelName}.js`, // En el directorio actual con extensión
249
+
250
+ // Rutas relativas a directorios superiores
251
+ `../models/${modelName}`, // En el directorio models del directorio padre
252
+ `../models/${modelName}.js`, // Con extensión explícita
253
+ `../../models/${modelName}`, // En el directorio models del directorio abuelo
254
+ `../../models/${modelName}.js`, // Con extensión explícita
255
+ `../../../models/${modelName}`, // En el directorio models del directorio bisabuelo
256
+ `../../../models/${modelName}.js`, // Con extensión explícita
257
+
258
+ ];
259
+
260
+ let lastError = null;
261
+
262
+ // Intentar cargar el modelo desde cada ruta posible
263
+ for (const relativePath of possiblePaths) {
264
+ try {
265
+ // Resolver la ruta absoluta
266
+ const fullPath = require.resolve(relativePath, { paths: [callerDir] });
267
+ ModelClass = require(fullPath);
268
+
269
+ // Si la importación fue exitosa, salir del bucle
270
+ break;
271
+ } catch (error) {
272
+ // Solo registrar el error si es diferente al anterior para evitar spam
273
+ if (!lastError || lastError.message !== error.message) {
274
+ lastError = error;
275
+ }
276
+ continue;
277
+ }
278
+ }
279
+
280
+ // Si no se encontró el modelo en ninguna ubicación
281
+ if (!ModelClass) {
282
+ throw new Error(`Modelo '${modelName}' no encontrado en ubicaciones estándar. Ultimo error: ${lastError?.message || 'desconocido'}`);
283
+ }
251
284
  }
252
285
 
253
- // Crear instancia del modelo con las opciones proporcionadas
254
- const modelInstance = new ModelClass(options);
286
+ // Si ModelClass no es una función constructora, verificar si es un objeto con el modelo
287
+ if (typeof ModelClass !== 'function') {
288
+ if (ModelClass.default && typeof ModelClass.default === 'function') {
289
+ ModelClass = ModelClass.default; // Soporte para ES6 modules
290
+ } else {
291
+ // Buscar la clase en el objeto exportado
292
+ const exportedKeys = Object.keys(ModelClass);
293
+ for (const key of exportedKeys) {
294
+ if (typeof ModelClass[key] === 'function' && ModelClass[key].prototype) {
295
+ ModelClass = ModelClass[key];
296
+ break;
297
+ }
298
+ }
299
+ }
300
+ }
255
301
 
256
- // Guardar referencia del modelo en el controlador
302
+ // Verificar si ya existe una instancia del modelo en este controlador
257
303
  if (!this.models) {
258
304
  this.models = {};
259
305
  }
260
306
 
307
+ // Si ya existe una instancia del modelo, devolverla y actualizar el adaptador si es necesario
308
+ if (this.models[modelName]) {
309
+ const existingModel = this.models[modelName];
310
+
311
+ // Si se proporciona un adaptador y el modelo no lo tiene, asignarlo
312
+ if (!existingModel.adapter && options.adapter) {
313
+ existingModel.setAdapter(options.adapter);
314
+ }
315
+
316
+ return existingModel;
317
+ }
318
+
319
+ // Crear nueva instancia del modelo con las opciones proporcionadas
320
+ const modelOptions = { ...options };
321
+ const modelInstance = new ModelClass(modelOptions);
322
+
323
+ // Si el modelo no tiene un adaptador y se proporcionó uno en las opciones, asignarlo
324
+ if (!modelInstance.adapter && options.adapter) {
325
+ modelInstance.setAdapter(options.adapter);
326
+ }
327
+
328
+ // Guardar referencia del modelo en el controlador
261
329
  this.models[modelName] = modelInstance;
262
330
 
263
331
  return modelInstance;
@@ -33,8 +33,23 @@ class RouteMatcher {
33
33
  };
34
34
  }
35
35
 
36
- // Buscar rutas parametrizadas antes que rutas estáticas (prefijos)
37
- // Esto evita que rutas estáticas como /api capturen rutas parametrizadas como /api/users/:id
36
+ // Buscar rutas estáticas que coincidan exactamente antes que rutas parametrizadas
37
+ // Esto permite que rutas estáticas como /css/style.css tengan prioridad sobre rutas parametrizadas
38
+ for (const route of routes) {
39
+ if (route.method !== method) continue;
40
+
41
+ // Verificar si es una ruta estática que coincide exactamente
42
+ if (route.isStatic && route.path === pathname) {
43
+ return {
44
+ route: route,
45
+ params: {}
46
+ };
47
+ }
48
+ }
49
+
50
+ // Buscar rutas parametrizadas
51
+ // Pero antes de devolver una ruta parametrizada, verificar si hay una ruta estática más específica
52
+ const parametrizedMatches = [];
38
53
  for (const route of routes) {
39
54
  if (route.method !== method) continue;
40
55
 
@@ -44,14 +59,15 @@ class RouteMatcher {
44
59
 
45
60
  if (match) {
46
61
  const params = this.extractParams(route.path, pathname);
47
- return {
62
+ parametrizedMatches.push({
48
63
  route: route,
49
- params
50
- };
64
+ params: params
65
+ });
51
66
  }
52
67
  }
53
68
 
54
- // Buscar rutas estáticas (prefijos) - solo después de buscar rutas parametrizadas
69
+ // Buscar rutas estáticas (prefijos)
70
+ const staticMatches = [];
55
71
  for (const route of routes) {
56
72
  if (route.method !== method) continue;
57
73
 
@@ -60,14 +76,33 @@ class RouteMatcher {
60
76
  // Verificar que sea exactamente el prefijo o que haya una barra después del prefijo
61
77
  const remainingPath = pathname.substring(route.path.length);
62
78
  if (route.path === '/' || remainingPath === '' || remainingPath.startsWith('/')) {
63
- return {
79
+ staticMatches.push({
64
80
  route: route,
65
- params: {}
66
- };
81
+ params: {},
82
+ specificity: route.path.length // Agregar medida de especificidad basada en longitud del prefijo
83
+ });
67
84
  }
68
85
  }
69
86
  }
70
87
 
88
+ // Prioridad: si hay rutas estáticas que coinciden, y la solicitud parece ser para un archivo
89
+ // (tiene extensión), dar prioridad a la ruta estática más específica
90
+ const hasFileExtension = /\.[^.]+$/.test(pathname);
91
+
92
+ if (hasFileExtension && staticMatches.length > 0) {
93
+ // Ordenar por especificidad (longitud del prefijo, descendente) y tomar la más específica
94
+ staticMatches.sort((a, b) => b.specificity - a.specificity);
95
+ // Para solicitudes de archivos, dar prioridad a rutas estáticas más específicas
96
+ return staticMatches[0]; // Devolver la coincidencia estática más específica
97
+ } else if (parametrizedMatches.length > 0) {
98
+ // Para solicitudes sin extensión o rutas API, dar prioridad a rutas parametrizadas
99
+ return parametrizedMatches[0]; // Devolver la primera coincidencia parametrizada
100
+ } else if (staticMatches.length > 0) {
101
+ // Si no hay coincidencias parametrizadas, usar la estática más específica
102
+ staticMatches.sort((a, b) => b.specificity - a.specificity);
103
+ return staticMatches[0];
104
+ }
105
+
71
106
  return null;
72
107
  }
73
108
 
@@ -86,7 +121,7 @@ class RouteMatcher {
86
121
  let escapedPath = '';
87
122
  for (let i = 0; i < path.length; i++) {
88
123
  const char = path[i];
89
- if (char.match(/[.+?^${}()|[\]\\]/) && !(i > 0 && path[i-1] === ':')) {
124
+ if (char.match(/[.+?^${}()|[\]\\-]/) && !(i > 0 && path[i-1] === ':')) {
90
125
  escapedPath += '\\' + char;
91
126
  } else {
92
127
  escapedPath += char;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jerkjs",
3
- "version": "2.5.2",
4
- "description": "JERK Framework v2.5.2 - A comprehensive framework for building secure and scalable APIs with frontend support, sessions, template engine, integration with qbuilderjs, complete MVC architecture with models, and enhanced route loading from directory",
3
+ "version": "2.5.6",
4
+ "description": "JERK Framework v2.5.6 - A comprehensive framework for building secure and scalable APIs with frontend support, sessions, template engine, integration with qbuilderjs, complete MVC architecture with models, enhanced route loading from directory, improved model loading system, and fixed routing issues with static and parametrized routes",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -39,11 +39,19 @@
39
39
  },
40
40
  "homepage": "https://jerk.page.gd/",
41
41
  "dependencies": {
42
- "qbuilderjs":"^1.0.1",
43
- "bcrypt": "^6.0.0",
42
+ "bcrypt": "^5.1.1",
43
+ "cacache": "^20.0.3",
44
44
  "jsonwebtoken": "^9.0.3",
45
+ "make-fetch-happen": "^14.0.3",
45
46
  "mariadb": "^3.4.5",
46
- "sqlite3": "^5.1.7"
47
+ "qbuilderjs": "^1.0.1",
48
+ "sqlite3": "^5.0.2",
49
+ "tar": "^7.5.7"
50
+ },
51
+ "overrides": {
52
+ "tar": "^7.5.7",
53
+ "cacache": "^20.0.3",
54
+ "make-fetch-happen": "^14.0.3"
47
55
  },
48
56
  "engines": {
49
57
  "node": ">=14.0.0"
@@ -1,46 +0,0 @@
1
- /**
2
- * Ejemplo de uso del componente RouteDirectoryLoader
3
- * Este ejemplo demuestra cómo cargar rutas desde un directorio con múltiples archivos JSON
4
- */
5
-
6
- const { APIServer, RouteDirectoryLoader, hooks } = require('./index.js');
7
-
8
- // Crear instancia del servidor
9
- const server = new APIServer({
10
- port: 3000,
11
- host: 'localhost'
12
- });
13
-
14
- // Crear instancia del cargador de rutas desde directorio
15
- const routeDirectoryLoader = new RouteDirectoryLoader();
16
-
17
- // Registrar hooks para ver eventos
18
- hooks.addAction('route_duplicate_detected', (data) => {
19
- console.log('Evento: Ruta duplicada detectada');
20
- console.log('Ruta sobreescrita:', data.overwrittenRoute);
21
- console.log('Nueva ruta:', data.newRoute);
22
- });
23
-
24
- hooks.addAction('route_loaded_from_directory', (data) => {
25
- console.log('Evento: Ruta cargada desde directorio');
26
- console.log('Ruta:', data.route.path, data.route.method);
27
- console.log('Archivo:', data.sourceFile);
28
- });
29
-
30
- // Directorio que contiene los archivos JSON de rutas
31
- const routesDirectory = './routes';
32
-
33
- // Cargar rutas desde el directorio
34
- routeDirectoryLoader.loadRoutesFromDirectory(server, routesDirectory)
35
- .then(routes => {
36
- console.log(`${routes.length} rutas cargadas exitosamente desde el directorio`);
37
-
38
- // Iniciar el servidor
39
- server.start();
40
- })
41
- .catch(error => {
42
- console.error('Error cargando rutas desde directorio:', error.message);
43
- });
44
-
45
- // Opcional: Observar cambios en el directorio de rutas
46
- // routeDirectoryLoader.watchRoutesDirectory(server, routesDirectory);
Binary file
@@ -1,93 +0,0 @@
1
- # Informe QA: Sistema BlackCoffee con Fix de Enrutamiento
2
-
3
- ## Resumen Ejecutivo
4
-
5
- El sistema BlackCoffee ha sido sometido a pruebas exhaustivas tras la implementación de un fix crítico para el enrutamiento de rutas parametrizadas. El problema que impedía el funcionamiento correcto de las rutas parametrizadas en modo directorio ha sido completamente resuelto.
6
-
7
- ## Estado Anterior vs Actual
8
-
9
- ### Antes del Fix
10
- - **Modo directorio (predeterminado)**: ❌ Rutas parametrizadas no funcionaban
11
- - **Modo archivo único**: ✅ Funcionaba correctamente
12
- - **Impacto**: Sistema de colas de Qwen no operativo en modo directorio
13
-
14
- ### Después del Fix
15
- - **Modo directorio**: ✅ **TOTALMENTE OPERATIVO**
16
- - **Modo archivo único**: ✅ **CONTINÚA FUNCIONANDO CORRECTAMENTE**
17
- - **Impacto**: Sistema de colas de Qwen completamente funcional
18
-
19
- ## Cambios Implementados
20
-
21
- ### Arquitectura de Enrutamiento
22
- - Se implementó un componente especializado `RouteMatcher.js` para encapsular toda la lógica de enrutamiento
23
- - Se separaron claramente las responsabilidades entre componentes
24
- - Se centralizó la lógica de matching de rutas
25
-
26
- ### Prioridad de Enrutamiento (Fix Principal)
27
- - **Antes**: Rutas estáticas (prefijos) tenían prioridad sobre rutas parametrizadas
28
- - **Después**: Rutas parametrizadas tienen prioridad sobre rutas estáticas
29
- - **Nuevo orden**: 1) Rutas exactas → 2) Rutas parametrizadas → 3) Rutas estáticas
30
-
31
- ## Resultados de las Pruebas
32
-
33
- ### Rutas Parametrizadas Funcionales
34
- | Ruta | Método | Controlador | Estado |
35
- |------|--------|-------------|---------|
36
- | `/api/products/:id` | GET | `getProductById` | ✅ Funcional |
37
- | `/api/qwen/result/:id` | GET | `getQwenResult` | ✅ Funcional |
38
- | `/api/queue/status/:id` | GET | `getJobStatus` | ✅ Funcional |
39
- | `/api/queue/jobs/:status` | GET | `getJobsByStatus` | ✅ Funcional |
40
-
41
- ### Sistema de Colas de Qwen
42
- | Endpoint | Funcionalidad | Estado |
43
- |----------|---------------|---------|
44
- | `POST /api/qwen/queue` | Creación de trabajos | ✅ Funcional |
45
- | `GET /api/qwen/result/:id` | Consulta de resultados | ✅ Funcional |
46
- | Procesamiento completo | Trabajo de extremo a extremo | ✅ Funcional |
47
-
48
- ### Validación de Parámetros
49
- - Extracción correcta de parámetros de rutas (ej: `:id`, `:status`)
50
- - Valores de parámetros correctamente pasados a controladores
51
- - Respuestas coherentes según valores de parámetros
52
-
53
- ## Cobertura de Pruebas
54
-
55
- ### Modo Directorio (Principal)
56
- - ✅ Carga de rutas desde múltiples archivos JSON
57
- - ✅ Registro correcto de todas las rutas
58
- - ✅ Prioridad correcta de rutas parametrizadas
59
- - ✅ Funcionamiento de rutas estáticas y dinámicas
60
- - ✅ Sistema de colas de Qwen operativo
61
-
62
- ### Modo Archivo Único (Regresión)
63
- - ✅ Continuidad del funcionamiento previo
64
- - ✅ No regresiones introducidas
65
- - ✅ Compatibilidad hacia atrás mantenida
66
-
67
- ## Métricas de Desempeño
68
-
69
- - **Tiempo de respuesta**: Dentro de rangos normales
70
- - **Consumo de memoria**: Estable
71
- - **Procesamiento de rutas**: Correcto en ambos modos
72
- - **Sistema de hooks**: Operativo sin interrupciones
73
-
74
- ## Validación de Endpoints Registrados
75
-
76
- Se verificó que los 19 endpoints se registran correctamente en modo directorio, incluyendo:
77
- - 4 rutas parametrizadas funcionales
78
- - 10 rutas estáticas funcionales
79
- - 5 rutas estáticas funcionales
80
-
81
- ## Conclusión
82
-
83
- ### ✅ ACEPTACIÓN TOTAL
84
-
85
- El fix implementado ha resuelto completamente el problema reportado. El sistema BlackCoffee ahora:
86
-
87
- 1. **Opera correctamente en modo directorio** con todas las rutas parametrizadas funcionando
88
- 2. **Mantiene compatibilidad** con el modo archivo único
89
- 3. **Soporta completamente** el sistema de colas de Qwen
90
- 4. **Sigue principios de arquitectura limpia** con separación de responsabilidades
91
- 5. **No introduce regresiones** en funcionalidades existentes
92
-
93
- **Recomendación**: Aprobar el despliegue del fix a producción.
@@ -1,36 +0,0 @@
1
- #!/bin/bash
2
-
3
- # Script to find a file's location using 'which' and show its real path with 'ls -ln'
4
-
5
- if [ $# -eq 0 ]; then
6
- echo "Usage: $0 <filename>"
7
- echo "Example: $0 python"
8
- exit 1
9
- fi
10
-
11
- filename="$1"
12
-
13
- # Find the file using which
14
- echo "Finding location of '$filename' using 'which':"
15
- which_result=$(which "$filename")
16
-
17
- if [ $? -eq 0 ]; then
18
- echo "Found: $which_result"
19
-
20
- # Get the directory containing the file
21
- file_dir=$(dirname "$which_result")
22
- file_basename=$(basename "$which_result")
23
-
24
- # Change to the directory and use ls -ln to show the real path
25
- echo ""
26
- echo "Using 'ls -ln' to show detailed info:"
27
- (cd "$file_dir" && ls -ln "$file_basename")
28
-
29
- # If the file is a symlink, show the real path using readlink
30
- echo ""
31
- echo "Real path (using readlink -f):"
32
- readlink -f "$which_result"
33
- else
34
- echo "File '$filename' not found in PATH"
35
- exit 1
36
- fi