- Al reactivar una suscripción (resume o detección X -> A en el dispatcher) se reinicia U_Retry de sus entregas pendientes, para que no vuelva a 'X' en el siguiente tick. - Nuevo comando subscription:delete: borra filtros, entregas fallidas y eventos de @OEVN que quedan huérfanos. - Evita TypeError en sendPendingDeliveries cuando la suscripción pasa a 'X' después de armar su grupo de envío. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
191 lines
7.3 KiB
TypeScript
191 lines
7.3 KiB
TypeScript
// CLI de administración de suscripciones (@OEVC/@EVC1) contra el Service
|
|
// Layer configurado en .env. Requiere que las 4 UserTables ya existan
|
|
// (`npm run provision`).
|
|
//
|
|
// Uso:
|
|
// npx tsx scripts/manage-subscriptions.ts subscription:add <webhookId> <webhookUrl> [--hmac-key <secreto>]
|
|
// npx tsx scripts/manage-subscriptions.ts subscription:list
|
|
// npx tsx scripts/manage-subscriptions.ts subscription:update-url <webhookId> <webhookUrl>
|
|
// npx tsx scripts/manage-subscriptions.ts subscription:pause <webhookId>
|
|
// npx tsx scripts/manage-subscriptions.ts subscription:resume <webhookId>
|
|
// npx tsx scripts/manage-subscriptions.ts subscription:delete <webhookId>
|
|
//
|
|
// npx tsx scripts/manage-subscriptions.ts filter:add <webhookId> <bizObject> <transType> <objectType>
|
|
// npx tsx scripts/manage-subscriptions.ts filter:remove <webhookId> <bizObject> <transType>
|
|
// npx tsx scripts/manage-subscriptions.ts filter:list
|
|
|
|
import { config } from '../src/config.ts';
|
|
import { ServiceLayerClient } from '../src/service-layer-client.ts';
|
|
import * as evn1 from '../src/dispatcher/evn1-repository.ts';
|
|
import * as oevn from '../src/dispatcher/oevn-repository.ts';
|
|
import { generateCode } from '../src/lib/codes.ts';
|
|
import { userTableEntity } from '../src/lib/service-layer-paths.ts';
|
|
|
|
const client = new ServiceLayerClient(config.serviceLayer);
|
|
const OEVC = userTableEntity('OEVC');
|
|
const EVC1 = userTableEntity('EVC1');
|
|
|
|
function fail(message: string): never {
|
|
console.error(`Error: ${message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function flag(args: string[], name: string): string | undefined {
|
|
const index = args.indexOf(`--${name}`);
|
|
return index === -1 ? undefined : args[index + 1];
|
|
}
|
|
|
|
async function main() {
|
|
const [command, ...args] = process.argv.slice(2);
|
|
|
|
switch (command) {
|
|
case 'subscription:add': {
|
|
const [webhookId, webhookUrl] = args;
|
|
if (!webhookId || !webhookUrl) fail('subscription:add <webhookId> <webhookUrl> [--hmac-key <secreto>]');
|
|
|
|
const hmacKey = flag(args, 'hmac-key');
|
|
await client.post(`/${OEVC}`, {
|
|
Code: webhookId,
|
|
Name: webhookId,
|
|
U_WebhookURL: webhookUrl,
|
|
U_AuthType: hmacKey ? 'H' : 'N',
|
|
U_AuthCred: hmacKey ? JSON.stringify({ Key: hmacKey, Algorithm: 'hmacsha256' }) : null,
|
|
U_State: 'A',
|
|
U_WorkMode: 'S',
|
|
});
|
|
console.log(`Suscripción ${webhookId} creada (${webhookUrl}).`);
|
|
break;
|
|
}
|
|
|
|
case 'subscription:list': {
|
|
const rows = await client.getAll(`/${OEVC}`);
|
|
console.table(
|
|
rows.map((row) => ({
|
|
webhookId: row.Code,
|
|
url: row.U_WebhookURL,
|
|
authType: row.U_AuthType,
|
|
state: row.U_State,
|
|
})),
|
|
);
|
|
break;
|
|
}
|
|
|
|
case 'subscription:update-url': {
|
|
const [webhookId, webhookUrl] = args;
|
|
if (!webhookId || !webhookUrl) fail('subscription:update-url <webhookId> <webhookUrl>');
|
|
await client.patch(`/${OEVC}('${webhookId}')`, { U_WebhookURL: webhookUrl });
|
|
console.log(`Suscripción ${webhookId} actualizada -> ${webhookUrl}.`);
|
|
break;
|
|
}
|
|
|
|
case 'subscription:pause': {
|
|
const [webhookId] = args;
|
|
if (!webhookId) fail('subscription:pause <webhookId>');
|
|
await client.patch(`/${OEVC}('${webhookId}')`, { U_State: 'I' });
|
|
console.log(`Suscripción ${webhookId} pausada (State='I').`);
|
|
break;
|
|
}
|
|
|
|
case 'subscription:resume': {
|
|
const [webhookId] = args;
|
|
if (!webhookId) fail('subscription:resume <webhookId>');
|
|
await client.patch(`/${OEVC}('${webhookId}')`, { U_State: 'A' });
|
|
const reset = await evn1.resetRetries(client, webhookId);
|
|
console.log(`Suscripción ${webhookId} reactivada (State='A'); ${reset} entrega(s) con U_Retry reiniciado a 0.`);
|
|
break;
|
|
}
|
|
|
|
case 'subscription:delete': {
|
|
const [webhookId] = args;
|
|
if (!webhookId) fail('subscription:delete <webhookId>');
|
|
|
|
// 1) Entregas fallidas/pendientes (Status='F') de esta suscripción. Las ya
|
|
// entregadas (Status='D') se conservan como historial.
|
|
const failed = await evn1.listPendingForWebhook(client, webhookId);
|
|
for (const row of failed) await evn1.deleteRow(client, row.code);
|
|
|
|
// 2) Eventos de @OEVN que quedaron sin ninguna entrega asociada (solo
|
|
// existían para esta suscripción) se borran; los demás se recalculan.
|
|
let eventsDeleted = 0;
|
|
for (const eventId of new Set(failed.map((r) => r.eventId))) {
|
|
const remaining = await evn1.listForEvent(client, eventId);
|
|
if (remaining.length === 0) {
|
|
await oevn.deleteEvent(client, eventId);
|
|
eventsDeleted++;
|
|
} else {
|
|
await oevn.setEventStatus(client, eventId, remaining.every((r) => r.status === 'D') ? 'D' : 'P');
|
|
}
|
|
}
|
|
|
|
// 3) Filtros de @EVC1.
|
|
const filters = await client.getAll(`/${EVC1}?$filter=${encodeURIComponent(`U_WebhookID eq '${webhookId}'`)}`);
|
|
for (const f of filters) await client.delete(`/${EVC1}('${f.Code}')`);
|
|
|
|
// 4) La suscripción en sí.
|
|
await client.delete(`/${OEVC}('${webhookId}')`);
|
|
console.log(
|
|
`Suscripción ${webhookId} eliminada: ${filters.length} filtro(s), ${failed.length} entrega(s) fallida(s), ` +
|
|
`${eventsDeleted} evento(s) huérfano(s) de @OEVN.`,
|
|
);
|
|
break;
|
|
}
|
|
|
|
case 'filter:add': {
|
|
const [webhookId, bizObject, transType, objectType] = args;
|
|
if (!webhookId || !bizObject || !transType || !objectType) {
|
|
fail('filter:add <webhookId> <bizObject> <transType> <objectType>');
|
|
}
|
|
await client.post(`/${EVC1}`, {
|
|
Code: generateCode(),
|
|
Name: `${webhookId}/${bizObject}/${transType}`,
|
|
U_WebhookID: webhookId,
|
|
U_BizObject: bizObject,
|
|
U_TransType: transType,
|
|
U_ObjectType: objectType,
|
|
});
|
|
console.log(`Filtro agregado: ${webhookId} -> ${bizObject}/${transType} (ObjectType ${objectType}).`);
|
|
break;
|
|
}
|
|
|
|
case 'filter:remove': {
|
|
const [webhookId, bizObject, transType] = args;
|
|
if (!webhookId || !bizObject || !transType) {
|
|
fail('filter:remove <webhookId> <bizObject> <transType>');
|
|
}
|
|
const filter = encodeURIComponent(
|
|
`U_WebhookID eq '${webhookId}' and U_BizObject eq '${bizObject}' and U_TransType eq '${transType}'`,
|
|
);
|
|
const res = await client.get(`/${EVC1}?$filter=${filter}`);
|
|
const rows = (res.data?.value as { Code: string }[]) ?? [];
|
|
if (rows.length === 0) fail(`no existe el filtro ${webhookId} -> ${bizObject}/${transType}`);
|
|
await client.delete(`/${EVC1}('${rows[0].Code}')`);
|
|
console.log(`Filtro eliminado: ${webhookId} -> ${bizObject}/${transType}.`);
|
|
break;
|
|
}
|
|
|
|
case 'filter:list': {
|
|
const rows = await client.getAll(`/${EVC1}`);
|
|
console.table(
|
|
rows.map((row) => ({
|
|
webhookId: row.U_WebhookID,
|
|
bizObject: row.U_BizObject,
|
|
transType: row.U_TransType,
|
|
objectType: row.U_ObjectType,
|
|
})),
|
|
);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
fail(
|
|
`comando desconocido: ${command ?? '(ninguno)'}\nComandos disponibles: subscription:add, subscription:list, ` +
|
|
'subscription:update-url, subscription:pause, subscription:resume, subscription:delete, filter:add, filter:remove, filter:list',
|
|
);
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exitCode = 1;
|
|
});
|