feat: version 4.12.0 -- Conexiones externas: pacientes de SAGA, empleados de nomina y medicos

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-29 17:06:10 -05:00
parent fabe5b530d
commit 0ee9ed65bf
43 changed files with 1402 additions and 69 deletions
+1
View File
@@ -73,6 +73,7 @@ function load() {
cacheMaxRecordsPerTable: Number(limits.cacheMaxRecordsPerTable || 100000),
cacheMaxTables: Number(limits.cacheMaxTables || 10),
requestTimeoutMs: Number(limits.requestTimeoutMs || 30000),
minReloadSeconds: Number(limits.minReloadSeconds ?? 60),
},
logDir: path.resolve(ROOT, raw.logDir || 'logs'),
};
+16 -4
View File
@@ -98,7 +98,8 @@ async function withSnapshot(ref, fn) {
function toPlain(value, type) {
if (value === null || value === undefined) return null;
if (value instanceof Date) {
if (Number.isNaN(value.getTime())) return null;
// Fecha vacía en FoxPro: la librería la convierte en el día juliano 0 (año -4713).
if (Number.isNaN(value.getTime()) || value.getUTCFullYear() < 1000) return null;
return type === 'D' ? value.toISOString().slice(0, 10) : value.toISOString();
}
if (typeof value === 'string') {
@@ -122,7 +123,10 @@ async function loadTable(source, ref, limits) {
const key = `${source.id}/${ref.table}`;
const sig = await signature(ref);
const hit = cache.get(key);
if (hit && hit.version === sig.version) {
// Tablas que el sistema origen escribe todo el día: no se vuelve a copiar más de una vez cada
// minReloadSeconds aunque la firma haya cambiado.
const fresh = hit && Date.now() - hit.loadedAt < limits.minReloadSeconds * 1000;
if (hit && (hit.version === sig.version || fresh)) {
cache.delete(key);
cache.set(key, hit); // LRU
return hit;
@@ -138,7 +142,7 @@ async function loadTable(source, ref, limits) {
for (const f of fields) row[f.name] = toPlain(r[f.name], f.type);
records.push(row);
}
return { version: sig.version, modifiedAt: sig.modifiedAt, fields, records };
return { version: sig.version, modifiedAt: sig.modifiedAt, loadedAt: Date.now(), fields, records };
});
if (entry.records.length <= limits.cacheMaxRecordsPerTable) cachePut(key, entry, limits);
return entry;
@@ -206,9 +210,17 @@ async function readRows(source, tableName, q, limits) {
.filter(([k]) => k.startsWith('eq.'))
.map(([k, v]) => [pick(k.slice(3), 'eq')[0], fold(v).trim()]);
const text = q.q ? fold(q.q).trim() : '';
// Sincronización incremental: solo lo agregado (afterRecno) y/o lo modificado desde una fecha en
// un campo de fecha/hora del origen (changedField + changedSince). Si se dan ambos, cualquiera basta.
const afterRecno = q.afterRecno !== undefined ? Math.max(parseInt(q.afterRecno, 10) || 0, 0) : null;
const changedField = q.changedField ? pick(q.changedField, 'changedField')[0] : null;
const changedSince = q.changedSince ? String(q.changedSince) : null;
if (!!changedField !== !!changedSince) throw new HttpError(400, 'changedField y changedSince van juntos');
const incremental = afterRecno !== null || changedField;
const qFields = text ? (q.qFields ? pick(q.qFields, 'qFields') : fields) : [];
const matches = data.records.filter((r) => {
if (incremental && !((afterRecno !== null && r._recno > afterRecno) || (changedField && r[changedField] !== null && String(r[changedField]) > changedSince))) return false;
if (!includeDeleted && r._deleted) return false;
for (const [f, v] of eq) if (r[f] === null || fold(r[f]).trim() !== v) return false;
if (text && !qFields.some((f) => r[f] !== null && fold(r[f]).includes(text))) return false;
@@ -221,7 +233,7 @@ async function readRows(source, tableName, q, limits) {
for (const f of fields) o[f] = r[f];
return o;
});
return { table: ref.table, version: data.version, modifiedAt: data.modifiedAt, total: matches.length, offset, limit, fields, rows };
return { table: ref.table, version: data.version, modifiedAt: data.modifiedAt, maxRecno: data.records.length, total: matches.length, offset, limit, fields, rows };
}
module.exports = { listTables, getSchema, readRows, HttpError };