nicola-framework 1.0.1 → 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/core/Core.js CHANGED
@@ -1,84 +1,89 @@
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';
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
- constructor() {
10
- super();
11
- }
12
-
13
-
14
-
15
-
16
- listen(port, callback) {
17
-
18
- const server = http.createServer((req, res) => {
19
- const myURL = new URL(req.url, 'http://' + req.headers.host);
20
- const pathURL = myURL.pathname;
21
- const urlParams = Object.fromEntries(myURL.searchParams);
22
-
23
- req.url = pathURL;
24
- req.query = urlParams;
25
-
26
- Shadowgraph(req, res, () => {
27
- this.__addHelper(res);
28
- EasyCors(req, res, () =>{
29
-
30
- Teleforce(req, res, () => {
31
- const done = (err) => {
32
- if (!err) {
33
- res.statusCode = 404;
34
- res.end('Not Found')
35
- }
36
- else {
37
- BlackBox.ignite(err, req, res);
38
- }
39
- }
40
-
41
- let dataString = ''
42
- req.on('data', chunk => {
43
- dataString += chunk;
44
-
45
- })
46
-
47
- req.on('end', () => {
48
- try {
49
- if (dataString) {
50
- req.body = JSON.parse(dataString);
51
- }
52
- else {
53
- req.body = {}
54
- }
55
- }
56
- catch (error) {
57
- req.body = {}
58
- }
59
- this.handle(req, res, done);
60
-
61
- });
62
- })
63
- })
64
- })
65
- })
66
- server.listen(port, callback);
67
- }
68
-
69
- __addHelper(res) {
70
- res.json = (data) => {
71
- res.setHeader('Content-Type', 'application/json');
72
- res.end(JSON.stringify(data));
73
- }
74
-
75
- res.send = (data) => {
76
- res.setHeader('Content-Type', 'text/plain');
77
- res.end(data);
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);
@@ -19,20 +19,15 @@ class Connection {
19
19
  await this.client.connect()
20
20
  return;
21
21
  }
22
- if(DB_DRIVER === 'mysql'){
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
@@ -34,7 +34,7 @@ class Model {
34
34
  }
35
35
 
36
36
  static orderBy(...params){
37
- return this.query().orderby(...params);
37
+ return this.query().orderBy(...params);
38
38
  }
39
39
  static limit(...params){
40
40
  return this.query().limit(...params);
@@ -40,10 +40,10 @@ class Postgres extends Driver {
40
40
  }
41
41
  catch (e) {
42
42
  if (e.code === 'ERR_MODULE_NOT_FOUND') {
43
- console.log('Por favor utiliza el comando npm install pg')
43
+ throw new Error('Por favor utiliza el comando npm install pg')
44
44
  }
45
45
  else {
46
- console.error(e)
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
- return console.log("Dynamo: No hay ninguna conexion activa")
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(', ')
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs/promises'
2
2
  import { spawn } from 'child_process'
3
- import chalk from 'chalk'
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(chalk.greenBright(`LiveCurrent: Iniciando Servidor...`));
17
+ console.log(green(`LiveCurrent: Iniciando Servidor...`));
18
18
  }
19
19
  reload(){
20
20
  if(this.process){
@@ -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
- const EasyCors = (req, res, next) =>{
4
- res.setHeader('Access-Control-Allow-Origin','*')
5
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH')
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
- else{
14
- next()
15
- }
16
- }
17
-
35
+ };
36
+ };
18
37
 
19
- export default EasyCors;
38
+ export default EasyCors;
@@ -1,10 +1,9 @@
1
- import chalk from 'chalk'
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(chalk.green(`[${req.method}] ${req.url} - ${res.statusCode} ${res.statusMessage} - ${duracion}ms`))
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.1",
4
- "description": "Zero-dependency web framework for Node.js",
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": "echo \"Error: no test specified\" && exit 1"
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
27
  "author": "Erick Mauricio Tiznado Rodriguez",
29
28
  "license": "MIT",
30
- "dependencies": {
31
- "chalk": "^5.6.2"
29
+ "devDependencies": {
30
+ "jest": "^30.2.0",
31
+ "supertest": "^7.2.2"
32
32
  }
33
33
  }
@@ -1,53 +1,78 @@
1
- import crypto from 'crypto'
2
- import Regulator from './Regulator.js';
1
+ import crypto from "crypto";
2
+ import Regulator from "./Regulator.js";
3
+ import { getExpTime } from "../utils/expTime.js";
3
4
 
4
- class Coherer{
5
- constructor(){
5
+ class Coherer {
6
+ constructor() {}
6
7
 
7
- }
8
- static SECRET = process.env.NICOLA_SECRET || 'nicola_secret_dev_key'
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
- static codec(jsonData){
11
- const dataString = JSON.stringify(jsonData);
12
- const buffer = Buffer.from(dataString);
13
- return buffer.toString('base64url')
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
- static sign(Payload){
17
- const payloadB64 = this.codec(Payload);
18
- const header = {
19
- alg: 'HS256',
20
- typ: 'JWT'
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
- const data = headerB64 + '.' + payloadB64
35
+ const data = headerB64 + "." + payloadB64;
25
36
 
26
- const signature = crypto.createHmac('sha256', this.SECRET)
27
- .update(data)
28
- .digest('base64url')
37
+ const signature = crypto
38
+ .createHmac("sha256", SECRET)
39
+ .update(data)
40
+ .digest("base64url");
29
41
 
30
- return data + '.' + signature
31
- }
32
- static verify(data){
33
- const [headerB64, payloadB64, signature] = data.token.split('.');
42
+ return data + "." + signature;
43
+ }
34
44
 
35
- const dataToCheck = headerB64 + '.' + payloadB64
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
- const signatureToChecks = crypto.createHmac('sha256', this.SECRET)
38
- .update(dataToCheck)
39
- .digest('base64url')
51
+ const dataToCheck = headerB64 + "." + payloadB64;
40
52
 
41
- if(signature === signatureToChecks){
42
- let decodedPayload = Buffer.from(payloadB64, 'base64url').toString('utf-8')
43
- decodedPayload = JSON.parse(decodedPayload)
44
- return decodedPayload
45
- }
46
- else{
47
- throw new Error('Token Invalido')
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
+ });