jerkjs 2.1.7 → 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 +30 -0
  2. package/README.md +201 -4
  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/mvc/GenericAdapter.js +136 -0
  7. package/lib/mvc/MariaDBAdapter.js +315 -0
  8. package/lib/mvc/MemoryAdapter.js +269 -0
  9. package/lib/mvc/ModelControllerExample.js +285 -0
  10. package/lib/mvc/controllerBase.js +60 -0
  11. package/lib/mvc/modelBase.js +383 -0
  12. package/lib/mvc/modelManager.js +284 -0
  13. package/lib/mvc/userModel.js +265 -0
  14. package/lib/mvc/viewEngine.js +32 -1
  15. package/lib/utils/mimeType.js +62 -0
  16. package/package.json +5 -3
  17. package/BUG_REPORTE_COMPRESION.txt +0 -72
  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
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Modelo de ejemplo para usuarios en el framework JERK
3
+ * Implementación del componente MVC userModel.js
4
+ * Extiende ModelBase para aprovechar todas las funcionalidades
5
+ */
6
+
7
+ const ModelBase = require('./modelBase');
8
+
9
+ class UserModel extends ModelBase {
10
+ /**
11
+ * Constructor del modelo de usuario
12
+ * @param {Object} options - Opciones de configuración del modelo
13
+ */
14
+ constructor(options = {}) {
15
+ super({
16
+ ...options,
17
+ tableName: options.tableName || 'users'
18
+ });
19
+
20
+ // Definir campos del modelo
21
+ this.fields = {
22
+ id: { type: 'integer', primaryKey: true, autoIncrement: true },
23
+ username: { type: 'string', required: true, unique: true },
24
+ email: { type: 'string', required: true, unique: true },
25
+ password: { type: 'string', required: true },
26
+ firstName: { type: 'string' },
27
+ lastName: { type: 'string' },
28
+ createdAt: { type: 'datetime', default: 'CURRENT_TIMESTAMP' },
29
+ updatedAt: { type: 'datetime', default: 'CURRENT_TIMESTAMP' }
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Valida los datos del usuario antes de guardar
35
+ * @param {string} operation - Operación a realizar
36
+ * @param {Object} data - Datos a validar
37
+ * @returns {Object} - Resultado de la validación
38
+ */
39
+ validate(operation, data) {
40
+ const result = { isValid: true, errors: [] };
41
+
42
+ // Validaciones específicas para creación
43
+ if (operation === 'create') {
44
+ if (!data.username) {
45
+ result.isValid = false;
46
+ result.errors.push('Username es requerido');
47
+ }
48
+
49
+ if (!data.email) {
50
+ result.isValid = false;
51
+ result.errors.push('Email es requerido');
52
+ }
53
+
54
+ if (!data.password) {
55
+ result.isValid = false;
56
+ result.errors.push('Password es requerido');
57
+ }
58
+ }
59
+
60
+ // Validaciones para actualización
61
+ if (operation === 'update') {
62
+ // Validar solo los campos que se están actualizando
63
+ if (data.hasOwnProperty('username') && !data.username) {
64
+ result.isValid = false;
65
+ result.errors.push('Username no puede estar vacío si se proporciona');
66
+ }
67
+
68
+ if (data.hasOwnProperty('email') && !data.email) {
69
+ result.isValid = false;
70
+ result.errors.push('Email no puede estar vacío si se proporciona');
71
+ }
72
+
73
+ if (data.hasOwnProperty('password') && !data.password) {
74
+ result.isValid = false;
75
+ result.errors.push('Password no puede estar vacío si se proporciona');
76
+ }
77
+
78
+ // Validar formato de email si se está actualizando
79
+ if (data.email && !this.isValidEmail(data.email)) {
80
+ result.isValid = false;
81
+ result.errors.push('Email no tiene un formato válido');
82
+ }
83
+ }
84
+
85
+ // Validaciones comunes para creación y actualización
86
+ if (operation === 'create' || operation === 'update') {
87
+ // Validar formato de email
88
+ if (data.email && !this.isValidEmail(data.email)) {
89
+ result.isValid = false;
90
+ result.errors.push('Email no tiene un formato válido');
91
+ }
92
+ }
93
+
94
+ // Disparar hook para validación
95
+ if (this.hooks) {
96
+ const validationResult = this.hooks.applyFilters(
97
+ 'user_model_validate',
98
+ result,
99
+ operation,
100
+ data
101
+ );
102
+
103
+ return validationResult;
104
+ }
105
+
106
+ return result;
107
+ }
108
+
109
+ /**
110
+ * Verifica si un email tiene formato válido
111
+ * @param {string} email - Email a validar
112
+ * @returns {boolean} - Verdadero si el email es válido
113
+ */
114
+ isValidEmail(email) {
115
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
116
+ return emailRegex.test(email);
117
+ }
118
+
119
+ /**
120
+ * Busca un usuario por su nombre de usuario
121
+ * @param {string} username - Nombre de usuario a buscar
122
+ * @returns {Promise<Object|null>} - Usuario encontrado o null
123
+ */
124
+ async findByUsername(username) {
125
+ return await this.findOne({ username });
126
+ }
127
+
128
+ /**
129
+ * Busca un usuario por su email
130
+ * @param {string} email - Email a buscar
131
+ * @returns {Promise<Object|null>} - Usuario encontrado o null
132
+ */
133
+ async findByEmail(email) {
134
+ return await this.findOne({ email });
135
+ }
136
+
137
+ /**
138
+ * Crea un nuevo usuario con validación
139
+ * @param {Object} userData - Datos del usuario a crear
140
+ * @returns {Promise<Object>} - Usuario creado
141
+ */
142
+ async createUser(userData) {
143
+ // Validar datos antes de crear
144
+ const validation = this.validate('create', userData);
145
+
146
+ if (!validation.isValid) {
147
+ throw new Error(`Validación fallida: ${validation.errors.join(', ')}`);
148
+ }
149
+
150
+ // Encriptar contraseña antes de guardar
151
+ if (userData.password) {
152
+ userData.password = await this.hashPassword(userData.password);
153
+ }
154
+
155
+ return await this.create(userData);
156
+ }
157
+
158
+ /**
159
+ * Actualiza un usuario con validación
160
+ * @param {Object} conditions - Condiciones para encontrar el usuario
161
+ * @param {Object} userData - Datos del usuario a actualizar
162
+ * @returns {Promise<number>} - Número de filas afectadas
163
+ */
164
+ async updateUser(conditions, userData) {
165
+ // Validar solo los campos que se están actualizando
166
+ const fieldsToUpdate = {};
167
+ for (const [key, value] of Object.entries(userData)) {
168
+ if (value !== undefined && value !== null) {
169
+ fieldsToUpdate[key] = value;
170
+ }
171
+ }
172
+
173
+ // Validar datos antes de actualizar
174
+ const validation = this.validate('update', fieldsToUpdate);
175
+
176
+ if (!validation.isValid) {
177
+ throw new Error(`Validación fallida: ${validation.errors.join(', ')}`);
178
+ }
179
+
180
+ // Encriptar contraseña si se proporciona
181
+ if (fieldsToUpdate.password) {
182
+ fieldsToUpdate.password = await this.hashPassword(fieldsToUpdate.password);
183
+ }
184
+
185
+ return await this.update(conditions, fieldsToUpdate);
186
+ }
187
+
188
+ /**
189
+ * Encripta una contraseña
190
+ * @param {string} password - Contraseña a encriptar
191
+ * @returns {Promise<string>} - Contraseña encriptada
192
+ */
193
+ async hashPassword(password) {
194
+ // Importar bcrypt dinámicamente
195
+ const bcrypt = require('bcrypt');
196
+ const saltRounds = 10;
197
+ return await bcrypt.hash(password, saltRounds);
198
+ }
199
+
200
+ /**
201
+ * Verifica si una contraseña coincide con el hash
202
+ * @param {string} password - Contraseña sin encriptar
203
+ * @param {string} hashedPassword - Contraseña encriptada
204
+ * @returns {Promise<boolean>} - Verdadero si coinciden
205
+ */
206
+ async verifyPassword(password, hashedPassword) {
207
+ // Importar bcrypt dinámicamente
208
+ const bcrypt = require('bcrypt');
209
+ return await bcrypt.compare(password, hashedPassword);
210
+ }
211
+
212
+ /**
213
+ * Obtiene usuarios con información paginada
214
+ * @param {Object} options - Opciones de paginación
215
+ * @param {number} options.page - Página a obtener (por defecto 1)
216
+ * @param {number} options.limit - Límite de resultados por página (por defecto 10)
217
+ * @param {Object} conditions - Condiciones de búsqueda
218
+ * @returns {Promise<Object>} - Resultados con información de paginación
219
+ */
220
+ async getUsersPaginated(options = {}, conditions = {}) {
221
+ const { page = 1, limit = 10 } = options;
222
+ const offset = (page - 1) * limit;
223
+
224
+ // Obtener los usuarios para la página solicitada
225
+ const users = await this.find(conditions, {
226
+ limit,
227
+ offset,
228
+ orderBy: 'createdAt DESC'
229
+ });
230
+
231
+ // Contar el total de usuarios
232
+ const totalCount = await this.count(conditions);
233
+
234
+ // Calcular información de paginación
235
+ const totalPages = Math.ceil(totalCount / limit);
236
+
237
+ return {
238
+ data: users,
239
+ pagination: {
240
+ currentPage: page,
241
+ totalPages,
242
+ totalItems: totalCount,
243
+ itemsPerPage: limit,
244
+ hasNextPage: page < totalPages,
245
+ hasPrevPage: page > 1
246
+ }
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Serializa el modelo de usuario a JSON
252
+ * @returns {Object} - Representación JSON del modelo
253
+ */
254
+ toJSON() {
255
+ const baseJson = super.toJSON();
256
+ return {
257
+ ...baseJson,
258
+ modelName: this.constructor.name,
259
+ tableName: this.tableName,
260
+ fieldCount: Object.keys(this.fields).length
261
+ };
262
+ }
263
+ }
264
+
265
+ module.exports = UserModel;
@@ -215,7 +215,7 @@ class ViewEngine {
215
215
  render(viewName, data = {}, options = {}) {
216
216
  // Obtener la ruta completa de la vista
217
217
  const viewPath = this.getViewPath(viewName);
218
-
218
+
219
219
  // Verificar si la vista existe
220
220
  if (!fs.existsSync(viewPath)) {
221
221
  throw new Error(`Vista no encontrada: ${viewPath}`);
@@ -238,6 +238,37 @@ class ViewEngine {
238
238
  }
239
239
  }
240
240
 
241
+ // Si es una vista de contenido (no un layout), procesarla y usarla como contenido para el layout
242
+ if (options.layout) {
243
+ // Procesar la vista actual con las variables
244
+ const processedViewContent = this.processTemplate(viewContent, data, options);
245
+
246
+ // Cargar el layout
247
+ const layoutPath = this.getViewPath(options.layout);
248
+ if (fs.existsSync(layoutPath)) {
249
+ let layoutContent = fs.readFileSync(layoutPath, 'utf8');
250
+
251
+ // Validar sintaxis del layout si está habilitado
252
+ if (options.validateSyntax !== false) {
253
+ const layoutValidationErrors = ViewEngine.validateTemplate(layoutContent);
254
+ if (layoutValidationErrors.length > 0) {
255
+ this.logger.warn(`Errores de sintaxis en el layout ${options.layout}:`, layoutValidationErrors);
256
+ }
257
+ }
258
+
259
+ // Reemplazar el placeholder {{content}} con el contenido procesado
260
+ layoutContent = layoutContent.replace(/\{\{content\}\}/g, processedViewContent);
261
+
262
+ // Procesar el layout con las variables
263
+ layoutContent = this.processTemplate(layoutContent, data, options);
264
+
265
+ return layoutContent;
266
+ } else {
267
+ // Si no existe el layout, devolver solo la vista procesada
268
+ return this.processTemplate(viewContent, data, options);
269
+ }
270
+ }
271
+
241
272
  // Procesar bloques de inclusión (similar a <?php include ?>)
242
273
  viewContent = this.processIncludes(viewContent, path.dirname(viewPath));
243
274
 
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Utilidad para determinar tipos MIME
3
+ * JERK Framework v2.1 - Sistema de tipos MIME con integración de hooks
4
+ */
5
+
6
+ const mimeTypes = {
7
+ '.html': 'text/html',
8
+ '.htm': 'text/html',
9
+ '.js': 'application/javascript',
10
+ '.css': 'text/css',
11
+ '.json': 'application/json',
12
+ '.xml': 'application/xml',
13
+ '.png': 'image/png',
14
+ '.jpg': 'image/jpeg',
15
+ '.jpeg': 'image/jpeg',
16
+ '.gif': 'image/gif',
17
+ '.svg': 'image/svg+xml',
18
+ '.ico': 'image/x-icon',
19
+ '.woff': 'font/woff',
20
+ '.woff2': 'font/woff2',
21
+ '.ttf': 'font/ttf',
22
+ '.eot': 'application/vnd.ms-fontobject',
23
+ '.pdf': 'application/pdf',
24
+ '.txt': 'text/plain',
25
+ '.csv': 'text/csv',
26
+ '.zip': 'application/zip',
27
+ '.mp3': 'audio/mpeg',
28
+ '.mp4': 'video/mp4',
29
+ '.wav': 'audio/wav',
30
+ '.ogg': 'audio/ogg',
31
+ '.webm': 'video/webm',
32
+ '.webp': 'image/webp',
33
+ '.avif': 'image/avif',
34
+ '.mjs': 'application/javascript',
35
+ '.jsx': 'text/jsx',
36
+ '.ts': 'application/typescript',
37
+ '.tsx': 'text/tsx',
38
+ '.md': 'text/markdown',
39
+ '.yaml': 'text/yaml',
40
+ '.yml': 'text/yaml',
41
+ '.rtf': 'application/rtf',
42
+ '.doc': 'application/msword',
43
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
44
+ '.xls': 'application/vnd.ms-excel',
45
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
46
+ '.ppt': 'application/vnd.ms-powerpoint',
47
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
48
+ };
49
+
50
+ /**
51
+ * Obtiene el tipo MIME para una extensión de archivo
52
+ * @param {string} filename - Nombre del archivo
53
+ * @returns {string} - Tipo MIME correspondiente
54
+ */
55
+ function getMimeType(filename) {
56
+ const extension = '.' + filename.split('.').pop().toLowerCase();
57
+ return mimeTypes[extension] || 'application/octet-stream';
58
+ }
59
+
60
+ module.exports = {
61
+ getMimeType
62
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jerkjs",
3
- "version": "2.1.7",
4
- "description": "JERK Framework v2.1 - A comprehensive framework for building secure and scalable APIs with frontend support, sessions, and template engine with performance optimizations",
3
+ "version": "2.2.0",
4
+ "description": "JERK Framework v2.2 - A comprehensive framework for building secure and scalable APIs with frontend support, sessions, and template engine with performance optimizations and complete MVC architecture with models",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -24,7 +24,9 @@
24
24
  "waf",
25
25
  "performance",
26
26
  "optimized",
27
- "hooked"
27
+ "hooked",
28
+ "mvc",
29
+ "models"
28
30
  ],
29
31
  "author": "JERK Framework Team / Benjamin Sanhez Cardenas",
30
32
  "license": "Apache-2.0",
@@ -1,72 +0,0 @@
1
- # Reporte de Bug: Falta de Header Content-Encoding en Respuestas Comprimidas
2
-
3
- ## Descripción del Problema
4
-
5
- El middleware de compresión del framework JERK no estaba enviando el header `Content-Encoding: gzip` (o `Content-Encoding: deflate`) cuando se comprimían las respuestas. Esto causaba que los navegadores y clientes HTTP no pudieran descomprimir automáticamente las respuestas comprimidas, resultando en contenido ilegible para el usuario final.
6
-
7
- ## Causa Raíz
8
-
9
- El problema se producía porque los controladores en el framework JERK llamaban directamente a `res.writeHead()` antes de que el middleware de compresión tuviera la oportunidad de modificar los headers. Cuando un controlador llama a `res.writeHead()`, los encabezados se envían inmediatamente al cliente, y una vez enviados, no se pueden modificar posteriormente.
10
-
11
- En el código del controlador de ejemplo:
12
- ```javascript
13
- res.writeHead(200, { 'Content-Type': 'application/json' });
14
- res.end(JSON.stringify(largeData));
15
- ```
16
-
17
- La llamada a `writeHead` se ejecutaba antes de que el middleware de compresión pudiera agregar el header `Content-Encoding`.
18
-
19
- ## Solución Implementada
20
-
21
- Se modificó el middleware de compresión (`lib/middleware/compressor.js`) para implementar un sistema de captura y posposición de encabezados:
22
-
23
- ### 1. Interceptación de writeHead
24
- Se sobreescribió el método `res.writeHead()` para capturar los encabezados originales pero no enviarlos inmediatamente:
25
-
26
- ```javascript
27
- res.writeHead = (code, headers) => {
28
- statusCode = code;
29
- if (typeof headers === 'object' && headers !== null) {
30
- responseHeaders = { ...headers };
31
- }
32
-
33
- // NO LLAMAR A ORIGINAL WRITEHEAD AÚN - ESPERAR A VER SI NECESITAMOS COMPRIMIR
34
- // Marcar que writeHead ha sido llamado pero diferir el envío real
35
- res.__hasCalledWriteHead = true;
36
- res.__pendingStatusCode = code;
37
- res.__pendingHeaders = { ...headers };
38
-
39
- return res;
40
- };
41
- ```
42
-
43
- ### 2. Envío Condicionado de Encabezados
44
- Cuando se determina si se debe comprimir la respuesta, se combinan los encabezados originales con el header `Content-Encoding` y se envían juntos:
45
-
46
- ```javascript
47
- if (res.__hasCalledWriteHead && res.__pendingHeaders) {
48
- // Combinar los encabezados pendientes con el encabezado de compresión
49
- const combinedHeaders = { ...res.__pendingHeaders, ...headersToSend };
50
- originalWriteHead.call(res, res.__pendingStatusCode || statusCode, combinedHeaders);
51
- } else {
52
- originalWriteHead.call(res, statusCode, headersToSend);
53
- }
54
- ```
55
-
56
- ## Resultado
57
-
58
- Después de la corrección:
59
- - Las respuestas comprimidas ahora incluyen correctamente el header `Content-Encoding: gzip`
60
- - Los navegadores pueden descomprimir automáticamente las respuestas
61
- - El contenido se muestra correctamente al usuario
62
- - Se mantiene la compatibilidad con los controladores existentes
63
-
64
- ## Archivos Modificados
65
-
66
- - `lib/middleware/compressor.js`: Implementación de la solución
67
-
68
- ## Pruebas Realizadas
69
-
70
- - Se verificó que el header `Content-Encoding: gzip` aparece en las respuestas cuando se solicita compresión
71
- - Se confirmó que la respuesta se puede descomprimir correctamente usando herramientas como `zcat`
72
- - Se probó con múltiples endpoints para asegurar la consistencia del comportamiento