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.
- package/CHANGELOG.md +36 -0
- package/README.md +202 -5
- package/index.js +29 -4
- package/lib/core/server.js +328 -27
- package/lib/loader/routeLoader.js +148 -117
- package/lib/middleware/compressor.js +87 -18
- package/lib/mvc/GenericAdapter.js +136 -0
- package/lib/mvc/MariaDBAdapter.js +315 -0
- package/lib/mvc/MemoryAdapter.js +269 -0
- package/lib/mvc/ModelControllerExample.js +285 -0
- package/lib/mvc/controllerBase.js +60 -0
- package/lib/mvc/modelBase.js +383 -0
- package/lib/mvc/modelManager.js +284 -0
- package/lib/mvc/userModel.js +265 -0
- package/lib/mvc/viewEngine.js +32 -1
- package/lib/utils/mimeType.js +62 -0
- package/package.json +5 -3
- package/JERK_FRAMEWORK_DIAGRAM.txt +0 -492
- package/JERK_FRAMEWORK_DIAGRAM_MERMAID.mmd +0 -124
- package/JERK_FRAMEWORK_DOCUMENTATION.md +0 -527
- package/LICENSE +0 -201
- package/README_EN.md +0 -230
- package/README_PT.md +0 -230
- package/docs/ARQUITECTURA_ROUTES.md +0 -140
- package/docs/EXTENSION_MANUAL.md +0 -955
- package/docs/FIREWALL_MANUAL.md +0 -416
- package/docs/HOOK-2.0.md +0 -512
- package/docs/HOOKS_REFERENCE_IMPROVED.md +0 -596
- package/docs/MANUAL_API_SDK.md +0 -536
- package/docs/MARIADB_TOKENS_IMPLEMENTATION.md +0 -110
- package/docs/MIDDLEWARE_MANUAL.md +0 -518
- package/docs/OAUTH2_GOOGLE_MANUAL.md +0 -405
- package/docs/ROUTING_WITHOUT_JSON_GUIDE.md +0 -454
- package/docs/frontend-and-sessions.md +0 -353
- package/docs/guia_inicio_rapido_jerkjs.md +0 -113
- package/examples/examples.arj +0 -0
- package/standard/CompressionTestController.js +0 -56
- package/standard/HealthController.js +0 -16
- package/standard/HomeController.js +0 -12
- package/standard/ProductController.js +0 -18
- package/standard/README.md +0 -47
- package/standard/UserController.js +0 -23
- package/standard/package.json +0 -22
- package/standard/routes.json +0 -65
- package/standard/server.js +0 -140
- package/standardA/controllers/AuthController.js +0 -82
- package/standardA/controllers/HomeController.js +0 -19
- package/standardA/controllers/UserController.js +0 -41
- package/standardA/server.js +0 -311
- package/standardA/views/auth/dashboard.html +0 -51
- package/standardA/views/auth/login.html +0 -47
- package/standardA/views/index.html +0 -32
- package/standardA/views/users/detail.html +0 -28
- 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;
|
package/lib/mvc/viewEngine.js
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "JERK Framework v2.
|
|
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",
|