@oondemand/oon-core-back 0.3.56 → 0.3.58

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.
@@ -0,0 +1,85 @@
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 };
@@ -0,0 +1,89 @@
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 };
@@ -0,0 +1,71 @@
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
+ };
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+
3
+ module.exports = {
4
+ ...require("./runtime/inbox"),
5
+ ...require("./runtime/lease"),
6
+ ...require("./runtime/outbox"),
7
+ ...require("./runtime/sync"),
8
+ ...require("./runtime/tracking"),
9
+ ...require("./runtime/utils"),
10
+ };
@@ -0,0 +1,75 @@
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: 6,
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 sanitizeIntegrationValue(value, options = {}, state = { depth: 0, seen: new WeakSet() }) {
17
+ const config = { ...DEFAULTS, ...options };
18
+ if (value == null || typeof value === "boolean" || typeof value === "number") return value;
19
+ if (typeof value === "bigint") return String(value);
20
+ if (typeof value === "string") return truncate(value, config.maxStringLength);
21
+ if (value instanceof Date) return value.toISOString();
22
+ if (value instanceof Error) {
23
+ return {
24
+ name: value.name,
25
+ message: truncate(String(value.message || value), config.maxStringLength),
26
+ code: value.code,
27
+ statusCode: value.statusCode || value.response?.status,
28
+ };
29
+ }
30
+ if (typeof value !== "object") return truncate(String(value), config.maxStringLength);
31
+ if (state.depth >= config.maxDepth) return "[limite-de-profundidade]";
32
+ if (state.seen.has(value)) return "[referência-circular]";
33
+ state.seen.add(value);
34
+
35
+ const nextState = { depth: state.depth + 1, seen: state.seen };
36
+ if (Array.isArray(value)) {
37
+ const items = value
38
+ .slice(0, config.maxArrayItems)
39
+ .map((item) => sanitizeIntegrationValue(item, config, nextState));
40
+ if (value.length > config.maxArrayItems) {
41
+ items.push({ _truncated: value.length - config.maxArrayItems });
42
+ }
43
+ return items;
44
+ }
45
+
46
+ const output = {};
47
+ const entries = Object.entries(value).slice(0, config.maxObjectKeys);
48
+ for (const [key, item] of entries) {
49
+ output[key] = SENSITIVE_KEY.test(key)
50
+ ? "[REDACTED]"
51
+ : sanitizeIntegrationValue(item, config, nextState);
52
+ }
53
+ if (Object.keys(value).length > config.maxObjectKeys) {
54
+ output._truncatedKeys = Object.keys(value).length - config.maxObjectKeys;
55
+ }
56
+ return output;
57
+ }
58
+
59
+ function sanitizeIntegrationError(error, options = {}) {
60
+ const sanitized = sanitizeIntegrationValue(error, options);
61
+ if (typeof sanitized === "string") return { message: sanitized };
62
+ return {
63
+ name: sanitized?.name || error?.name || "Error",
64
+ message: sanitized?.message || String(error?.message || error || "Falha desconhecida."),
65
+ code: sanitized?.code || error?.code,
66
+ statusCode: sanitized?.statusCode || error?.statusCode || error?.response?.status,
67
+ };
68
+ }
69
+
70
+ module.exports = {
71
+ DEFAULT_SANITIZE_OPTIONS: DEFAULTS,
72
+ SENSITIVE_KEY,
73
+ sanitizeIntegrationError,
74
+ sanitizeIntegrationValue,
75
+ };
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+
3
+ const { loadCentral } = require("../../boot/loadCentral");
4
+ const { connectDB } = require("../../boot/database");
5
+ const { listIntegrationProviders } = require("./registry");
6
+ const { processIntegrationQueue, processWebhookInbox } = require("./runtime");
7
+
8
+ function delay(ms) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms));
10
+ }
11
+
12
+ async function drainOnce(options = {}) {
13
+ const providers = options.provider
14
+ ? [{ key: String(options.provider).trim().toLowerCase() }]
15
+ : listIntegrationProviders();
16
+ const results = [];
17
+ for (const provider of providers) {
18
+ const [queue, events] = await Promise.all([
19
+ processIntegrationQueue({ ...options, provider: provider.key, limit: options.batchSize || options.limit || 20 }),
20
+ processWebhookInbox({ ...options, provider: provider.key, limit: options.webhookBatchSize || 20 }),
21
+ ]);
22
+ results.push({ provider: provider.key, queue, events });
23
+ }
24
+ return results;
25
+ }
26
+
27
+ async function runIntegrationWorker(options = {}) {
28
+ const cwd = options.cwd || process.cwd();
29
+ loadCentral(cwd);
30
+ await connectDB();
31
+ const once = options.once === true;
32
+ const intervalMs = Math.max(
33
+ 1000,
34
+ Number(options.intervalMs || process.env.OON_INTEGRATION_WORKER_INTERVAL_MS || 15_000),
35
+ );
36
+ let stopping = false;
37
+ const stop = () => { stopping = true; };
38
+ process.once("SIGTERM", stop);
39
+ process.once("SIGINT", stop);
40
+
41
+ do {
42
+ const results = await drainOnce(options);
43
+ if (options.logger !== false) {
44
+ const processed = results.reduce(
45
+ (sum, item) => sum + item.queue.processed + item.events.processed,
46
+ 0,
47
+ );
48
+ if (processed) console.log(`[integration-worker] processados=${processed}`);
49
+ }
50
+ if (!once && !stopping) await delay(intervalMs);
51
+ } while (!once && !stopping);
52
+
53
+ return { stopped: true };
54
+ }
55
+
56
+ module.exports = { drainOnce, runIntegrationWorker };
@@ -0,0 +1,252 @@
1
+ "use strict";
2
+
3
+ const axios = require("axios");
4
+ const GenericError = require("../../core/errors/GenericError");
5
+ const {
6
+ sanitizeIntegrationError,
7
+ sanitizeIntegrationValue,
8
+ } = require("../integrations/sanitizer");
9
+
10
+ const RETRYABLE_HTTP = new Set([408, 425, 429, 500, 502, 503, 504]);
11
+
12
+ class OmieApiError extends GenericError {
13
+ constructor(message, options = {}) {
14
+ super(message, {
15
+ statusCode: options.statusCode || 502,
16
+ details: options.details,
17
+ cause: options.cause,
18
+ });
19
+ this.name = "OmieApiError";
20
+ this.code = options.code || "OMIE_API_ERROR";
21
+ this.retryable = options.retryable !== false;
22
+ this.response = options.response;
23
+ this.trace = options.trace;
24
+ }
25
+ }
26
+
27
+ function getPath(value, path, fallback) {
28
+ if (!path) return value ?? fallback;
29
+ const result = String(path).split(".").filter(Boolean).reduce(
30
+ (current, part) => (current == null ? undefined : current[part]),
31
+ value,
32
+ );
33
+ return result === undefined ? fallback : result;
34
+ }
35
+
36
+ function resolveTemplate(value, scope = {}) {
37
+ if (Array.isArray(value)) return value.map((item) => resolveTemplate(item, scope));
38
+ if (!value || typeof value !== "object") {
39
+ if (typeof value !== "string" || !value.startsWith("$")) return value;
40
+ const [root, ...parts] = value.slice(1).split(".");
41
+ return getPath(scope[root], parts.join("."));
42
+ }
43
+ if (Object.prototype.hasOwnProperty.call(value, "$value")) return value.$value;
44
+ if (Object.prototype.hasOwnProperty.call(value, "$path")) {
45
+ const [root, ...parts] = String(value.$path).replace(/^\$/, "").split(".");
46
+ return getPath(scope[root], parts.join("."), value.default);
47
+ }
48
+ return Object.fromEntries(
49
+ Object.entries(value).map(([key, item]) => [key, resolveTemplate(item, scope)]),
50
+ );
51
+ }
52
+
53
+ function normalizeEndpoint(baseUrl, endpoint) {
54
+ const base = String(baseUrl || "").trim().replace(/\/+$/, "");
55
+ const value = String(endpoint || "").trim();
56
+ if (!base || !value) {
57
+ throw new OmieApiError("A chamada Omie exige baseUrl e endpoint declarados pela Central.", {
58
+ statusCode: 422,
59
+ retryable: false,
60
+ code: "OMIE_CALL_INVALID",
61
+ });
62
+ }
63
+ const absolute = /^https?:\/\//i.test(value) ? value : `${base}/${value.replace(/^\/+/, "")}`;
64
+ let parsed;
65
+ try {
66
+ parsed = new URL(absolute);
67
+ } catch (error) {
68
+ throw new OmieApiError("Endpoint Omie inválido.", {
69
+ statusCode: 422,
70
+ retryable: false,
71
+ code: "OMIE_ENDPOINT_INVALID",
72
+ cause: error,
73
+ });
74
+ }
75
+ if (!["https:", "http:"].includes(parsed.protocol)) {
76
+ throw new OmieApiError("Endpoint Omie deve usar HTTP ou HTTPS.", {
77
+ statusCode: 422,
78
+ retryable: false,
79
+ code: "OMIE_ENDPOINT_PROTOCOL",
80
+ });
81
+ }
82
+ return parsed.toString();
83
+ }
84
+
85
+ function responseError(body, statusCode) {
86
+ const code = body?.faultcode ?? body?.code ?? body?.codigo_status;
87
+ const description = body?.faultstring ?? body?.description ?? body?.descricao_status;
88
+ if (statusCode >= 200 && statusCode < 300 && !description) return null;
89
+ if (statusCode >= 200 && statusCode < 300 && String(code ?? "0") === "0") return null;
90
+ return new OmieApiError(description || `Omie retornou HTTP ${statusCode}.`, {
91
+ statusCode,
92
+ code: String(code || "OMIE_RESPONSE_ERROR"),
93
+ retryable: RETRYABLE_HTTP.has(Number(statusCode)),
94
+ response: sanitizeIntegrationValue(body),
95
+ });
96
+ }
97
+
98
+ function retryableTransportError(error) {
99
+ const statusCode = Number(error?.response?.status || error?.statusCode || 0);
100
+ return RETRYABLE_HTTP.has(statusCode)
101
+ || ["ECONNABORTED", "ECONNRESET", "ETIMEDOUT", "EAI_AGAIN"].includes(error?.code)
102
+ || error?.name === "AbortError";
103
+ }
104
+
105
+ function wait(ms) {
106
+ return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
107
+ }
108
+
109
+ function createOmieClient(options = {}) {
110
+ const credentials = options.credentials || {};
111
+ const appKey = String(credentials.appKey || "").trim();
112
+ const appSecret = String(credentials.appSecret || "").trim();
113
+ const baseUrl = String(credentials.baseUrl || options.baseUrl || "").trim();
114
+ const timeoutMs = Math.max(100, Number(options.timeoutMs || 30_000));
115
+ const maxAttempts = Math.max(1, Number(options.maxAttempts || 3));
116
+ const retryInitialMs = Math.max(0, Number(options.retryInitialMs ?? 500));
117
+ const transport = options.transport || (async ({ url, body, timeout }) => {
118
+ const response = await axios.post(url, body, {
119
+ timeout,
120
+ headers: { "content-type": "application/json", accept: "application/json" },
121
+ validateStatus: () => true,
122
+ });
123
+ return { status: response.status, data: response.data };
124
+ });
125
+
126
+ if (!appKey || !appSecret) {
127
+ throw new OmieApiError("Credenciais Omie não configuradas.", {
128
+ statusCode: 422,
129
+ retryable: false,
130
+ code: "OMIE_CREDENTIALS_MISSING",
131
+ });
132
+ }
133
+
134
+ async function execute(callDefinition, input = {}, context = {}) {
135
+ if (!callDefinition || typeof callDefinition !== "object") {
136
+ throw new OmieApiError("Definição de chamada Omie inválida.", {
137
+ statusCode: 422,
138
+ retryable: false,
139
+ code: "OMIE_CALL_INVALID",
140
+ });
141
+ }
142
+ const url = normalizeEndpoint(baseUrl, callDefinition.endpoint);
143
+ const call = String(callDefinition.call || "").trim();
144
+ if (!call) {
145
+ throw new OmieApiError("A Central deve declarar a operação/call Omie.", {
146
+ statusCode: 422,
147
+ retryable: false,
148
+ code: "OMIE_CALL_MISSING",
149
+ });
150
+ }
151
+ const resolvedParam = resolveTemplate(callDefinition.param ?? [{}], {
152
+ input,
153
+ context,
154
+ cursor: context.cursor,
155
+ });
156
+ const body = {
157
+ call,
158
+ app_key: appKey,
159
+ app_secret: appSecret,
160
+ param: Array.isArray(resolvedParam) ? resolvedParam : [resolvedParam],
161
+ };
162
+ let lastError;
163
+
164
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
165
+ const startedAt = Date.now();
166
+ const trace = {
167
+ endpoint: callDefinition.endpoint,
168
+ call,
169
+ attempt,
170
+ request: sanitizeIntegrationValue({ call, param: body.param }),
171
+ };
172
+ try {
173
+ const response = await transport({
174
+ url,
175
+ body,
176
+ timeout: Number(callDefinition.timeoutMs || timeoutMs),
177
+ attempt,
178
+ });
179
+ const status = Number(response?.status || 200);
180
+ const data = response?.data ?? {};
181
+ trace.httpStatus = status;
182
+ trace.response = sanitizeIntegrationValue(data);
183
+ trace.durationMs = Date.now() - startedAt;
184
+ const normalized = responseError(data, status);
185
+ if (normalized) throw normalized;
186
+ trace.status = "success";
187
+ context.recordRequest?.(trace);
188
+ return { data, trace };
189
+ } catch (error) {
190
+ const normalized = error instanceof OmieApiError
191
+ ? error
192
+ : new OmieApiError(error?.message || "Falha ao chamar a API Omie.", {
193
+ statusCode: Number(error?.response?.status || 502),
194
+ retryable: retryableTransportError(error),
195
+ code: error?.code || "OMIE_TRANSPORT_ERROR",
196
+ response: sanitizeIntegrationValue(error?.response?.data || {}),
197
+ cause: error,
198
+ });
199
+ trace.status = "error";
200
+ trace.durationMs = Date.now() - startedAt;
201
+ trace.error = sanitizeIntegrationError(normalized);
202
+ context.recordRequest?.(trace);
203
+ normalized.trace = trace;
204
+ lastError = normalized;
205
+ if (!normalized.retryable || attempt >= maxAttempts) throw normalized;
206
+ const delay = Math.min(5_000, retryInitialMs * (2 ** (attempt - 1)));
207
+ await wait(delay);
208
+ }
209
+ }
210
+ throw lastError;
211
+ }
212
+
213
+ async function paginate(callDefinition, input = {}, context = {}) {
214
+ const pagination = callDefinition.pagination || {};
215
+ const items = [];
216
+ let page = Number(input.page || pagination.startPage || 1);
217
+ let totalPages = page;
218
+ let lastResponse = {};
219
+ do {
220
+ const pageInput = {
221
+ ...input,
222
+ page,
223
+ pageSize: Number(input.pageSize || pagination.pageSize || 100),
224
+ };
225
+ const result = await execute(callDefinition, pageInput, context);
226
+ lastResponse = result.data;
227
+ const pageItems = getPath(lastResponse, pagination.itemsPath, []);
228
+ if (!Array.isArray(pageItems)) {
229
+ throw new OmieApiError(`itemsPath inválido na chamada ${callDefinition.call}.`, {
230
+ statusCode: 422,
231
+ retryable: false,
232
+ code: "OMIE_ITEMS_PATH_INVALID",
233
+ });
234
+ }
235
+ items.push(...pageItems);
236
+ totalPages = Number(getPath(lastResponse, pagination.totalPagesPath, page)) || page;
237
+ page += 1;
238
+ } while (page <= totalPages && (!pagination.maxPages || page <= Number(pagination.maxPages)));
239
+ return { items, response: lastResponse, totalPages };
240
+ }
241
+
242
+ return { execute, paginate };
243
+ }
244
+
245
+ module.exports = {
246
+ OmieApiError,
247
+ createOmieClient,
248
+ getPath,
249
+ normalizeEndpoint,
250
+ resolveTemplate,
251
+ responseError,
252
+ };