@opensas/xml-rest-adapter 0.1.0
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 +4 -0
- package/dist/auth.d.ts +8 -0
- package/dist/auth.js +125 -0
- package/dist/condition.d.ts +25 -0
- package/dist/condition.js +60 -0
- package/dist/create-adapter.d.ts +8 -0
- package/dist/create-adapter.js +95 -0
- package/dist/db.d.ts +34 -0
- package/dist/db.js +134 -0
- package/dist/endpoints.d.ts +3 -0
- package/dist/endpoints.js +17 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +42 -0
- package/dist/http-status.d.ts +8 -0
- package/dist/http-status.js +7 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +25 -0
- package/dist/parse-request.d.ts +12 -0
- package/dist/parse-request.js +80 -0
- package/dist/request-to-xml.d.ts +8 -0
- package/dist/request-to-xml.js +219 -0
- package/dist/result.d.ts +14 -0
- package/dist/result.js +6 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.js +1 -0
- package/dist/valid.d.ts +15 -0
- package/dist/valid.js +92 -0
- package/dist/xml.d.ts +25 -0
- package/dist/xml.js +36 -0
- package/package.json +99 -0
package/README.md
ADDED
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type AppError } from './errors.js';
|
|
2
|
+
import { type Result } from './result.js';
|
|
3
|
+
import type { Aplicacion, Permisos, Usuario } from './types.js';
|
|
4
|
+
export declare const ANONYMOUS_USER: Usuario;
|
|
5
|
+
export declare function getDomainUser(request: Request, as?: string | null): string;
|
|
6
|
+
export declare function parseGrupos(raw: string): string[];
|
|
7
|
+
export declare function parsePermisos(raw: string): Permisos;
|
|
8
|
+
export declare function resolveUser(app: Aplicacion, identity: string | Request): Promise<Result<Usuario, AppError>>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { execXmlSentencia } from './db.js';
|
|
2
|
+
import { appError } from './errors.js';
|
|
3
|
+
import { err, ok } from './result.js';
|
|
4
|
+
import { xmlAlta } from './xml.js';
|
|
5
|
+
export const ANONYMOUS_USER = {
|
|
6
|
+
usuarioId: 0,
|
|
7
|
+
codigo: 'ANONIMO',
|
|
8
|
+
apellido: 'Anónimo',
|
|
9
|
+
nombres: '',
|
|
10
|
+
apellidoNombres: 'Anónimo',
|
|
11
|
+
activo: true,
|
|
12
|
+
grupos: [],
|
|
13
|
+
permisos: {},
|
|
14
|
+
domainUser: '',
|
|
15
|
+
};
|
|
16
|
+
// Identidad cruda tal como la inyecta IIS-node (Windows Integrated Auth) — ver
|
|
17
|
+
// `authentication: { type: 'iis-node' }`. `as` (ej. `?as=DOMINIO\usuario`) permite pisarla desde
|
|
18
|
+
// afuera del header, típicamente en dev/tests.
|
|
19
|
+
export function getDomainUser(request, as) {
|
|
20
|
+
return (request.headers.get('x-iisnode-auth_user') ||
|
|
21
|
+
request.headers.get('iisnode-remote_user') ||
|
|
22
|
+
as ||
|
|
23
|
+
'');
|
|
24
|
+
}
|
|
25
|
+
export function parseGrupos(raw) {
|
|
26
|
+
return raw
|
|
27
|
+
? raw
|
|
28
|
+
.split(';')
|
|
29
|
+
.map((grupo) => grupo.trim())
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
: [];
|
|
32
|
+
}
|
|
33
|
+
// Ej: "Recurso(ALTA;CONSULTA),OtroRecurso(CONSULTA)" -> [recurso, acciones] por cada match
|
|
34
|
+
const RECURSO_ACCIONES_PATTERN = /([^(,]+)\(([^)]+)\)/g;
|
|
35
|
+
export function parsePermisos(raw) {
|
|
36
|
+
const permisos = {};
|
|
37
|
+
for (const match of raw.matchAll(RECURSO_ACCIONES_PATTERN)) {
|
|
38
|
+
const recurso = match[1].trim();
|
|
39
|
+
const acciones = match[2].split(';').map((accion) => accion.toLowerCase());
|
|
40
|
+
permisos[recurso] = acciones;
|
|
41
|
+
}
|
|
42
|
+
return permisos;
|
|
43
|
+
}
|
|
44
|
+
// No exportada: es el mecanismo de bajo nivel que orquesta `resolveUser` (más abajo) para
|
|
45
|
+
// authorization: 'metaSSC' — no hay caller fuera de este archivo, ni falta: todo lo que necesita
|
|
46
|
+
// "identidad + permisos" pasa por `resolveUser`, que además sabe cuándo NO hace falta pegarle a
|
|
47
|
+
// ninguna base (authentication/authorization en 'none').
|
|
48
|
+
async function readUsuario(dbInfo, aplicacionCodigo, usuario) {
|
|
49
|
+
const sentencia = xmlAlta({
|
|
50
|
+
Operador: usuario,
|
|
51
|
+
Parametro: {
|
|
52
|
+
Tabla: 'UsuarioAutenticacion',
|
|
53
|
+
AplicacionCodigo: aplicacionCodigo, // esta SI se usa, la lee xmlUsuarioAutenticacion_Validacion
|
|
54
|
+
UsuarioCodigo: usuario,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const resultRow = await execXmlSentencia(dbInfo, 'UsuarioAutenticacion', sentencia);
|
|
58
|
+
if (!resultRow.ok) {
|
|
59
|
+
// Un RAISERROR de negocio (usuario/aplicación inválidos) es un 401 real. Un error de
|
|
60
|
+
// sistema (base caída, etc.) no lo es — se propaga tal cual en vez de disfrazarlo de
|
|
61
|
+
// "no autorizado", que induciría a buscar el problema en las credenciales.
|
|
62
|
+
if (resultRow.error.code === 'VALIDATION_ERROR') {
|
|
63
|
+
return err(appError({ title: resultRow.error.title, code: 'UNAUTHORIZED' }));
|
|
64
|
+
}
|
|
65
|
+
return resultRow;
|
|
66
|
+
}
|
|
67
|
+
const [row] = resultRow.data;
|
|
68
|
+
if (!row) {
|
|
69
|
+
return err(appError({
|
|
70
|
+
title: 'No se pudo autenticar al usuario',
|
|
71
|
+
detail: 'xmlUsuarioAutenticacion_Alta no retornó ningún registro',
|
|
72
|
+
code: 'UNAUTHORIZED',
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
return ok({
|
|
76
|
+
usuarioId: row.UsuarioId,
|
|
77
|
+
codigo: row.UsuarioCodigo,
|
|
78
|
+
apellido: row.UsuarioApellido,
|
|
79
|
+
nombres: row.UsuarioNombres,
|
|
80
|
+
apellidoNombres: row.UsuarioApellidoNombres,
|
|
81
|
+
activo: row.UsuarioActivo === 1,
|
|
82
|
+
grupos: parseGrupos(row.UsuarioGrupos),
|
|
83
|
+
permisos: parsePermisos(row.UsuarioPermisos),
|
|
84
|
+
domainUser: usuario,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Orquesta authentication + authorization (ver types.ts) para resolver el Usuario de un request,
|
|
88
|
+
// la contraparte de `authUser` en convenios_web/src/hooks.server.ts — sin la resolución de
|
|
89
|
+
// "aplicación" (siempre es la única `Aplicacion` del adapter), sin cache (ver auth/cache.ts en
|
|
90
|
+
// convenios_web, queda para después como mejora) y sin el registro central para el authProvider
|
|
91
|
+
// de otra app (authorization.dbInfo ya es la connection string directa, ver types.ts).
|
|
92
|
+
//
|
|
93
|
+
// Segundo parámetro: string | Request, mismo patrón que DbInfo | string en db.ts (aceptar la
|
|
94
|
+
// forma "ya resuelta" o la más rica). Con `authentication` ya en la config, el consumidor no
|
|
95
|
+
// tiene por qué saber CÓMO se extrae la identidad ('iis-node' es siempre el mismo mecanismo fijo,
|
|
96
|
+
// ver getDomainUser) — le alcanza con pasar el `Request` tal cual. La forma string sigue
|
|
97
|
+
// existiendo para tests (pasar 'TEST\ADMIN' directo, sin fabricar un Request) y para quien ya
|
|
98
|
+
// resolvió el domainUser por su cuenta.
|
|
99
|
+
//
|
|
100
|
+
// A propósito, esto NO lee ningún `?as=` de la URL: soportar impersonation vía querystring no es
|
|
101
|
+
// un feature de la librería (sería un footgun de seguridad tenerlo de fábrica) — es una
|
|
102
|
+
// conveniencia de dev/tests que arma el caller (ver src/routes/fixture/[...path]/+server.ts),
|
|
103
|
+
// pasando el string ya resuelto en vez del Request cuando corresponde.
|
|
104
|
+
export async function resolveUser(app, identity) {
|
|
105
|
+
const domainUser = typeof identity === 'string' ? identity : getDomainUser(identity);
|
|
106
|
+
const aplicacionCodigo = app.aplicacionCodigo || app.name;
|
|
107
|
+
if (app.authentication.type === 'none') {
|
|
108
|
+
// app pública/anónima: no hay identidad real de un caller que resolver.
|
|
109
|
+
if (app.authorization.type === 'none')
|
|
110
|
+
return ok(ANONYMOUS_USER);
|
|
111
|
+
// Igual se puede acotar por Meta con una cuenta de servicio de solo lectura (ver
|
|
112
|
+
// types.ts:Authorization) — sin ella, no hay con quién autenticar, sigue anónimo.
|
|
113
|
+
const { dbInfo, serviceUser } = app.authorization;
|
|
114
|
+
if (!serviceUser)
|
|
115
|
+
return ok(ANONYMOUS_USER);
|
|
116
|
+
return readUsuario(dbInfo, aplicacionCodigo, serviceUser);
|
|
117
|
+
}
|
|
118
|
+
// authentication.type === 'iis-node': domainUser ya viene resuelto (getDomainUser), como el
|
|
119
|
+
// login de Windows real de quien hizo el request.
|
|
120
|
+
if (!domainUser)
|
|
121
|
+
return err(appError('Usuario desconocido', 'UNAUTHORIZED'));
|
|
122
|
+
if (app.authorization.type === 'none')
|
|
123
|
+
return ok({ ...ANONYMOUS_USER, domainUser });
|
|
124
|
+
return readUsuario(app.authorization.dbInfo, aplicacionCodigo, domainUser);
|
|
125
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type Operator = '=' | '!=' | '<' | '>' | '<=' | '>=' | 'CONTAINS' | 'BEGINS_WITH' | 'ENDS_WITH' | 'IN' | 'BETWEEN';
|
|
2
|
+
export type ValueCondition = string | number | boolean | string[] | number[] | boolean[] | null;
|
|
3
|
+
export type SimpleCondition = {
|
|
4
|
+
[field: string]: ValueCondition | {
|
|
5
|
+
operator: Operator;
|
|
6
|
+
value: ValueCondition;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export type Condition = SimpleCondition | {
|
|
10
|
+
AND: Condition[];
|
|
11
|
+
};
|
|
12
|
+
export declare const condition: {
|
|
13
|
+
eq: (field: string, value: ValueCondition) => Condition;
|
|
14
|
+
neq: (field: string, value: ValueCondition) => Condition;
|
|
15
|
+
lt: (field: string, value: ValueCondition) => Condition;
|
|
16
|
+
gt: (field: string, value: ValueCondition) => Condition;
|
|
17
|
+
lte: (field: string, value: ValueCondition) => Condition;
|
|
18
|
+
gte: (field: string, value: ValueCondition) => Condition;
|
|
19
|
+
contains: (field: string, value: ValueCondition) => Condition;
|
|
20
|
+
beginWith: (field: string, value: ValueCondition) => Condition;
|
|
21
|
+
endsWith: (field: string, value: ValueCondition) => Condition;
|
|
22
|
+
in: (field: string, values: ValueCondition) => Condition;
|
|
23
|
+
between: (field: string, min: ValueCondition, max: ValueCondition) => Condition;
|
|
24
|
+
};
|
|
25
|
+
export declare const getXmlConditionValue: (operator: Operator, value: ValueCondition) => string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Constructores de condiciones para armar `Parametro.Condiciones` en las sentencias XML que
|
|
2
|
+
// consume xml.ts. Se arman con el objeto `condition` (`condition.eq('Campo', valor)`,
|
|
3
|
+
// `condition.between('Fecha', min, max)`, etc.) y se combinan con `{ AND: [cond1, cond2] }`.
|
|
4
|
+
// `getXmlConditionValue` traduce una condición ya resuelta (operador + valor) al string que
|
|
5
|
+
// espera el SP (`=valor`, `*valor*`, `min..max`, etc.).
|
|
6
|
+
function cond(field, operator, value) {
|
|
7
|
+
return { [field]: { operator, value } };
|
|
8
|
+
}
|
|
9
|
+
const eq = (field, value) => cond(field, '=', value);
|
|
10
|
+
const neq = (field, value) => cond(field, '!=', value);
|
|
11
|
+
const lt = (field, value) => cond(field, '<', value);
|
|
12
|
+
const gt = (field, value) => cond(field, '>', value);
|
|
13
|
+
const lte = (field, value) => cond(field, '<=', value);
|
|
14
|
+
const gte = (field, value) => cond(field, '>=', value);
|
|
15
|
+
const contains = (field, value) => cond(field, 'CONTAINS', value);
|
|
16
|
+
const beginWith = (field, value) => cond(field, 'BEGINS_WITH', value);
|
|
17
|
+
const endsWith = (field, value) => cond(field, 'ENDS_WITH', value);
|
|
18
|
+
const inOp = (field, values) => cond(field, 'IN', values);
|
|
19
|
+
const between = (field, min, max) => cond(field, 'BETWEEN', [min, max]);
|
|
20
|
+
export const condition = {
|
|
21
|
+
eq,
|
|
22
|
+
neq,
|
|
23
|
+
lt,
|
|
24
|
+
gt,
|
|
25
|
+
lte,
|
|
26
|
+
gte,
|
|
27
|
+
contains,
|
|
28
|
+
beginWith,
|
|
29
|
+
endsWith,
|
|
30
|
+
in: inOp,
|
|
31
|
+
between,
|
|
32
|
+
};
|
|
33
|
+
export const getXmlConditionValue = (operator, value) => {
|
|
34
|
+
// Solo null/undefined son "sin valor" — 0 y false son valores válidos y distintos de
|
|
35
|
+
// "no especificado" (`!value` los trataría igual que null, perdiéndolos).
|
|
36
|
+
if (value === null || value === undefined)
|
|
37
|
+
value = '';
|
|
38
|
+
switch (operator) {
|
|
39
|
+
case '=':
|
|
40
|
+
case '<':
|
|
41
|
+
case '>':
|
|
42
|
+
case '<=':
|
|
43
|
+
case '>=':
|
|
44
|
+
return operator + value.toString();
|
|
45
|
+
case '!=':
|
|
46
|
+
return '<>' + value.toString();
|
|
47
|
+
case 'CONTAINS':
|
|
48
|
+
return '*' + value.toString() + '*';
|
|
49
|
+
case 'BEGINS_WITH':
|
|
50
|
+
return value.toString() + '*';
|
|
51
|
+
case 'ENDS_WITH':
|
|
52
|
+
return '*' + value.toString();
|
|
53
|
+
case 'IN':
|
|
54
|
+
return value.toString();
|
|
55
|
+
case 'BETWEEN':
|
|
56
|
+
if (!Array.isArray(value) || value.length !== 2)
|
|
57
|
+
throw new Error('BETWEEN espera exactamente 2 valores (min, max)');
|
|
58
|
+
return `${value[0]}..${value[1]}`;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RequestEvent } from '@sveltejs/kit';
|
|
2
|
+
import type { AppError } from './errors.js';
|
|
3
|
+
import type { Result } from './result.js';
|
|
4
|
+
import type { Aplicacion, Usuario } from './types.js';
|
|
5
|
+
export declare function createXmlRestAdapter(app: Aplicacion): {
|
|
6
|
+
resolveUser: (identity: string | Request) => Promise<Result<Usuario, AppError>>;
|
|
7
|
+
handle: (event: RequestEvent, usuario: Usuario) => Promise<Response>;
|
|
8
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { json } from '@sveltejs/kit';
|
|
2
|
+
import { resolveUser } from './auth.js';
|
|
3
|
+
import { execXmlConsulta, execXmlSentencia } from './db.js';
|
|
4
|
+
import { appError, jsonError } from './errors.js';
|
|
5
|
+
import { parseRequest } from './parse-request.js';
|
|
6
|
+
function validarAplicacion(app) {
|
|
7
|
+
if (!app.name)
|
|
8
|
+
throw new Error('createXmlRestAdapter: "name" es obligatorio');
|
|
9
|
+
if (!app.dbInfo)
|
|
10
|
+
throw new Error('createXmlRestAdapter: "dbInfo" es obligatorio');
|
|
11
|
+
if (app.pagination !== 'sql' && app.pagination !== 'com+') {
|
|
12
|
+
throw new Error(`createXmlRestAdapter: "pagination" inválido (${JSON.stringify(app.pagination)}) — debe ser "sql" o "com+"`);
|
|
13
|
+
}
|
|
14
|
+
if (!app.authentication) {
|
|
15
|
+
throw new Error('createXmlRestAdapter: "authentication" es obligatorio');
|
|
16
|
+
}
|
|
17
|
+
// Tipado como string, no como Authentication['type']: una vez descartados 'none'/'iis-node' acá
|
|
18
|
+
// abajo, TS angosta el tipo a `never` — accederlo de nuevo para el mensaje de error rompería
|
|
19
|
+
// la comparación (ver el mismo patrón para authorization más abajo).
|
|
20
|
+
const authenticationType = app.authentication.type;
|
|
21
|
+
if (authenticationType !== 'none' && authenticationType !== 'iis-node') {
|
|
22
|
+
throw new Error(`createXmlRestAdapter: "authentication.type" inválido (${JSON.stringify(authenticationType)}) — debe ser "none" o "iis-node"`);
|
|
23
|
+
}
|
|
24
|
+
if (!app.authorization) {
|
|
25
|
+
throw new Error('createXmlRestAdapter: "authorization" es obligatorio');
|
|
26
|
+
}
|
|
27
|
+
const authorizationType = app.authorization.type;
|
|
28
|
+
if (authorizationType !== 'none' && authorizationType !== 'metaSSC') {
|
|
29
|
+
throw new Error(`createXmlRestAdapter: "authorization.type" inválido (${JSON.stringify(authorizationType)}) — debe ser "none" o "metaSSC"`);
|
|
30
|
+
}
|
|
31
|
+
if (app.authorization.type === 'metaSSC' && !app.authorization.dbInfo) {
|
|
32
|
+
throw new Error('createXmlRestAdapter: "authorization.dbInfo" es obligatorio cuando "authorization.type" es "metaSSC"');
|
|
33
|
+
}
|
|
34
|
+
if (!Array.isArray(app.endpoints)) {
|
|
35
|
+
throw new TypeError('createXmlRestAdapter: "endpoints" debe ser un array');
|
|
36
|
+
}
|
|
37
|
+
for (const endpoint of app.endpoints) {
|
|
38
|
+
if (!endpoint.name)
|
|
39
|
+
throw new Error('createXmlRestAdapter: cada endpoint necesita "name"');
|
|
40
|
+
if (!endpoint.recurso) {
|
|
41
|
+
throw new Error(`createXmlRestAdapter: el endpoint "${endpoint.name}" necesita "recurso"`);
|
|
42
|
+
}
|
|
43
|
+
if (!endpoint.fieldId) {
|
|
44
|
+
throw new Error(`createXmlRestAdapter: el endpoint "${endpoint.name}" necesita "fieldId"`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function createXmlRestAdapter(app) {
|
|
49
|
+
validarAplicacion(app);
|
|
50
|
+
// Resuelve identidad (authentication) + permisos (authorization) — ver auth.ts. Acepta el
|
|
51
|
+
// Request tal cual (extrae domainUser según app.authentication) o un string ya resuelto (para
|
|
52
|
+
// tests, o un consumidor que ya lo extrajo por su cuenta). Devuelve Result, no throw ni
|
|
53
|
+
// Usuario a secas: mismo vocabulario (Result/AppError/jsonError) que el resto de la librería,
|
|
54
|
+
// incluido lo que ya usa `handle` puertas adentro.
|
|
55
|
+
function resolveUserFn(identity) {
|
|
56
|
+
return resolveUser(app, identity);
|
|
57
|
+
}
|
|
58
|
+
async function handle(event, usuario) {
|
|
59
|
+
try {
|
|
60
|
+
// Genérico a propósito: cada consumidor define su propia carpeta `[...path]`, así que
|
|
61
|
+
// el nombre real del param lo tipa `$types` del lado del consumidor, no acá.
|
|
62
|
+
const path = event.params.path ?? '';
|
|
63
|
+
const parsed = await parseRequest(app, event.request, path, usuario);
|
|
64
|
+
if (!parsed.ok)
|
|
65
|
+
return jsonError(parsed.error);
|
|
66
|
+
const { objeto, id, sentencia, archivos, archivoParams } = parsed.data;
|
|
67
|
+
if (sentencia.Tipo === 'Consulta') {
|
|
68
|
+
const result = await execXmlConsulta(app.dbInfo, objeto, sentencia);
|
|
69
|
+
if (!result.ok)
|
|
70
|
+
return jsonError(result.error);
|
|
71
|
+
// Consulta por id sin resultados = 404. (En convenios_web este chequeo existía
|
|
72
|
+
// pero comparaba contra el objeto {rows,count} entero, que nunca es falsy — no
|
|
73
|
+
// llegaba a dispararse nunca; acá sí compara lo que corresponde.)
|
|
74
|
+
if (id && result.data.rows.length === 0) {
|
|
75
|
+
return jsonError(appError(`${objeto} ${id} no encontrado`, 'NOT_FOUND'));
|
|
76
|
+
}
|
|
77
|
+
return json(result.data);
|
|
78
|
+
}
|
|
79
|
+
const result = await execXmlSentencia(app.dbInfo, objeto, sentencia, archivoParams, archivos);
|
|
80
|
+
if (!result.ok)
|
|
81
|
+
return jsonError(result.error);
|
|
82
|
+
if (sentencia.Tipo === 'Baja')
|
|
83
|
+
return json(result.data);
|
|
84
|
+
return json({ rows: result.data, count: 1 });
|
|
85
|
+
}
|
|
86
|
+
catch (err_) {
|
|
87
|
+
return jsonError(appError({
|
|
88
|
+
title: 'Error interno del servidor',
|
|
89
|
+
code: 'INTERNAL_ERROR',
|
|
90
|
+
detail: String(err_),
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { resolveUser: resolveUserFn, handle };
|
|
95
|
+
}
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { XmlSentencia } from './xml.js';
|
|
2
|
+
export type DbInfo = {
|
|
3
|
+
user: string;
|
|
4
|
+
password: string;
|
|
5
|
+
server: string;
|
|
6
|
+
database: string;
|
|
7
|
+
options: {
|
|
8
|
+
encrypt: boolean;
|
|
9
|
+
trustServerCertificate: boolean;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export declare function closeAllPools(): Promise<void>;
|
|
13
|
+
export declare function execXmlSentencia<T>(dbInfo: DbInfo | string, objeto: string, sentencia: XmlSentencia, archivoParams?: string[], archivos?: Buffer[]): Promise<{
|
|
14
|
+
ok: false;
|
|
15
|
+
data: null;
|
|
16
|
+
error: import("./errors.js").AppError;
|
|
17
|
+
} | {
|
|
18
|
+
ok: true;
|
|
19
|
+
data: T[];
|
|
20
|
+
error: null;
|
|
21
|
+
}>;
|
|
22
|
+
export declare function execXmlConsulta<T = Record<string, string>>(dbInfo: DbInfo | string, objeto: string, sentencia: XmlSentencia): Promise<{
|
|
23
|
+
ok: false;
|
|
24
|
+
data: null;
|
|
25
|
+
error: import("./errors.js").AppError;
|
|
26
|
+
} | {
|
|
27
|
+
ok: true;
|
|
28
|
+
data: {
|
|
29
|
+
rows: T[];
|
|
30
|
+
count: number;
|
|
31
|
+
};
|
|
32
|
+
error: null;
|
|
33
|
+
}>;
|
|
34
|
+
export declare function postProcessRows<T>(rows: T[]): T[];
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Ejecuta los SPs xmlXXX_Alta|Baja|Modificacion|Consulta contra SQL Server, serializando la
|
|
2
|
+
// XmlSentencia al XML que esperan (ver request-to-xml.ts) y devolviendo el resultado como
|
|
3
|
+
// Result (ver result.ts) en vez de tirar excepciones.
|
|
4
|
+
import sql from 'mssql';
|
|
5
|
+
import { appError } from './errors.js';
|
|
6
|
+
import { toXmlParametros } from './request-to-xml.js';
|
|
7
|
+
import { err, ok } from './result.js';
|
|
8
|
+
const pools = new Map();
|
|
9
|
+
// Cierra los pools abiertos — se registra en SIGTERM/SIGINT más abajo, y se expone también para
|
|
10
|
+
// quien necesite cerrarlos a mano (tests de integración, scripts). A diferencia de convenios_web
|
|
11
|
+
// (una app dueña de todo el proceso), acá NO se llama process.exit(): esto es una librería, puede
|
|
12
|
+
// convivir con otro código en el mismo proceso que también quiera reaccionar a la señal, y forzar
|
|
13
|
+
// la salida del proceso completo no es una decisión que le corresponda tomar a un cliente de la
|
|
14
|
+
// librería.
|
|
15
|
+
export async function closeAllPools() {
|
|
16
|
+
for (const pool of pools.values())
|
|
17
|
+
await pool.close();
|
|
18
|
+
pools.clear();
|
|
19
|
+
}
|
|
20
|
+
/* eslint-disable unicorn/no-top-level-side-effects */
|
|
21
|
+
process.on('SIGTERM', closeAllPools);
|
|
22
|
+
process.on('SIGINT', closeAllPools);
|
|
23
|
+
async function getPool(dbInfo) {
|
|
24
|
+
const key = typeof dbInfo === 'string' ? dbInfo : JSON.stringify(dbInfo);
|
|
25
|
+
let pool = pools.get(key);
|
|
26
|
+
if (!pool || !pool.connected) {
|
|
27
|
+
pool = await new sql.ConnectionPool(dbInfo).connect();
|
|
28
|
+
pools.set(key, pool);
|
|
29
|
+
}
|
|
30
|
+
return pool;
|
|
31
|
+
}
|
|
32
|
+
const SQL_USER_ERROR_NUMBER = 50_000;
|
|
33
|
+
async function execStoredProcedure(spName, params = [], dbInfo) {
|
|
34
|
+
try {
|
|
35
|
+
const pool = await getPool(dbInfo);
|
|
36
|
+
const request = pool.request();
|
|
37
|
+
for (const p of params)
|
|
38
|
+
request.input(p.name, p.value);
|
|
39
|
+
const result = await request.execute(spName);
|
|
40
|
+
return ok(result.recordset);
|
|
41
|
+
}
|
|
42
|
+
catch (err_) {
|
|
43
|
+
const sqlErr = err_;
|
|
44
|
+
if (sqlErr.number === SQL_USER_ERROR_NUMBER) {
|
|
45
|
+
// RAISERROR del SP — error de negocio, seguro mostrar al usuario
|
|
46
|
+
return err(appError({ title: sqlErr.message ?? String(err_), code: 'VALIDATION_ERROR' }));
|
|
47
|
+
}
|
|
48
|
+
// Error de sistema — loguear server-side, no exponer detalle al cliente
|
|
49
|
+
console.error('[db] execStoredProcedure error:', String(err_));
|
|
50
|
+
return err(appError({
|
|
51
|
+
title: 'Error interno del servidor',
|
|
52
|
+
code: 'INTERNAL_ERROR',
|
|
53
|
+
detail: String(err_),
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function execXml(dbInfo, objeto, operacion, xmlParameters, archivoParams = [], archivos = []) {
|
|
58
|
+
if (archivos.length > archivoParams.length) {
|
|
59
|
+
return err(appError({
|
|
60
|
+
title: `El objeto ${objeto} admite hasta ${archivoParams.length} archivo(s).`,
|
|
61
|
+
code: 'BAD_REQUEST',
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
const xmlIn = '<?xml version="1.0" encoding="iso-8859-1"?>\r\n' + xmlParameters;
|
|
65
|
+
const fileParams = archivos.map((buffer, index) => ({
|
|
66
|
+
name: archivoParams[index],
|
|
67
|
+
type: sql.VarBinary(sql.MAX),
|
|
68
|
+
value: buffer,
|
|
69
|
+
}));
|
|
70
|
+
return await execStoredProcedure(`xml${objeto}_${operacion}`, [{ name: 'xml', type: sql.VarChar, value: xmlIn }, ...fileParams], dbInfo);
|
|
71
|
+
}
|
|
72
|
+
// dbInfo/archivoParams van directo, no una Aplicacion completa — el adapter siempre opera sobre
|
|
73
|
+
// la única app que tiene configurada (ver docs/proximos-pasos.md), así que quien llama ya tiene
|
|
74
|
+
// esos dos datos a mano (app.dbInfo, getEndpoint(app, objeto)?.archivoParams) sin que db.ts necesite
|
|
75
|
+
// conocer la forma de Aplicacion para nada más que eso.
|
|
76
|
+
export async function execXmlSentencia(dbInfo, objeto, sentencia, archivoParams = [], archivos = []) {
|
|
77
|
+
// El resto del envelope (Operador, Clase, AplicacionCodigo, ServicioCodigo) se descarta acá
|
|
78
|
+
// — ver el comentario sobre XmlSentencia en xml.ts.
|
|
79
|
+
const xmlParameters = toXmlParametros(sentencia.Parametro, 'Parametro');
|
|
80
|
+
const result = await execXml(dbInfo, objeto, sentencia.Tipo, xmlParameters, archivoParams, archivos);
|
|
81
|
+
if (!result.ok)
|
|
82
|
+
return result;
|
|
83
|
+
// Bug de convenios_web portado y corregido acá: el original solo hexificaba `Stamp` en
|
|
84
|
+
// execXmlConsulta — un Alta/Modificacion devolvía la columna como Buffer crudo (serializa
|
|
85
|
+
// como `{type:"Buffer",data:[...]}` en JSON), inconsistente con lo que devuelve el GET
|
|
86
|
+
// inmediatamente después para la misma fila.
|
|
87
|
+
return ok(postProcessRows(result.data));
|
|
88
|
+
}
|
|
89
|
+
export async function execXmlConsulta(dbInfo, objeto, sentencia) {
|
|
90
|
+
const countParametro = {
|
|
91
|
+
...sentencia.Parametro,
|
|
92
|
+
Campos: 'Count(*) as count',
|
|
93
|
+
Orden: 'count',
|
|
94
|
+
SqlPaginate: 0,
|
|
95
|
+
};
|
|
96
|
+
const xmlParameters = toXmlParametros(sentencia.Parametro, 'Parametro');
|
|
97
|
+
const xmlCountParameters = toXmlParametros(countParametro, 'Parametro');
|
|
98
|
+
const [resultRows, resultCountData] = await Promise.all([
|
|
99
|
+
execXml(dbInfo, objeto, 'Consulta', xmlParameters),
|
|
100
|
+
execXml(dbInfo, objeto, 'Consulta', xmlCountParameters),
|
|
101
|
+
]);
|
|
102
|
+
if (!resultRows.ok)
|
|
103
|
+
return resultRows;
|
|
104
|
+
if (!resultCountData.ok)
|
|
105
|
+
return resultCountData;
|
|
106
|
+
const [rows, countData] = [resultRows.data, resultCountData.data];
|
|
107
|
+
// El SP de paginación COM+ (a diferencia de la SQL) devuelve Pagina + PaginaLong + 1 filas
|
|
108
|
+
// desde el principio — hay que recortar el offset y la fila de "hay más" acá.
|
|
109
|
+
if (rows &&
|
|
110
|
+
sentencia.Parametro.SqlPaginate === 0 &&
|
|
111
|
+
sentencia.Parametro.PaginaLong &&
|
|
112
|
+
sentencia.Parametro.Pagina) {
|
|
113
|
+
rows.splice(0, sentencia.Parametro.PaginaLong * (sentencia.Parametro.Pagina - 1));
|
|
114
|
+
if (rows.length > sentencia.Parametro.PaginaLong)
|
|
115
|
+
rows.pop();
|
|
116
|
+
}
|
|
117
|
+
const count = countData?.length > 0 ? countData[0].count : 0;
|
|
118
|
+
return ok({ rows: postProcessRows(rows), count });
|
|
119
|
+
}
|
|
120
|
+
// El tipo SQL Server `rowversion`/`timestamp` (columna `Stamp`, usada para optimistic
|
|
121
|
+
// concurrency) llega como Buffer — se hexifica para que viaje bien en JSON.
|
|
122
|
+
export function postProcessRows(rows) {
|
|
123
|
+
// Un SP sin SELECT final (ej. xmlXXX_Baja: solo DELETE) deja `result.recordset` en
|
|
124
|
+
// `undefined` — execStoredProcedure lo tipa igual como Array<T> (ver más arriba), pero acá
|
|
125
|
+
// no hay filas que recorrer, no un error.
|
|
126
|
+
if (!Array.isArray(rows))
|
|
127
|
+
return rows;
|
|
128
|
+
for (const row of rows) {
|
|
129
|
+
if (typeof row === 'object' && !!row && 'Stamp' in row) {
|
|
130
|
+
row.Stamp = '0x' + row.Stamp.toString('hex');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return rows;
|
|
134
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function endpoints(items) {
|
|
2
|
+
return items.map((item) => {
|
|
3
|
+
if (typeof item === 'string') {
|
|
4
|
+
return { name: item, recurso: item, fieldId: `${item}Id`, archivoParams: [] };
|
|
5
|
+
}
|
|
6
|
+
const recurso = item.recurso || item.name;
|
|
7
|
+
return {
|
|
8
|
+
name: item.name,
|
|
9
|
+
recurso,
|
|
10
|
+
// fieldId por default sale de `recurso` (no de `name`): si el endpoint ya cambió el
|
|
11
|
+
// nombre de recurso, el id por convención acompaña al recurso, no al nombre expuesto.
|
|
12
|
+
fieldId: item.fieldId || `${recurso}Id`,
|
|
13
|
+
acciones: item.acciones,
|
|
14
|
+
archivoParams: item.archivoParams || [],
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type HTTP_STATUS } from './http-status.js';
|
|
2
|
+
export declare const APP_ERROR_CODES: readonly ["VALIDATION_ERROR", "BAD_REQUEST", "UNAUTHORIZED", "FORBIDDEN", "NOT_FOUND", "INTERNAL_ERROR"];
|
|
3
|
+
export type AppErrorCode = (typeof APP_ERROR_CODES)[number];
|
|
4
|
+
export type AppError = {
|
|
5
|
+
code: AppErrorCode;
|
|
6
|
+
title: string;
|
|
7
|
+
detail?: string;
|
|
8
|
+
status: HTTP_STATUS;
|
|
9
|
+
};
|
|
10
|
+
type AppErrorOptions = {
|
|
11
|
+
code?: AppErrorCode;
|
|
12
|
+
title?: string;
|
|
13
|
+
detail?: string;
|
|
14
|
+
status?: HTTP_STATUS;
|
|
15
|
+
};
|
|
16
|
+
export declare function appError(options?: AppErrorOptions | string, code?: AppErrorCode): AppError;
|
|
17
|
+
export declare function jsonError(...params: Parameters<typeof appError>): Response;
|
|
18
|
+
export {};
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Error de negocio estructurado y homogéneo para toda la librería. Se arma con `appError(...)`
|
|
2
|
+
// (título + código + detalle opcional) y se devuelve al cliente con `jsonError(...)`, que lo
|
|
3
|
+
// envuelve en un `Response` JSON con el status HTTP que corresponde al código.
|
|
4
|
+
import { json } from '@sveltejs/kit';
|
|
5
|
+
import { BAD_REQUEST, FORBIDDEN, INTERNAL_ERROR, NOT_FOUND, UNAUTHORIZED, UNPROCESSABLE_ENTITY, } from './http-status.js';
|
|
6
|
+
export const APP_ERROR_CODES = [
|
|
7
|
+
'VALIDATION_ERROR',
|
|
8
|
+
'BAD_REQUEST',
|
|
9
|
+
'UNAUTHORIZED',
|
|
10
|
+
'FORBIDDEN',
|
|
11
|
+
'NOT_FOUND',
|
|
12
|
+
'INTERNAL_ERROR',
|
|
13
|
+
];
|
|
14
|
+
function defaultStatus(code) {
|
|
15
|
+
switch (code) {
|
|
16
|
+
case 'VALIDATION_ERROR':
|
|
17
|
+
return UNPROCESSABLE_ENTITY;
|
|
18
|
+
case 'BAD_REQUEST':
|
|
19
|
+
return BAD_REQUEST;
|
|
20
|
+
case 'UNAUTHORIZED':
|
|
21
|
+
return UNAUTHORIZED;
|
|
22
|
+
case 'FORBIDDEN':
|
|
23
|
+
return FORBIDDEN;
|
|
24
|
+
case 'NOT_FOUND':
|
|
25
|
+
return NOT_FOUND;
|
|
26
|
+
case 'INTERNAL_ERROR':
|
|
27
|
+
return INTERNAL_ERROR;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function appError(options = {}, code) {
|
|
31
|
+
if (typeof options === 'string')
|
|
32
|
+
options = { title: options };
|
|
33
|
+
if (code)
|
|
34
|
+
options = { ...options, code };
|
|
35
|
+
const { title = 'Error desconocido', detail, status } = options;
|
|
36
|
+
const finalCode = options.code || 'INTERNAL_ERROR';
|
|
37
|
+
return { code: finalCode, title, detail, status: status ?? defaultStatus(finalCode) };
|
|
38
|
+
}
|
|
39
|
+
export function jsonError(...params) {
|
|
40
|
+
const e = appError(...params);
|
|
41
|
+
return json(e, { status: e.status });
|
|
42
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const OK = 200;
|
|
2
|
+
export declare const BAD_REQUEST = 400;
|
|
3
|
+
export declare const UNAUTHORIZED = 401;
|
|
4
|
+
export declare const FORBIDDEN = 403;
|
|
5
|
+
export declare const NOT_FOUND = 404;
|
|
6
|
+
export declare const UNPROCESSABLE_ENTITY = 422;
|
|
7
|
+
export declare const INTERNAL_ERROR = 500;
|
|
8
|
+
export type HTTP_STATUS = typeof OK | typeof BAD_REQUEST | typeof UNAUTHORIZED | typeof FORBIDDEN | typeof NOT_FOUND | typeof UNPROCESSABLE_ENTITY | typeof INTERNAL_ERROR;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createXmlRestAdapter } from './create-adapter.js';
|
|
2
|
+
export { endpoints, type PartialEndpoint } from './endpoints.js';
|
|
3
|
+
export { getDomainUser } from './auth.js';
|
|
4
|
+
export { appError, jsonError, type AppError, type AppErrorCode } from './errors.js';
|
|
5
|
+
export { closeAllPools } from './db.js';
|
|
6
|
+
export type { Result } from './result.js';
|
|
7
|
+
export type { Accion, Aplicacion, Authentication, Authorization, Endpoint, Permisos, Usuario, } from './types.js';
|
|
8
|
+
export type { DbInfo } from './db.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Forma pública de la librería — lo único que un consumidor necesita importar. Ver
|
|
2
|
+
// docs/proximos-pasos.md ("API pública") para el porqué de cada decisión de diseño.
|
|
3
|
+
//
|
|
4
|
+
// Uso típico:
|
|
5
|
+
//
|
|
6
|
+
// const adapter = createXmlRestAdapter({ name: 'MiApp', ..., endpoints: endpoints([...]) });
|
|
7
|
+
//
|
|
8
|
+
// export const fallback: RequestHandler = async (event) => {
|
|
9
|
+
// const result = await adapter.resolveUser(event.request);
|
|
10
|
+
// if (!result.ok) return jsonError(result.error);
|
|
11
|
+
// return adapter.handle(event, result.data);
|
|
12
|
+
// };
|
|
13
|
+
export { createXmlRestAdapter } from './create-adapter.js';
|
|
14
|
+
export { endpoints } from './endpoints.js';
|
|
15
|
+
// Identidad: getDomainUser es el único helper que un consumidor necesita para armar su propia
|
|
16
|
+
// ruta de prueba/dev con impersonation vía `?as=` (ver src/routes/fixture/[...path]/+server.ts)
|
|
17
|
+
// — a propósito NO es un feature de resolveUser (sería impersonation de fábrica).
|
|
18
|
+
export { getDomainUser } from './auth.js';
|
|
19
|
+
// Errores: mismo vocabulario que usa la librería puertas adentro, para que un consumidor pueda
|
|
20
|
+
// devolver un error propio (antes de llegar a `handle`, por ejemplo) con la misma forma.
|
|
21
|
+
export { appError, jsonError } from './errors.js';
|
|
22
|
+
// Apagado prolijo de los pools de SQL Server — útil para tests de integración del consumidor o
|
|
23
|
+
// scripts que necesiten cerrar la conexión explícitamente (la librería ya lo hace sola en
|
|
24
|
+
// SIGTERM/SIGINT, ver db.ts).
|
|
25
|
+
export { closeAllPools } from './db.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type AppError } from './errors.js';
|
|
2
|
+
import { type Result } from './result.js';
|
|
3
|
+
import type { Aplicacion, Usuario } from './types.js';
|
|
4
|
+
import type { XmlSentencia } from './xml.js';
|
|
5
|
+
export type ParsedRequest = {
|
|
6
|
+
objeto: string;
|
|
7
|
+
id: number;
|
|
8
|
+
sentencia: XmlSentencia;
|
|
9
|
+
archivos: Buffer[];
|
|
10
|
+
archivoParams: string[];
|
|
11
|
+
};
|
|
12
|
+
export declare function parseRequest(app: Aplicacion, request: Request, path: string, usuario: Usuario): Promise<Result<ParsedRequest, AppError>>;
|