dgr-tributario-libraries
v1.0.0
Published
Lógica de negocio migrada de las librerías PL/SQL legacy de Oracle Forms de la DGR Corrientes: motor de metadata de formularios, validaciones y catálogo de mensajes.
Maintainers
Readme
dgr-tributario-libraries
Lógica de negocio migrada de las librerías PL/SQL legacy de Oracle Forms de la DGR
Corrientes (TCSLIB, TCSMSJ, TCSPRMTRZ): motor de metadata de formularios, validaciones,
catálogo de mensajes y aritmética de calendario.
Parte de la familia dgr-* publicada en npm. Cero dependencias de runtime.
Estado: scaffolding. El paquete todavía no exporta funciones migradas. El backlog vive como work items en GitLab: 24 issues de backend con 5 tasks cada una y sus relaciones de bloqueo. Ver el listado de issues.
Instalación
npm install dgr-tributario-librariesCero dependencias de runtime. El paquete no instala nada más.
Si el consumidor es TypeScript, necesita además los tipos de Oracle, porque las firmas del SDK
reciben una Connection:
npm install -D @types/oracledboracledb en sí lo aporta el consumidor —es quien abre la conexión—, y el SDK lo importa
sólo como tipo: no aparece en el bundle.
Cómo se consume
El SDK no abre ni gestiona conexiones: recibe por parámetro una Connection de Oracle ya
abierta por el consumidor. No lee process.env, no tiene defaults de infraestructura y no
guarda estado entre llamadas.
Igual que el resto de la familia dgr-*, no se importa directo desde el código de negocio: va
detrás de un wrapper en src/integrations/<nombre>/client.ts del BFF, que es quien lee el
entorno y decide qué hacer si falta configuración.
// src/integrations/tributario-libraries/client.ts (en el BFF)
import { traducirMensaje, DgrSdkError } from 'dgr-tributario-libraries';
import { getConnection } from '../../db/pool.js';
export async function traducir(codigo: string): Promise<string> {
const conn = await getConnection();
try {
return await traducirMensaje(conn, codigo);
} finally {
await conn.close();
}
}Modos de falla
Todo error del SDK es un DgrSdkError. Nunca un Error pelado ni un string.
import { DgrSdkError } from 'dgr-tributario-libraries';
try {
await traducirMensaje(conn, '0138');
} catch (e) {
if (e instanceof DgrSdkError) {
e.message; // "No se pudo leer el catálogo (ORA-00942)" — seguro para mostrar
e.code; // "ORA-00942" | undefined
e.cause; // el error original de oracledb, para loguear del lado del consumidor
}
}De un error de Oracle sólo sale el código, nunca el texto. Los mensajes de la base pueden
traer datos del contribuyente embebidos (nombres, CUIT, valores de columnas), así que el
message se arma con el contexto más el ORA-##### y nada más. El error crudo queda en
cause para que el consumidor lo loguee donde corresponda.
El nombre de cada función anticipa su modo de falla: si una traga errores en vez de tirar, lo dice en el nombre o en la primera línea del JSDoc.
Funciones
emitirMensaje(conn, input): Promise<MensajeResuelto>
Resuelve un código contra el catálogo TBL_MENSAJES, sustituye los parámetros posicionales
#1/#2 y devuelve qué mostrar y si la operación debe abortar. Es el único canal por el que el
sistema legacy le habla al operador: 24 de los 25 forms lo usan.
No lanza por razones de negocio. El único error que sale es un DgrSdkError de
infraestructura si falla la consulta. El patrón en el call site:
const m = await emitirMensaje(conn, { mensaje: 'SUM-00001', tipo: 'E', falla: true });
if (m.debeFallar) throw new DgrSdkError(textoSanitizado, undefined, m.codigo);| Entrada | Resultado |
| -------------------------------- | ------------------------------------------------------------ |
| SUM-00001 | texto del catálogo, con el prefijo SUM-00001: |
| ORA-20000: SUM-00001 | idéntico al anterior: se pela el RAISE_APPLICATION_ERROR |
| RCD-00011 #1LIQUIDACION#2ITEMS | texto con los #n sustituidos, truncado a 69 |
| PER-99999 (no existe) | Mensaje PER-99999 no se encuentra en la tabla de mensajes. |
| ORA-01722: invalid number | el texto crudo, en alerta |
| FRM-40350: ... | el texto crudo, en la barra de estado |
| hola mundo | passthrough, sin consultar la base |
debeFallar puede ser null. Para tipo W y D mostrados en alerta, la decisión depende
del botón que apriete el usuario, y eso el backend no lo ve. Cuando es null,
requiereRespuesta es true y el consumidor resuelve así:
| tipo | Botón | Qué hace el consumidor |
| ------ | ------------- | --------------------------------- |
| W | 1 (izquierdo) | continúa la operación |
| W | 2 o 3 | aborta |
| D | 1 | sigue con la respuesta afirmativa |
| D | 2 | sigue con la respuesta negativa |
| D | 3 o ninguno | aborta |
Tres comportamientos del legado que se replican y conviene conocer.
- Un mensaje suprimido nunca aborta, aunque se haya pedido
falla: true, y vuelve con los#nsin sustituir.- Un
WoDque va a la barra de estado aborta siempre, sin importarfalla. Es un bug deTCSmsj.pld:141: la comparación es contra' N'con un espacio, y la variable es de un solo carácter, así que nunca es falsa.- El truncado a 69 se aplica en cada sustitución, no al final: un mensaje con dos parámetros se corta dos veces y sale mutilado. Los mensajes sin parámetros conservan 500.
Reemplaza tres reimplementaciones a mano
at-sumarios-be/src/lib/mensajes.ts (getMensaje) y las copias hardcodeadas del catálogo en
at-sumarios-mf/src/features/dgrsuma000{1,2,3}/lib/mensajes.ts hacen una parte de esto. Migrar
no es un reemplazo drop-in: getMensaje devuelve MENSAJE_TEXTO pelado y emitirMensaje
devuelve CODIGO: MENSAJE_TEXTO, con el código adelante. El comentario de cabecera de esos
archivos dice que "el usuario NUNCA ve el código crudo"; contra el .pld eso es falso. Cambiar
sin mirarlo altera lo que ve el operador en 24 pantallas.
Tampoco hacen HELP_TEXTO — que hoy es NULL en las 2.318 filas del catálogo, así que el botón
de ayuda del legado es código muerto de facto.
getMessage2(msgno, param1?, param2?, param3?, param4?): string | null
Arma un mensaje del catálogo genérico con sus parámetros ya interpolados. Junta las otras dos
piezas: pide la plantilla a catalogoMensajesGenerador y la parte con splitString2.
getMessage2('4', 'el rubro', 'las DDJJ');
// 'No se puede actualizar el rubro mientras existan dependencias en las DDJJ'No lanza por razones de negocio. Devuelve null para un código desconocido sin parámetros.
Tres comportamientos del legado que conviene conocer antes de usarla.
param1ausente corta todo. Los cuatroifque cuentan parámetros son secuenciales, noelsif, así que manda el primero que falte:getMessage2('4', null, 'las DDJJ')devuelve la plantilla entera con sus%sy descarta'las DDJJ'en silencio.- Los parámetros que sobran se pegan sin separador. La concatenación final intercala los cuatro aunque el conteo sea menor:
getMessage2('3', 'TBL', 'COD')da'…en la tabla TBLCOD'.- Un código desconocido con parámetros devuelve los parámetros y pierde el texto.
getMessage2('lo que sea', 'A', 'B', 'C')da'ABC'.
'',nullyundefinedson el mismo valor —ausente—: en Oracle'' IS NULL, así que el legado no distingue "no lo pasé" de "lo pasé vacío".
catalogoMensajesGenerador(msgno): string | null
Resuelve un código numérico al texto castellano del catálogo de mensajes genéricos: los errores transversales que pueden aparecer en cualquier pantalla (no existe el registro, ya hay uno con la misma clave, falta el motivo de baja).
Es el complemento de emitirMensaje, que resuelve los mensajes específicos de cada trámite
contra TBL_MENSAJES. Los dos catálogos usan protocolos de parámetros distintos:
| | Catálogo genérico | Catálogo de trámites |
| --------------- | ---------------------------- | ------------------------------ |
| Función | catalogoMensajesGenerador | emitirMensaje |
| Dónde vive | en el código, 35 entradas | TBL_MENSAJES, 2.318 filas |
| Marcador | %s | #1, #2 |
| Quién sustituye | splitString2 + el llamador | emitirMensaje, con REPLACE |
catalogoMensajesGenerador('4');
// 'No se puede actualizar %s mientras existan dependencias en %s'
catalogoMensajesGenerador('999'); // null — un código desconocido no es errorNo lanza nunca. No interpola los %s: para eso está splitString2.
La comparación es exacta y sin normalizar:
'03',' 3 'y'3 'no matchean'3'. Y cuatro plantillas tienen espacios al borde que son parte del texto — quitarlos cambia lo que ve el operador cuando el llamador concatena.
También se exporta MENSAJES_GENERADOR, el objeto congelado, por si el consumidor necesita
recorrer el catálogo entero.
splitString2(msg, pcount): SegmentosMensaje
Parte una plantilla de mensaje por sus marcadores %s en hasta 4 tramos más la cola. Es la
pieza que usa getMessage2 para intercalar los parámetros: corta la plantilla y después
concatena tramo1 + param1 + tramo2 + param2 + … + cola.
splitString2('No se puede actualizar %s mientras existan dependencias en %s', 2);
// { antesDeParam1: 'No se puede actualizar ',
// antesDeParam2: ' mientras existan dependencias en ',
// antesDeParam3: '', antesDeParam4: '', resto: '' }Corta tantas veces como diga
pcount, aunque la plantilla tenga menos%s. Y cada corte de más descarta 2 caracteres reales, porque el puntero avanza igual sin haber encontrado el marcador. Una plantilla sin ningún%sconpcount: 1pierde sus dos primeros caracteres. Es el comportamiento del legado.La cuarta ranura se llena sólo con
pcountexactamente 4, no con 4 o más.
Lanza DgrSdkError con código MSG_MAYOR_A_200 si la plantilla supera los 200 caracteres,
replicando el VALUE_ERROR del varchar2(200) legacy. El mensaje del error reporta la longitud,
nunca el contenido.
traducirMensaje(mensaje: string): string
Traduce los mensajes que el runtime de Oracle Forms emite en inglés a los códigos internos del
catálogo de la DGR. Para los mensajes de integridad maestro-detalle extrae los nombres de las
entidades y los adjunta como parámetros posicionales #1 (maestro) y #2 (detalle).
No lanza nunca: lo que no matchea vuelve sin modificar. No resuelve el texto final — eso es responsabilidad del consumidor.
| Entrada (mensaje de Forms) | Salida |
| -------------------------------------------------------- | --------------------------------------- |
| Cannot delete LIQUIDACION while dependent ITEMS exist. | RCD-00011 #1LIQUIDACION#2ITEMS |
| Key not valid in this context | RCD-00012 |
| Query not allowed in this block | RCD-00013 |
| Query of ITEMS must be in the master LIQUIDACION | RCD-00014 #1LIQUIDACION#2ITEMS |
| Insert of ITEMS must be in the master LIQUIDACION | RCD-00015 #1LIQUIDACION#2ITEMS |
| ORA-01722: invalid number | ORA-01722: invalid number (sin tocar) |
Las dos ramas sin parámetros comparan por igualdad estricta: son sensibles a mayúsculas y no toleran espacios ni puntuación de más. Cualquier variante cae en el passthrough.
Cuidado al mostrarle la salida al operador. Cuando el mensaje arranca con
Cannot deletepero no contiene' while depen', el legado devuelve un fragmento del texto original a partir de los 24 caracteres — por ejemploCannot delete ORA-00001: unique constraint violateddaRCD-00011 #1#2A-00001: unique constraint v. Es el comportamiento del original y se replica; si el texto puede venir de Oracle, filtralo del lado del consumidor.
armoClave(conn, input): Promise<string | null>
Arma la clave primaria —o una de las tres claves únicas alternativas— de un registro del motor de
metadata: concatena las columnas que la metadata marca como parte de la clave, cada una rellenada
a LONGITUD + 1 con ceros a la izquierda, unidas con -.
const clave = await armoClave(conn, {
parametro: 4, // 4 = clave primaria, 5 = UK1, 6 = UK2; cualquier otro valor = UK3
cadenasInfo, // las 30 cadenas INFO01..INFO30 que escribió PROG_CONFIGURAR
valoresColumna, // los 30 valores COL01..COL30
});Con parametro: 4 y la clave vacía cae a la secuencia: SQ_CLAVE_DJ para el tipo de entidad D,
SQ_GRUPOS_INF_X_IMPONIBLE para G, y tira para cualquier otro. Con 5, 6 o 7 devuelve null
sin tocar la base.
El barrido corta por hueco — no lo reescribas con sort
Busca la columna cuyo orden vale exactamente j, la anexa, incrementa j y reinicia el barrido
desde INFO01. Si un barrido completo no encuentra j, corta.
Ordenes 1, 2, 4 producen una clave de dos segmentos: la columna con orden 4 no aparece
nunca. Ordenes 2, 3 producen null. Un sort incluiría las tres y devolvería otra clave — y
sobre datos ya grabados eso significa no poder volver a encontrar el registro.
El ejemplo verificado contra la base es la entidad G/0004, donde los ordenes 1, 2, 3, 4 caen en
las columnas físicas 08, 01, 07, 02, y la clave grabada respeta esa secuencia, no la numérica:
COL08 = '124186' (orden 1, LONGITUD 10) COL01 = '0002' (orden 2, LONGITUD 4)
COL07 = 'S' (orden 3, LONGITUD 2) COL02 = '1' (orden 4, LONGITUD 9)
→ 00000124186-00002-00S-0000000001Un valor
nullhace desaparecer su segmento sin dejar relleno, y el-se agrega antes de saber si el segmento va a existir. De ahí salen el guion colgante ('00X-') y la clave que arranca directamente por el segundo segmento. Dos registros distintos pueden colapsar en la misma clave. Es el comportamiento del original y se replica.
LPAD trunca. Un valor más largo que LONGITUD + 1 se corta a los primeros caracteres, no se
devuelve entero. Y LONGITUD ausente anula el segmento completo, porque en Oracle NULL + 1
es NULL.
insertarAutog(input): string | null
Calcula el dígito verificador de un valor tipeado, en una de dos variantes de módulo 11, o
devuelve el dato de pantalla que la rutina configurada pida. El nombre miente: no inserta nada y
no toca la base — la "inserción" la hace el llamador con un COPY sobre el ítem destino.
| rutina | Qué hace |
| -------- | ------------------------------------------------------------------------------- |
| 'DV1' | módulo 11 sobre 7 posiciones, factores 2,7,6,5,4,3,2. Resto 0 → 'A' |
| 'DV2' | módulo 11 sobre 6 posiciones, factores 7,6,5,4,3,2. Resto 0 → '1' |
| '0001' | ignora valor y devuelve cuitPantalla |
| '0002' | ignora valor y devuelve tipoAltaPantalla |
insertarAutog({ rutina: 'DV1', valor: '1234567' }); // '4'
insertarAutog({ rutina: 'DV1', valor: '0' }); // 'A' ← una letra
insertarAutog({ rutina: 'DV1', valor: '' }); // null ← no un dígito
DV1puede devolver la letra'A'. Por eso el retorno esstringy nonumber: tipar esto como número es un bug esperando. El llamador copia el resultado a unaCOLnnque la metadata puede tener declarada como numérica.
Un valor vacío o nulo devuelve null, no un dígito. En el legado el NULL se arrastra por toda
la cadena de TO_NUMBER y la función retorna NULL sin error. Si alguien "arregla" el padding
sobre '', obtiene '0000000' y inventa un 'A' para un campo vacío.
LPAD trunca por la derecha. '1234567' en DV2 conserva los primeros 6 caracteres: el
dígito que se pierde es el último. Un slice(-6) sería el error natural y da otro resultado.
DV2 no es inyectiva: el dígito '1' sale tanto del resto 0 como del resto 10 (11 - 10),
así que detecta menos errores de tipeo que un módulo 11 clásico. DV1 esquiva la colisión con la
'A'. Es del diseño original y se replica.
No hay paridad posible contra datos históricos, y no es por falta de infra. Medido contra la base: de las 2.517 filas de
TBL_DATOS_X_ENTIDADES, 2.516 no tienen rutina configurada y una sola tiene'0001'. NiDV1niDV2ni'0002'están en uso, así que no existe una sola columna con un dígito calculado por esta función contra la cual comparar. La corrección se fija con un diferencial contra Oracle como implementación de referencia —2.000 valores por corrida— y la tabla de ruteo queda versionada para que el test avise el día que eso cambie.
verificarUnicidad(input): ResultadoUnicidad
⚠️ Es un no-op heredado, y es intencional
No valida unicidad y nunca consulta
TBL_IMPONIBLES. La consulta está comentada en el legado desde hace años (TCSPRMTRZ.pld:2484-2530, 47 líneas dentro de un/* … */), así que devuelve siempre{ unico: true, error: null }. No es un pendiente de la migración: es el comportamiento vigente en producción, portado tal cual. La validación de unicidad no existe en el sistema legacy.
Lo único que sí hace es un chequeo de carga parcial: si entre las columnas que participan de
una clave única alternativa hay al menos una cargada y al menos una vacía, tira un DgrSdkError
enumerando sus etiquetas.
verificarUnicidad({ cual: 1, infoColumnas, valoresColumnas, valoresImpo, etiquetasImpo, ... });
// -> { unico: true, error: null }
// -> throw DgrSdkError('El ingreso de CUIT/CUIL/CDI y Establecimiento es obligatorio.')No abre conexión y no es async: el tablas_oracle: [TBL_IMPONIBLES] del análisis es una
trampa, porque la única sentencia SQL vive dentro del comentario.
valoresColumnas y valoresImpo son campos separados a propósito. El chequeo de carga
parcial lee impo.colNN hardcodeado (pld:2405), ignorando el bloque que le pasaron: en los
call sites de GRPINFXIMP y VGRPINFXIMP compara la metadata de grupos contra los datos del
imponible. Unificarlos cambiaría cuándo aborta el trigger en dos de los tres bloques.
Con cual: 0 el chequeo no se dispara (pld:2418), y la obligatoriedad no trimea mientras
el WHERE sí: un campo con un espacio cuenta como cargado en un lado y como vacío en el
otro. Son dos lecturas distintas del mismo dato; se replica.
Si alguna vez se reactiva la validación, no alcanza con descomentar: el WHERE del legado
indexa por orden en la clave en vez de por número de columna, y contra la metadata real eso
apunta a la columna equivocada en 45 de 49 filas con ORDEN_EN_UK1.
traerDescripcion(conn, input): Promise<TraerDescripcionResultado>
Resuelve la descripción legible de un código que el operador tiene cargado en una columna
genérica (COL01..COL30), según la consulta parametrizada que la metadata define para esa
columna.
const r = await traerDescripcion(conn, { infoColumna, codigo: '0001', resolver });
// -> { escribir: true, descripcion: 'Baldio' }
// -> { escribir: true, descripcion: null } ← no hay fila, o falló la ejecución
// -> { escribir: false } ← el código está vacío: no se toca el campo⚠️ Arma un bloque PL/SQL anónimo y lo manda a ejecutar al servidor
Tiene dos puntos de interpolación sin escapar: el que hereda de
prepararCursory uno propio, elc_codigo_a_buscar. El código que tipeó el operador se pega entre comillas sinREPLACE, sinDBMS_ASSERT, sin lista blanca y sin chequeo de tipo.Está medido:
TBL_DIVISIONES_GEOGRAFICASya tiene hoy una filaO'HIGGINS - BS AS, y ese valor produceORA-06550en pantalla y aborta el trigger. Se replica por decisión explícita del responsable del proyecto.Que el SDK le pase el bloque al package con un bind no mitiga nada: la inyección no está en cómo el SDK habla con Oracle, está en el contenido del texto.
Los errores de parseo escapan, los de ejecución no. El exception when others then null que
el legado pega al final del bloque generado sólo cubre la sección ejecutable: un TOO_MANY_ROWS
por un filtro perdido, un NO_DATA_FOUND o una conversión fallida devuelven
{ escribir: true, descripcion: null } sin log, sin mensaje y sin auditoría.
El retorno es una unión discriminada, no un string | null. El legado hace COPY sólo cuando
el código no está vacío, así que "no escribir nada" y "escribir null" son cosas distintas: la
primera deja el campo como estaba, la segunda lo blanquea.
Los tres topes varchar2 del legado —código 50, consulta 2000, descripción 500— se replican
como DgrSdkError con code ORA-06502. Es una divergencia deliberada con prepararCursor, que
decidió no replicar el suyo: allá no replicarlo alarga un string que después falla ruidosamente
igual; acá lo convertiría en un dato en blanco, porque el handler está esperando más abajo.
validarCol(input): ValidarColResultado
El validador de campo del motor de metadata: cuando el operador termina de tipear en una
columna parametrizable, decide si el valor entra o se rechaza. Aplica tres controles en cascada
—longitud, tipo/formato y rango— y además normaliza: reescribe el valor en su forma
canónica según la máscara (1/1/26 → 01/01/2026, 1234.5 → 1,234.50) y le saca los espacios
de los bordes.
validarCol({ valor: '1/1/26', propiedades, estadoRegistro: 'CHANGED' });
// -> { validado: true, valorNormalizado: '01/01/2026', autogeneracion: null }No toca la base y no es async. Con estadoRegistro distinto de 'CHANGED'/'INSERT' es un
no-op completo: no valida, no normaliza y no autogenera.
⚠️ Dos agujeros silenciosos que se replican
1. Una fecha sin FORMAT_MASK borra el dato. TO_DATE(v, NULL) es NULL y TO_CHAR(NULL,
NULL) también, así que el campo se vacía sin un solo mensaje. Y el productor lo hace
probable: cuando el formato es nulo, PROG_CONFIGURAR no emite [FORMAT_MASK…] sino un literal
sin corchetes que buscarPropiedad no puede encontrar. Hoy los 71 campos F de DDJJ tienen
máscara; alcanza con que alguien cargue un set de fecha sin formato.
2. Un rango numérico sin máscara es inerte. TO_NUMBER(x, NULL) es NULL, así que las dos
comparaciones dan NULL y la rama no se toma. Son 14 campos de DDJJ con rango configurado que
nunca se controla: 0031 (Mes, 1-12), 0049 (Establecimiento, 0-99), 0069 (Año Mera Compra,
2005-9999) y 0204 (Rectificativa, 0-99). Metadata que alguien cargó creyendo que valida.
Los dos están medidos contra producción y fijados por tests de smoke que avisan si cambian.
El orden importa: cada paso ve el resultado del anterior
| # | Paso | Detalle que no se puede reordenar |
| --- | -------------- | ------------------------------------------------- |
| 1 | Longitud | sobre el valor crudo, con espacios incluidos |
| 2 | Tipo/formato | deja el valor en su forma canónica |
| 3 | Rango | ve el valor ya reescrito y antes del trim |
| 4 | Trim | recorta sólo U+0020, ni tabs ni saltos de línea |
| 5 | Autogeneración | recibe el valor ya trimeado |
Consecuencia concreta del 3-antes-del-4: para TIPO_DATO = 'A', que no pasa por el paso 2, la
comparación de rango se hace con los espacios que tipeó el operador.
El mensaje de fuera de rango no dice de qué campo habla. El nombre que lo encabezaba está
comentado en el legado, así que el operador ve literalmente " debe estar entre 0 y 999999999.99",
con espacio inicial. Con 472 campos de DDJJ compartiendo ese mismo rango, es casi inútil. Se
conserva carácter por carácter.
Un extremo de rango nulo no acota, y TIPO_DATO distinto de 'F'/'N' no valida tipo ni
formato y compara el rango como strings, con orden binario ('a' < 'B' es falso).
preparar(conn, input): Promise<PrepararResult>
El orquestador del motor de metadata, y la función más usada: 7 forms, 11 call sites. Dado un tipo de entidad y un tipo concreto, arma toda la configuración de una pantalla de búsqueda: qué columnas existen, de qué tipo son, con qué se validan y qué consulta alimenta el buscador.
const r = await preparar(conn, {
tipoEntidad: 'I',
codigoTipo: '0002',
nombreLov: 'LOV',
permiteInsert: true,
permiteUpdate: true,
ambitoValores: 'BLOQUE',
});
// r.columnas[n].descriptorCrudo → el string `#…#` que consume wNII
// r.lov.consulta.sql / .binds → el SQL del buscador, parametrizado
// r.descripcionTipo → 'Inmobiliario Rural'No aplica nada. En el legado todo esto se escribía sobre ítems de Oracle Forms; acá se devuelve y el MFE decide. Toda la geometría queda afuera —posiciones, anchos, alineación—: el descriptor es semántico y el layout lo resuelve el front con su propio sistema de grilla.
El descriptor crudo es un contrato vivo
wNII lo lee contando # y extrae el campo 9 (el hint) por posición fija. Cualquier cambio en
los paddings rompe el hint de 4 forms.
#<tipo> #<desde> #<hasta> #<orden>#<uk1>#<uk2>#<uk3>#<longitud>#<hint 200>#<valida 3>#<query>#El LPAD del hint trunca a 200 caracteres: padStart no truncaría. Hoy el hint más largo mide
60, así que la diferencia es latente — y hay un test de smoke que avisa si alguno se acerca.
La columna del LOV se elige por el último dígito
El legado mira el último carácter del nombre del ítem, no el número de columna. Para 1..9 da lo mismo, pero la columna 11 la trata como la 1 y la 10 no matchea ninguna. El segundo recorrido, en cambio, usa igualdad numérica: esa asimetría es parte del comportamiento.
Cuatro ramas, y en producción sólo corre una
| Rama | Qué produce |
| ----- | ---------------------------------------------------------------------- |
| 'I' | columna oculta, deshabilitada y sin LOV; no cuenta como activa |
| 'S' | la query de salvado pasada por prepararQuery y prepararQuerySalvar |
| 'T' | dos queries fijas contra las tablas menores, sin pipeline |
| resto | sin validación, pero visible y con LOV |
Los 11 call sites pasan
'I'como entidad, y ninguna columna de'I'tiene validación'I','S'ni'T': las cinco son'N'. O sea que en producción sólo se ejecuta la rama'N'. Las otras tres se ejercitan en el smoke con entidades que ningún form usa.
La query del LOV de la rama 'S' se calcula y se descarta, y no se puede saltear: el paso la
pasa por prepararQuery, que resuelve los marcadores [Variable] y aborta toda la corrida si
alguno no existe. Sacarlo cambiaría qué corridas fallan.
codigoTipo: null es inutilizable en la entidad 'I'. El cursor lo contempla y trae las
columnas de todos los tipos, pero la consulta de la descripción no tiene ese OR, así que no
encuentra fila y aborta con ORA-01403. Se replica; ningún call site pasa null.
configurarSpread(conn, input): Promise<ColumnaDescriptorSpread[]>
Traduce la parametrización de una familia de entidades al contrato de cada campo: tipo de dato, longitud, rango, obligatoriedad, editabilidad, máscara, lista de valores posibles y consulta de búsqueda asociada. Es la variante spread (grilla horizontal), la que usan los bloques multi-registro.
const columnas = await configurarSpread(conn, { tipoDatoEntidad: 'D', codigoTipo: '0009' });
// columnas[n].cadenaPropiedades → el descriptor `[NOMBREvalor]` que lee buscarPropiedad
// columnas[n].estadoDato → visible, habilitado, requerido, actualizable…
// columnas[n].opciones → los valores posibles, con validación tipo 'I'No devuelve nada visual: sin width, sin position, sin alineación. En el legado el resultado
se escribía sobre la pantalla y la función devolvía la coordenada X donde terminó de dibujar.
Tiene una hermana melliza, y las divergencias no se parametrizan
configurarPantalla(PROG_CONFIGURAR) hace lo mismo con layout vertical. Comparten el 90% del cuerpo semántico —lo idéntico vive eninternal/parametrizacion.ts, que no se exporta— y difieren en ocho puntos, escritos inline en cada archivo aunque el código quede duplicado.No hay ni va a haber un
variante: 'spread', unif (esSpread)ni una tabla de estrategias. El criterio: si mañana una de las dos cambia, tiene que poder cambiar sola.
Las divergencias que más se notan:
| Qué | configurarSpread | configurarPantalla |
| ------------------------------ | ---------------------------- | -------------------------------------------------- |
| Etiqueta | NOMBRE_DATO_ABREV | NOMBRE_DATO — 237 filas los tienen distintos |
| consultable de autogenerados | autogenerado === 'N' | siempre true (el if está comentado) |
| Alcance de mostrar | sólo la sub-rama de la lista | una columna oculta no se configura |
| Tag MOSTRAR | no se emite | se emite |
| Modo sólo-consulta | vivo | comentado, no existe |
Una de las dos está mal en el consultable de los autogenerados, y no se sabe cuál. El if
comentado de una es tan sospechoso como el if vivo de la otra. No se unifican.
Dos rarezas del descriptor que se replican
Una fecha sin formato emite el tag sin corchete de apertura (FXDD-MM-YYYY #), así que
buscarPropiedad('FORMAT_MASK', …) no lo encuentra nunca. Afecta a 48 de los 50 sets tipo I.
El descriptor de una columna autogenerada queda pisado por el de su generadora. Las dos copy
del legado leen la generadora y escriben en la columna actual, así que la segunda pisa a la
primera. Hoy no dispara —DEPENDE_DE_ID está en 0 de 2.517 filas— pero si mañana cargan esa
metadata, la versión migrada se rompe exactamente igual que el legado.
configurarPantalla(conn, opciones): Promise<PantallaConfigurada>
La variante vertical del mismo mecanismo que configurarSpread, y la función más grande de
la librería (826 líneas de legado). Traduce la parametrización al contrato de cada campo.
const { campos } = await configurarPantalla(conn, { tipoEntidad: 'I', codigoTipo: '0002' });
// campos[n].info → el descriptor `[NOMBREvalor]` que lee buscarPropiedad
// campos[n].dato → label, visible, requerido, edicion, alineacion, lov…
// campos[n].control → 'dato' | 'lista' | 'oculto'Devuelve datos: no muta pantalla, no crea record groups, no calcula geometría. Las 76
SET_ITEM_PROPERTY del legado se volvieron el árbol PantallaConfigurada, y el RETURN NUMBER
—el alto de canvas en píxeles— no tiene equivalente. Dos de los cuatro call sites ya lo
descartaban.
Todo booleano del descriptor es opcional, y ausente no es
falseEn el legado
SET_ITEM_PROPERTYsólo se llama en algunas ramas; lo que no se llama conserva el valor de diseño del form. Devolverfalsedonde el legado no escribió nada sería inventar comportamiento.El caso testigo: un campo autogenerado y no modificable queda
edicion.habilitado: falsey nada más —requeridoyactualizacionPermitidaquedan ausentes.
Qué hace distinto de configurarSpread
| Qué | configurarPantalla | configurarSpread |
| ------------------ | ------------------------------------------------------ | ---------------------------------------- |
| Etiqueta | NOMBRE_DATO, en el ítem de dato y en el de lista | NOMBRE_DATO_ABREV, en un prompt aparte |
| consultable | siempre true (el if está comentado) | autogenerado === 'N' |
| Columna oculta | no se configura en absoluto | se configura y al final se oculta |
| Tags | emite MOSTRAR, POS_X, POS_Y, ANCHO | no los emite |
| Modo sólo-consulta | código muerto: aux := 'N' hardcodeado | vivo |
POS_X, POS_Y y ANCHO van en la cadena info aunque sean geometría. No es una excepción
a la regla de no portar el layout: info es un formato de serialización cuyo lector parsea por
offsets, y sacar tres bloques del medio correría todos los que vienen después. Lo que no se porta
es el SET_ITEM_PROPERTY(position, …), no el dato.
El modo sólo-consulta no se expone como parámetro, aunque la hermana sí lo tenga: en este
legado aux := 'N' está hardcodeado y la lectura quedó comentada en la misma línea, así que
v_solo_consulta es siempre falso. Exponerlo cambiaría el comportamiento observable.
Desarrollo
npm install
npm run verify # typecheck + lint + format:check + test| Comando | Qué hace |
| ---------------------- | --------------------------------------------------------------------------------------------------------- |
| npm run verify | Todo lo de abajo salvo los smoke. Es lo que corre CI. |
| npm run typecheck | tsc --noEmit |
| npm run lint | ESLint |
| npm run format:check | Prettier |
| npm test | Unit tests (Vitest, Oracle mockeado) |
| npm run smoke | Smoke tests contra Oracle real. No va en verify ni en CI. Ver smoke/README.md. |
| npm run build | Compila ESM + CJS a dist/ |
Layout
src/
├── index.ts # ÚNICA superficie pública
├── errors.ts # DgrSdkError + sanitización de ORA-#####
├── types.ts # tipos compartidos
├── metadata/ # motor de metadata de formularios (TCSPRMTRZ) — 14 funciones
├── mensajes/ # catálogo y traducción de mensajes (TCSMSJ) — 7 funciones
├── calendario/ # aritmética de fechas del calendario (TCSLIB) — 3 funciones
└── __tests__/
smoke/ # tests contra Oracle real, fuera de `verify`Todo lo que no se re-exporte desde src/index.ts es interno y puede cambiar sin bump de major.
Publicación
Se publica ./dist, con su propio manifiesto generado por dist-config.cjs (que
auto-incrementa el patch consultando el registry). El job de CI es manual: publicar es
irreversible y nunca se dispara solo en un push.
Documentación
docs/ no se versiona: vive en el working copy de cada quien, no en el repo. Contiene los
documentos normativos y la evidencia de cada migración, agrupada por librería legacy en
docs/migracion/<LIBRERIA>/<funcion>.md. Pedísela a quien la tenga si clonaste limpio.
docs/estandar-sdk-dgr.md— normativo. Cómo se escribe el código del SDK.docs/principio-de-fidelidad.md— normativo. Por qué se replican los bugs del legado en vez de corregirlos.docs/flujo-de-migracion.md— normativo. El paso a paso para cerrar una issue.docs/migracion/<LIBRERIA>/— la evidencia de cada función migrada, con todas sus pruebas. Cuando una librería se cierra, suREADME.mdresume lo que se aprendió del conjunto.- Backlog de migración —
los work items en GitLab. El material del que salieron (incluidas las 52 de frontend, que no
se cargan) está en
issues/, local y no versionada.
Licencia
MIT
