nicola-framework 1.0.3 → 1.0.5

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/README.md CHANGED
@@ -10,30 +10,60 @@ Nicola expone un **servidor HTTP nativo** con un **router tipo Express** y utili
10
10
 
11
11
  ---
12
12
 
13
- ## Estado actual del proyecto (lo que realmente hay)
13
+ ## 📌 Índice
14
+
15
+ - [Qué incluye](#-qué-incluye)
16
+ - [Instalación](#-instalación)
17
+ - [Crear un proyecto (CLI)](#-crear-un-proyecto-cli)
18
+ - [Levantar el servidor (dev)](#-levantar-el-servidor-dev)
19
+ - [Quickstart (manual)](#-quickstart-manual)
20
+ - [Guía del Router](#-guía-del-router)
21
+ - [Request/Response (lo que hay)](#-requestresponse-lo-que-hay)
22
+ - [Manejo de errores](#-manejo-de-errores)
23
+ - [Middlewares](#-middlewares)
24
+ - [Seguridad (Regulator + JWT)](#-seguridad-regulator--jwt)
25
+ - [Dynamo ORM (Postgres)](#-dynamo-orm-postgres)
26
+ - [Variables de entorno](#-variables-de-entorno)
27
+ - [Tests](#-tests)
28
+ - [Troubleshooting](#-troubleshooting)
29
+
30
+ ---
31
+
32
+ ## ✅ Qué incluye
33
+
34
+ Esta lista está alineada con el **código actual** del repositorio:
14
35
 
15
36
  - **Core/Router**: `Nicola` (default) extiende `Remote`.
16
37
  - **Body parsing**: JSON si `Content-Type` incluye `application/json` (límite ~2MB). Si no, `req.body = {}`.
17
- - **Helpers de response**: `res.json(data)` y `res.send(text)`. (No existe `res.status()`.)
18
- - **CORS**: `EasyCors` permite `*` y responde `OPTIONS` con `204`.
38
+ - **Helpers de response**: `res.json(data)` y `res.send(text)`.
39
+ - Nota: no existe `res.status()`; usa `res.statusCode`.
40
+ - **CORS**: `EasyCors()` permite `*` y responde `OPTIONS` con `204`.
19
41
  - **Security headers**: `Teleforce` aplica headers básicos (no-sniff, frame deny, etc.).
20
42
  - **Logger**: `Shadowgraph` loggea al terminar la respuesta.
21
43
  - **Errores**: si un handler lanza error o llama `next(err)`, se responde HTML via `BlackBox`.
22
- - **JWT**: `Coherer` (HS256) funciona vía métodos **estáticos** y requiere `NICOLA_SECRET` en env.
23
- - **ORM**: `Dynamo` soporta **Postgres** hoy (driver `postgres`). La lib `pg` es **dependencia opcional** (se instala aparte).
24
- - **Hot reload**: `LiveCurrent` reinicia el proceso Node al detectar cambios en el directorio.
44
+ - En `NODE_ENV=production` se ocultan mensaje/stack al cliente.
45
+ - **JWT**: `Coherer` (HS256) via métodos **estáticos** y requiere `NICOLA_SECRET`.
46
+ - **ORM**: `Dynamo` soporta **Postgres** (driver `postgres`). `pg` es dependencia opcional.
47
+ - **Hot reload**: `LiveCurrent` reinicia el proceso Node al detectar cambios (en `process.cwd()`).
25
48
 
26
49
  ---
27
50
 
28
51
  ## 📦 Instalación
29
52
 
53
+ Requisitos:
54
+
55
+ - Node.js >= 16
56
+ - Proyecto ESM (Nicola es ESM)
57
+
58
+ Instalar como dependencia del proyecto:
59
+
30
60
  ```bash
31
61
  npm install nicola-framework
32
62
  ```
33
63
 
34
64
  ### (Opcional) Postgres
35
65
 
36
- El dialecto Postgres usa `pg` por import dinámico.
66
+ El dialecto Postgres usa `pg` por import dinámico. Si vas a usar Dynamo con Postgres:
37
67
 
38
68
  ```bash
39
69
  npm install pg
@@ -41,139 +71,376 @@ npm install pg
41
71
 
42
72
  ---
43
73
 
44
- ## Quickstart
74
+ ## 🧰 Crear un proyecto (CLI)
75
+
76
+ Nicola incluye un CLI con dos comandos:
77
+
78
+ - `init <nombre>`: crea una estructura mínima.
79
+ - `start`: ejecuta `app.js` con hot reload (LiveCurrent).
45
80
 
46
- ### Servidor HTTP básico
81
+ ### Opción A: sin instalar global (recomendado)
82
+
83
+ ```bash
84
+ npx nicola init mi-api
85
+ cd mi-api
86
+ npm install
87
+ npm start
88
+ ```
89
+
90
+ ### Opción B: instalando global
91
+
92
+ ```bash
93
+ npm install -g nicola-framework
94
+ nicola init mi-api
95
+ cd mi-api
96
+ npm install
97
+ nicola start
98
+ ```
99
+
100
+ ### Qué genera `nicola init`
101
+
102
+ La CLI crea:
103
+
104
+ - `app.js`
105
+ - `src/controllers/user.controller.js`
106
+ - `src/routes/user.Routes.js`
107
+ - `package.json` con `"type": "module"` y script `start`.
108
+
109
+ El `app.js` generado monta las rutas así:
47
110
 
48
111
  ```js
49
- import Nicola from 'nicola-framework';
112
+ import Nicola, { Regulator } from "nicola-framework";
113
+ import UserRoute from "./src/routes/user.Routes.js";
114
+
115
+ Regulator.load();
50
116
 
51
117
  const app = new Nicola();
52
118
 
53
- app.get('/', (req, res) => {
54
- res.json({ ok: true, message: 'Hello from Nicola!' });
119
+ app.use("/user", UserRoute);
120
+
121
+ app.get("/", (req, res) => {
122
+ res.json({ message: "Bienvenido a tu proyecto en Nicola" });
55
123
  });
56
124
 
57
125
  app.listen(3000, () => {
58
- console.log('Server running on http://localhost:3000');
126
+ console.log("Servidor corriendo en http://localhost:3000");
59
127
  });
60
128
  ```
61
129
 
62
- ### Router anidado y params
130
+ ---
131
+
132
+ ## 🚀 Levantar el servidor (dev)
133
+
134
+ Si tu entrypoint es `app.js` (como genera la CLI), tienes dos opciones:
135
+
136
+ - `npm start` (en proyecto generado)
137
+ - `nicola start` / `npx nicola start`
138
+
139
+ `start` usa LiveCurrent, que:
140
+
141
+ - observa cambios en el directorio actual (recursivo)
142
+ - ignora `node_modules`
143
+ - reinicia el proceso cuando detecta un cambio
144
+
145
+ ---
146
+
147
+ ## ⚡ Quickstart (manual)
148
+
149
+ Servidor HTTP básico:
63
150
 
64
151
  ```js
65
- import { Nicola, Remote } from 'nicola-framework';
152
+ import Nicola from "nicola-framework";
66
153
 
67
154
  const app = new Nicola();
68
- const api = new Remote();
69
155
 
70
- api.get('/users', (req, res) => {
71
- res.json({ users: ['Alice', 'Bob'] });
156
+ app.get("/", (req, res) => {
157
+ res.json({ ok: true, message: "Hello from Nicola!" });
72
158
  });
73
159
 
74
- api.get('/users/:id', (req, res) => {
75
- res.json({ userId: req.params.id });
160
+ app.listen(3000, () => {
161
+ console.log("Server running on http://localhost:3000");
76
162
  });
163
+ ```
77
164
 
78
- app.use('/api', api);
79
- app.listen(3000);
165
+ Opcional: timeouts del server (en ms). Nicola lee estas variables al llamar `listen()`:
166
+
167
+ ```env
168
+ NICOLA_REQUEST_TIMEOUT=30000
169
+ NICOLA_HEADERS_TIMEOUT=10000
170
+ NICOLA_KEEP_ALIVE_TIMEOUT=65000
80
171
  ```
81
172
 
82
173
  ---
83
174
 
84
- ## 🧠 API (resumen fiel al código)
175
+ ## 🧭 Guía del Router
176
+
177
+ ### 1) Rutas básicas
178
+
179
+ `Nicola` y `Remote` soportan:
85
180
 
86
- ### `Nicola` (Core)
181
+ - `get`, `post`, `put`, `patch`, `delete`
87
182
 
88
- - `new Nicola()`
89
- - `app.get/post/put/patch/delete(path, ...handlers)`
90
- - `app.use([path], ...handlers | router)`
91
- - `app.listen(port, [callback])`
183
+ ```js
184
+ import Nicola from "nicola-framework";
185
+
186
+ const app = new Nicola();
92
187
 
93
- Notas:
94
- - `Nicola.listen()` ejecuta internamente `Shadowgraph`, `EasyCors` y `Teleforce` en cada request.
95
- - `req.query` se construye desde querystring.
188
+ app.get("/ping", (req, res) => {
189
+ res.statusCode = 200;
190
+ res.end("pong");
191
+ });
96
192
 
97
- ### `Remote` (Router)
193
+ app.listen(3000);
194
+ ```
98
195
 
99
- `Remote` es el router base. Soporta middlewares y rutas con params (`/users/:id`).
196
+ ### 2) Params (`/users/:id`)
100
197
 
101
- Ejemplo de middleware simple:
198
+ Cuando la ruta tiene `:param`, Nicola crea:
199
+
200
+ - `req.params` (objeto con strings)
201
+
202
+ ```js
203
+ app.get("/users/:id", (req, res) => {
204
+ res.json({ id: req.params.id });
205
+ });
206
+ ```
207
+
208
+ ### 3) Routers anidados (`use`)
209
+
210
+ Puedes montar un router dentro de otro:
211
+
212
+ ```js
213
+ import { Nicola, Remote } from "nicola-framework";
214
+
215
+ const app = new Nicola();
216
+ const api = new Remote();
217
+
218
+ api.get("/ping", (req, res) => {
219
+ res.end("pong");
220
+ });
221
+
222
+ app.use("/api", api);
223
+ app.listen(3000);
224
+ ```
225
+
226
+ Importante: el mount path es estricto. `/api` hace match con `/api/...` pero NO con `/apix/...`.
227
+
228
+ ### 4) Middlewares
229
+
230
+ Un middleware tiene firma `(req, res, next)`:
102
231
 
103
232
  ```js
104
233
  app.use((req, res, next) => {
105
- // No existe res.status(); usa statusCode
106
- if (req.url === '/blocked') {
234
+ // no existe res.status(); usa res.statusCode
235
+ if (req.url === "/blocked") {
107
236
  res.statusCode = 403;
108
- res.end('Forbidden');
237
+ res.end("Forbidden");
109
238
  return;
110
239
  }
111
240
  next();
112
241
  });
113
242
  ```
114
243
 
115
- Errores:
244
+ Nicola soporta handlers sync y async (Promise). Si un handler async rechaza, el error se propaga a `next(err)`.
245
+
246
+ ---
247
+
248
+ ## 🧾 Request/Response (lo que hay)
249
+
250
+ ### Request (`req`)
251
+
252
+ Nicola trabaja sobre `http.IncomingMessage` y añade:
253
+
254
+ - `req.url`: **solo pathname** (sin querystring). Se reescribe internamente.
255
+ - `req.query`: objeto creado desde `?a=1&b=hola`.
256
+ - `req.params`: solo existe en rutas con `:param`.
257
+ - `req.body`: solo se parsea si `Content-Type` incluye `application/json`.
258
+ - inválido => `400 Bad Request: Invalid JSON`
259
+ - > ~2MB => `413 Request Entity Too Large`
260
+ - si no es JSON => `{}`
261
+
262
+ ### Response (`res`)
263
+
264
+ Nicola trabaja sobre `http.ServerResponse` y añade helpers:
265
+
266
+ - `res.json(data)` → setea `Content-Type: application/json` y serializa.
267
+ - `res.send(text)` → setea `Content-Type: text/plain`.
268
+
269
+ Para status codes, usa:
270
+
271
+ ```js
272
+ res.statusCode = 201;
273
+ res.json({ created: true });
274
+ ```
275
+
276
+ ---
277
+
278
+ ## 💥 Manejo de errores
279
+
280
+ Si ocurre un error en la cadena de handlers:
281
+
282
+ - `throw new Error(...)`
283
+ - o `next(err)`
284
+
285
+ Nicola responde con `BlackBox` (HTML):
286
+
287
+ - en `NODE_ENV=production` el cliente ve `Internal Server Error` sin stack
288
+ - en dev, incluye `err.message` y stack
289
+
290
+ Ejemplo:
116
291
 
117
292
  ```js
118
- app.get('/boom', (req, res) => {
119
- throw new Error('Boom');
293
+ app.get("/boom", (req, res) => {
294
+ throw new Error("Boom");
120
295
  });
121
296
  ```
122
297
 
123
298
  ---
124
299
 
125
- ## 🔐 Seguridad
300
+ ## 🧩 Middlewares
126
301
 
127
- ### `Regulator` (.env)
302
+ ### `Insulator(schema)` (validación de body)
128
303
 
129
- Lee `.env` desde el directorio actual (`process.cwd()`) y lo copia a `process.env`.
304
+ Valida que existan campos y que su tipo coincida con `typeof`.
130
305
 
131
306
  ```js
132
- import { Regulator } from 'nicola-framework';
307
+ import { Insulator } from "nicola-framework";
133
308
 
134
- Regulator.load();
309
+ const schema = {
310
+ name: "string",
311
+ age: "number",
312
+ };
313
+
314
+ app.post("/users", Insulator(schema), (req, res) => {
315
+ res.json({ ok: true });
316
+ });
317
+ ```
318
+
319
+ Respuestas típicas:
320
+
321
+ - falta campo → `400` y mensaje `Falta campo: name`
322
+ - tipo incorrecto → `400` y mensaje `El campo age debe ser number`
323
+
324
+ ### `EasyCors(options)`
325
+
326
+ Soporta:
327
+
328
+ - `origin: "*"` (default)
329
+ - `origin: ["https://app.com", "http://localhost:5173"]`
330
+
331
+ ```js
332
+ import { EasyCors } from "nicola-framework";
333
+
334
+ app.use(EasyCors({ origin: ["https://mi-front.com"] }));
335
+ ```
336
+
337
+ Nota importante sobre `Nicola.listen()`: internamente siempre ejecuta `EasyCors()` **antes** de tu router.
338
+
339
+ - puedes sobreescribir headers CORS en tus handlers para requests normales
340
+ - pero el preflight `OPTIONS` se resuelve ahí mismo (204), antes de que corran tus rutas
341
+
342
+ ### `Teleforce`
343
+
344
+ Agrega headers de seguridad básicos:
345
+
346
+ - `X-Content-Type-Options: nosniff`
347
+ - `X-Frame-Options: Deny`
348
+ - `X-XSS-Protection: 1`
349
+
350
+ ### `Shadowgraph`
351
+
352
+ Logger simple al finalizar la respuesta:
353
+
354
+ `[GET] /ruta - 200 OK - 12ms`
355
+
356
+ ---
357
+
358
+ ## 🔐 Seguridad (Regulator + JWT)
359
+
360
+ ### `Regulator.load()` (.env)
361
+
362
+ Lee `.env` desde `process.cwd()` y copia valores a `process.env`.
363
+
364
+ Formato soportado:
365
+
366
+ - `KEY=value`
367
+ - líneas vacías OK
368
+ - comentarios con `#` al inicio
369
+
370
+ Ejemplo:
371
+
372
+ ```env
373
+ NICOLA_SECRET=mi-secreto-super-seguro
374
+ NODE_ENV=production
135
375
  ```
136
376
 
137
377
  ### `Coherer` (JWT HS256)
138
378
 
139
- `Coherer` es una clase con métodos **estáticos**.
379
+ `Coherer` es una clase con métodos estáticos:
380
+
381
+ - `Coherer.sign(payload, { expiresIn })`
382
+ - `Coherer.verify(token)`
383
+
384
+ `expiresIn` soporta formato **número + unidad**:
385
+
386
+ - `10s`, `15m`, `24h`, `7d`, `1y`
387
+
388
+ Ejemplo:
140
389
 
141
390
  ```js
142
- import { Regulator, Coherer } from 'nicola-framework';
391
+ import { Regulator, Coherer } from "nicola-framework";
143
392
 
144
393
  Regulator.load();
145
394
 
146
395
  const token = Coherer.sign(
147
- { userId: 123, role: 'admin' },
148
- { expiresIn: '24h' }
396
+ { userId: 123, role: "admin" },
397
+ { expiresIn: "24h" }
149
398
  );
150
399
 
151
400
  const payload = Coherer.verify(token);
152
401
  console.log(payload.userId);
153
402
  ```
154
403
 
155
- `.env` mínimo:
404
+ Middleware típico para proteger rutas (Bearer token):
156
405
 
157
- ```env
158
- NICOLA_SECRET=mi-secreto-super-seguro
406
+ ```js
407
+ import { Coherer } from "nicola-framework";
408
+
409
+ const auth = (req, res, next) => {
410
+ const authHeader = req.headers.authorization || "";
411
+ const [, token] = authHeader.split(" ");
412
+ try {
413
+ req.user = Coherer.verify(token);
414
+ next();
415
+ } catch (err) {
416
+ res.statusCode = 401;
417
+ res.end("Unauthorized");
418
+ }
419
+ };
159
420
  ```
160
421
 
161
422
  ---
162
423
 
163
- ## 🗃️ Dynamo (ORM)
424
+ ## 🗃️ Dynamo ORM (Postgres)
164
425
 
165
- ### Configuración
426
+ ### Conexión
166
427
 
167
- `Dynamo.connect()` **no recibe config**: toma la configuración desde variables de entorno.
428
+ `Dynamo.connect()` no recibe config: lee variables de entorno.
168
429
 
169
430
  ```js
170
- import { Regulator, Dynamo } from 'nicola-framework';
431
+ import { Regulator, Dynamo } from "nicola-framework";
171
432
 
172
433
  Regulator.load();
173
434
  await Dynamo.connect();
435
+
436
+ // ... usar modelos/queries ...
437
+
438
+ await Dynamo.disconnect();
174
439
  ```
175
440
 
176
- `.env` para Postgres:
441
+ ### Variables soportadas
442
+
443
+ Mínimo:
177
444
 
178
445
  ```env
179
446
  DB_DRIVER=postgres
@@ -184,80 +451,110 @@ DB_PASS=postgres
184
451
  DB_NAME=mydb
185
452
  ```
186
453
 
454
+ Alternativa: `DB_URL` (tiene prioridad sobre las variables separadas):
455
+
456
+ ```env
457
+ DB_DRIVER=postgres
458
+ DB_URL=postgres://user:pass@localhost:5432/mydb
459
+ ```
460
+
461
+ SSL opcional via `DB_SSLMODE`:
462
+
463
+ - `require` → SSL sin verificación estricta
464
+ - `verify-ca` / `verify-full` → SSL con verificación
465
+ - `disable` / `prefer` → sin SSL
466
+
187
467
  ### Modelos
188
468
 
469
+ Un modelo es una clase que extiende `Dynamo.Model` y define:
470
+
471
+ - `static tableName` (requerido)
472
+ - `static schema` (opcional, para validar en `create`)
473
+
189
474
  ```js
190
- import { Dynamo } from 'nicola-framework';
475
+ import { Dynamo } from "nicola-framework";
191
476
 
192
477
  export default class User extends Dynamo.Model {
193
- static tableName = 'users';
478
+ static tableName = "users";
194
479
 
195
480
  static schema = {
196
- name: { type: 'string', required: true },
197
- email: { type: 'string', required: true },
198
- age: { type: 'number', required: false }
481
+ name: { type: "string", required: true },
482
+ email: { type: "string", required: true },
483
+ age: { type: "number", required: false },
199
484
  };
200
485
  }
201
486
  ```
202
487
 
203
- ### Queries
488
+ ### Operaciones comunes
204
489
 
205
490
  ```js
206
- // All
491
+ // Obtener todo
207
492
  const users = await User.all();
208
493
 
209
- // Where
210
- const active = await User.where('active', true).get();
494
+ // Where (si omites operador, asume '=')
495
+ const active = await User.where("active", true).get();
211
496
 
212
- // Insert (valida contra schema)
213
- const created = await User.create({ name: 'Alice', email: 'a@a.com', age: 20 });
497
+ // Select (string con comas o array)
498
+ const names = await User.select("name,email").get();
214
499
 
215
- // Update / Delete
216
- await User.where('id', 1).update({ name: 'Alice 2' });
217
- await User.where('id', 1).delete();
500
+ // Insert (valida con schema)
501
+ const created = await User.create({ name: "Alice", email: "a@a.com", age: 20 });
218
502
 
219
- // Select específico (usa string con coma)
220
- const names = await User.select('name,email').get();
503
+ // Update / Delete (recomendado: siempre con where)
504
+ await User.where("id", 1).update({ name: "Alice 2" });
505
+ await User.where("id", 1).delete();
221
506
 
222
- // Order/limit/offset vía QueryBuilder
223
- const latest = await User.query().orderBy('id', 'DESC').limit(10).offset(0).get();
507
+ // Order + limit + offset
508
+ const latest = await User.query().orderBy("id", "DESC").limit(10).offset(0).get();
224
509
  ```
225
510
 
511
+ Notas importantes:
512
+
513
+ - `update()` y `delete()` devuelven `count` (rowCount).
514
+ - Evita `User.update({...})` o `User.delete()` sin `where(...)` porque operaría sobre toda la tabla.
515
+
226
516
  ---
227
517
 
228
- ## 🧩 Middlewares
518
+ ## 🌱 Variables de entorno
229
519
 
230
- ### `Insulator(schema)`
520
+ Nicola lee:
231
521
 
232
- Valida `req.body` con un esquema **simple** de tipos (`typeof`).
522
+ - `NODE_ENV` (`production` activa modo seguro en errores)
523
+ - `NICOLA_SECRET` (JWT)
524
+ - `NICOLA_REQUEST_TIMEOUT`, `NICOLA_HEADERS_TIMEOUT`, `NICOLA_KEEP_ALIVE_TIMEOUT`
525
+ - `DB_DRIVER`, `DB_URL` o `DB_HOST/DB_PORT/DB_USER/DB_PASS/DB_NAME`, `DB_SSLMODE`
233
526
 
234
- ```js
235
- import { Insulator } from 'nicola-framework';
527
+ ---
236
528
 
237
- const schema = {
238
- name: 'string',
239
- age: 'number'
240
- };
529
+ ## 🧪 Tests
241
530
 
242
- app.post('/users', Insulator(schema), (req, res) => {
243
- res.json({ ok: true });
244
- });
531
+ Este repo incluye tests con Jest + Supertest.
532
+
533
+ ```bash
534
+ npm test
245
535
  ```
246
536
 
247
- ### `Shadowgraph`, `Teleforce`, `EasyCors`
537
+ ---
248
538
 
249
- Se ejecutan automáticamente dentro de `Nicola.listen()`. También puedes llamarlos manualmente si estás usando `Remote` por separado.
539
+ ## 🧯 Troubleshooting
250
540
 
251
- ---
541
+ ### 1) "Please configure, NICOLA_SECRET..."
252
542
 
253
- ## 🔥 LiveCurrent (hot reload)
543
+ - define `NICOLA_SECRET` en tu `.env` y corre `Regulator.load()` antes de usar `Coherer`.
254
544
 
255
- ```js
256
- import { LiveCurrent } from 'nicola-framework';
545
+ ### 2) "Por favor utiliza el comando npm install pg"
257
546
 
258
- const dev = new LiveCurrent('app.js');
259
- dev.boot();
260
- ```
547
+ - instala `pg` si vas a usar `DB_DRIVER=postgres`.
548
+
549
+ ### 3) El body llega vacío
550
+
551
+ - Nicola solo parsea JSON cuando `Content-Type` incluye `application/json`.
552
+ - `multipart/form-data` y `application/x-www-form-urlencoded` no están soportados (por ahora).
553
+
554
+ ### 4) CORS en preflight no aplica como esperas
555
+
556
+ - `Nicola.listen()` ejecuta `EasyCors()` y responde `OPTIONS` con `204` antes de tus rutas.
557
+ - si necesitas lógica avanzada de preflight, usa `Remote` + `http.createServer(...)` y monta tus middlewares manualmente.
261
558
 
262
559
  ---
263
560
 
File without changes
package/bin/init.js ADDED
@@ -0,0 +1,68 @@
1
+ import fs from "fs/promises"
2
+ import { cyan, green, magent } from "../utils/console.js";
3
+ import path from "path";
4
+ import { app } from "./schemas/app.schema.js";
5
+ import { route } from "./schemas/route.schema.js";
6
+ import { controller } from "./schemas/controller.schema.js";
7
+
8
+
9
+ export async function runInit(projectName){
10
+ if (!projectName || typeof projectName !== 'string' || projectName.trim().length === 0) {
11
+ console.log(magent('Uso: nicola init <nombre-del-proyecto>'))
12
+ return;
13
+ }
14
+
15
+ const normalizedName = projectName.trim();
16
+ const rootPath = path.join(process.cwd(), normalizedName)
17
+ const srcPath = path.join(rootPath, 'src')
18
+ const packageJSON = {
19
+ name: normalizedName,
20
+ version: "1.0.0",
21
+ type: "module",
22
+ main: "app.js",
23
+ dependencies: {
24
+ "nicola-framework" : "latest"
25
+ },
26
+ scripts:{
27
+ "start": "nicola start"
28
+ }
29
+ }
30
+ try{
31
+ // Fail fast if the directory already exists
32
+ await fs.access(rootPath)
33
+ throw new Error(`El directorio '${normalizedName}' ya existe.`)
34
+ } catch (err) {
35
+ // access failed => directory does not exist yet, continue
36
+ if (!(err && (err.code === 'ENOENT'))) {
37
+ throw err;
38
+ }
39
+ }
40
+
41
+ try{
42
+ await fs.mkdir(rootPath, {recursive: true});
43
+ console.log(magent('✓ Carpeta raiz creada con exito.'))
44
+ await fs.mkdir(srcPath, {recursive:true})
45
+ console.log(magent('✓ Carpeta src creada con exito.'))
46
+ await fs.writeFile(path.join(rootPath, 'package.json'), JSON.stringify(packageJSON, null, 2))
47
+ console.log(magent("✓ Archivo package.json creado con exito."))
48
+ await fs.mkdir(path.join(srcPath, "controllers"), { recursive: true })
49
+ console.log(magent('✓ Carpeta controllers creada con exito.'))
50
+ await fs.writeFile(path.join(srcPath, "controllers", "user.controller.js"), controller)
51
+ console.log(cyan('✓ Controlador creado con exito.'))
52
+ await fs.mkdir(path.join(srcPath, "routes"), { recursive: true })
53
+ console.log(magent('✓ Carpeta routes creada con exito.'))
54
+ await fs.writeFile(path.join(srcPath, "routes", "user.Routes.js"), route)
55
+ console.log(cyan('✓ Rutas creadas con exito.'))
56
+ await fs.writeFile(path.join(rootPath, "app.js"), app)
57
+ console.log(green('✓ Archivo principal app.js creado con exito.'))
58
+ console.log(green(`\nProyecto ${normalizedName} inicializado con exito!`))
59
+ console.log(green(`\nPara iniciar tu proyecto, ejecuta los siguientes comandos:\n`))
60
+ console.log(cyan(`cd ${normalizedName}`))
61
+ console.log(cyan(`npm install`))
62
+ console.log(cyan(`nicola start\n`))
63
+ }
64
+ catch(err){
65
+ console.error(err)
66
+ throw err;
67
+ }
68
+ }
package/bin/nicola.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import { yellow } from "../utils/console.js";
3
+ import { runInit } from "./init.js"
4
+ import { runStart } from "./start.js";
5
+
6
+ const verb = process.argv[2]
7
+
8
+ try {
9
+ switch (verb) {
10
+ case "init": {
11
+ const projectName = process.argv[3]
12
+ await runInit(projectName);
13
+ break;
14
+ }
15
+ case "start": {
16
+ runStart();
17
+ break;
18
+ }
19
+ default: {
20
+ console.log(yellow("Comando no reconocido. Prueba: nicola init <nombre> o nicola start"))
21
+ process.exitCode = 1;
22
+ }
23
+ }
24
+ } catch (err) {
25
+ console.error(err);
26
+ process.exitCode = 1;
27
+ }
@@ -0,0 +1,20 @@
1
+ export const app = `
2
+ import Nicola, { Regulator } from "nicola-framework"
3
+ import UserRoute from "./src/routes/user.Routes.js"
4
+
5
+ Regulator.load()
6
+
7
+ const app = new Nicola()
8
+
9
+ app.use('/user', UserRoute);
10
+
11
+ app.get('/', (req, res)=>{
12
+ res.json({
13
+ message: 'Bienvenido a tu proyecto en Nicola'
14
+ })
15
+ })
16
+
17
+ app.listen(3000, () =>{
18
+ console.log('Servidor corriendo en http://localhost:3000')
19
+ })
20
+ `
@@ -0,0 +1,35 @@
1
+ export const controller =`
2
+
3
+ let users = [
4
+ {
5
+ "id":1,
6
+ "nombre": 'Erick Tiznado'
7
+ },
8
+ {
9
+ "id":2,
10
+ "nombre": 'Jane Doe'
11
+ }
12
+ ];
13
+
14
+ const getAllUsers = (req, res) =>{
15
+ res.json(users)
16
+ }
17
+
18
+ const addNewUser = (req, res) => {
19
+ const {user} = req.body;
20
+
21
+ users.push(user);
22
+
23
+ res.json({
24
+ message: "Usuario registrado con exito",
25
+ status: true
26
+ })
27
+ }
28
+
29
+
30
+
31
+ export {
32
+ getAllUsers,
33
+ addNewUser
34
+ }
35
+ `
@@ -0,0 +1,12 @@
1
+ export const route = `
2
+
3
+ import {getAllUsers, addNewUser} from "../controllers/user.controller.js"
4
+ import { Remote } from "nicola-framework"
5
+
6
+ const RemoteRouter = new Remote()
7
+
8
+ RemoteRouter.get('/', getAllUsers);
9
+ RemoteRouter.post('/add-user', addNewUser);
10
+
11
+ export default RemoteRouter;
12
+ `
package/bin/start.js ADDED
@@ -0,0 +1,8 @@
1
+ import LiveCurrent from "../dev-tools/LiveCurrent.js"
2
+ export const runStart = () =>{
3
+ const live = new LiveCurrent("app.js");
4
+
5
+ live.boot()
6
+ }
7
+
8
+
package/core/Core.js CHANGED
@@ -11,7 +11,7 @@ class Core extends Remote {
11
11
 
12
12
  listen(port, callback) {
13
13
  const server = http.createServer((req, res) => {
14
- const myURL = new URL(req.url, "http://" + req.headers.host);
14
+ const myURL = new URL(req.url, "http://" + (req.headers.host ?? 'localhost'));
15
15
  const pathURL = myURL.pathname;
16
16
  const urlParams = Object.fromEntries(myURL.searchParams);
17
17
 
@@ -33,19 +33,22 @@ class Core extends Remote {
33
33
  if (req.headers["content-type"]?.includes("application/json")) {
34
34
  let dataString = [];
35
35
  let chunklenght = 0;
36
+ let tooLarge = false;
36
37
  req.on("data", (chunk) => {
37
38
  dataString.push(chunk);
38
39
  chunklenght = chunklenght + chunk.length;
39
40
 
40
41
  if (chunklenght > 2e6) {
41
- req.pause();
42
+ tooLarge = true;
42
43
  res.statusCode = 413;
43
44
  res.end("Request Entity Too Large");
45
+ req.destroy();
44
46
  return;
45
47
  }
46
48
  });
47
49
 
48
50
  req.on("end", () => {
51
+ if (tooLarge) return;
49
52
  try {
50
53
  if (dataString.length > 0) {
51
54
  const buffer = Buffer.concat(dataString).toString();
@@ -69,6 +72,22 @@ class Core extends Remote {
69
72
  });
70
73
  });
71
74
  });
75
+
76
+ const requestTimeout = Number(process.env.NICOLA_REQUEST_TIMEOUT)
77
+ if (!Number.isNaN(requestTimeout) && requestTimeout > 0) {
78
+ server.requestTimeout = requestTimeout
79
+ }
80
+
81
+ const headersTimeout = Number(process.env.NICOLA_HEADERS_TIMEOUT)
82
+ if (!Number.isNaN(headersTimeout) && headersTimeout > 0) {
83
+ server.headersTimeout = headersTimeout
84
+ }
85
+
86
+ const keepAliveTimeout = Number(process.env.NICOLA_KEEP_ALIVE_TIMEOUT)
87
+ if (!Number.isNaN(keepAliveTimeout) && keepAliveTimeout > 0) {
88
+ server.keepAliveTimeout = keepAliveTimeout
89
+ }
90
+
72
91
  server.listen(port, callback);
73
92
  return server;
74
93
  }
package/core/Remote.js CHANGED
@@ -91,6 +91,14 @@ class Remote {
91
91
  handle(req, res, done) {
92
92
  let index = 0
93
93
  let match = false
94
+
95
+ const matchesMountPath = (url, mountPath) => {
96
+ if (mountPath === '/') return true;
97
+ if (!url.startsWith(mountPath)) return false;
98
+ const nextChar = url.charAt(mountPath.length);
99
+ return nextChar === '' || nextChar === '/';
100
+ }
101
+
94
102
  const next = (err) => {
95
103
  if (err) {
96
104
  return done(err)
@@ -102,7 +110,7 @@ class Remote {
102
110
  if (!route) {
103
111
  return done()
104
112
  }
105
- const middleware = route.method === 'USE' && req.url.startsWith(route.path);
113
+ const middleware = route.method === 'USE' && matchesMountPath(req.url, route.path);
106
114
  const rutaCoincidence = route.path === req.url && route.method === req.method;
107
115
  match = route.regex && route.regex.test(req.url) && route.method === req.method;
108
116
  if(match){
@@ -5,7 +5,10 @@ class Connection {
5
5
 
6
6
 
7
7
  static async connect() {
8
- const DB_DRIVER = process.env.DB_DRIVER
8
+ const DB_DRIVER = process.env.DB_DRIVER
9
+ if (!DB_DRIVER) {
10
+ throw new Error('Missing DB_DRIVER environment variable')
11
+ }
9
12
  let config = {}
10
13
  if(process.env.DB_URL){
11
14
  const url = new URL(process.env.DB_URL);
@@ -14,7 +17,7 @@ class Connection {
14
17
  user: url.username,
15
18
  password: decodeURIComponent(url.password),
16
19
  host: url.hostname,
17
- port: url.port,
20
+ port: url.port ? Number(url.port) : undefined,
18
21
  database: url.pathname.slice(1)
19
22
  }
20
23
  }
@@ -23,7 +26,7 @@ class Connection {
23
26
  user: process.env.DB_USER,
24
27
  password: process.env.DB_PASS,
25
28
  host: process.env.DB_HOST,
26
- port: process.env.DB_PORT,
29
+ port: process.env.DB_PORT ? Number(process.env.DB_PORT) : undefined,
27
30
  database: process.env.DB_NAME
28
31
  }
29
32
  }
@@ -43,6 +46,14 @@ class Connection {
43
46
  if(!this.client) throw new Error("No hay conexion activa")
44
47
  return this.client.query(sql, params);
45
48
  }
49
+
50
+ static async disconnect() {
51
+ if (!this.client) return
52
+ if (typeof this.client.disconnect === 'function') {
53
+ await this.client.disconnect()
54
+ }
55
+ this.client = null
56
+ }
46
57
  }
47
58
 
48
59
 
@@ -53,16 +53,23 @@ class Postgres extends Driver {
53
53
  if (!this.client) {
54
54
  throw new Error("Database not connected")
55
55
  }
56
- try{
57
- const result = await this.client.query(sql, params);
58
- return {
59
- rows: result.rows,
60
- count: result.rowCount
56
+ try {
57
+ const result = await this.client.query(sql, params);
58
+ return {
59
+ rows: result.rows,
60
+ count: result.rowCount
61
+ }
62
+ } catch (err) {
63
+ const code = (err && err.code) ? `:${err.code}` : ''
64
+ const message = (err && err.message) ? err.message : String(err)
65
+ throw new Error(`Database Query Failed${code}: ${message}`)
61
66
  }
62
67
  }
63
- catch(err){
64
- throw new Error(`Database Query Failed:${err.code}: ${err.message}`)
65
- }
68
+
69
+ async disconnect() {
70
+ if (!this.client) return
71
+ await this.client.end()
72
+ this.client = null
66
73
  }
67
74
 
68
75
  compileSelect(builder) {
package/database/index.js CHANGED
@@ -7,6 +7,10 @@ class Dynamo{
7
7
  return Connection.connect()
8
8
  }
9
9
 
10
+ static async disconnect(){
11
+ return Connection.disconnect()
12
+ }
13
+
10
14
  static async query(sql, params){
11
15
  return Connection.query(sql, params)
12
16
  }
@@ -1,4 +1,4 @@
1
- import fs from 'fs/promises'
1
+ import fs from 'fs'
2
2
  import { spawn } from 'child_process'
3
3
  import { green } from '../utils/console.js';
4
4
  class LiveCurrent{
@@ -25,7 +25,10 @@ class LiveCurrent{
25
25
 
26
26
  watch(){
27
27
  fs.watch(process.cwd(), {recursive: true}, (eventType, filename) => {
28
- if(filename.includes('node_modules') || filename === ''){
28
+ if (!filename) {
29
+ return
30
+ }
31
+ if(filename.includes('node_modules')){
29
32
  return
30
33
  }
31
34
  this.reload()
@@ -1,15 +1,21 @@
1
1
  import { error } from '../templates/error.js'
2
- class BlackBox{
3
- constructor(){
4
2
 
5
- }
3
+ class BlackBox {
4
+ constructor() {}
5
+
6
+ static ignite(err, req, res) {
7
+ const isProd = process.env.NODE_ENV === 'production'
8
+
9
+ if (isProd) {
10
+ console.error(err)
11
+ }
6
12
 
7
- static ignite(err, req , res){
8
-
9
- res.writeHead(500, {'Content-Type': 'text/html'})
10
- res.end(error(err.message, err.stack))
13
+ const message = isProd ? 'Internal Server Error' : (err?.message || 'Error')
14
+ const stack = isProd ? null : (err?.stack || null)
15
+
16
+ res.writeHead(500, { 'Content-Type': 'text/html' })
17
+ res.end(error(message, stack))
11
18
  }
12
19
  }
13
20
 
14
-
15
21
  export default BlackBox;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nicola-framework",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Web framework for Node.js",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -24,6 +24,10 @@
24
24
  "middleware",
25
25
  "orm"
26
26
  ],
27
+ "bin": {
28
+ "nicola": "./bin/nicola.js",
29
+ "nicola-framework": "./bin/nicola.js"
30
+ },
27
31
  "author": "Erick Mauricio Tiznado Rodriguez",
28
32
  "license": "MIT",
29
33
  "devDependencies": {
@@ -1,10 +1,17 @@
1
1
  import crypto from "crypto";
2
- import Regulator from "./Regulator.js";
3
2
  import { getExpTime } from "../utils/expTime.js";
4
3
 
5
4
  class Coherer {
6
5
  constructor() {}
7
6
 
7
+ static _getSecret() {
8
+ const secret = process.env.NICOLA_SECRET
9
+ if (!secret) {
10
+ throw new Error("Please configure, NICOLA_SECRET in the .env file")
11
+ }
12
+ return secret
13
+ }
14
+
8
15
  static codec(jsonData) {
9
16
  const dataString = JSON.stringify(jsonData);
10
17
  const buffer = Buffer.from(dataString);
@@ -12,13 +19,11 @@ class Coherer {
12
19
  }
13
20
 
14
21
  static sign(Payload, options) {
15
- const SECRET = process.env.NICOLA_SECRET
16
- if (!SECRET)
17
- throw new Error("Please configure, NICOLA_SECRET in the .env file");
22
+ const SECRET = this._getSecret()
18
23
 
19
24
  let payloadB64 = "";
20
25
 
21
- if ("expiresIn" in options) {
26
+ if (options && "expiresIn" in options) {
22
27
  const time = getExpTime(options.expiresIn);
23
28
  const newPayload = { ...Payload, exp: time };
24
29
  payloadB64 = this.codec(newPayload);
@@ -43,10 +48,30 @@ class Coherer {
43
48
  }
44
49
 
45
50
  static verify(token) {
46
- const SECRET = process.env.NICOLA_SECRET
47
- if (!SECRET)
48
- throw new Error("Please configure, NICOLA_SECRET in the .env file");
49
- const [headerB64, payloadB64, signature] = token.split(".");
51
+ const SECRET = this._getSecret()
52
+
53
+ if (typeof token !== "string") {
54
+ throw new Error("Token Invalido")
55
+ }
56
+
57
+ const parts = token.split(".")
58
+ if (parts.length !== 3) {
59
+ throw new Error("Token Invalido")
60
+ }
61
+
62
+ const [headerB64, payloadB64, signature] = parts;
63
+
64
+ let decodedHeader;
65
+ try {
66
+ decodedHeader = Buffer.from(headerB64, "base64url").toString("utf-8")
67
+ decodedHeader = JSON.parse(decodedHeader)
68
+ } catch {
69
+ throw new Error("Token Invalido")
70
+ }
71
+
72
+ if (decodedHeader?.alg !== "HS256" || decodedHeader?.typ !== "JWT") {
73
+ throw new Error("Token Invalido")
74
+ }
50
75
 
51
76
  const dataToCheck = headerB64 + "." + payloadB64;
52
77
 
@@ -55,23 +80,28 @@ class Coherer {
55
80
  .update(dataToCheck)
56
81
  .digest("base64url");
57
82
 
58
- if (signature === signatureToChecks) {
59
- let decodedPayload = Buffer.from(payloadB64, "base64url").toString(
60
- "utf-8"
61
- );
83
+ const sigA = Buffer.from(signature)
84
+ const sigB = Buffer.from(signatureToChecks)
85
+ if (sigA.length !== sigB.length || !crypto.timingSafeEqual(sigA, sigB)) {
86
+ throw new Error("Token Invalido")
87
+ }
62
88
 
63
- decodedPayload = JSON.parse(decodedPayload);
64
- if ("exp" in decodedPayload) {
65
- const datenow = Date.now() / 1000;
89
+ let decodedPayload;
90
+ try {
91
+ decodedPayload = Buffer.from(payloadB64, "base64url").toString("utf-8")
92
+ decodedPayload = JSON.parse(decodedPayload)
93
+ } catch {
94
+ throw new Error("Token Invalido")
95
+ }
66
96
 
67
- if (datenow > decodedPayload.exp) {
68
- throw new Error("Token Expired");
69
- }
97
+ if ("exp" in decodedPayload) {
98
+ const datenow = Date.now() / 1000;
99
+ if (datenow > decodedPayload.exp) {
100
+ throw new Error("Token Expired")
70
101
  }
71
- return decodedPayload;
72
- } else {
73
- throw new Error("Token Invalido");
74
102
  }
103
+
104
+ return decodedPayload;
75
105
  }
76
106
  }
77
107
 
@@ -1,6 +1,15 @@
1
-
1
+ const escapeHtml = (value) => {
2
+ return String(value ?? '')
3
+ .replace(/&/g, '&amp;')
4
+ .replace(/</g, '&lt;')
5
+ .replace(/>/g, '&gt;')
6
+ .replace(/"/g, '&quot;')
7
+ .replace(/'/g, '&#39;')
8
+ }
2
9
 
3
10
  export const error = (message, stack) =>{
11
+ const safeMessage = escapeHtml(message)
12
+ const safeStack = stack ? escapeHtml(stack) : null
4
13
 
5
14
  return `
6
15
  <html>
@@ -26,10 +35,9 @@ export const error = (message, stack) =>{
26
35
  <body>
27
36
  <div class = "error-box">
28
37
  <h1> Reporte de Error </h1>
29
- <h2>${message}</h2>
38
+ <h2>${safeMessage}</h2>
30
39
  </div>
31
- <h3> Reporte de Error </h3>
32
- <pre>${stack}</pre>
40
+ ${safeStack ? `<h3> Reporte de Error </h3><pre>${safeStack}</pre>` : ''}
33
41
  </body>
34
42
  </html>
35
43
  `
@@ -10,6 +10,13 @@ describe('🚦 Router System (Remote.js)', () => {
10
10
  beforeAll(() => {
11
11
  router = new Remote();
12
12
 
13
+ const api = new Remote();
14
+ api.get('/ping', (req, res) => {
15
+ res.statusCode = 200;
16
+ res.end('pong');
17
+ });
18
+ router.use('/api', api);
19
+
13
20
  // 1. Ruta Básica
14
21
  router.get('/', (req, res) => {
15
22
  res.statusCode = 200;
@@ -61,6 +68,15 @@ describe('🚦 Router System (Remote.js)', () => {
61
68
  expect(response.statusCode).toBe(200);
62
69
  expect(response.body).toEqual({ id: '555' });
63
70
  });
71
+
72
+ test('Router montado: /api no debe hacer match con /apix', async () => {
73
+ const ok = await request(server).get('/api/ping');
74
+ expect(ok.statusCode).toBe(200);
75
+ expect(ok.text).toBe('pong');
76
+
77
+ const no = await request(server).get('/apix/ping');
78
+ expect(no.statusCode).toBe(404);
79
+ });
64
80
  });
65
81
 
66
82
  describe('🛡️ Blindaje de Errores (P0 Fix)', () => {
@@ -1,6 +0,0 @@
1
-
2
- import LiveCurrent from "./LiveCurrent.js";
3
-
4
- const Live = new LiveCurrent('app.js')
5
-
6
- Live.boot()