@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 ADDED
@@ -0,0 +1,4 @@
1
+ # xml-rest-adapter
2
+
3
+ Proyecto _de uso interno_ que expone mediante una API REST las bases de datos que utilizan el
4
+ formato propio de `xmlEntidad_Accion` para encapsular la lógica de negocio.
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,3 @@
1
+ import type { Endpoint } from './types.js';
2
+ export type PartialEndpoint = Pick<Endpoint, 'name'> & Partial<Omit<Endpoint, 'name'>>;
3
+ export declare function endpoints(items: Array<string | PartialEndpoint>): Endpoint[];
@@ -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
+ }
@@ -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;
@@ -0,0 +1,7 @@
1
+ export const OK = 200;
2
+ export const BAD_REQUEST = 400;
3
+ export const UNAUTHORIZED = 401;
4
+ export const FORBIDDEN = 403;
5
+ export const NOT_FOUND = 404;
6
+ export const UNPROCESSABLE_ENTITY = 422;
7
+ export const INTERNAL_ERROR = 500;
@@ -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>>;