fix/T2026-04-023-respaldo-cliente-no-identificado
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -121,36 +121,52 @@ export async function lookupNodeByNodoOrBdName(nodoName: string): Promise<any |
|
||||
|
||||
/**
|
||||
* Asocia el nombre de archivo de respaldo (sin extensión) a una fila de `database_nodes`.
|
||||
* Cubre: igualdad sin mayúsculas y prefijos tipo `NODO001_full_20240414` → nodo `NODO001`.
|
||||
* Cubre:
|
||||
* - igualdad sin mayúsculas;
|
||||
* - el primer segmento separado por punto como token de nodo
|
||||
* (ej. `08037natm001.KNOWNWORLD.000` → nodo `08037NATM001`; el sufijo
|
||||
* `.KNOWNWORLD` = nombre lógico de la BD origen y `.000` = secuencia se ignoran);
|
||||
* - prefijos con separadores `.`, `_` o `-` (ej. `NODO001_full_20240414` → `NODO001`).
|
||||
*/
|
||||
export function matchNodeRowFromBackupStem(stem: string, nodes: any[]): any | null {
|
||||
const raw = String(stem ?? '').trim();
|
||||
if (!raw || !nodes?.length) return null;
|
||||
const key = raw.toLowerCase();
|
||||
|
||||
// Candidatos para igualdad exacta: el stem completo y su primer segmento (token de nodo).
|
||||
const head = key.split('.')[0];
|
||||
const exactKeys = head && head !== key ? [key, head] : [key];
|
||||
|
||||
const nodoOf = (row: any) => String(row.NodoSubNodo ?? '').trim().toLowerCase();
|
||||
const bdOf = (row: any) => String(row.BDName ?? '').trim().toLowerCase();
|
||||
|
||||
for (const row of nodes) {
|
||||
const n = nodoOf(row);
|
||||
const b = bdOf(row);
|
||||
if (n && n === key) return row;
|
||||
if (b && b === key) return row;
|
||||
for (const k of exactKeys) {
|
||||
if (n && n === k) return row;
|
||||
if (b && b === k) return row;
|
||||
}
|
||||
}
|
||||
|
||||
// Coincidencia por prefijo: el nodo/BD seguido de un separador (`.`, `_` o `-`).
|
||||
const startsWithToken = (token: string) =>
|
||||
key.startsWith(`${token}.`) || key.startsWith(`${token}_`) || key.startsWith(`${token}-`);
|
||||
|
||||
type Cand = { row: any; len: number };
|
||||
const cands: Cand[] = [];
|
||||
for (const row of nodes) {
|
||||
const n = nodoOf(row);
|
||||
const b = bdOf(row);
|
||||
if (n && (key === n || key.startsWith(`${n}_`) || key.startsWith(`${n}-`))) {
|
||||
if (n && (key === n || startsWithToken(n))) {
|
||||
cands.push({ row, len: n.length });
|
||||
}
|
||||
if (b && b !== n && (key === b || key.startsWith(`${b}_`) || key.startsWith(`${b}-`))) {
|
||||
if (b && b !== n && (key === b || startsWithToken(b))) {
|
||||
cands.push({ row, len: b.length });
|
||||
}
|
||||
}
|
||||
if (!cands.length) return null;
|
||||
// Ganar el match más específico (token más largo) para no confundir nodos con prefijo común.
|
||||
cands.sort((a, b) => b.len - a.len);
|
||||
return cands[0].row;
|
||||
}
|
||||
|
||||
85
src/lib/server/notification-emails-pg.ts
Normal file
85
src/lib/server/notification-emails-pg.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Correos de notificación adicionales en PostgreSQL (esquema a24c; DDL aprovisionado por la app a24c).
|
||||
* Dos catálogos paralelos, ambos ligados al nodo por `node_subnode_key`:
|
||||
* - additional_emails: destinatarios extra para alertas de respaldos / actividad.
|
||||
* - authority_notification_emails: destinatarios para el ingreso de la autoridad al portal.
|
||||
* a24c los lee en vivo al notificar (comparación case-insensitive + trim), por eso aquí se usa el
|
||||
* mismo criterio `lower(btrim(...))` para evitar duplicados que a24c terminaría deduplicando.
|
||||
*/
|
||||
import { pgPool } from './db';
|
||||
|
||||
function schemaName(): string {
|
||||
const s = 'a24c';
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(s)) return 'a24c';
|
||||
return s;
|
||||
}
|
||||
|
||||
function qAdditional(): string {
|
||||
const s = schemaName();
|
||||
return `"${s.replace(/"/g, '""')}"."additional_emails"`;
|
||||
}
|
||||
|
||||
function qAuthority(): string {
|
||||
const s = schemaName();
|
||||
return `"${s.replace(/"/g, '""')}"."authority_notification_emails"`;
|
||||
}
|
||||
|
||||
const ROW_EMAIL = `
|
||||
id AS "ID",
|
||||
node_subnode_key AS "NodoSubNodo",
|
||||
email AS "Correo"
|
||||
`;
|
||||
|
||||
export async function listAdditionalEmails(): Promise<any[]> {
|
||||
const r = await pgPool.query(`SELECT ${ROW_EMAIL} FROM ${qAdditional()} ORDER BY id`);
|
||||
return r.rows;
|
||||
}
|
||||
|
||||
export async function listAuthorityEmails(): Promise<any[]> {
|
||||
const r = await pgPool.query(`SELECT ${ROW_EMAIL} FROM ${qAuthority()} ORDER BY id`);
|
||||
return r.rows;
|
||||
}
|
||||
|
||||
async function emailExists(table: string, nodeSubnodeKey: string, email: string): Promise<boolean> {
|
||||
const sql = `
|
||||
SELECT 1
|
||||
FROM ${table}
|
||||
WHERE LOWER(BTRIM(node_subnode_key)) = LOWER(BTRIM($1::text))
|
||||
AND LOWER(BTRIM(email)) = LOWER(BTRIM($2::text))
|
||||
LIMIT 1
|
||||
`;
|
||||
const r = await pgPool.query(sql, [nodeSubnodeKey, email]);
|
||||
return (r.rowCount ?? 0) > 0;
|
||||
}
|
||||
|
||||
export async function additionalEmailExists(nodeSubnodeKey: string, email: string): Promise<boolean> {
|
||||
return emailExists(qAdditional(), nodeSubnodeKey, email);
|
||||
}
|
||||
|
||||
export async function authorityEmailExists(nodeSubnodeKey: string, email: string): Promise<boolean> {
|
||||
return emailExists(qAuthority(), nodeSubnodeKey, email);
|
||||
}
|
||||
|
||||
export async function insertAdditionalEmail(nodeSubnodeKey: string, email: string): Promise<any> {
|
||||
const r = await pgPool.query(
|
||||
`INSERT INTO ${qAdditional()} (node_subnode_key, email) VALUES ($1, $2) RETURNING ${ROW_EMAIL}`,
|
||||
[nodeSubnodeKey, email]
|
||||
);
|
||||
return r.rows[0];
|
||||
}
|
||||
|
||||
export async function insertAuthorityEmail(nodeSubnodeKey: string, email: string): Promise<any> {
|
||||
const r = await pgPool.query(
|
||||
`INSERT INTO ${qAuthority()} (node_subnode_key, email) VALUES ($1, $2) RETURNING ${ROW_EMAIL}`,
|
||||
[nodeSubnodeKey, email]
|
||||
);
|
||||
return r.rows[0];
|
||||
}
|
||||
|
||||
export async function deleteAdditionalEmail(id: number): Promise<void> {
|
||||
await pgPool.query(`DELETE FROM ${qAdditional()} WHERE id = $1`, [id]);
|
||||
}
|
||||
|
||||
export async function deleteAuthorityEmail(id: number): Promise<void> {
|
||||
await pgPool.query(`DELETE FROM ${qAuthority()} WHERE id = $1`, [id]);
|
||||
}
|
||||
Reference in New Issue
Block a user