nicola-framework 1.0.0 → 1.0.2
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 +274 -0
- package/core/Core.js +85 -80
- package/core/Remote.js +7 -2
- package/database/Connection.js +2 -7
- package/database/Model.js +1 -1
- package/database/dialects/Postgres.js +8 -3
- package/dev-tools/DevRunner.js +1 -1
- package/dev-tools/LiveCurrent.js +2 -2
- package/index.js +5 -6
- package/middlewares/EasyCors.js +33 -14
- package/middlewares/Shadowgraph.js +2 -3
- package/package.json +7 -7
- package/security/Coherer.js +63 -38
- package/test/Coherer.test.js +73 -0
- package/test/Core.test.js +96 -0
- package/test/Router.test.js +76 -0
- package/utils/console.js +31 -0
- package/utils/expTime.js +30 -0
- package/utils/isPromise.js +7 -0
- package/database/dialects/Mysql.js +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# ⚡ Nicola Framework
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/nicola-framework)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
|
|
7
|
+
> Framework HTTP minimalista para Node.js (ESM): servidor, router, middlewares, JWT y una capa ORM sencilla.
|
|
8
|
+
|
|
9
|
+
Nicola expone un **servidor HTTP nativo** con un **router tipo Express** y utilidades integradas. El proyecto está escrito como **ES Modules** (`"type": "module"`), por lo que los ejemplos usan `import`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## ✅ Estado actual del proyecto (lo que realmente hay)
|
|
14
|
+
|
|
15
|
+
- **Core/Router**: `Nicola` (default) extiende `Remote`.
|
|
16
|
+
- **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`.
|
|
19
|
+
- **Security headers**: `Teleforce` aplica headers básicos (no-sniff, frame deny, etc.).
|
|
20
|
+
- **Logger**: `Shadowgraph` loggea al terminar la respuesta.
|
|
21
|
+
- **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.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 📦 Instalación
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install nicola-framework
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### (Opcional) Postgres
|
|
35
|
+
|
|
36
|
+
El dialecto Postgres usa `pg` por import dinámico.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install pg
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## ⚡ Quickstart
|
|
45
|
+
|
|
46
|
+
### Servidor HTTP básico
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
import Nicola from 'nicola-framework';
|
|
50
|
+
|
|
51
|
+
const app = new Nicola();
|
|
52
|
+
|
|
53
|
+
app.get('/', (req, res) => {
|
|
54
|
+
res.json({ ok: true, message: 'Hello from Nicola!' });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
app.listen(3000, () => {
|
|
58
|
+
console.log('Server running on http://localhost:3000');
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Router anidado y params
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import { Nicola, Remote } from 'nicola-framework';
|
|
66
|
+
|
|
67
|
+
const app = new Nicola();
|
|
68
|
+
const api = new Remote();
|
|
69
|
+
|
|
70
|
+
api.get('/users', (req, res) => {
|
|
71
|
+
res.json({ users: ['Alice', 'Bob'] });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
api.get('/users/:id', (req, res) => {
|
|
75
|
+
res.json({ userId: req.params.id });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
app.use('/api', api);
|
|
79
|
+
app.listen(3000);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 🧠 API (resumen fiel al código)
|
|
85
|
+
|
|
86
|
+
### `Nicola` (Core)
|
|
87
|
+
|
|
88
|
+
- `new Nicola()`
|
|
89
|
+
- `app.get/post/put/patch/delete(path, ...handlers)`
|
|
90
|
+
- `app.use([path], ...handlers | router)`
|
|
91
|
+
- `app.listen(port, [callback])`
|
|
92
|
+
|
|
93
|
+
Notas:
|
|
94
|
+
- `Nicola.listen()` ejecuta internamente `Shadowgraph`, `EasyCors` y `Teleforce` en cada request.
|
|
95
|
+
- `req.query` se construye desde querystring.
|
|
96
|
+
|
|
97
|
+
### `Remote` (Router)
|
|
98
|
+
|
|
99
|
+
`Remote` es el router base. Soporta middlewares y rutas con params (`/users/:id`).
|
|
100
|
+
|
|
101
|
+
Ejemplo de middleware simple:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
app.use((req, res, next) => {
|
|
105
|
+
// No existe res.status(); usa statusCode
|
|
106
|
+
if (req.url === '/blocked') {
|
|
107
|
+
res.statusCode = 403;
|
|
108
|
+
res.end('Forbidden');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
next();
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Errores:
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
app.get('/boom', (req, res) => {
|
|
119
|
+
throw new Error('Boom');
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 🔐 Seguridad
|
|
126
|
+
|
|
127
|
+
### `Regulator` (.env)
|
|
128
|
+
|
|
129
|
+
Lee `.env` desde el directorio actual (`process.cwd()`) y lo copia a `process.env`.
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
import { Regulator } from 'nicola-framework';
|
|
133
|
+
|
|
134
|
+
Regulator.load();
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `Coherer` (JWT HS256)
|
|
138
|
+
|
|
139
|
+
`Coherer` es una clase con métodos **estáticos**.
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
import { Regulator, Coherer } from 'nicola-framework';
|
|
143
|
+
|
|
144
|
+
Regulator.load();
|
|
145
|
+
|
|
146
|
+
const token = Coherer.sign(
|
|
147
|
+
{ userId: 123, role: 'admin' },
|
|
148
|
+
{ expiresIn: '24h' }
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const payload = Coherer.verify(token);
|
|
152
|
+
console.log(payload.userId);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`.env` mínimo:
|
|
156
|
+
|
|
157
|
+
```env
|
|
158
|
+
NICOLA_SECRET=mi-secreto-super-seguro
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 🗃️ Dynamo (ORM)
|
|
164
|
+
|
|
165
|
+
### Configuración
|
|
166
|
+
|
|
167
|
+
`Dynamo.connect()` **no recibe config**: toma la configuración desde variables de entorno.
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
import { Regulator, Dynamo } from 'nicola-framework';
|
|
171
|
+
|
|
172
|
+
Regulator.load();
|
|
173
|
+
await Dynamo.connect();
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`.env` para Postgres:
|
|
177
|
+
|
|
178
|
+
```env
|
|
179
|
+
DB_DRIVER=postgres
|
|
180
|
+
DB_HOST=localhost
|
|
181
|
+
DB_PORT=5432
|
|
182
|
+
DB_USER=postgres
|
|
183
|
+
DB_PASS=postgres
|
|
184
|
+
DB_NAME=mydb
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Modelos
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
import { Dynamo } from 'nicola-framework';
|
|
191
|
+
|
|
192
|
+
export default class User extends Dynamo.Model {
|
|
193
|
+
static tableName = 'users';
|
|
194
|
+
|
|
195
|
+
static schema = {
|
|
196
|
+
name: { type: 'string', required: true },
|
|
197
|
+
email: { type: 'string', required: true },
|
|
198
|
+
age: { type: 'number', required: false }
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Queries
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
// All
|
|
207
|
+
const users = await User.all();
|
|
208
|
+
|
|
209
|
+
// Where
|
|
210
|
+
const active = await User.where('active', true).get();
|
|
211
|
+
|
|
212
|
+
// Insert (valida contra schema)
|
|
213
|
+
const created = await User.create({ name: 'Alice', email: 'a@a.com', age: 20 });
|
|
214
|
+
|
|
215
|
+
// Update / Delete
|
|
216
|
+
await User.where('id', 1).update({ name: 'Alice 2' });
|
|
217
|
+
await User.where('id', 1).delete();
|
|
218
|
+
|
|
219
|
+
// Select específico (usa string con coma)
|
|
220
|
+
const names = await User.select('name,email').get();
|
|
221
|
+
|
|
222
|
+
// Order/limit/offset vía QueryBuilder
|
|
223
|
+
const latest = await User.query().orderBy('id', 'DESC').limit(10).offset(0).get();
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## 🧩 Middlewares
|
|
229
|
+
|
|
230
|
+
### `Insulator(schema)`
|
|
231
|
+
|
|
232
|
+
Valida `req.body` con un esquema **simple** de tipos (`typeof`).
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
import { Insulator } from 'nicola-framework';
|
|
236
|
+
|
|
237
|
+
const schema = {
|
|
238
|
+
name: 'string',
|
|
239
|
+
age: 'number'
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
app.post('/users', Insulator(schema), (req, res) => {
|
|
243
|
+
res.json({ ok: true });
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### `Shadowgraph`, `Teleforce`, `EasyCors`
|
|
248
|
+
|
|
249
|
+
Se ejecutan automáticamente dentro de `Nicola.listen()`. También puedes llamarlos manualmente si estás usando `Remote` por separado.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 🔥 LiveCurrent (hot reload)
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
import { LiveCurrent } from 'nicola-framework';
|
|
257
|
+
|
|
258
|
+
const dev = new LiveCurrent('app.js');
|
|
259
|
+
dev.boot();
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## 🤝 Contribuir
|
|
265
|
+
|
|
266
|
+
1. Fork
|
|
267
|
+
2. Rama feature
|
|
268
|
+
3. PR
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## 📝 Licencia
|
|
273
|
+
|
|
274
|
+
MIT © Erick Mauricio Tiznado Rodriguez
|
package/core/Core.js
CHANGED
|
@@ -1,84 +1,89 @@
|
|
|
1
|
-
import http from
|
|
2
|
-
import Remote from
|
|
3
|
-
import BlackBox from
|
|
4
|
-
import Shadowgraph from
|
|
5
|
-
import Teleforce from
|
|
6
|
-
import EasyCors from
|
|
1
|
+
import http from "http";
|
|
2
|
+
import Remote from "./Remote.js";
|
|
3
|
+
import BlackBox from "../middlewares/BlackBox.js";
|
|
4
|
+
import Shadowgraph from "../middlewares/Shadowgraph.js";
|
|
5
|
+
import Teleforce from "../middlewares/Teleforce.js";
|
|
6
|
+
import EasyCors from "../middlewares/EasyCors.js";
|
|
7
7
|
class Core extends Remote {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
listen(port, callback) {
|
|
13
|
+
const server = http.createServer((req, res) => {
|
|
14
|
+
const myURL = new URL(req.url, "http://" + req.headers.host);
|
|
15
|
+
const pathURL = myURL.pathname;
|
|
16
|
+
const urlParams = Object.fromEntries(myURL.searchParams);
|
|
17
|
+
|
|
18
|
+
req.url = pathURL;
|
|
19
|
+
req.query = urlParams;
|
|
20
|
+
|
|
21
|
+
Shadowgraph(req, res, () => {
|
|
22
|
+
this.__addHelper(res);
|
|
23
|
+
EasyCors()(req, res, () => {
|
|
24
|
+
Teleforce(req, res, () => {
|
|
25
|
+
const done = (err) => {
|
|
26
|
+
if (!err) {
|
|
27
|
+
res.statusCode = 404;
|
|
28
|
+
res.end("Not Found");
|
|
29
|
+
} else {
|
|
30
|
+
BlackBox.ignite(err, req, res);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
if (req.headers["content-type"]?.includes("application/json")) {
|
|
34
|
+
let dataString = [];
|
|
35
|
+
let chunklenght = 0;
|
|
36
|
+
req.on("data", (chunk) => {
|
|
37
|
+
dataString.push(chunk);
|
|
38
|
+
chunklenght = chunklenght + chunk.length;
|
|
39
|
+
|
|
40
|
+
if (chunklenght > 2e6) {
|
|
41
|
+
req.pause();
|
|
42
|
+
res.statusCode = 413;
|
|
43
|
+
res.end("Request Entity Too Large");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
req.on("end", () => {
|
|
49
|
+
try {
|
|
50
|
+
if (dataString.length > 0) {
|
|
51
|
+
const buffer = Buffer.concat(dataString).toString();
|
|
52
|
+
req.body = JSON.parse(buffer);
|
|
53
|
+
} else {
|
|
54
|
+
req.body = {};
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
res.statusCode = 400;
|
|
58
|
+
res.end("Bad Request: Invalid JSON");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.handle(req, res, done);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
req.body = {};
|
|
66
|
+
this.handle(req, res, done);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
server.listen(port, callback);
|
|
73
|
+
return server;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
__addHelper(res) {
|
|
77
|
+
res.json = (data) => {
|
|
78
|
+
res.setHeader("Content-Type", "application/json");
|
|
79
|
+
res.end(JSON.stringify(data));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
res.send = (data) => {
|
|
83
|
+
res.setHeader("Content-Type", "text/plain");
|
|
84
|
+
res.end(data);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
export default Core;
|
package/core/Remote.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isPromise } from "../utils/isPromise.js";
|
|
2
|
+
|
|
1
3
|
|
|
2
4
|
class Remote {
|
|
3
5
|
|
|
@@ -29,7 +31,11 @@ class Remote {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
try {
|
|
32
|
-
fn(req, res, internalNext);
|
|
34
|
+
let results = fn(req, res, internalNext);
|
|
35
|
+
|
|
36
|
+
if(isPromise(results)){
|
|
37
|
+
results.catch(internalNext);
|
|
38
|
+
}
|
|
33
39
|
}
|
|
34
40
|
catch (error) {
|
|
35
41
|
internalNext(error);
|
|
@@ -118,7 +124,6 @@ class Remote {
|
|
|
118
124
|
}
|
|
119
125
|
route.handler.handle(req, res, done);
|
|
120
126
|
|
|
121
|
-
console.log('Esto es un sub router')
|
|
122
127
|
} else {
|
|
123
128
|
|
|
124
129
|
route.handler(req, res, next);
|
package/database/Connection.js
CHANGED
|
@@ -19,20 +19,15 @@ class Connection {
|
|
|
19
19
|
await this.client.connect()
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
await this.__connectMysql()
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
22
|
+
|
|
26
23
|
throw new Error("Driver no soportado" + DB_DRIVER)
|
|
27
24
|
}
|
|
28
25
|
|
|
29
26
|
|
|
30
27
|
|
|
31
|
-
static async __connectMysql(){
|
|
32
|
-
|
|
33
|
-
}
|
|
34
28
|
|
|
35
29
|
static async query(sql, params){
|
|
30
|
+
if(!this.client) throw new Error("No hay conexion activa")
|
|
36
31
|
return this.client.query(sql, params);
|
|
37
32
|
}
|
|
38
33
|
}
|
package/database/Model.js
CHANGED
|
@@ -40,10 +40,10 @@ class Postgres extends Driver {
|
|
|
40
40
|
}
|
|
41
41
|
catch (e) {
|
|
42
42
|
if (e.code === 'ERR_MODULE_NOT_FOUND') {
|
|
43
|
-
|
|
43
|
+
throw new Error('Por favor utiliza el comando npm install pg')
|
|
44
44
|
}
|
|
45
45
|
else {
|
|
46
|
-
|
|
46
|
+
throw new Error(e.message)
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -51,14 +51,19 @@ class Postgres extends Driver {
|
|
|
51
51
|
|
|
52
52
|
async query(sql, params) {
|
|
53
53
|
if (!this.client) {
|
|
54
|
-
|
|
54
|
+
throw new Error("Database not connected")
|
|
55
55
|
}
|
|
56
|
+
try{
|
|
56
57
|
const result = await this.client.query(sql, params);
|
|
57
58
|
return {
|
|
58
59
|
rows: result.rows,
|
|
59
60
|
count: result.rowCount
|
|
60
61
|
}
|
|
61
62
|
}
|
|
63
|
+
catch(err){
|
|
64
|
+
throw new Error(`Database Query Failed:${err.code}: ${err.message}`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
62
67
|
|
|
63
68
|
compileSelect(builder) {
|
|
64
69
|
const stringColums = builder.columns.join(', ')
|
package/dev-tools/DevRunner.js
CHANGED
package/dev-tools/LiveCurrent.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs/promises'
|
|
2
2
|
import { spawn } from 'child_process'
|
|
3
|
-
import
|
|
3
|
+
import { green } from '../utils/console.js';
|
|
4
4
|
class LiveCurrent{
|
|
5
5
|
constructor(entryPoint){
|
|
6
6
|
this.entryPoint = entryPoint;
|
|
@@ -14,7 +14,7 @@ class LiveCurrent{
|
|
|
14
14
|
|
|
15
15
|
ignite(){
|
|
16
16
|
this.process= spawn('node', [this.entryPoint], {stdio: 'inherit'});
|
|
17
|
-
console.log(
|
|
17
|
+
console.log(green(`LiveCurrent: Iniciando Servidor...`));
|
|
18
18
|
}
|
|
19
19
|
reload(){
|
|
20
20
|
if(this.process){
|
package/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Nicola Framework - Main Export
|
|
3
3
|
*
|
|
4
4
|
* Punto de entrada principal del framework
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// Core
|
|
8
|
-
export { default as
|
|
8
|
+
export { default as Nicola } from './core/Core.js';
|
|
9
9
|
export { default as Remote } from './core/Remote.js';
|
|
10
10
|
|
|
11
11
|
// Middlewares
|
|
@@ -21,11 +21,10 @@ export { default as Regulator } from './security/Regulator.js';
|
|
|
21
21
|
|
|
22
22
|
// Dev Tools
|
|
23
23
|
export { default as LiveCurrent } from './dev-tools/LiveCurrent.js';
|
|
24
|
-
export { default as DevRunner } from './dev-tools/DevRunner.js';
|
|
25
24
|
|
|
26
25
|
// Database (Dynamo ORM)
|
|
27
26
|
export { default as Dynamo } from './database/index.js';
|
|
28
27
|
|
|
29
|
-
// Default export (
|
|
30
|
-
import
|
|
31
|
-
export default
|
|
28
|
+
// Default export (Nicola Core)
|
|
29
|
+
import Nicola from './core/Core.js';
|
|
30
|
+
export default Nicola;
|
package/middlewares/EasyCors.js
CHANGED
|
@@ -1,19 +1,38 @@
|
|
|
1
|
+
const EasyCors = (options = {}) => {
|
|
2
|
+
const allowedOrigin = options.origin || "*";
|
|
1
3
|
|
|
4
|
+
return (req, res, next) => {
|
|
5
|
+
const whitelist = Array.isArray(allowedOrigin)
|
|
6
|
+
? allowedOrigin
|
|
7
|
+
: [allowedOrigin];
|
|
8
|
+
const incomingOrigin = req.headers.origin;
|
|
9
|
+
if ((incomingOrigin && whitelist.includes(incomingOrigin)) || (whitelist.includes('*'))) {
|
|
10
|
+
if (whitelist.includes("*")) {
|
|
11
|
+
res.setHeader("Access-Control-Allow-Origin", `*`);
|
|
12
|
+
}
|
|
13
|
+
else{
|
|
14
|
+
res.setHeader("Access-Control-Allow-Origin", `${req.headers.origin}`);
|
|
15
|
+
}
|
|
16
|
+
res.setHeader(
|
|
17
|
+
"Access-Control-Allow-Methods",
|
|
18
|
+
"GET, POST, PUT, DELETE, OPTIONS, PATCH"
|
|
19
|
+
);
|
|
20
|
+
res.setHeader(
|
|
21
|
+
"Access-Control-Allow-Headers",
|
|
22
|
+
"Content-Type, Authorization"
|
|
23
|
+
);
|
|
2
24
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
|
7
|
-
|
|
8
|
-
if(req.method === 'OPTIONS'){
|
|
9
|
-
res.statusCode = 204
|
|
10
|
-
res.end()
|
|
25
|
+
if (req.method === "OPTIONS") {
|
|
26
|
+
res.statusCode = 204;
|
|
27
|
+
res.end();
|
|
11
28
|
return;
|
|
29
|
+
} else {
|
|
30
|
+
next();
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
next();
|
|
12
34
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
35
|
+
};
|
|
36
|
+
};
|
|
18
37
|
|
|
19
|
-
export default EasyCors;
|
|
38
|
+
export default EasyCors;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { green } from "../utils/console.js";
|
|
3
2
|
const Shadowgraph = (req, res, next) =>{
|
|
4
3
|
const inicio = Date.now()
|
|
5
4
|
res.on('finish', () =>{
|
|
6
5
|
const duracion = Date.now() - inicio
|
|
7
|
-
console.log(
|
|
6
|
+
console.log(green(`[${req.method}] ${req.url} - ${res.statusCode} ${res.statusMessage} - ${duracion}ms`))
|
|
8
7
|
})
|
|
9
8
|
next()
|
|
10
9
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nicola-framework",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Web framework for Node.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"./database": "./database/index.js"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
|
-
"test": "
|
|
16
|
+
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
19
|
"framework",
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"server",
|
|
23
23
|
"router",
|
|
24
24
|
"middleware",
|
|
25
|
-
"zero-dependency",
|
|
26
25
|
"orm"
|
|
27
26
|
],
|
|
28
|
-
"author": "
|
|
27
|
+
"author": "Erick Mauricio Tiznado Rodriguez",
|
|
29
28
|
"license": "MIT",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"jest": "^30.2.0",
|
|
31
|
+
"supertest": "^7.2.2"
|
|
32
32
|
}
|
|
33
33
|
}
|
package/security/Coherer.js
CHANGED
|
@@ -1,53 +1,78 @@
|
|
|
1
|
-
import crypto from
|
|
2
|
-
import Regulator from
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import Regulator from "./Regulator.js";
|
|
3
|
+
import { getExpTime } from "../utils/expTime.js";
|
|
3
4
|
|
|
4
|
-
class Coherer{
|
|
5
|
-
|
|
5
|
+
class Coherer {
|
|
6
|
+
constructor() {}
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
static codec(jsonData) {
|
|
9
|
+
const dataString = JSON.stringify(jsonData);
|
|
10
|
+
const buffer = Buffer.from(dataString);
|
|
11
|
+
return buffer.toString("base64url");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
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");
|
|
18
|
+
|
|
19
|
+
let payloadB64 = "";
|
|
9
20
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
if ("expiresIn" in options) {
|
|
22
|
+
const time = getExpTime(options.expiresIn);
|
|
23
|
+
const newPayload = { ...Payload, exp: time };
|
|
24
|
+
payloadB64 = this.codec(newPayload);
|
|
25
|
+
} else {
|
|
26
|
+
throw new Error("Expire time invalid");
|
|
14
27
|
}
|
|
15
28
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
const headerB64 = this.codec(header)
|
|
29
|
+
const header = {
|
|
30
|
+
alg: "HS256",
|
|
31
|
+
typ: "JWT",
|
|
32
|
+
};
|
|
33
|
+
const headerB64 = this.codec(header);
|
|
23
34
|
|
|
24
|
-
|
|
35
|
+
const data = headerB64 + "." + payloadB64;
|
|
25
36
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
const signature = crypto
|
|
38
|
+
.createHmac("sha256", SECRET)
|
|
39
|
+
.update(data)
|
|
40
|
+
.digest("base64url");
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
static verify(data){
|
|
33
|
-
const [headerB64, payloadB64, signature] = data.token.split('.');
|
|
42
|
+
return data + "." + signature;
|
|
43
|
+
}
|
|
34
44
|
|
|
35
|
-
|
|
45
|
+
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(".");
|
|
36
50
|
|
|
37
|
-
|
|
38
|
-
.update(dataToCheck)
|
|
39
|
-
.digest('base64url')
|
|
51
|
+
const dataToCheck = headerB64 + "." + payloadB64;
|
|
40
52
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
const signatureToChecks = crypto
|
|
54
|
+
.createHmac("sha256", SECRET)
|
|
55
|
+
.update(dataToCheck)
|
|
56
|
+
.digest("base64url");
|
|
57
|
+
|
|
58
|
+
if (signature === signatureToChecks) {
|
|
59
|
+
let decodedPayload = Buffer.from(payloadB64, "base64url").toString(
|
|
60
|
+
"utf-8"
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
decodedPayload = JSON.parse(decodedPayload);
|
|
64
|
+
if ("exp" in decodedPayload) {
|
|
65
|
+
const datenow = Date.now() / 1000;
|
|
66
|
+
|
|
67
|
+
if (datenow > decodedPayload.exp) {
|
|
68
|
+
throw new Error("Token Expired");
|
|
48
69
|
}
|
|
70
|
+
}
|
|
71
|
+
return decodedPayload;
|
|
72
|
+
} else {
|
|
73
|
+
throw new Error("Token Invalido");
|
|
49
74
|
}
|
|
75
|
+
}
|
|
50
76
|
}
|
|
51
77
|
|
|
52
|
-
|
|
53
|
-
export default Coherer;
|
|
78
|
+
export default Coherer;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import Coherer from '../security/Coherer.js'; // Ajusta la ruta según tu estructura
|
|
2
|
+
// Nota: Jest inyecta 'describe', 'test', 'expect' globalmente, no hace falta importarlos.
|
|
3
|
+
|
|
4
|
+
describe('🛡️ Módulo de Seguridad (Coherer)', () => {
|
|
5
|
+
|
|
6
|
+
// Antes de todos los tests, configuramos el entorno
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
process.env.NICOLA_SECRET = 'test_secret_key_123';
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Limpiamos después (buena práctica)
|
|
12
|
+
afterAll(() => {
|
|
13
|
+
delete process.env.NICOLA_SECRET;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('Happy Path (Lo que debe salir bien)', () => {
|
|
17
|
+
|
|
18
|
+
test('Debe firmar y verificar un token correctamente', () => {
|
|
19
|
+
const payload = { userId: 1, role: 'admin' };
|
|
20
|
+
const options = { expiresIn: '1h' };
|
|
21
|
+
|
|
22
|
+
// 1. Firmar
|
|
23
|
+
const token = Coherer.sign(payload, options);
|
|
24
|
+
|
|
25
|
+
// Verificamos que sea un string y tenga 3 partes (header.payload.signature)
|
|
26
|
+
expect(typeof token).toBe('string');
|
|
27
|
+
expect(token.split('.')).toHaveLength(3);
|
|
28
|
+
|
|
29
|
+
// 2. Verificar
|
|
30
|
+
const decoded = Coherer.verify(token);
|
|
31
|
+
|
|
32
|
+
// Verificamos que la data sea la misma
|
|
33
|
+
expect(decoded.userId).toBe(payload.userId);
|
|
34
|
+
expect(decoded.role).toBe(payload.role);
|
|
35
|
+
|
|
36
|
+
// Verificamos que se haya agregado la expiración
|
|
37
|
+
expect(decoded).toHaveProperty('exp');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('Sad Path (Errores esperados)', () => {
|
|
42
|
+
|
|
43
|
+
test('Debe fallar si no se define una expiración', () => {
|
|
44
|
+
const payload = { userId: 1 };
|
|
45
|
+
|
|
46
|
+
// En Jest, para probar errores, envolvemos la llamada en una función anónima () =>
|
|
47
|
+
expect(() => {
|
|
48
|
+
Coherer.sign(payload, {});
|
|
49
|
+
|
|
50
|
+
}).toThrow('Expire time invalid');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('Debe fallar si el token es basura', () => {
|
|
54
|
+
const tokenBasura = 'esto.no.es.valido';
|
|
55
|
+
|
|
56
|
+
expect(() => {
|
|
57
|
+
Coherer.verify(tokenBasura);
|
|
58
|
+
}).toThrow(); // Esperamos cualquier error (Firma inválida, malformado, etc.)
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('Debe fallar si la firma ha sido manipulada', () => {
|
|
62
|
+
const tokenReal = Coherer.sign({ id: 1 }, { expiresIn: '1h' });
|
|
63
|
+
const partes = tokenReal.split('.');
|
|
64
|
+
|
|
65
|
+
// Hack: Cambiamos la firma (última parte) por otra cosa
|
|
66
|
+
const tokenFalso = `${partes[0]}.${partes[1]}.FIRMA_FALSA`;
|
|
67
|
+
|
|
68
|
+
expect(() => {
|
|
69
|
+
Coherer.verify(tokenFalso);
|
|
70
|
+
}).toThrow('Token Invalido');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import request from "supertest";
|
|
2
|
+
import Core from "../core/Core.js";
|
|
3
|
+
|
|
4
|
+
describe("🧠 Core System (Core.js)", () => {
|
|
5
|
+
let app;
|
|
6
|
+
let server;
|
|
7
|
+
|
|
8
|
+
beforeAll((done) => {
|
|
9
|
+
app = new Core();
|
|
10
|
+
|
|
11
|
+
app.get("/ping", (req, res) => {
|
|
12
|
+
res.statusCode = 200;
|
|
13
|
+
res.end("pong");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
app.get("/query", (req, res) => {
|
|
17
|
+
res.json({ query: req.query });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
app.post("/json", (req, res) => {
|
|
21
|
+
res.json({ body: req.body });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
server = app.listen(0, done);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterAll((done) => {
|
|
28
|
+
server.close(done);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("GET /ping responde 200", async () => {
|
|
32
|
+
const res = await request(server).get("/ping");
|
|
33
|
+
expect(res.statusCode).toBe(200);
|
|
34
|
+
expect(res.text).toBe("pong");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("Aplica headers de Teleforce", async () => {
|
|
38
|
+
const res = await request(server).get("/ping");
|
|
39
|
+
expect(res.headers["x-content-type-options"]).toBe("nosniff");
|
|
40
|
+
expect(res.headers["x-frame-options"]).toBe("Deny");
|
|
41
|
+
expect(res.headers["x-xss-protection"]).toBe("1");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("Parsea querystring y lo expone en req.query", async () => {
|
|
45
|
+
const res = await request(server).get("/query?x=1&y=hola");
|
|
46
|
+
expect(res.statusCode).toBe(200);
|
|
47
|
+
expect(res.body).toEqual({ query: { x: "1", y: "hola" } });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("POST JSON válido -> req.body parseado", async () => {
|
|
51
|
+
const res = await request(server)
|
|
52
|
+
.post("/json")
|
|
53
|
+
.set("Content-Type", "application/json; charset=utf-8")
|
|
54
|
+
.send(JSON.stringify({ a: 1 }));
|
|
55
|
+
|
|
56
|
+
expect(res.statusCode).toBe(200);
|
|
57
|
+
expect(res.body).toEqual({ body: { a: 1 } });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("POST JSON inválido -> 400", async () => {
|
|
61
|
+
const res = await request(server)
|
|
62
|
+
.post("/json")
|
|
63
|
+
.set("Content-Type", "application/json")
|
|
64
|
+
.send("{invalid");
|
|
65
|
+
|
|
66
|
+
expect(res.statusCode).toBe(400);
|
|
67
|
+
expect(res.text).toBe("Bad Request: Invalid JSON");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("Sin Content-Type -> req.body = {}", async () => {
|
|
71
|
+
const res = await request(server).post("/json").send("no-json");
|
|
72
|
+
expect(res.statusCode).toBe(200);
|
|
73
|
+
expect(res.body).toEqual({ body: {} });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("OPTIONS responde 204 y headers CORS", async () => {
|
|
77
|
+
const res = await request(server).options("/ping");
|
|
78
|
+
expect(res.statusCode).toBe(204);
|
|
79
|
+
expect(res.headers["access-control-allow-origin"]).toBe("*");
|
|
80
|
+
expect(res.headers["access-control-allow-methods"]).toBe(
|
|
81
|
+
"GET, POST, PUT, DELETE, OPTIONS, PATCH"
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("Body > ~2MB -> 413", async () => {
|
|
86
|
+
const big = "{\"x\":\"" + "a".repeat(2_000_001) + "\"}";
|
|
87
|
+
|
|
88
|
+
const res = await request(server)
|
|
89
|
+
.post("/json")
|
|
90
|
+
.set("Content-Type", "application/json")
|
|
91
|
+
.send(big);
|
|
92
|
+
|
|
93
|
+
expect(res.statusCode).toBe(413);
|
|
94
|
+
expect(res.text).toBe("Request Entity Too Large");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import request from 'supertest';
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import Remote from '../core/Remote.js'; // Ajusta la ruta si es necesario
|
|
4
|
+
|
|
5
|
+
describe('🚦 Router System (Remote.js)', () => {
|
|
6
|
+
let router;
|
|
7
|
+
let server;
|
|
8
|
+
|
|
9
|
+
// Antes de todos los tests, configuramos una "App de Mentira"
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
router = new Remote();
|
|
12
|
+
|
|
13
|
+
// 1. Ruta Básica
|
|
14
|
+
router.get('/', (req, res) => {
|
|
15
|
+
res.statusCode = 200;
|
|
16
|
+
res.end('Hola Nicola');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// 2. Ruta con Parámetros
|
|
20
|
+
router.get('/user/:id', (req, res) => {
|
|
21
|
+
res.statusCode = 200;
|
|
22
|
+
res.setHeader('Content-Type', 'application/json');
|
|
23
|
+
res.end(JSON.stringify({ id: req.params.id }));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// 3. Ruta Suicida (Async Error) - Para probar tu arreglo P0
|
|
27
|
+
router.get('/crash', async (req, res) => {
|
|
28
|
+
throw new Error('Boom Async!');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Creamos el servidor nativo que usa tu Router
|
|
32
|
+
server = createServer((req, res) => {
|
|
33
|
+
// Tu router recibe (req, res, done)
|
|
34
|
+
router.handle(req, res, (err) => {
|
|
35
|
+
// Este es el "Final Handler" (lo que sería tu BlackBox o default)
|
|
36
|
+
if (err) {
|
|
37
|
+
res.statusCode = 500;
|
|
38
|
+
res.end(err.message); // Devolvemos el error capturado
|
|
39
|
+
} else {
|
|
40
|
+
res.statusCode = 404;
|
|
41
|
+
res.end('Not Found');
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('Rutas y Navegación', () => {
|
|
48
|
+
test('GET / debe responder 200 OK', async () => {
|
|
49
|
+
const response = await request(server).get('/');
|
|
50
|
+
expect(response.statusCode).toBe(200);
|
|
51
|
+
expect(response.text).toBe('Hola Nicola');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('GET /unknown debe responder 404', async () => {
|
|
55
|
+
const response = await request(server).get('/ruta-que-no-existe');
|
|
56
|
+
expect(response.statusCode).toBe(404);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('GET /user/123 debe capturar parámetros', async () => {
|
|
60
|
+
const response = await request(server).get('/user/555');
|
|
61
|
+
expect(response.statusCode).toBe(200);
|
|
62
|
+
expect(response.body).toEqual({ id: '555' });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('🛡️ Blindaje de Errores (P0 Fix)', () => {
|
|
67
|
+
test('Debe capturar errores en handlers async y no colgarse', async () => {
|
|
68
|
+
// Si tu fix del principio funciona, esto devolverá 500.
|
|
69
|
+
// Si NO funciona, este test dará timeout porque el servidor se quedará colgado.
|
|
70
|
+
const response = await request(server).get('/crash');
|
|
71
|
+
|
|
72
|
+
expect(response.statusCode).toBe(500);
|
|
73
|
+
expect(response.text).toBe('Boom Async!');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
package/utils/console.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const CODE = {
|
|
2
|
+
reset: "\x1b[0m",
|
|
3
|
+
negrita: "\x1b[1m",
|
|
4
|
+
brigth: "\x1b[1m",
|
|
5
|
+
red: "\x1b[31m",
|
|
6
|
+
green: "\x1b[32m",
|
|
7
|
+
yellow: "\x1b[33m",
|
|
8
|
+
blue: "\x1b[34m",
|
|
9
|
+
cyan: "\x1b[36m",
|
|
10
|
+
magent: "\x1b[35m",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export const paint = (color, text) =>{
|
|
16
|
+
if(!process.stdout.isTTY) return text;
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
return `${CODE[color] || ''}${text}${CODE.reset}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
export const red = (text) => paint('red', text);
|
|
25
|
+
export const green = (text) => paint('green', text);
|
|
26
|
+
export const yellow = (text) => paint('yellow', text);
|
|
27
|
+
export const blue = (text) => paint('blue', text);
|
|
28
|
+
export const cyan = (text) => paint('cyan', text);
|
|
29
|
+
export const magent = (text) => paint('magent', text);
|
|
30
|
+
export const brigth = (text) => paint('brigth', text);
|
|
31
|
+
export const bold = (text) => paint('negrita', text);
|
package/utils/expTime.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
|
|
2
|
+
export const getExpTime = (durationStr) => {
|
|
3
|
+
|
|
4
|
+
const nowInSeconds = Math.floor(Date.now() / 1000);
|
|
5
|
+
|
|
6
|
+
if (!durationStr || typeof durationStr !== 'string') return null;
|
|
7
|
+
|
|
8
|
+
const match = durationStr.match(/^(\d+)([smhdy])$/);
|
|
9
|
+
|
|
10
|
+
if(!match){
|
|
11
|
+
throw new Error(`Invalid Format, use for example: 10h, 10s, 10m, 10d`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const value = parseInt(match[1], 10);
|
|
15
|
+
const unit = match[2];
|
|
16
|
+
|
|
17
|
+
let multiplier = 1;
|
|
18
|
+
switch(unit){
|
|
19
|
+
case 'm' : multiplier = 60; break;
|
|
20
|
+
case 'h' : multiplier = 60 * 60; break;
|
|
21
|
+
case 'd' : multiplier = 24 * 60 * 60; break;
|
|
22
|
+
case 'y' : multiplier = 365 * 24 * 60 * 60; break;
|
|
23
|
+
default: multiplier = 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const secondsToadd = value * multiplier;
|
|
27
|
+
|
|
28
|
+
return nowInSeconds + secondsToadd;
|
|
29
|
+
|
|
30
|
+
}
|
|
File without changes
|