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 +393 -96
- package/bin/controller.js +0 -0
- package/bin/init.js +68 -0
- package/bin/nicola.js +27 -0
- package/bin/schemas/app.schema.js +20 -0
- package/bin/schemas/controller.schema.js +35 -0
- package/bin/schemas/route.schema.js +12 -0
- package/bin/start.js +8 -0
- package/core/Core.js +21 -2
- package/core/Remote.js +9 -1
- package/database/Connection.js +14 -3
- package/database/dialects/Postgres.js +15 -8
- package/database/index.js +4 -0
- package/dev-tools/LiveCurrent.js +5 -2
- package/middlewares/BlackBox.js +14 -8
- package/package.json +5 -1
- package/security/Coherer.js +52 -22
- package/templates/error.js +12 -4
- package/test/Router.test.js +16 -0
- package/dev-tools/DevRunner.js +0 -6
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
|
-
##
|
|
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)`.
|
|
18
|
-
-
|
|
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
|
-
-
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
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
|
-
##
|
|
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
|
-
###
|
|
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
|
|
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.
|
|
54
|
-
|
|
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(
|
|
126
|
+
console.log("Servidor corriendo en http://localhost:3000");
|
|
59
127
|
});
|
|
60
128
|
```
|
|
61
129
|
|
|
62
|
-
|
|
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
|
|
152
|
+
import Nicola from "nicola-framework";
|
|
66
153
|
|
|
67
154
|
const app = new Nicola();
|
|
68
|
-
const api = new Remote();
|
|
69
155
|
|
|
70
|
-
|
|
71
|
-
res.json({
|
|
156
|
+
app.get("/", (req, res) => {
|
|
157
|
+
res.json({ ok: true, message: "Hello from Nicola!" });
|
|
72
158
|
});
|
|
73
159
|
|
|
74
|
-
|
|
75
|
-
|
|
160
|
+
app.listen(3000, () => {
|
|
161
|
+
console.log("Server running on http://localhost:3000");
|
|
76
162
|
});
|
|
163
|
+
```
|
|
77
164
|
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
##
|
|
175
|
+
## 🧭 Guía del Router
|
|
176
|
+
|
|
177
|
+
### 1) Rutas básicas
|
|
178
|
+
|
|
179
|
+
`Nicola` y `Remote` soportan:
|
|
85
180
|
|
|
86
|
-
|
|
181
|
+
- `get`, `post`, `put`, `patch`, `delete`
|
|
87
182
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
183
|
+
```js
|
|
184
|
+
import Nicola from "nicola-framework";
|
|
185
|
+
|
|
186
|
+
const app = new Nicola();
|
|
92
187
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
188
|
+
app.get("/ping", (req, res) => {
|
|
189
|
+
res.statusCode = 200;
|
|
190
|
+
res.end("pong");
|
|
191
|
+
});
|
|
96
192
|
|
|
97
|
-
|
|
193
|
+
app.listen(3000);
|
|
194
|
+
```
|
|
98
195
|
|
|
99
|
-
|
|
196
|
+
### 2) Params (`/users/:id`)
|
|
100
197
|
|
|
101
|
-
|
|
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
|
-
//
|
|
106
|
-
if (req.url ===
|
|
234
|
+
// no existe res.status(); usa res.statusCode
|
|
235
|
+
if (req.url === "/blocked") {
|
|
107
236
|
res.statusCode = 403;
|
|
108
|
-
res.end(
|
|
237
|
+
res.end("Forbidden");
|
|
109
238
|
return;
|
|
110
239
|
}
|
|
111
240
|
next();
|
|
112
241
|
});
|
|
113
242
|
```
|
|
114
243
|
|
|
115
|
-
|
|
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(
|
|
119
|
-
throw new Error(
|
|
293
|
+
app.get("/boom", (req, res) => {
|
|
294
|
+
throw new Error("Boom");
|
|
120
295
|
});
|
|
121
296
|
```
|
|
122
297
|
|
|
123
298
|
---
|
|
124
299
|
|
|
125
|
-
##
|
|
300
|
+
## 🧩 Middlewares
|
|
126
301
|
|
|
127
|
-
### `
|
|
302
|
+
### `Insulator(schema)` (validación de body)
|
|
128
303
|
|
|
129
|
-
|
|
304
|
+
Valida que existan campos y que su tipo coincida con `typeof`.
|
|
130
305
|
|
|
131
306
|
```js
|
|
132
|
-
import {
|
|
307
|
+
import { Insulator } from "nicola-framework";
|
|
133
308
|
|
|
134
|
-
|
|
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
|
|
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
|
|
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:
|
|
148
|
-
{ expiresIn:
|
|
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
|
-
|
|
404
|
+
Middleware típico para proteger rutas (Bearer token):
|
|
156
405
|
|
|
157
|
-
```
|
|
158
|
-
|
|
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 (
|
|
424
|
+
## 🗃️ Dynamo ORM (Postgres)
|
|
164
425
|
|
|
165
|
-
###
|
|
426
|
+
### Conexión
|
|
166
427
|
|
|
167
|
-
`Dynamo.connect()`
|
|
428
|
+
`Dynamo.connect()` no recibe config: lee variables de entorno.
|
|
168
429
|
|
|
169
430
|
```js
|
|
170
|
-
import { Regulator, Dynamo } from
|
|
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
|
-
|
|
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
|
|
475
|
+
import { Dynamo } from "nicola-framework";
|
|
191
476
|
|
|
192
477
|
export default class User extends Dynamo.Model {
|
|
193
|
-
static tableName =
|
|
478
|
+
static tableName = "users";
|
|
194
479
|
|
|
195
480
|
static schema = {
|
|
196
|
-
name: { type:
|
|
197
|
-
email: { type:
|
|
198
|
-
age: { type:
|
|
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
|
-
###
|
|
488
|
+
### Operaciones comunes
|
|
204
489
|
|
|
205
490
|
```js
|
|
206
|
-
//
|
|
491
|
+
// Obtener todo
|
|
207
492
|
const users = await User.all();
|
|
208
493
|
|
|
209
|
-
// Where
|
|
210
|
-
const active = await User.where(
|
|
494
|
+
// Where (si omites operador, asume '=')
|
|
495
|
+
const active = await User.where("active", true).get();
|
|
211
496
|
|
|
212
|
-
//
|
|
213
|
-
const
|
|
497
|
+
// Select (string con comas o array)
|
|
498
|
+
const names = await User.select("name,email").get();
|
|
214
499
|
|
|
215
|
-
//
|
|
216
|
-
await User.
|
|
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
|
-
//
|
|
220
|
-
|
|
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
|
|
223
|
-
const latest = await User.query().orderBy(
|
|
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
|
-
##
|
|
518
|
+
## 🌱 Variables de entorno
|
|
229
519
|
|
|
230
|
-
|
|
520
|
+
Nicola lee:
|
|
231
521
|
|
|
232
|
-
|
|
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
|
-
|
|
235
|
-
import { Insulator } from 'nicola-framework';
|
|
527
|
+
---
|
|
236
528
|
|
|
237
|
-
|
|
238
|
-
name: 'string',
|
|
239
|
-
age: 'number'
|
|
240
|
-
};
|
|
529
|
+
## 🧪 Tests
|
|
241
530
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
531
|
+
Este repo incluye tests con Jest + Supertest.
|
|
532
|
+
|
|
533
|
+
```bash
|
|
534
|
+
npm test
|
|
245
535
|
```
|
|
246
536
|
|
|
247
|
-
|
|
537
|
+
---
|
|
248
538
|
|
|
249
|
-
|
|
539
|
+
## 🧯 Troubleshooting
|
|
250
540
|
|
|
251
|
-
|
|
541
|
+
### 1) "Please configure, NICOLA_SECRET..."
|
|
252
542
|
|
|
253
|
-
|
|
543
|
+
- define `NICOLA_SECRET` en tu `.env` y corre `Regulator.load()` antes de usar `Coherer`.
|
|
254
544
|
|
|
255
|
-
|
|
256
|
-
import { LiveCurrent } from 'nicola-framework';
|
|
545
|
+
### 2) "Por favor utiliza el comando npm install pg"
|
|
257
546
|
|
|
258
|
-
|
|
259
|
-
|
|
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
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
|
-
|
|
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
|
|
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){
|
package/database/Connection.js
CHANGED
|
@@ -5,7 +5,10 @@ class Connection {
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
static async connect() {
|
|
8
|
-
const 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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
package/dev-tools/LiveCurrent.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fs from 'fs
|
|
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
|
|
28
|
+
if (!filename) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
if(filename.includes('node_modules')){
|
|
29
32
|
return
|
|
30
33
|
}
|
|
31
34
|
this.reload()
|
package/middlewares/BlackBox.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
res.
|
|
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
|
+
"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": {
|
package/security/Coherer.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
package/templates/error.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
const escapeHtml = (value) => {
|
|
2
|
+
return String(value ?? '')
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"')
|
|
7
|
+
.replace(/'/g, ''')
|
|
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>${
|
|
38
|
+
<h2>${safeMessage}</h2>
|
|
30
39
|
</div>
|
|
31
|
-
|
|
32
|
-
<pre>${stack}</pre>
|
|
40
|
+
${safeStack ? `<h3> Reporte de Error </h3><pre>${safeStack}</pre>` : ''}
|
|
33
41
|
</body>
|
|
34
42
|
</html>
|
|
35
43
|
`
|
package/test/Router.test.js
CHANGED
|
@@ -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)', () => {
|