@oondemand/oon-core-back 0.5.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -3
- package/src/api/core.routes.js +0 -2
- package/src/boot/createApp.js +0 -7
- package/src/boot/database.js +0 -34
- package/src/boot/loadCentral.js +0 -6
- package/src/capabilities/pdfRendering.js +9 -9
- package/src/core/define.js +1 -6
- package/src/core/models/Sistema.js +2 -10
- package/src/core/registry.js +0 -8
- package/src/deploy/render.js +0 -2
- package/src/deploy/templates/secret.yaml +0 -2
- package/src/index.js +2 -16
- package/src/manifest/appManifest.js +5 -22
- package/bin/integration-worker.js +0 -31
- package/src/modules/integrations/index.js +0 -26
- package/src/modules/integrations/models.js +0 -112
- package/src/modules/integrations/registry.js +0 -171
- package/src/modules/integrations/routes.js +0 -343
- package/src/modules/integrations/runtime/inbox.js +0 -134
- package/src/modules/integrations/runtime/lease.js +0 -94
- package/src/modules/integrations/runtime/outbox.js +0 -199
- package/src/modules/integrations/runtime/sync.js +0 -85
- package/src/modules/integrations/runtime/tracking.js +0 -89
- package/src/modules/integrations/runtime/utils.js +0 -71
- package/src/modules/integrations/runtime.js +0 -10
- package/src/modules/integrations/sanitizer.js +0 -96
- package/src/modules/integrations/worker.js +0 -56
- package/src/modules/omie/client.js +0 -282
- package/src/modules/omie/definitions.js +0 -212
- package/src/modules/omie/index.js +0 -33
- package/src/modules/omie/models.js +0 -377
- package/src/modules/omie/provider.js +0 -529
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const crypto = require("node:crypto");
|
|
4
|
-
const GenericError = require("../../../core/errors/GenericError");
|
|
5
|
-
const { DEFAULT_HEARTBEAT_MS, DEFAULT_LEASE_MS } = require("./utils");
|
|
6
|
-
|
|
7
|
-
async function acquireNext(Model, query, { worker, leaseMs }) {
|
|
8
|
-
const now = new Date();
|
|
9
|
-
const leaseId = crypto.randomUUID();
|
|
10
|
-
const document = await Model.findOneAndUpdate(
|
|
11
|
-
{
|
|
12
|
-
...query,
|
|
13
|
-
$and: [
|
|
14
|
-
{
|
|
15
|
-
$or: [
|
|
16
|
-
{ nextAttemptAt: { $lte: now } },
|
|
17
|
-
{ nextAttemptAt: null },
|
|
18
|
-
{ nextAttemptAt: { $exists: false } },
|
|
19
|
-
],
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
$or: [
|
|
23
|
-
{ leaseExpiresAt: { $lte: now } },
|
|
24
|
-
{ leaseExpiresAt: null },
|
|
25
|
-
{ leaseExpiresAt: { $exists: false } },
|
|
26
|
-
],
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
$set: {
|
|
32
|
-
status: "Processando",
|
|
33
|
-
leaseId,
|
|
34
|
-
lockedAt: now,
|
|
35
|
-
lockedBy: worker,
|
|
36
|
-
heartbeatAt: now,
|
|
37
|
-
leaseExpiresAt: new Date(now.getTime() + leaseMs),
|
|
38
|
-
},
|
|
39
|
-
$inc: { attempts: 1 },
|
|
40
|
-
},
|
|
41
|
-
{ sort: { createdAt: 1, receivedAt: 1 }, new: true },
|
|
42
|
-
).lean();
|
|
43
|
-
return document ? { ...document, leaseId } : null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function heartbeat(Model, id, leaseId, leaseMs = DEFAULT_LEASE_MS) {
|
|
47
|
-
const now = new Date();
|
|
48
|
-
const result = await Model.updateOne(
|
|
49
|
-
{ _id: id, leaseId, status: "Processando" },
|
|
50
|
-
{ $set: { heartbeatAt: now, leaseExpiresAt: new Date(now.getTime() + leaseMs) } },
|
|
51
|
-
);
|
|
52
|
-
if (result.matchedCount !== 1) {
|
|
53
|
-
throw new GenericError("Lease da integração não pertence mais a este worker.", { statusCode: 409 });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function leaseHeartbeat(Model, document, options = {}) {
|
|
58
|
-
const leaseMs = Number(options.leaseMs || DEFAULT_LEASE_MS);
|
|
59
|
-
const intervalMs = Math.min(leaseMs / 2, Number(options.heartbeatMs || DEFAULT_HEARTBEAT_MS));
|
|
60
|
-
let timer;
|
|
61
|
-
let stopped = false;
|
|
62
|
-
const beat = () => heartbeat(Model, document._id, document.leaseId, leaseMs);
|
|
63
|
-
return {
|
|
64
|
-
start() {
|
|
65
|
-
timer = setInterval(() => {
|
|
66
|
-
beat().catch((error) => {
|
|
67
|
-
if (!stopped) console.error("[integration-heartbeat]", error.message);
|
|
68
|
-
});
|
|
69
|
-
}, intervalMs);
|
|
70
|
-
timer.unref?.();
|
|
71
|
-
},
|
|
72
|
-
async beat() { return beat(); },
|
|
73
|
-
stop() { stopped = true; if (timer) clearInterval(timer); },
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function finalizeLease(Model, event, patch) {
|
|
78
|
-
const result = await Model.updateOne(
|
|
79
|
-
{ _id: event._id, leaseId: event.leaseId, status: "Processando" },
|
|
80
|
-
{
|
|
81
|
-
$set: {
|
|
82
|
-
...patch,
|
|
83
|
-
leaseId: "",
|
|
84
|
-
lockedAt: null,
|
|
85
|
-
lockedBy: "",
|
|
86
|
-
leaseExpiresAt: null,
|
|
87
|
-
heartbeatAt: null,
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
);
|
|
91
|
-
return result.matchedCount === 1;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
module.exports = { acquireNext, finalizeLease, heartbeat, leaseHeartbeat };
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const GenericError = require("../../../core/errors/GenericError");
|
|
4
|
-
const { getIntegrationModels } = require("../models");
|
|
5
|
-
const { getIntegrationProvider, resolveIntegrationHandler } = require("../registry");
|
|
6
|
-
const { sanitizeIntegrationError, sanitizeIntegrationValue } = require("../sanitizer");
|
|
7
|
-
const { acquireNext, finalizeLease, leaseHeartbeat } = require("./lease");
|
|
8
|
-
const { runTrackedOperation } = require("./tracking");
|
|
9
|
-
const { DEFAULT_LEASE_MS, calculateBackoff, classifyError, isEnabled, workerId } = require("./utils");
|
|
10
|
-
|
|
11
|
-
async function enqueueIntegration(input) {
|
|
12
|
-
const provider = getIntegrationProvider(input.provider);
|
|
13
|
-
const handler = String(input.handler || input.type || "").trim();
|
|
14
|
-
const idempotencyKey = String(input.idempotencyKey || "").trim();
|
|
15
|
-
if (!handler) throw new GenericError("Informe o handler da integração.", { statusCode: 422 });
|
|
16
|
-
if (!idempotencyKey) throw new GenericError("Informe a chave de idempotência.", { statusCode: 422 });
|
|
17
|
-
if (!provider.handlers[handler]) {
|
|
18
|
-
throw new GenericError(`Handler ${handler} não registrado para ${provider.label}.`, { statusCode: 422 });
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const { Outbox } = getIntegrationModels();
|
|
22
|
-
return Outbox.findOneAndUpdate(
|
|
23
|
-
{ idempotencyKey },
|
|
24
|
-
{
|
|
25
|
-
$setOnInsert: {
|
|
26
|
-
provider: provider.key,
|
|
27
|
-
handler,
|
|
28
|
-
resource: String(input.resource || ""),
|
|
29
|
-
operation: String(input.operation || "execute"),
|
|
30
|
-
aggregateType: String(input.aggregateType || ""),
|
|
31
|
-
aggregateId: String(input.aggregateId || ""),
|
|
32
|
-
idempotencyKey,
|
|
33
|
-
payload: sanitizeIntegrationValue(input.payload || {}),
|
|
34
|
-
status: "Pendente",
|
|
35
|
-
attempts: 0,
|
|
36
|
-
nextAttemptAt: new Date(),
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
{ upsert: true, new: true, setDefaultsOnInsert: true },
|
|
40
|
-
).lean();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function processOutboxEvent(event, options = {}) {
|
|
44
|
-
const { Outbox } = getIntegrationModels();
|
|
45
|
-
const pulse = leaseHeartbeat(Outbox, event, options);
|
|
46
|
-
pulse.start();
|
|
47
|
-
let provider;
|
|
48
|
-
let handlerKey = event.handler;
|
|
49
|
-
try {
|
|
50
|
-
const resolved = resolveIntegrationHandler(event.provider, event.handler);
|
|
51
|
-
provider = resolved.provider;
|
|
52
|
-
handlerKey = resolved.handlerKey;
|
|
53
|
-
const handler = resolved.handler;
|
|
54
|
-
if (!isEnabled(provider)) {
|
|
55
|
-
throw new GenericError(`A integração ${provider.label} está desativada.`, { statusCode: 503 });
|
|
56
|
-
}
|
|
57
|
-
const tracked = await runTrackedOperation({
|
|
58
|
-
provider: provider.key,
|
|
59
|
-
resource: event.resource || "",
|
|
60
|
-
operation: event.operation || "execute",
|
|
61
|
-
title: `${provider.label}: ${handlerKey}`,
|
|
62
|
-
metadata: {
|
|
63
|
-
source: "outbox",
|
|
64
|
-
outboxId: String(event._id),
|
|
65
|
-
aggregateType: event.aggregateType || "",
|
|
66
|
-
aggregateId: event.aggregateId || "",
|
|
67
|
-
attempts: event.attempts,
|
|
68
|
-
},
|
|
69
|
-
runner: (trace) => handler(event, {
|
|
70
|
-
...options,
|
|
71
|
-
...trace,
|
|
72
|
-
provider,
|
|
73
|
-
heartbeat: pulse.beat,
|
|
74
|
-
sanitize: sanitizeIntegrationValue,
|
|
75
|
-
}),
|
|
76
|
-
});
|
|
77
|
-
const response = tracked.result;
|
|
78
|
-
const committed = await finalizeLease(Outbox, event, {
|
|
79
|
-
status: "Concluído",
|
|
80
|
-
completedAt: new Date(),
|
|
81
|
-
responseSummary: sanitizeIntegrationValue(tracked.summary || response || {}),
|
|
82
|
-
lastError: {},
|
|
83
|
-
nextAttemptAt: null,
|
|
84
|
-
});
|
|
85
|
-
if (!committed) throw new GenericError("Resultado ignorado porque o lease expirou.", { statusCode: 409 });
|
|
86
|
-
return { id: String(event._id), handler: handlerKey, status: "Concluído", response };
|
|
87
|
-
} catch (error) {
|
|
88
|
-
const attempts = Number(event.attempts || 1);
|
|
89
|
-
const effectiveProvider = provider || { maxAttempts: 1, retryPolicy: { jitter: false } };
|
|
90
|
-
const classification = classifyError(effectiveProvider, error, attempts);
|
|
91
|
-
const retryPolicy = { ...effectiveProvider.retryPolicy, ...(error?.retryPolicy || {}) };
|
|
92
|
-
const committed = await finalizeLease(Outbox, event, {
|
|
93
|
-
status: classification.definitive ? "Erro definitivo" : "Erro temporário",
|
|
94
|
-
lastError: sanitizeIntegrationError(error),
|
|
95
|
-
nextAttemptAt: classification.definitive
|
|
96
|
-
? null
|
|
97
|
-
: new Date(Date.now() + calculateBackoff(attempts, retryPolicy)),
|
|
98
|
-
});
|
|
99
|
-
if (!committed) throw new GenericError("Erro ignorado porque o lease expirou.", { statusCode: 409 });
|
|
100
|
-
return {
|
|
101
|
-
id: String(event._id),
|
|
102
|
-
handler: handlerKey,
|
|
103
|
-
status: classification.definitive ? "Erro definitivo" : "Erro temporário",
|
|
104
|
-
error: sanitizeIntegrationError(error),
|
|
105
|
-
};
|
|
106
|
-
} finally {
|
|
107
|
-
pulse.stop();
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function processIntegrationQueue(options = {}) {
|
|
112
|
-
const { Outbox } = getIntegrationModels();
|
|
113
|
-
const limit = Math.min(500, Math.max(1, Number(options.limit || 20)));
|
|
114
|
-
const worker = options.worker || workerId("outbox");
|
|
115
|
-
const leaseMs = Number(options.leaseMs || DEFAULT_LEASE_MS);
|
|
116
|
-
const statuses = ["Pendente", "Erro temporário", "Processando"];
|
|
117
|
-
const results = [];
|
|
118
|
-
|
|
119
|
-
for (let index = 0; index < limit; index += 1) {
|
|
120
|
-
const query = { status: { $in: statuses } };
|
|
121
|
-
if (options.provider) query.provider = String(options.provider).trim().toLowerCase();
|
|
122
|
-
const event = await acquireNext(Outbox, query, { worker, leaseMs });
|
|
123
|
-
if (!event) break;
|
|
124
|
-
try {
|
|
125
|
-
results.push(await processOutboxEvent(event, options));
|
|
126
|
-
} catch (error) {
|
|
127
|
-
results.push({ id: String(event._id), status: "Erro", error: sanitizeIntegrationError(error) });
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
processed: results.filter((item) => item.status === "Concluído").length,
|
|
133
|
-
errors: results.filter((item) => item.status !== "Concluído"),
|
|
134
|
-
results,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function archiveIntegrationTicket(id, reason) {
|
|
139
|
-
const { Outbox } = getIntegrationModels();
|
|
140
|
-
const ticket = await Outbox.findById(id).lean();
|
|
141
|
-
if (!ticket) throw new GenericError("Ticket de integração não encontrado.", { statusCode: 404 });
|
|
142
|
-
if (!["Erro temporário", "Erro definitivo"].includes(ticket.status)) {
|
|
143
|
-
throw new GenericError("Somente tickets com erro podem ser arquivados.", { statusCode: 409 });
|
|
144
|
-
}
|
|
145
|
-
return Outbox.findOneAndUpdate(
|
|
146
|
-
{ _id: id, status: ticket.status },
|
|
147
|
-
{
|
|
148
|
-
$set: {
|
|
149
|
-
previousStatus: ticket.status,
|
|
150
|
-
status: "Arquivado",
|
|
151
|
-
archivedAt: new Date(),
|
|
152
|
-
archiveReason: String(reason || "Erro reconhecido e arquivado manualmente.").slice(0, 500),
|
|
153
|
-
nextAttemptAt: null,
|
|
154
|
-
leaseId: "",
|
|
155
|
-
lockedAt: null,
|
|
156
|
-
lockedBy: "",
|
|
157
|
-
leaseExpiresAt: null,
|
|
158
|
-
heartbeatAt: null,
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
{ new: true },
|
|
162
|
-
).lean();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async function reprocessIntegrationTicket(id) {
|
|
166
|
-
const { Outbox } = getIntegrationModels();
|
|
167
|
-
const ticket = await Outbox.findById(id).lean();
|
|
168
|
-
if (!ticket) throw new GenericError("Ticket de integração não encontrado.", { statusCode: 404 });
|
|
169
|
-
if (!["Erro temporário", "Erro definitivo", "Arquivado"].includes(ticket.status)) {
|
|
170
|
-
throw new GenericError("Este ticket não permite reprocessamento.", { statusCode: 409 });
|
|
171
|
-
}
|
|
172
|
-
return Outbox.findOneAndUpdate(
|
|
173
|
-
{ _id: id, status: ticket.status },
|
|
174
|
-
{
|
|
175
|
-
$set: {
|
|
176
|
-
previousStatus: ticket.status,
|
|
177
|
-
status: "Pendente",
|
|
178
|
-
attempts: 0,
|
|
179
|
-
nextAttemptAt: new Date(),
|
|
180
|
-
lastError: {},
|
|
181
|
-
archivedAt: null,
|
|
182
|
-
archiveReason: "",
|
|
183
|
-
leaseId: "",
|
|
184
|
-
lockedAt: null,
|
|
185
|
-
lockedBy: "",
|
|
186
|
-
leaseExpiresAt: null,
|
|
187
|
-
heartbeatAt: null,
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
{ new: true },
|
|
191
|
-
).lean();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
module.exports = {
|
|
195
|
-
archiveIntegrationTicket,
|
|
196
|
-
enqueueIntegration,
|
|
197
|
-
processIntegrationQueue,
|
|
198
|
-
reprocessIntegrationTicket,
|
|
199
|
-
};
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const GenericError = require("../../../core/errors/GenericError");
|
|
4
|
-
const { getIntegrationModels } = require("../models");
|
|
5
|
-
const { findResource, getIntegrationProvider, listIntegrationProviders, resolveIntegrationHandler } = require("../registry");
|
|
6
|
-
const { sanitizeIntegrationError, sanitizeIntegrationValue } = require("../sanitizer");
|
|
7
|
-
const { runTrackedOperation } = require("./tracking");
|
|
8
|
-
const { isEnabled } = require("./utils");
|
|
9
|
-
|
|
10
|
-
async function synchronizeResource(providerKey, resourceKey, options = {}) {
|
|
11
|
-
const { provider, resource } = findResource(providerKey, resourceKey);
|
|
12
|
-
if (!isEnabled(provider)) {
|
|
13
|
-
throw new GenericError(`A integração ${provider.label} está desativada.`, { statusCode: 503 });
|
|
14
|
-
}
|
|
15
|
-
const handlerKey = resource.syncHandler || resource.handler;
|
|
16
|
-
const { handler } = resolveIntegrationHandler(provider.key, handlerKey);
|
|
17
|
-
return runTrackedOperation({
|
|
18
|
-
provider: provider.key,
|
|
19
|
-
resource: resource.key,
|
|
20
|
-
operation: "sync",
|
|
21
|
-
title: resource.syncTitle || `Sincronização de ${resource.label}`,
|
|
22
|
-
metadata: options.metadata || {},
|
|
23
|
-
runner: (context) => handler({
|
|
24
|
-
provider: provider.key,
|
|
25
|
-
handler: handlerKey,
|
|
26
|
-
resource: resource.key,
|
|
27
|
-
operation: "sync",
|
|
28
|
-
payload: sanitizeIntegrationValue(options.payload || {}),
|
|
29
|
-
}, { ...options, ...context, provider, resource }),
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function synchronizeAll(providerKey, options = {}) {
|
|
34
|
-
const provider = getIntegrationProvider(providerKey);
|
|
35
|
-
const resources = provider.resources
|
|
36
|
-
.filter((resource) => resource.includeInFullSync && (resource.syncHandler || resource.handler))
|
|
37
|
-
.sort((left, right) => left.order - right.order);
|
|
38
|
-
const results = [];
|
|
39
|
-
for (const resource of resources) {
|
|
40
|
-
try {
|
|
41
|
-
results.push({
|
|
42
|
-
resource: resource.key,
|
|
43
|
-
ok: true,
|
|
44
|
-
result: await synchronizeResource(provider.key, resource.key, options),
|
|
45
|
-
});
|
|
46
|
-
} catch (error) {
|
|
47
|
-
results.push({ resource: resource.key, ok: false, error: sanitizeIntegrationError(error) });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
provider: provider.key,
|
|
52
|
-
status: results.some((item) => !item.ok) ? "Concluído com erros" : "Concluído",
|
|
53
|
-
results,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function integrationHealth() {
|
|
58
|
-
const { Outbox, Inbox, Execution } = getIntegrationModels();
|
|
59
|
-
const providers = listIntegrationProviders();
|
|
60
|
-
return Promise.all(providers.map(async (provider) => {
|
|
61
|
-
const [queue, inbox, lastExecution] = await Promise.all([
|
|
62
|
-
Outbox.aggregate([
|
|
63
|
-
{ $match: { provider: provider.key } },
|
|
64
|
-
{ $group: { _id: "$status", count: { $sum: 1 }, oldest: { $min: "$createdAt" } } },
|
|
65
|
-
]),
|
|
66
|
-
Inbox.aggregate([
|
|
67
|
-
{ $match: { provider: provider.key } },
|
|
68
|
-
{ $group: { _id: "$status", count: { $sum: 1 } } },
|
|
69
|
-
]),
|
|
70
|
-
Execution.findOne({ provider: provider.key }).sort({ startedAt: -1 }).lean(),
|
|
71
|
-
]);
|
|
72
|
-
return {
|
|
73
|
-
...provider,
|
|
74
|
-
queue: Object.fromEntries(queue.map((item) => [item._id, item.count])),
|
|
75
|
-
inbox: Object.fromEntries(inbox.map((item) => [item._id, item.count])),
|
|
76
|
-
oldestPendingAt: queue
|
|
77
|
-
.filter((item) => ["Pendente", "Erro temporário", "Processando"].includes(item._id) && item.oldest)
|
|
78
|
-
.map((item) => new Date(item.oldest))
|
|
79
|
-
.sort((left, right) => left.getTime() - right.getTime())[0] || null,
|
|
80
|
-
lastExecution: lastExecution ? sanitizeIntegrationValue(lastExecution) : null,
|
|
81
|
-
};
|
|
82
|
-
}));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
module.exports = { integrationHealth, synchronizeAll, synchronizeResource };
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const { getIntegrationModels } = require("../models");
|
|
4
|
-
const { sanitizeIntegrationError, sanitizeIntegrationValue } = require("../sanitizer");
|
|
5
|
-
const { DEFAULT_ITEM_LIMIT, DEFAULT_TRACE_LIMIT } = require("./utils");
|
|
6
|
-
|
|
7
|
-
async function runTrackedOperation({ provider, resource = "", operation = "execute", title, metadata = {}, runner }) {
|
|
8
|
-
const { Execution } = getIntegrationModels();
|
|
9
|
-
const startedAt = new Date();
|
|
10
|
-
const execution = await Execution.create({
|
|
11
|
-
provider,
|
|
12
|
-
resource,
|
|
13
|
-
operation,
|
|
14
|
-
title: title || `${provider}:${resource || operation}`,
|
|
15
|
-
status: "Executando",
|
|
16
|
-
startedAt,
|
|
17
|
-
metadata: sanitizeIntegrationValue(metadata),
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const requests = [];
|
|
21
|
-
const errors = [];
|
|
22
|
-
const items = [];
|
|
23
|
-
let requestCount = 0;
|
|
24
|
-
let errorCount = 0;
|
|
25
|
-
let itemCount = 0;
|
|
26
|
-
const context = {
|
|
27
|
-
executionId: String(execution._id),
|
|
28
|
-
recordRequest(value) {
|
|
29
|
-
requestCount += 1;
|
|
30
|
-
if (requests.length < DEFAULT_TRACE_LIMIT) requests.push(sanitizeIntegrationValue(value));
|
|
31
|
-
},
|
|
32
|
-
recordError(value) {
|
|
33
|
-
errorCount += 1;
|
|
34
|
-
if (errors.length < DEFAULT_TRACE_LIMIT) errors.push(sanitizeIntegrationValue(value));
|
|
35
|
-
},
|
|
36
|
-
recordItem(value) {
|
|
37
|
-
itemCount += 1;
|
|
38
|
-
if (items.length < DEFAULT_ITEM_LIMIT) items.push(sanitizeIntegrationValue(value));
|
|
39
|
-
},
|
|
40
|
-
sanitize: sanitizeIntegrationValue,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const result = await runner(context);
|
|
45
|
-
const summary = sanitizeIntegrationValue(result?.summary || result || {});
|
|
46
|
-
const status = errors.length ? "Concluído com erros" : "Concluído";
|
|
47
|
-
await Execution.updateOne({ _id: execution._id }, {
|
|
48
|
-
$set: {
|
|
49
|
-
status,
|
|
50
|
-
concludedAt: new Date(),
|
|
51
|
-
durationMs: Date.now() - startedAt.getTime(),
|
|
52
|
-
message: String(result?.message || ""),
|
|
53
|
-
summary,
|
|
54
|
-
requests,
|
|
55
|
-
requestCount,
|
|
56
|
-
requestsLimited: requestCount > requests.length,
|
|
57
|
-
errors,
|
|
58
|
-
errorCount,
|
|
59
|
-
errorsLimited: errorCount > errors.length,
|
|
60
|
-
items,
|
|
61
|
-
itemCount,
|
|
62
|
-
itemsLimited: itemCount > items.length,
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
return { executionId: String(execution._id), status, summary, result };
|
|
66
|
-
} catch (error) {
|
|
67
|
-
const sanitized = sanitizeIntegrationError(error);
|
|
68
|
-
await Execution.updateOne({ _id: execution._id }, {
|
|
69
|
-
$set: {
|
|
70
|
-
status: "Erro",
|
|
71
|
-
concludedAt: new Date(),
|
|
72
|
-
durationMs: Date.now() - startedAt.getTime(),
|
|
73
|
-
error: sanitized,
|
|
74
|
-
requests,
|
|
75
|
-
requestCount,
|
|
76
|
-
requestsLimited: requestCount > requests.length,
|
|
77
|
-
errors: [...errors, sanitized].slice(0, DEFAULT_TRACE_LIMIT),
|
|
78
|
-
errorCount: errorCount + 1,
|
|
79
|
-
errorsLimited: errorCount + 1 > DEFAULT_TRACE_LIMIT,
|
|
80
|
-
items,
|
|
81
|
-
itemCount,
|
|
82
|
-
itemsLimited: itemCount > items.length,
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
throw error;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
module.exports = { runTrackedOperation };
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const crypto = require("node:crypto");
|
|
4
|
-
const os = require("node:os");
|
|
5
|
-
|
|
6
|
-
const DEFAULT_LEASE_MS = 120_000;
|
|
7
|
-
const DEFAULT_HEARTBEAT_MS = 30_000;
|
|
8
|
-
const DEFAULT_ITEM_LIMIT = 100;
|
|
9
|
-
const DEFAULT_TRACE_LIMIT = 100;
|
|
10
|
-
|
|
11
|
-
function workerId(prefix = "integration") {
|
|
12
|
-
return `${prefix}:${os.hostname()}:${process.pid}`;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function stableStringify(value) {
|
|
16
|
-
if (value == null || typeof value !== "object") return JSON.stringify(value);
|
|
17
|
-
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
18
|
-
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function payloadHash(payload) {
|
|
22
|
-
return crypto.createHash("sha256").update(stableStringify(payload)).digest("hex");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function calculateBackoff(attempt, policy = {}) {
|
|
26
|
-
const strategy = policy.strategy || "exponential";
|
|
27
|
-
const initialMs = Math.max(1, Number(policy.initialMs || 60_000));
|
|
28
|
-
const maxMs = Math.max(initialMs, Number(policy.maxMs || 21_600_000));
|
|
29
|
-
let delay = strategy === "fixed"
|
|
30
|
-
? initialMs
|
|
31
|
-
: Math.min(maxMs, initialMs * (2 ** Math.max(0, Number(attempt || 1) - 1)));
|
|
32
|
-
if (policy.jitter !== false) {
|
|
33
|
-
const factor = 0.75 + Math.random() * 0.5;
|
|
34
|
-
delay = Math.max(1, Math.round(delay * factor));
|
|
35
|
-
}
|
|
36
|
-
return delay;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function classifyError(provider, error, attempts) {
|
|
40
|
-
const custom = typeof provider.classifyError === "function"
|
|
41
|
-
? provider.classifyError(error, { attempts })
|
|
42
|
-
: null;
|
|
43
|
-
const retryable = custom?.retryable ?? (
|
|
44
|
-
typeof provider.isRetryable === "function"
|
|
45
|
-
? provider.isRetryable(error)
|
|
46
|
-
: error?.retryable !== false && Number(error?.statusCode || error?.response?.status || 500) >= 500
|
|
47
|
-
);
|
|
48
|
-
const maxAttempts = Math.max(1, Number(custom?.maxAttempts || provider.maxAttempts || 5));
|
|
49
|
-
return {
|
|
50
|
-
retryable: Boolean(retryable),
|
|
51
|
-
definitive: !retryable || attempts >= maxAttempts,
|
|
52
|
-
maxAttempts,
|
|
53
|
-
code: custom?.code || error?.code,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function isEnabled(provider) {
|
|
58
|
-
return typeof provider.enabled === "function" ? Boolean(provider.enabled()) : provider.enabled !== false;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
module.exports = {
|
|
62
|
-
DEFAULT_HEARTBEAT_MS,
|
|
63
|
-
DEFAULT_ITEM_LIMIT,
|
|
64
|
-
DEFAULT_LEASE_MS,
|
|
65
|
-
DEFAULT_TRACE_LIMIT,
|
|
66
|
-
calculateBackoff,
|
|
67
|
-
classifyError,
|
|
68
|
-
isEnabled,
|
|
69
|
-
payloadHash,
|
|
70
|
-
workerId,
|
|
71
|
-
};
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const SENSITIVE_KEY = /(authorization|cookie|password|passwd|secret|token|api[-_]?key|app[-_]?key|app[-_]?secret|private[-_]?key|credential|senha)/i;
|
|
4
|
-
const DEFAULTS = {
|
|
5
|
-
maxDepth: 12,
|
|
6
|
-
maxArrayItems: 20,
|
|
7
|
-
maxObjectKeys: 100,
|
|
8
|
-
maxStringLength: 4000,
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
function truncate(value, limit) {
|
|
12
|
-
if (value.length <= limit) return value;
|
|
13
|
-
return `${value.slice(0, limit)}…[truncado:${value.length - limit}]`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function binarySummary(value) {
|
|
17
|
-
const size = Number(value?.byteLength ?? value?.length ?? 0);
|
|
18
|
-
const type = value?.constructor?.name || "Binary";
|
|
19
|
-
return `[${type}:${size} bytes]`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function bsonObjectId(value) {
|
|
23
|
-
if (!value || typeof value !== "object") return null;
|
|
24
|
-
const isObjectId = value._bsontype === "ObjectId" || value.constructor?.name === "ObjectId";
|
|
25
|
-
if (!isObjectId) return null;
|
|
26
|
-
if (typeof value.toHexString === "function") return value.toHexString();
|
|
27
|
-
const text = String(value);
|
|
28
|
-
return /^[a-f\d]{24}$/i.test(text) ? text : null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function sanitizeIntegrationValue(value, options = {}, state = { depth: 0, seen: new WeakSet() }) {
|
|
32
|
-
const config = { ...DEFAULTS, ...options };
|
|
33
|
-
if (value == null || typeof value === "boolean" || typeof value === "number") return value;
|
|
34
|
-
if (typeof value === "bigint") return String(value);
|
|
35
|
-
if (typeof value === "string") return truncate(value, config.maxStringLength);
|
|
36
|
-
if (value instanceof Date) return value.toISOString();
|
|
37
|
-
if (value instanceof Error) {
|
|
38
|
-
return {
|
|
39
|
-
name: value.name,
|
|
40
|
-
message: truncate(String(value.message || value), config.maxStringLength),
|
|
41
|
-
code: value.code,
|
|
42
|
-
statusCode: value.statusCode || value.response?.status,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
if (typeof value !== "object") return truncate(String(value), config.maxStringLength);
|
|
46
|
-
|
|
47
|
-
const objectId = bsonObjectId(value);
|
|
48
|
-
if (objectId) return objectId;
|
|
49
|
-
if (Buffer.isBuffer(value) || ArrayBuffer.isView(value)) return binarySummary(value);
|
|
50
|
-
if (value instanceof ArrayBuffer) return binarySummary(value);
|
|
51
|
-
|
|
52
|
-
if (state.depth >= config.maxDepth) return "[limite-de-profundidade]";
|
|
53
|
-
if (state.seen.has(value)) return "[referência-circular]";
|
|
54
|
-
state.seen.add(value);
|
|
55
|
-
|
|
56
|
-
const nextState = { depth: state.depth + 1, seen: state.seen };
|
|
57
|
-
if (Array.isArray(value)) {
|
|
58
|
-
const items = value
|
|
59
|
-
.slice(0, config.maxArrayItems)
|
|
60
|
-
.map((item) => sanitizeIntegrationValue(item, config, nextState));
|
|
61
|
-
if (value.length > config.maxArrayItems) {
|
|
62
|
-
items.push({ _truncated: value.length - config.maxArrayItems });
|
|
63
|
-
}
|
|
64
|
-
return items;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const output = {};
|
|
68
|
-
const entries = Object.entries(value).slice(0, config.maxObjectKeys);
|
|
69
|
-
for (const [key, item] of entries) {
|
|
70
|
-
output[key] = SENSITIVE_KEY.test(key)
|
|
71
|
-
? "[REDACTED]"
|
|
72
|
-
: sanitizeIntegrationValue(item, config, nextState);
|
|
73
|
-
}
|
|
74
|
-
if (Object.keys(value).length > config.maxObjectKeys) {
|
|
75
|
-
output._truncatedKeys = Object.keys(value).length - config.maxObjectKeys;
|
|
76
|
-
}
|
|
77
|
-
return output;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function sanitizeIntegrationError(error, options = {}) {
|
|
81
|
-
const sanitized = sanitizeIntegrationValue(error, options);
|
|
82
|
-
if (typeof sanitized === "string") return { message: sanitized };
|
|
83
|
-
return {
|
|
84
|
-
name: sanitized?.name || error?.name || "Error",
|
|
85
|
-
message: sanitized?.message || String(error?.message || error || "Falha desconhecida."),
|
|
86
|
-
code: sanitized?.code || error?.code,
|
|
87
|
-
statusCode: sanitized?.statusCode || error?.statusCode || error?.response?.status,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
module.exports = {
|
|
92
|
-
DEFAULT_SANITIZE_OPTIONS: DEFAULTS,
|
|
93
|
-
SENSITIVE_KEY,
|
|
94
|
-
sanitizeIntegrationError,
|
|
95
|
-
sanitizeIntegrationValue,
|
|
96
|
-
};
|