@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.
@@ -0,0 +1,80 @@
1
+ // Arma un XmlSentencia completo a partir de una request HTTP + el objeto/id ya resuelto del
2
+ // `[...path]` — la contraparte de http/request-body.ts en convenios_web, más la orquestación que
3
+ // ahí vivía repartida entre los 4 handlers (GET/POST/PUT/DELETE) de cada `+server.ts`: validar
4
+ // params, chequear permisos, parsear el body, y armar la sentencia según el método. No ejecuta
5
+ // nada contra la base — deja el XmlSentencia listo para pasarle a execXmlSentencia/
6
+ // execXmlConsulta.
7
+ import { appError } from './errors.js';
8
+ import { toXmlAlta, toXmlBaja, toXmlConsulta, toXmlConsultaId, toXmlModificacion, } from './request-to-xml.js';
9
+ import { err, ok } from './result.js';
10
+ import { checkPermiso, validParams } from './valid.js';
11
+ const MULTIPART_CONTENT_TYPE = 'multipart/form-data';
12
+ // Los browsers agregan un File vacío (size 0, name '') cuando un <input type="file"> queda sin
13
+ // seleccionar — se descarta para no numerarlo como @fileN.
14
+ function esArchivoFantasma(file) {
15
+ return file.size === 0 && file.name === '';
16
+ }
17
+ async function parseMultipart(request) {
18
+ const formData = await request.formData();
19
+ const record = {};
20
+ const archivos = [];
21
+ for (const [key, value] of formData.entries()) {
22
+ if (value instanceof File) {
23
+ if (!esArchivoFantasma(value))
24
+ archivos.push(Buffer.from(await value.arrayBuffer()));
25
+ }
26
+ else {
27
+ record[key] = value;
28
+ }
29
+ }
30
+ return { record, archivos };
31
+ }
32
+ async function parseRequestBody(request) {
33
+ const contentType = request.headers.get('content-type') || '';
34
+ if (contentType.includes(MULTIPART_CONTENT_TYPE))
35
+ return parseMultipart(request);
36
+ const record = (await request.json());
37
+ return { record, archivos: [] };
38
+ }
39
+ const METODOS_SOPORTADOS = new Set(['GET', 'POST', 'PUT', 'DELETE']);
40
+ export async function parseRequest(app, request, path, usuario) {
41
+ if (!METODOS_SOPORTADOS.has(request.method)) {
42
+ return err(appError(`Método ${request.method} no soportado`, 'BAD_REQUEST'));
43
+ }
44
+ const metodo = request.method;
45
+ const paramsResult = validParams(app, path, metodo === 'PUT' || metodo === 'DELETE' ? 'withId' : 'noId');
46
+ if (!paramsResult.ok)
47
+ return paramsResult;
48
+ const { objeto, id, fieldId, archivoParams } = paramsResult.data;
49
+ // checkPermiso se llama siempre, sin importar authorization: la Capa 1 (acciones permitidas
50
+ // por objeto) es estática y tiene que regir incluso en apps sin permisos dinámicos — es
51
+ // checkPermiso quien decide puertas adentro si además corresponde la Capa 2 (ver valid.ts).
52
+ const permisoResult = checkPermiso(app, objeto, metodo, usuario.permisos);
53
+ if (!permisoResult.ok)
54
+ return permisoResult;
55
+ const operador = usuario.domainUser;
56
+ const url = new URL(request.url);
57
+ if (metodo === 'GET') {
58
+ const sentencia = id
59
+ ? toXmlConsultaId(url.searchParams, app, objeto, id, operador, fieldId)
60
+ : toXmlConsulta(url.searchParams, app, objeto, operador);
61
+ return ok({ objeto, id, sentencia, archivos: [], archivoParams });
62
+ }
63
+ if (metodo === 'DELETE') {
64
+ const sentencia = toXmlBaja(app, objeto, operador);
65
+ sentencia.Parametro[fieldId] = id;
66
+ return ok({ objeto, id, sentencia, archivos: [], archivoParams });
67
+ }
68
+ // POST / PUT — llevan body
69
+ const { record, archivos } = await parseRequestBody(request);
70
+ record.OperadorId = String(usuario.usuarioId);
71
+ if (metodo === 'POST') {
72
+ const sentencia = toXmlAlta(app, objeto, operador, record);
73
+ return ok({ objeto, id, sentencia, archivos, archivoParams });
74
+ }
75
+ // PUT: el id de la URL siempre gana sobre lo que venga en el body, aunque el body traiga
76
+ // otro valor (o ninguno) para ese campo.
77
+ const sentencia = toXmlModificacion(app, objeto, operador, record);
78
+ sentencia.Parametro[fieldId] = id;
79
+ return ok({ objeto, id, sentencia, archivos, archivoParams });
80
+ }
@@ -0,0 +1,8 @@
1
+ import { type Aplicacion } from './types.js';
2
+ import { type XmlSentencia } from './xml.js';
3
+ export declare function toXmlConsulta(urlSearchParams: URLSearchParams, app: Aplicacion, objeto: string, operador: string): XmlSentencia;
4
+ export declare function toXmlConsultaId(urlSearchParams: URLSearchParams, app: Aplicacion, objeto: string, id: number, username: string, fieldId?: string): XmlSentencia;
5
+ export declare function toXmlAlta(app: Aplicacion, objeto: string, username: string, data: Record<string, string>): XmlSentencia;
6
+ export declare function toXmlModificacion(app: Aplicacion, objeto: string, username: string, data: Record<string, string>): XmlSentencia;
7
+ export declare function toXmlBaja(app: Aplicacion, objeto: string, username: string, data?: Record<string, string>): XmlSentencia;
8
+ export declare function toXmlParametros(condicion: Record<string, unknown>, Parametro: string): string;
@@ -0,0 +1,219 @@
1
+ // Arma un `XmlSentencia` a partir de una request HTTP y serializa su `Parametro` al XML que
2
+ // esperan los SPs xmlXXX. `toXmlConsulta`/`toXmlConsultaId` parsean filtros de
3
+ // `URLSearchParams` — el operador va embebido en el propio valor (`=`, `!=`, `*valor*`,
4
+ // `min..max`, `v1;v2`, etc., ver condition.ts); `toXmlAlta`/`toXmlModificacion`/`toXmlBaja` arman
5
+ // la sentencia a partir del body ya parseado. `toXmlParametros` serializa el resultado final
6
+ // (`Parametro`/`Condiciones`) al string XML que espera el SP — lo consume db.ts.
7
+ import { condition, getXmlConditionValue, } from './condition.js';
8
+ import {} from './types.js';
9
+ import { xmlAlta, xmlBaja, xmlConsulta, xmlModificacion } from './xml.js';
10
+ const QUERY_KEYS = {
11
+ metodo: 'metodo',
12
+ servicio: 'servicio',
13
+ longitudPagina: '_limit',
14
+ pagina: '_page',
15
+ campos: '_fields',
16
+ orden: '_order',
17
+ };
18
+ export function toXmlConsulta(urlSearchParams, app, objeto, operador) {
19
+ const metodoDefault = 'Consulta';
20
+ const metodo = urlSearchParams.get(QUERY_KEYS.metodo) || metodoDefault;
21
+ const servicio = urlSearchParams.get(QUERY_KEYS.servicio) || undefined;
22
+ const longitudPagina = urlSearchParams.get(QUERY_KEYS.longitudPagina);
23
+ const pagina = urlSearchParams.get(QUERY_KEYS.pagina);
24
+ const campos = urlSearchParams.get(QUERY_KEYS.campos);
25
+ const orden = urlSearchParams.get(QUERY_KEYS.orden);
26
+ return xmlConsulta({
27
+ Operador: operador,
28
+ Metodo: metodo,
29
+ AplicacionCodigo: app.name,
30
+ ServicioCodigo: servicio,
31
+ Parametro: {
32
+ Tabla: objeto,
33
+ // El '0' adelante evita paginación negativa: parseInt corta en el primer caracter no
34
+ // numérico, así que '0' + '-5' da 0 en vez de -5 — Number('0-5') da NaN, por eso tiene
35
+ // que ser parseInt y no la conversión que sugiere unicorn/prefer-number-coercion.
36
+ // eslint-disable-next-line unicorn/prefer-number-coercion
37
+ PaginaLong: longitudPagina ? Number.parseInt('0' + longitudPagina, 10) : undefined,
38
+ // eslint-disable-next-line unicorn/prefer-number-coercion
39
+ Pagina: pagina ? Number.parseInt('0' + pagina, 10) : undefined,
40
+ SqlPaginate: app.pagination === 'sql' ? 1 : 0,
41
+ Campos: campos || '*',
42
+ Orden: orden ? getXmlOrden(orden) : undefined,
43
+ Condiciones: keysToCondition(urlSearchParams),
44
+ },
45
+ });
46
+ }
47
+ export function toXmlConsultaId(urlSearchParams, app, objeto, id, username, fieldId) {
48
+ fieldId ||= objeto + 'Id';
49
+ return xmlConsulta({
50
+ Operador: username,
51
+ Metodo: urlSearchParams.get(QUERY_KEYS.metodo) || 'Consulta',
52
+ AplicacionCodigo: app.name,
53
+ ServicioCodigo: urlSearchParams.get(QUERY_KEYS.servicio) || undefined,
54
+ Parametro: {
55
+ Tabla: objeto,
56
+ PaginaLong: 20,
57
+ Pagina: 1,
58
+ Campos: '*',
59
+ Orden: fieldId,
60
+ Condiciones: condition.eq(fieldId, id),
61
+ },
62
+ });
63
+ }
64
+ export function toXmlAlta(app, objeto, username, data) {
65
+ return xmlAlta({
66
+ Operador: username,
67
+ AplicacionCodigo: app.name,
68
+ ServicioCodigo: app.name,
69
+ Parametro: { Tabla: objeto, ...data },
70
+ });
71
+ }
72
+ export function toXmlModificacion(app, objeto, username, data) {
73
+ return xmlModificacion({
74
+ Operador: username,
75
+ AplicacionCodigo: app.name,
76
+ ServicioCodigo: app.name,
77
+ Parametro: { Tabla: objeto, ...data },
78
+ });
79
+ }
80
+ export function toXmlBaja(app, objeto, username, data = {}) {
81
+ return xmlBaja({
82
+ Operador: username,
83
+ AplicacionCodigo: app.name,
84
+ ServicioCodigo: app.name,
85
+ Parametro: { Tabla: objeto, ...data },
86
+ });
87
+ }
88
+ const getXmlOrden = (orden) => orden
89
+ .split(',')
90
+ .map((field) => field.trim())
91
+ .map((field) => {
92
+ if (field.startsWith('-'))
93
+ return field.slice(1) + ' DESC';
94
+ if (field.startsWith('+'))
95
+ return field.slice(1);
96
+ return field;
97
+ })
98
+ .join(',');
99
+ const keysToCondition = (params) => {
100
+ const cond = [];
101
+ for (const [key, value] of params) {
102
+ if (!key.startsWith('_')) {
103
+ cond.push(getCondition(key, value));
104
+ }
105
+ }
106
+ if (cond.length === 0)
107
+ return {};
108
+ if (cond.length === 1)
109
+ return cond[0];
110
+ return { AND: cond };
111
+ };
112
+ const getCondition = (key, value) => {
113
+ if (value.startsWith('*') && value.endsWith('*')) {
114
+ // value === '*' (un solo caracter): substring(1, 0) hace swap de argumentos y
115
+ // devuelve '*'; slice(1, 0) devolvería '' — Math.min/Math.max replican el swap
116
+ // de substring para no cambiar el comportamiento en ese caso límite.
117
+ return condition.contains(key, value.slice(Math.min(1, value.length - 1), Math.max(1, value.length - 1)));
118
+ }
119
+ if (value.startsWith('*'))
120
+ return condition.endsWith(key, value.slice(1));
121
+ if (value.endsWith('*'))
122
+ return condition.beginWith(key, value.slice(0, Math.max(0, value.length - 1)));
123
+ if (value.includes('..')) {
124
+ const [left, right] = value.split('..');
125
+ return condition.between(key, left, right);
126
+ }
127
+ if (value.startsWith('!=') || value.startsWith('<>'))
128
+ return condition.neq(key, value.slice(2));
129
+ if (value.startsWith('<='))
130
+ return condition.lte(key, value.slice(2));
131
+ if (value.startsWith('<'))
132
+ return condition.lt(key, value.slice(1));
133
+ if (value.startsWith('>='))
134
+ return condition.gte(key, value.slice(2));
135
+ if (value.startsWith('>'))
136
+ return condition.gt(key, value.slice(1));
137
+ if (value.includes(';')) {
138
+ const pos = value.startsWith('=;') ? 2 : value.startsWith('=') || value.startsWith(';') ? 1 : 0;
139
+ return condition.in(key, value.slice(Math.max(0, pos)));
140
+ }
141
+ if (value.startsWith('='))
142
+ return condition.eq(key, value.slice(1));
143
+ return condition.beginWith(key, value);
144
+ };
145
+ export function toXmlParametros(condicion, Parametro) {
146
+ let xml = `<${Parametro}>\n`;
147
+ let level = 0;
148
+ const processField = (fieldName, operator, value) => {
149
+ const tag = substringAfterLast(fieldName, '.');
150
+ const nodeContent = fieldName.startsWith('Condiciones.')
151
+ ? getXmlConditionValue(operator, value)
152
+ : operator === '='
153
+ ? formatValueForXml(value)
154
+ : `${operator}${formatValueForXml(value)}`;
155
+ xml += `${spaces((level + 1) * 2)}<${escapeXmlTag(tag)}>${escapeXmlContent(nodeContent)}</${escapeXmlTag(tag)}>\n`;
156
+ };
157
+ const traverseObject = (obj, path = '') => {
158
+ for (const key in obj) {
159
+ let currentPath = path ? `${path}.${key}` : key;
160
+ const value = obj[key];
161
+ if (substringAfterLast(currentPath, '.') === 'AND') {
162
+ const arr = value;
163
+ for (const [index, element] of arr.entries()) {
164
+ currentPath = `${path}.${key}_${index}`;
165
+ traverseObject(element, currentPath);
166
+ }
167
+ }
168
+ else if (value &&
169
+ typeof value === 'object' &&
170
+ !Array.isArray(value) &&
171
+ 'operator' in value) {
172
+ // Condición con operador explícito
173
+ const typed = value;
174
+ processField(currentPath, typed.operator, typed.value);
175
+ }
176
+ else if (value && typeof value === 'object' && !Array.isArray(value)) {
177
+ // Objeto anidado
178
+ level++;
179
+ xml += `${spaces(level * 2)}<${substringAfterLast(currentPath, '.')}>\n`;
180
+ traverseObject(value, currentPath);
181
+ xml += `${spaces(level * 2)}</${substringAfterLast(currentPath, '.')}>\n`;
182
+ level--;
183
+ }
184
+ else {
185
+ // Condición simple (igualdad implícita)
186
+ processField(currentPath, '=', value);
187
+ }
188
+ }
189
+ };
190
+ if (Array.isArray(condicion)) {
191
+ for (const item of condicion)
192
+ traverseObject(item);
193
+ }
194
+ else {
195
+ traverseObject(condicion);
196
+ }
197
+ xml += `</${Parametro}>`;
198
+ return xml;
199
+ }
200
+ function formatValueForXml(value) {
201
+ if (value === null || value === undefined)
202
+ return '';
203
+ if (Array.isArray(value))
204
+ return value.join(',');
205
+ if (typeof value === 'boolean')
206
+ return value ? 'true' : 'false';
207
+ return String(value);
208
+ }
209
+ function escapeXmlContent(content) {
210
+ return content.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
211
+ }
212
+ function escapeXmlTag(tag) {
213
+ return tag.replaceAll(/[^a-zA-Z0-9_.-]/g, '_');
214
+ }
215
+ const substringAfterLast = (text, char) => {
216
+ const lastIndex = text.lastIndexOf(char);
217
+ return lastIndex === -1 ? text : text.slice(lastIndex + 1);
218
+ };
219
+ const spaces = (n) => ' '.repeat(n);
@@ -0,0 +1,14 @@
1
+ type Ok<Data> = {
2
+ ok: true;
3
+ data: Data;
4
+ error: null;
5
+ };
6
+ type Err<Error> = {
7
+ ok: false;
8
+ data: null;
9
+ error: Error;
10
+ };
11
+ export type Result<Data, Error> = Ok<Data> | Err<Error>;
12
+ export declare function ok<Data>(data: Data): Ok<Data>;
13
+ export declare function err<Error>(error: Error): Err<Error>;
14
+ export {};
package/dist/result.js ADDED
@@ -0,0 +1,6 @@
1
+ export function ok(data) {
2
+ return { ok: true, data, error: null };
3
+ }
4
+ export function err(error) {
5
+ return { ok: false, data: null, error };
6
+ }
@@ -0,0 +1,45 @@
1
+ import type { DbInfo } from './db.js';
2
+ export type Accion = 'consulta' | 'alta' | 'baja' | 'modificacion';
3
+ export type Permisos = Record<string, Accion[]>;
4
+ export type Usuario = {
5
+ usuarioId: number;
6
+ codigo: string;
7
+ apellido: string;
8
+ nombres: string;
9
+ apellidoNombres: string;
10
+ activo: boolean;
11
+ grupos: string[];
12
+ permisos: Permisos;
13
+ domainUser: string;
14
+ };
15
+ export type Authentication = {
16
+ type: 'none';
17
+ } | {
18
+ type: 'iis-node';
19
+ };
20
+ export type Authorization = {
21
+ type: 'none';
22
+ } | {
23
+ type: 'metaSSC';
24
+ dbInfo: DbInfo | string;
25
+ serviceUser?: string;
26
+ };
27
+ export type Aplicacion = {
28
+ name: string;
29
+ aplicacionCodigo?: string;
30
+ servicioCodigo?: string;
31
+ authentication: Authentication;
32
+ authorization: Authorization;
33
+ dbInfo: DbInfo | string;
34
+ pagination: 'sql' | 'com+';
35
+ endpoints: Endpoint[];
36
+ };
37
+ type AccionOrAll = Accion | '*';
38
+ export type Endpoint = {
39
+ name: string;
40
+ recurso: string;
41
+ fieldId: string;
42
+ acciones?: AccionOrAll[];
43
+ archivoParams?: string[];
44
+ };
45
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import { type AppError } from './errors.js';
2
+ import { type Result } from './result.js';
3
+ import type { Accion, Aplicacion, Permisos } from './types.js';
4
+ export declare function validParams(app: Aplicacion, path: string, withId?: 'withId' | 'noId'): Result<{
5
+ objeto: string;
6
+ id: number;
7
+ fieldId: string;
8
+ archivoParams: string[];
9
+ }, AppError>;
10
+ export type Metodo = 'GET' | 'POST' | 'PUT' | 'DELETE';
11
+ export declare function checkPermiso(app: Aplicacion, objeto: string, metodo: Metodo, permisos: Permisos): Result<{
12
+ accion: Accion;
13
+ recurso?: string;
14
+ permisos?: Accion[];
15
+ }, AppError>;
package/dist/valid.js ADDED
@@ -0,0 +1,92 @@
1
+ // Parseo del `[...path]` de la request (objeto + id opcional) y chequeo de permisos por acción.
2
+ // A diferencia de convenios_web, acá no hay `aplicacion` que resolver: el adapter siempre opera
3
+ // sobre la única `Aplicacion` con la que se lo creó (ver docs/proximos-pasos.md), así que `app`
4
+ // llega ya resuelta — lo único variable en cada request es el objeto/id (el `[...path]`) y el
5
+ // método HTTP.
6
+ import { appError } from './errors.js';
7
+ import { err, ok } from './result.js';
8
+ // Único caller de getEndpoint en toda la librería — vive acá en vez de en types.ts porque ahí
9
+ // solo van formas de datos, no funciones.
10
+ //
11
+ // El segmento de URL puede venir en snake_case (`tipo_dato`) o ya en PascalCase (`TipoDato`,
12
+ // que es como se configura `Endpoint.name`) — se soportan ambos normalizando el segmento antes de
13
+ // comparar. Portado de `convenios_web` (`snakeToPascal` en `$lib/utils/string.ts`): si ya está en
14
+ // PascalCase queda igual (`TipoDato` → `TipoDato`), snake_case se junta y capitaliza cada parte
15
+ // (`tipo_dato` → `TipoDato`), y un segmento todo en mayúsculas no queda gritando (`PROVINCIA` →
16
+ // `Provincia`).
17
+ function toPascalCase(str) {
18
+ return str
19
+ .split('_')
20
+ .map((part) => {
21
+ const tail = part === part.toUpperCase() ? part.slice(1).toLowerCase() : part.slice(1);
22
+ return part.charAt(0).toUpperCase() + tail;
23
+ })
24
+ .join('');
25
+ }
26
+ const getEndpoint = (app, objeto) => app.endpoints.find((ep) => ep.name === toPascalCase(objeto));
27
+ export function validParams(app, path, withId = 'noId') {
28
+ const [objetoRaw, idRaw] = path.split('/');
29
+ if (!objetoRaw)
30
+ return err(appError('Objeto no especificado', 'BAD_REQUEST'));
31
+ // getEndpoint matchea case-insensitive (y snake_case) y devuelve el nombre canónico
32
+ // configurado — no hace falta normalizar el segmento de URL a mano, alcanza con el endpoint
33
+ // que realmente matcheó.
34
+ const endpointConfig = getEndpoint(app, objetoRaw);
35
+ if (!endpointConfig)
36
+ return err(appError(`Objeto ${objetoRaw} inválido`, 'BAD_REQUEST'));
37
+ // Dos chequeos independientes, no uno solo: si vino un segundo segmento en el path, tiene que
38
+ // ser un entero válido sin importar el método — un GET con id basura (`/Provincia/abc`) no
39
+ // puede degradar en silencio a "listar todo" solo porque `withId` (que exige el id) es 'noId'
40
+ // para GET. Bug real, encontrado recién probando contra un vite dev real (Playwright,
41
+ // errors.http.test.ts): `id` colapsaba "no vino idRaw" y "vino pero es inválido" al mismo 0,
42
+ // y noId nunca miraba ese 0 para GET. En convenios_web esto no podía pasar: GET-con-id y
43
+ // GET-sin-id eran rutas de SvelteKit distintas ([objeto]/+server.ts vs [objeto]/[id]/
44
+ // +server.ts) — acá, con un solo [...path], hay que recrear esa distinción a mano.
45
+ const idProvisto = idRaw !== undefined && idRaw !== '';
46
+ const id = idProvisto && isInteger(idRaw) ? Number(idRaw) : 0;
47
+ if (idProvisto && id === 0)
48
+ return err(appError(`Id ${id} inválido`, 'BAD_REQUEST'));
49
+ if (withId === 'withId' && !idProvisto)
50
+ return err(appError(`Id ${id} inválido`, 'BAD_REQUEST'));
51
+ return ok({
52
+ objeto: endpointConfig.name,
53
+ id,
54
+ fieldId: endpointConfig.fieldId,
55
+ archivoParams: endpointConfig.archivoParams || [],
56
+ });
57
+ }
58
+ const isInteger = (str) => /^-?\d+$/.test(str.trim());
59
+ const metodos = {
60
+ GET: 'consulta',
61
+ POST: 'alta',
62
+ PUT: 'modificacion',
63
+ DELETE: 'baja',
64
+ };
65
+ export function checkPermiso(app, objeto, metodo, permisos) {
66
+ const accion = metodos[metodo];
67
+ const endpointConfig = getEndpoint(app, objeto);
68
+ if (!endpointConfig)
69
+ return err(appError('Objeto inválido', 'BAD_REQUEST'));
70
+ // Capa 1: acciones permitidas por endpoint — estática, siempre activa.
71
+ if (endpointConfig.acciones &&
72
+ !endpointConfig.acciones.includes('*') &&
73
+ !endpointConfig.acciones.includes(accion)) {
74
+ return err(appError('Acceso denegado', 'FORBIDDEN'));
75
+ }
76
+ if (app.authorization.type === 'none')
77
+ return ok({ accion });
78
+ // Capa 2: permisos dinámicos contra Meta — búsqueda relajada de recurso (mayúsculas, sin
79
+ // espacios/guiones/"TABLAS "), no controlamos cómo se llaman los recursos ahí.
80
+ const recurso = endpointConfig.recurso;
81
+ const permisoKey = Object.keys(permisos).find((r) => rawRecurso(r) === rawRecurso(recurso));
82
+ if (!permisoKey || !permisos[permisoKey].includes(accion)) {
83
+ return err(appError('Acceso denegado', 'FORBIDDEN'));
84
+ }
85
+ return ok({ accion, recurso: permisoKey, permisos: permisos[permisoKey] });
86
+ }
87
+ function rawRecurso(recurso) {
88
+ return recurso
89
+ .toUpperCase()
90
+ .replace(/^TABLAS /, '')
91
+ .replaceAll(/[^A-Z0-9]/g, '');
92
+ }
package/dist/xml.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { Condition } from './condition.js';
2
+ export type XmlTipo = 'Consulta' | 'Alta' | 'Baja' | 'Modificacion';
3
+ export type XmlParametro = {
4
+ Tabla: string;
5
+ PaginaLong?: number;
6
+ Pagina?: number;
7
+ SqlPaginate?: number;
8
+ Campos?: string;
9
+ Orden?: string;
10
+ Condiciones?: Condition;
11
+ [key: string]: unknown;
12
+ };
13
+ export type XmlSentencia = {
14
+ Operador: string;
15
+ Clase: string;
16
+ Metodo: string;
17
+ Tipo: XmlTipo;
18
+ AplicacionCodigo: string;
19
+ ServicioCodigo: string;
20
+ Parametro: XmlParametro;
21
+ };
22
+ export declare function xmlConsulta(options: Partial<XmlSentencia>): XmlSentencia;
23
+ export declare const xmlAlta: (options: Partial<XmlSentencia>) => XmlSentencia;
24
+ export declare const xmlModificacion: (options: Partial<XmlSentencia>) => XmlSentencia;
25
+ export declare const xmlBaja: (options: Partial<XmlSentencia>) => XmlSentencia;
package/dist/xml.js ADDED
@@ -0,0 +1,36 @@
1
+ // Tipos y builders para armar el `XmlSentencia` que consumen execXmlSentencia/execXmlConsulta
2
+ // (ver db.ts) al invocar los SPs `xmlXXX_Alta|Baja|Modificacion|Consulta`. Cada builder devuelve
3
+ // una sentencia con los defaults de su tipo — `xmlConsulta` además completa los defaults de
4
+ // paginación de `Parametro`; los otros tres solo fijan `Metodo`/`Tipo` y clonan `Parametro` tal
5
+ // como venga (nunca comparten referencia con el `options.Parametro` de entrada).
6
+ function xmlInit(tipo, options) {
7
+ return {
8
+ Operador: options.Operador || 'ANONYMOUS',
9
+ Clase: 'Tabla',
10
+ Metodo: options.Metodo || tipo,
11
+ Tipo: tipo,
12
+ AplicacionCodigo: options.AplicacionCodigo || '',
13
+ ServicioCodigo: options.ServicioCodigo || '',
14
+ Parametro: { Tabla: '', ...options.Parametro },
15
+ };
16
+ }
17
+ export function xmlConsulta(options) {
18
+ return xmlInit('Consulta', {
19
+ ...options,
20
+ Parametro: {
21
+ Tabla: options.Parametro?.Tabla || '',
22
+ // `??`, no `||`: 0 es un valor válido y distinto de "no especificado" (ver
23
+ // request-to-xml.ts, que clampea paginación negativa a 0 — con `||` ese 0 quedaría
24
+ // pisado por el default y el clamp no serviría de nada).
25
+ PaginaLong: options.Parametro?.PaginaLong ?? 20,
26
+ Pagina: options.Parametro?.Pagina ?? 1,
27
+ SqlPaginate: options.Parametro?.SqlPaginate || 0,
28
+ Campos: options.Parametro?.Campos || '*',
29
+ Orden: options.Parametro?.Orden,
30
+ Condiciones: options.Parametro?.Condiciones,
31
+ },
32
+ });
33
+ }
34
+ export const xmlAlta = (options) => xmlInit('Alta', options);
35
+ export const xmlModificacion = (options) => xmlInit('Modificacion', options);
36
+ export const xmlBaja = (options) => xmlInit('Baja', options);
package/package.json ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "@opensas/xml-rest-adapter",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "volta": {
8
+ "node": "24.19.0",
9
+ "pnpm": "11.21.0",
10
+ "npm": "12.0.2"
11
+ },
12
+ "scripts": {
13
+ "dev": "vite dev",
14
+ "build": "vite build && npm run prepack",
15
+ "preview": "vite preview",
16
+ "prepare": "svelte-kit sync || echo ''",
17
+ "prepack": "svelte-kit sync && svelte-package && publint",
18
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
19
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
20
+ "check:all": "npm run format && eslint . --fix --cache && npm run check",
21
+ "format": "prettier --write --cache .",
22
+ "lint": "eslint . --cache",
23
+ "verify": "npm run format & eslint . --fix --cache & npm run check",
24
+ "up:safe": "pnpm --config.minimumReleaseAge=1440 up -i",
25
+ "test:unit": "vitest run --project=unit-tests",
26
+ "test:vite": "vitest run",
27
+ "test:api": "playwright test --project=api-tests --pass-with-no-tests",
28
+ "test:e2e": "playwright test --project=e2e-tests --pass-with-no-tests",
29
+ "test:play": "playwright test --pass-with-no-tests",
30
+ "test": "npm run test:vite && npm run test:play",
31
+ "test:ci": "tsx scripts/test-ci.ts",
32
+ "fixture:init": "tsx scripts/fixture-init.ts",
33
+ "fixture:purge": "docker compose -f docker/fixture/docker-compose.yaml down -v",
34
+ "fixture:start": "docker compose -f docker/fixture/docker-compose.yaml up -d",
35
+ "fixture:stop": "docker compose -f docker/fixture/docker-compose.yaml stop",
36
+ "fixture:status": "docker ps --filter name=fixture-db --format 'table {{.Names}}\\t{{.Status}}'",
37
+ "gitlab-runner:start": "sudo gitlab-runner start",
38
+ "gitlab-runner:stop": "sudo gitlab-runner stop",
39
+ "gitlab-runner:status": "sudo gitlab-runner status",
40
+ "gitlab-runner:logs": "sudo journalctl -u gitlab-runner -f"
41
+ },
42
+ "files": [
43
+ "dist",
44
+ "!dist/**/*.test.*",
45
+ "!dist/**/*.spec.*"
46
+ ],
47
+ "sideEffects": [
48
+ "**/*.css"
49
+ ],
50
+ "svelte": "./dist/index.js",
51
+ "types": "./dist/index.d.ts",
52
+ "type": "module",
53
+ "exports": {
54
+ ".": {
55
+ "types": "./dist/index.d.ts",
56
+ "svelte": "./dist/index.js",
57
+ "default": "./dist/index.js"
58
+ }
59
+ },
60
+ "peerDependencies": {
61
+ "@sveltejs/kit": "^2.0.0",
62
+ "svelte": "^5.0.0"
63
+ },
64
+ "devDependencies": {
65
+ "@eslint/js": "^10.0.1",
66
+ "@playwright/test": "^1.62.1",
67
+ "@sveltejs/adapter-auto": "^7.0.1",
68
+ "@sveltejs/kit": "^2.63.0",
69
+ "@sveltejs/package": "^2.5.8",
70
+ "@sveltejs/vite-plugin-svelte": "^7.1.2",
71
+ "@types/mssql": "^12.3.0",
72
+ "@types/node": "^24",
73
+ "dotenv": "^17.4.2",
74
+ "eslint": "^10.4.1",
75
+ "eslint-config-prettier": "^10.1.8",
76
+ "eslint-plugin-check-file": "^3.3.2",
77
+ "eslint-plugin-simple-import-sort": "^13.0.0",
78
+ "eslint-plugin-svelte": "^3.19.0",
79
+ "eslint-plugin-unicorn": "^71.1.0",
80
+ "globals": "^17.6.0",
81
+ "jiti": "^2.7.0",
82
+ "prettier": "^3.8.3",
83
+ "prettier-plugin-svelte": "^4.1.0",
84
+ "publint": "^0.3.21",
85
+ "svelte": "^5.56.1",
86
+ "svelte-check": "^4.6.0",
87
+ "tsx": "^4.23.12",
88
+ "typescript": "^6.0.3",
89
+ "typescript-eslint": "^8.60.1",
90
+ "vite": "^8.0.16",
91
+ "vitest": "^4.1.8"
92
+ },
93
+ "keywords": [
94
+ "svelte"
95
+ ],
96
+ "dependencies": {
97
+ "mssql": "^12.7.0"
98
+ }
99
+ }