@nano-app/backend 0.5.0 → 0.6.1
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/dist/http/Endpoint.d.ts +1 -1
- package/dist/http/Endpoint.js +2 -2
- package/dist/odata/Entities.d.ts +9 -0
- package/dist/odata/Entities.js +50 -0
- package/dist/odata/QueryBuilder.d.ts +33 -0
- package/dist/odata/QueryBuilder.js +109 -0
- package/dist/prisma/Client.d.ts +12 -0
- package/dist/prisma/Client.js +34 -0
- package/dist/types/api.d.ts +6 -0
- package/dist/types/api.js +2 -0
- package/dist/types/app.d.ts +17 -0
- package/dist/types/app.js +2 -0
- package/dist/types/odata.d.ts +12 -0
- package/dist/types/odata.js +2 -0
- package/dist/utils/Formatter.d.ts +4 -0
- package/dist/utils/Formatter.js +19 -0
- package/package.json +2 -4
package/dist/http/Endpoint.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare abstract class Endpoint<BeforeResult = any> {
|
|
|
7
7
|
constructor(path?: string);
|
|
8
8
|
/** Metodos envoltorios */
|
|
9
9
|
protected BEFORE(_req: Request): Promise<BeforeResult>;
|
|
10
|
-
protected AFTER(_result: ApiResponse): Promise<ApiResponse>;
|
|
10
|
+
protected AFTER(_req: Request, _result: ApiResponse, _method: string): Promise<ApiResponse>;
|
|
11
11
|
/**
|
|
12
12
|
* Métodos HTTP opcionales - las clases hijas solo implementan los que necesiten
|
|
13
13
|
* Si no se implementan, se lanza un error por defecto
|
package/dist/http/Endpoint.js
CHANGED
|
@@ -19,7 +19,7 @@ class Endpoint {
|
|
|
19
19
|
BEFORE(_req) {
|
|
20
20
|
return Promise.resolve({});
|
|
21
21
|
}
|
|
22
|
-
AFTER(_result) {
|
|
22
|
+
AFTER(_req, _result, _method) {
|
|
23
23
|
return Promise.resolve(_result);
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
@@ -50,7 +50,7 @@ class Endpoint {
|
|
|
50
50
|
}
|
|
51
51
|
this.BEFORE(req)
|
|
52
52
|
.then((beforeParams) => handler(req, res, beforeParams))
|
|
53
|
-
.then(result => this.AFTER(result))
|
|
53
|
+
.then(result => this.AFTER(req, result, method))
|
|
54
54
|
.then(result => res.json({
|
|
55
55
|
success: true,
|
|
56
56
|
...result
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Endpoint, type ApiResponse, type BeforeResult } from "../index";
|
|
2
|
+
import type { Request, Response } from 'express';
|
|
3
|
+
export declare class Entities extends Endpoint<BeforeResult> {
|
|
4
|
+
protected BEFORE({ params: { endpoint, id }, path }: Request): Promise<BeforeResult>;
|
|
5
|
+
protected GET(req: Request, _res: Response, { entity, id }: BeforeResult): Promise<ApiResponse>;
|
|
6
|
+
protected POST({ body }: Request, _res: Response, { entity }: BeforeResult): Promise<ApiResponse>;
|
|
7
|
+
protected PUT({ body }: Request, _res: Response, { entity, id }: BeforeResult): Promise<ApiResponse>;
|
|
8
|
+
protected DELETE(_req: Request, _res: Response, { entity, id }: BeforeResult): Promise<ApiResponse>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Entities = void 0;
|
|
4
|
+
const index_1 = require("../index");
|
|
5
|
+
class Entities extends index_1.Endpoint {
|
|
6
|
+
BEFORE({ params: { endpoint, id }, path }) {
|
|
7
|
+
// Valida que la entidad exista
|
|
8
|
+
const entity = this.prismaClient.getModel(endpoint);
|
|
9
|
+
if (!entity) {
|
|
10
|
+
throw { message: `Endpoint ${path} no encontrado` };
|
|
11
|
+
}
|
|
12
|
+
return Promise.resolve({ entity, id: Number(id) });
|
|
13
|
+
}
|
|
14
|
+
async GET(req, _res, { entity, id }) {
|
|
15
|
+
const query = req.query;
|
|
16
|
+
let result;
|
|
17
|
+
// Si se proporciona un ID, se busca un registro único
|
|
18
|
+
if (id) {
|
|
19
|
+
query.id = id.toString();
|
|
20
|
+
result = await entity.findUnique(index_1.QueryBuilder.build(query));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
result = await entity.findMany(index_1.QueryBuilder.build(query));
|
|
24
|
+
}
|
|
25
|
+
return { result };
|
|
26
|
+
}
|
|
27
|
+
async POST({ body }, _res, { entity }) {
|
|
28
|
+
const result = await entity.create({ data: body });
|
|
29
|
+
return { result, message: `Registro creado correctamente` };
|
|
30
|
+
}
|
|
31
|
+
async PUT({ body }, _res, { entity, id }) {
|
|
32
|
+
if (!id) {
|
|
33
|
+
throw { message: "ID es requerido para actualizar" };
|
|
34
|
+
}
|
|
35
|
+
const result = await entity.update({
|
|
36
|
+
where: { id },
|
|
37
|
+
data: body
|
|
38
|
+
});
|
|
39
|
+
return { result, message: `Registro actualizado correctamente` };
|
|
40
|
+
}
|
|
41
|
+
async DELETE(_req, _res, { entity, id }) {
|
|
42
|
+
let result;
|
|
43
|
+
if (!id) {
|
|
44
|
+
throw { message: "ID es requerido para eliminar" };
|
|
45
|
+
}
|
|
46
|
+
result = await entity.delete({ where: { id } });
|
|
47
|
+
return { result, message: `Registro eliminado correctamente` };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.Entities = Entities;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { QueryParams } from "../index";
|
|
2
|
+
export declare class QueryBuilder {
|
|
3
|
+
private static readonly OPERATORS;
|
|
4
|
+
private static parseValue;
|
|
5
|
+
/**
|
|
6
|
+
* Construye el objeto expand
|
|
7
|
+
*/
|
|
8
|
+
private static buildExpand;
|
|
9
|
+
/**
|
|
10
|
+
* Parsea una expresión de filtro individual
|
|
11
|
+
*/
|
|
12
|
+
private static parseExpression;
|
|
13
|
+
/**
|
|
14
|
+
* Construye el objeto filter
|
|
15
|
+
*/
|
|
16
|
+
private static buildFilter;
|
|
17
|
+
/**
|
|
18
|
+
* Construye un objeto de ordenamiento
|
|
19
|
+
* Soporta propiedades encadenadas como "subscription.description"
|
|
20
|
+
*/
|
|
21
|
+
private static buildOrderBy;
|
|
22
|
+
/**
|
|
23
|
+
* Construye el objeto query
|
|
24
|
+
*/
|
|
25
|
+
static build(query: QueryParams): {
|
|
26
|
+
where: {
|
|
27
|
+
[x: string]: any;
|
|
28
|
+
};
|
|
29
|
+
include: Record<string, boolean> | undefined;
|
|
30
|
+
take: number;
|
|
31
|
+
orderBy: any;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QueryBuilder = void 0;
|
|
4
|
+
class QueryBuilder {
|
|
5
|
+
static OPERATORS = {
|
|
6
|
+
eq: (f, v) => ({ [f]: v }),
|
|
7
|
+
ne: (f, v) => ({ [f]: { not: v } }),
|
|
8
|
+
ge: (f, v) => ({ [f]: { gte: v } }),
|
|
9
|
+
le: (f, v) => ({ [f]: { lte: v } }),
|
|
10
|
+
contains: (f, v) => ({
|
|
11
|
+
[f]: { contains: v, mode: 'insensitive' }
|
|
12
|
+
}),
|
|
13
|
+
startswith: (f, v) => ({
|
|
14
|
+
[f]: { startsWith: v, mode: 'insensitive' }
|
|
15
|
+
}),
|
|
16
|
+
endswith: (f, v) => ({
|
|
17
|
+
[f]: { endsWith: v, mode: 'insensitive' }
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
static parseValue(value) {
|
|
21
|
+
if (value === undefined)
|
|
22
|
+
return value;
|
|
23
|
+
if (!isNaN(Number(value)))
|
|
24
|
+
return Number(value);
|
|
25
|
+
if (value === 'true')
|
|
26
|
+
return true;
|
|
27
|
+
if (value === 'false')
|
|
28
|
+
return false;
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Construye el objeto expand
|
|
33
|
+
*/
|
|
34
|
+
static buildExpand(expand) {
|
|
35
|
+
return expand?.split(',').reduce((acc, path) => {
|
|
36
|
+
acc[path] = true;
|
|
37
|
+
return acc;
|
|
38
|
+
}, {});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parsea una expresión de filtro individual
|
|
42
|
+
*/
|
|
43
|
+
static parseExpression(expr) {
|
|
44
|
+
const [field, op, ...rest] = expr.trim().split(' ');
|
|
45
|
+
if (!this.OPERATORS[op])
|
|
46
|
+
throw { message: `Operador '${op}' no soportado` };
|
|
47
|
+
const value = this.parseValue(rest.join(' '));
|
|
48
|
+
return this.OPERATORS[op](field, value);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Construye el objeto filter
|
|
52
|
+
*/
|
|
53
|
+
static buildFilter(filter) {
|
|
54
|
+
if (!filter)
|
|
55
|
+
return {};
|
|
56
|
+
if (filter.includes(' or ')) {
|
|
57
|
+
return {
|
|
58
|
+
OR: filter.split(' or ').map(this.parseExpression.bind(this))
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (filter.includes(' and ')) {
|
|
62
|
+
return {
|
|
63
|
+
AND: filter.split(' and ').map(this.parseExpression.bind(this))
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return this.parseExpression.bind(this)(filter);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Construye un objeto de ordenamiento
|
|
70
|
+
* Soporta propiedades encadenadas como "subscription.description"
|
|
71
|
+
*/
|
|
72
|
+
static buildOrderBy(orderby) {
|
|
73
|
+
if (!orderby)
|
|
74
|
+
return undefined;
|
|
75
|
+
// Obtiene el campo y la dirección
|
|
76
|
+
const [field, direction] = orderby.split(' ');
|
|
77
|
+
// Solo acepta asc o desc
|
|
78
|
+
if (direction !== 'asc' && direction !== 'desc')
|
|
79
|
+
return undefined;
|
|
80
|
+
// Construye el objeto de ordenamiento
|
|
81
|
+
const parts = field.split(".");
|
|
82
|
+
let result = {};
|
|
83
|
+
let current = result;
|
|
84
|
+
for (let i = 0; i < parts.length; i++) {
|
|
85
|
+
if (i === parts.length - 1) {
|
|
86
|
+
current[parts[i]] = direction;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
current[parts[i]] = {};
|
|
90
|
+
current = current[parts[i]];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Construye el objeto query
|
|
97
|
+
*/
|
|
98
|
+
static build(query) {
|
|
99
|
+
return {
|
|
100
|
+
where: {
|
|
101
|
+
...this.buildFilter(query.filter)
|
|
102
|
+
},
|
|
103
|
+
include: this.buildExpand(query.expand),
|
|
104
|
+
take: Number(query.top || 10) + Number(query.skip || 0),
|
|
105
|
+
orderBy: this.buildOrderBy(query.orderby),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.QueryBuilder = QueryBuilder;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
declare const Prisma: any;
|
|
3
|
+
type GeneratedPrismaClient = typeof Prisma;
|
|
4
|
+
export type AppModels = Omit<GeneratedPrismaClient, `$${string}` | symbol>;
|
|
5
|
+
/** Cliente Prisma. getModel retorna el tipo correcto según el nombre del modelo. */
|
|
6
|
+
export declare class PrismaClient {
|
|
7
|
+
private prisma;
|
|
8
|
+
getModel<K extends keyof AppModels>(entityName: K): AppModels[K];
|
|
9
|
+
/** Para consultas que comparan columnas u otras condiciones no soportadas en findMany */
|
|
10
|
+
queryRaw<T = unknown>(strings: TemplateStringsArray, ...values: any[]): Promise<T>;
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PrismaClient = void 0;
|
|
7
|
+
require("dotenv/config");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const adapter_pg_1 = require("@prisma/adapter-pg");
|
|
10
|
+
// Crea el adaptador a la base de datos
|
|
11
|
+
if (!process.env.DATABASE_URL) {
|
|
12
|
+
throw { message: 'DATABASE_URL no está configurada' };
|
|
13
|
+
}
|
|
14
|
+
const adapter = new adapter_pg_1.PrismaPg({
|
|
15
|
+
connectionString: process.env.DATABASE_URL
|
|
16
|
+
});
|
|
17
|
+
// Carga el cliente generado por el binario
|
|
18
|
+
const clientPath = path_1.default.join(process.cwd(), "node_modules/.prisma/prisma-client/client");
|
|
19
|
+
const client = require(clientPath);
|
|
20
|
+
const Prisma = client.PrismaClient ?? client.default?.PrismaClient ?? client;
|
|
21
|
+
// Crea una instancia de Prisma
|
|
22
|
+
const prismaInstance = new Prisma({ adapter });
|
|
23
|
+
/** Cliente Prisma. getModel retorna el tipo correcto según el nombre del modelo. */
|
|
24
|
+
class PrismaClient {
|
|
25
|
+
prisma = prismaInstance;
|
|
26
|
+
getModel(entityName) {
|
|
27
|
+
return this.prisma[entityName];
|
|
28
|
+
}
|
|
29
|
+
/** Para consultas que comparan columnas u otras condiciones no soportadas en findMany */
|
|
30
|
+
async queryRaw(strings, ...values) {
|
|
31
|
+
return this.prisma.$queryRaw(strings, ...values);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.PrismaClient = PrismaClient;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Endpoint } from "../http/Endpoint";
|
|
2
|
+
import type { ErrorRequestHandler, RequestHandler } from "express";
|
|
3
|
+
import type { Options } from "express-rate-limit";
|
|
4
|
+
export type NanoAppConfig = {
|
|
5
|
+
endpoints: Endpoint[];
|
|
6
|
+
enableEntities?: boolean;
|
|
7
|
+
errorMiddleware?: ErrorRequestHandler;
|
|
8
|
+
middlewares?: RequestHandler[];
|
|
9
|
+
port?: number;
|
|
10
|
+
rateLimit?: Partial<Options>;
|
|
11
|
+
expressConfig?: Partial<Express.Application>;
|
|
12
|
+
setters?: Record<string, any>;
|
|
13
|
+
morgan?: {
|
|
14
|
+
tokens?: Record<string, any>;
|
|
15
|
+
format?: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Formatter = void 0;
|
|
4
|
+
class Formatter {
|
|
5
|
+
static price(value) {
|
|
6
|
+
return `$${new Intl.NumberFormat("de-DE", { maximumFractionDigits: 0 }).format(value || 0)}`;
|
|
7
|
+
}
|
|
8
|
+
static logDate(date = new Date()) {
|
|
9
|
+
return new Intl.DateTimeFormat("es-ES", {
|
|
10
|
+
year: "2-digit",
|
|
11
|
+
month: "2-digit",
|
|
12
|
+
day: "2-digit",
|
|
13
|
+
hour: "2-digit",
|
|
14
|
+
minute: "2-digit",
|
|
15
|
+
second: "2-digit",
|
|
16
|
+
}).format(date);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.Formatter = Formatter;
|
package/package.json
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nano-app/backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Backend for nano-app architecture",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
|
-
"
|
|
9
|
-
"*.d.ts",
|
|
10
|
-
"http"
|
|
8
|
+
"dist"
|
|
11
9
|
],
|
|
12
10
|
"scripts": {
|
|
13
11
|
"build": "tsc",
|