RAMAFLEX MOVIL - LAS TRES OPERACIONES SOBRE LAS TABLAS DE RAMAFLEX Pseudocodigo de referencia. Especificacion: https://docurm.random.cl/integracion-erp.html#tablas Version 2 del esquema, 6 de noviembre de 2026. Este texto es la regla. El codigo de referencia (Node, Python, C# y Java) lo implementa tal cual y pasa la prueba de conformidad. Si su implementacion se aparta en algo, gana este texto. ================================================================================================== 0. QUE NECESITA LA IMPLEMENTACION ================================================================================================== CATALOGO = el JSON que el script de las tablas guarda en la base: SELECT CATALOGO FROM XESQUEMA WHERE VERSION = (SELECT MAX(VERSION) FROM XESQUEMA) Trae, por tabla: su clave, su columna autonumerica (si tiene), si es solo de lectura y sus columnas con tipo, largo, si acepta nulo y su valor por defecto. Se lee al atender la primera llamada y se vuelve a leer cuando pasaron 60 segundos. Si una relectura falla y ya habia uno, se sigue con el anterior (y se anota en el log). Si nunca se pudo leer (XESQUEMA sin la columna CATALOGO o sin filas: falta ejecutar el script actual), la llamada responde ERROR_INTERNO y el log lo dice. Con cada version nueva solo se ejecuta el script SQL de su motor: guarda el catalogo nuevo y el codigo lo toma solo, sin reiniciar. No hay archivo de catalogo. RUTA_BASE = la que ustedes elijan, por ejemplo "/ramaflex/tablas". Nos la informan. Tipos del catalogo: texto (con "largo"; null = sin limite), entero, decimal, logico, fecha. Respuesta de error, siempre: ESTADO HTTP + cuerpo { "ok": false, "codigo": C, "mensaje": M } codigo estado cuando NO_AUTENTICADO 401 sin credencial o credencial invalida (401 o 403; si la rechaza su gateway antes de llegar aqui, basta el estado y el cuerpo puede ser otro) TABLA_DESCONOCIDA 404 la tabla no esta en el CATALOGO COLUMNA_DESCONOCIDA 400 un filtro, un orden o una columna de una fila no esta en el CATALOGO CLAVE_INCOMPLETA 400 una fila no trae todas las columnas de la clave, o trae alguna en null DATO_INVALIDO 400 un valor no corresponde al tipo, excede el largo, falta un dato obligatorio, o el cuerpo no es { "filas": [...] } con hasta 500 filas FILTRO_REQUERIDO 400 DELETE sin ningun filtro SOLO_LECTURA 405 PUT o DELETE sobre una tabla de solo lectura (XESQUEMA) METODO_NO_PERMITIDO 405 un metodo distinto de GET, PUT y DELETE CONFLICTO 409 dos escrituras simultaneas chocaron: clave duplicada o interbloqueo (deadlock). Ya se deshizo todo: se puede reintentar ERROR_INTERNO 500 cualquier otro error. No incluir el SQL ni datos de la base en el mensaje. La respuesta HTTP, siempre (de exito y de error): - "Content-Type: application/json". Con otro tipo, Ramaflex recibe texto y no lo puede leer. - El JSON en la raiz, SIN envoltorio: { "filas": [...] } o { "borradas": n } o el error de arriba, aunque la llamada traiga las cabeceras del resto de su API (por ejemplo una que cambie el formato). - Ramaflex llama con "Connection: close". Si su servidor cierra la conexion despues de responder, la respuesta debe decir "Connection: close" (si no, el cliente puede reusarla y la llamada siguiente se corta: le pasa al HttpServer del JDK). ================================================================================================== 1. ENRUTAMIENTO Y SEGURIDAD ================================================================================================== al recibir METODO en RUTA_BASE + "/" + TABLA: si no autenticado(peticion): error NO_AUTENTICADO si TABLA no es una clave de CATALOGO.tablas (comparacion exacta, en mayusculas): error TABLA_DESCONOCIDA def = CATALOGO.tablas[TABLA] segun METODO: GET -> listar(def, parametros de la URL) PUT -> si def.soloLectura: error SOLO_LECTURA guardar(def, cuerpo JSON) DELETE -> si def.soloLectura: error SOLO_LECTURA borrar(def, parametros de la URL) otro -> error METODO_NO_PERMITIDO REGLA DE ORO: los nombres de tabla y de columna que llegan en la peticion solo se usan despues de encontrarlos en el CATALOGO, y en el SQL se escribe el nombre DEL CATALOGO, nunca el recibido. Los valores viajan SIEMPRE como parametros de la consulta, jamas concatenados. autenticado(peticion): la misma autenticacion del resto de su API, con una credencial de servicio. ================================================================================================== 2. CONVERSION DE VALORES (de JSON o de la URL hacia la base) ================================================================================================== convertir(columna, valor): # valor viene de JSON (cualquier tipo) o de la URL (texto) si valor es null: si columna.nulo es falso: error DATO_INVALIDO "COLUMNA no acepta null" devolver null segun columna.tipo: texto: v = valor como texto (un numero de JSON se acepta como su texto; un booleano, un objeto o una lista: DATO_INVALIDO) si columna.largo no es null y largo(v) > columna.largo: error DATO_INVALIDO devolver v entero: numero entero (JSON numero sin decimales, o texto "-?[0-9]+") entre -2147483648 y 2147483647 (INT de 32 bits); otro: DATO_INVALIDO decimal: numero (JSON numero, o texto numerico con punto) que quepa en la columna: a lo mas (precision - escala) digitos enteros; otro: DATO_INVALIDO. Los decimales de mas los redondea la base a "escala". logico: true / false (en la URL: "true", "false", "1", "0"); otro: DATO_INVALIDO fecha: texto ISO-8601 con zona ("2026-11-03T14:22:05.000Z" o "...-03:00"). Se convierte a UTC y se guarda sin zona. Sin zona o mal formada: DATO_INVALIDO. hacia JSON (al responder), por cada columna del CATALOGO, en ese orden: null -> null texto -> texto sin espacios a la derecha (los CHAR vienen rellenos) entero, decimal -> numero logico -> true / false (MySQL devuelve 0/1: convertir) fecha -> "AAAA-MM-DDTHH:mm:ss.sssZ" (el valor guardado ES UTC: no aplicar la zona del servidor) fila_json(registro) = { COLUMNA: hacia_json(valor) para CADA columna del catalogo } Todas las columnas, tambien las nulas. Los nombres en mayusculas, como en el catalogo, aunque PostgreSQL los devuelva en minusculas. ================================================================================================== 3. LISTAR GET {RUTA_BASE}/{TABLA}?COL=v&COL=v2&COL.desde=a&COL.hasta=b&orden=-COL&pagina=1&limite=500 ================================================================================================== listar(def, parametros): filtros = armar_filtros(def, parametros) # ver 6 orden = [] por cada valor o de parametros["orden"] (puede repetirse; tambien "A,-B"): desc = o empieza con "-"; col = o sin el "-" si col no esta en def.columnas: error COLUMNA_DESCONOCIDA si col ya esta en orden: ignorarla (SQL Server rechaza una columna repetida) agregar (col, desc) a orden agregar al final las columnas de def.clave que no esten ya en orden (ascendente) # sin un orden total, las paginas se pisan # "pagina" y "limite": si se repiten, el primero pagina = entero(parametros["pagina"], por defecto 1); si pagina < 1: DATO_INVALIDO limite = entero(parametros["limite"], por defecto 1000); si limite < 1 o > 1000: DATO_INVALIDO sql = SELECT FROM WHERE ORDER BY + paginacion del motor: SQL Server: OFFSET (pagina-1)*limite ROWS FETCH NEXT limite ROWS ONLY PostgreSQL / MySQL: LIMIT limite OFFSET (pagina-1)*limite responder 200 { "filas": [ fila_json(r) por cada registro ] } ================================================================================================== 4. GUARDAR PUT {RUTA_BASE}/{TABLA} cuerpo { "filas": [ {...}, ... ] } ================================================================================================== guardar(def, cuerpo): si cuerpo no es un objeto con "filas" lista: error DATO_INVALIDO si largo(filas) > 500: error DATO_INVALIDO # 1) validar TODO antes de tocar la base por cada fila: si fila no es un objeto: DATO_INVALIDO por cada nombre en fila: si nombre no esta en def.columnas: error COLUMNA_DESCONOCIDA por cada col en def.clave: si col no viene en fila o viene null: error CLAVE_INCOMPLETA valores[fila] = { col: convertir(def.columnas[col], fila[col]) para cada col de la fila, EXCEPTO def.autonumerico } # la columna autonumerica se ignora si viene: la asigna la base y nunca se actualiza # 2) escribir, todo o nada iniciar transaccion resultado = [] por cada fila (en orden): v = valores[fila] clave = { col: v[col] para col en def.clave } existe = SELECT 1 FROM WHERE (con bloqueo de actualizacion si el motor lo permite) si existe: resto = v sin las columnas de la clave si resto no esta vacio: UPDATE SET WHERE # SOLO lo enviado si no: por cada col de def.columnas con nulo falso, sin "defecto", que no sea autonumerica: si col no viene en v: error DATO_INVALIDO "falta COLUMNA" (deshace todo) INSERT INTO () VALUES () # las que no vienen toman su DEFAULT o null, lo pone la base agregar a resultado: SELECT FROM WHERE confirmar transaccion responder 200 { "filas": [ fila_json(r) por cada r en resultado ] } si algo falla: deshacer la transaccion y responder el error. Nada queda guardado. si la base reporta clave duplicada (dos PUT simultaneos crearon la misma clave) o elige esta transaccion como victima de un interbloqueo: deshacer y responder CONFLICTO. Nosotros reintentamos. Codigos: SQL Server 2627, 2601 (duplicada) y 1205 (interbloqueo) PostgreSQL 23505 y 40P01 MySQL 1062 y 1213 ================================================================================================== 5. BORRAR DELETE {RUTA_BASE}/{TABLA}?COL=v ================================================================================================== borrar(def, parametros): filtros = armar_filtros(def, parametros) si filtros esta vacio: error FILTRO_REQUERIDO # un filtro olvidado no vacia una tabla n = DELETE FROM WHERE responder 200 { "borradas": n } ================================================================================================== 6. FILTROS (comunes a LISTAR y BORRAR) ================================================================================================== armar_filtros(def, parametros): condiciones = [] por cada NOMBRE distinto en parametros, salvo "orden", "pagina" y "limite": si NOMBRE termina en ".desde" o ".hasta": col = NOMBRE sin el sufijo si col no esta en def.columnas: error COLUMNA_DESCONOCIDA x = convertir(def.columnas[col], valor) # un solo valor; si se repite, el primero ".desde" -> col >= ? ".hasta" -> col <= ? (ambos extremos incluidos) si no: col = NOMBRE si col no esta en def.columnas: error COLUMNA_DESCONOCIDA valores = todos los valores de NOMBRE (puede repetirse), cada uno convertido un valor -> col = ? varios -> col IN (?, ?, ...) # basta con que sea igual a cualquiera el texto "null" no es especial: es el texto "null" devolver condiciones unidas con AND ================================================================================================== 7. LO QUE NO SE DEBE HACER ================================================================================================== - Aceptar nombres de tabla o columna que no esten en el CATALOGO, o armar SQL con lo recibido. - Exponer otras tablas de su base por esta ruta. - Responder exito si algo no se guardo, o guardar parte de un PUT que respondio error. - Borrar sin filtro. - Devolver fechas en hora local o sin la "Z". - Escribir a mano en XREGISTER, XREGISTERDEV o XLICCONF: sus filas llevan una firma nuestra. - Escribir en XESQUEMA por la API: solo la escribe el script de las tablas.