npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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-libraries

Cero 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/oracledb

oracledb 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.

  1. Un mensaje suprimido nunca aborta, aunque se haya pedido falla: true, y vuelve con los #n sin sustituir.
  2. Un W o D que va a la barra de estado aborta siempre, sin importar falla. Es un bug de TCSmsj.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.
  3. 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.

  1. param1 ausente corta todo. Los cuatro if que cuentan parámetros son secuenciales, no elsif, así que manda el primero que falte: getMessage2('4', null, 'las DDJJ') devuelve la plantilla entera con sus %s y descarta 'las DDJJ' en silencio.
  2. 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'.
  3. Un código desconocido con parámetros devuelve los parámetros y pierde el texto. getMessage2('lo que sea', 'A', 'B', 'C') da 'ABC'.

'', null y undefined son 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 error

No 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 %s con pcount: 1 pierde sus dos primeros caracteres. Es el comportamiento del legado.

La cuarta ranura se llena sólo con pcount exactamente 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 delete pero no contiene ' while depen', el legado devuelve un fragmento del texto original a partir de los 24 caracteres — por ejemplo Cannot delete ORA-00001: unique constraint violated da RCD-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-0000000001

Un valor null hace 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

DV1 puede devolver la letra 'A'. Por eso el retorno es string y no number: tipar esto como número es un bug esperando. El llamador copia el resultado a una COLnn que 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'. Ni DV1 ni DV2 ni '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 : 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 prepararCursor y uno propio, el c_codigo_a_buscar. El código que tipeó el operador se pega entre comillas sin REPLACE, sin DBMS_ASSERT, sin lista blanca y sin chequeo de tipo.

Está medido: TBL_DIVISIONES_GEOGRAFICAS ya tiene hoy una fila O'HIGGINS - BS AS, y ese valor produce ORA-06550 en 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/2601/01/2026, 1234.51,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 en internal/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', un if (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_DATO237 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 false

En el legado SET_ITEM_PROPERTY sólo se llama en algunas ramas; lo que no se llama conserva el valor de diseño del form. Devolver false donde el legado no escribió nada sería inventar comportamiento.

El caso testigo: un campo autogenerado y no modificable queda edicion.habilitado: false y nada másrequerido y actualizacionPermitida quedan 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, su README.md resume 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