@oondemand/oon-core-back 0.3.57 → 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.
- package/package.json +1 -1
- package/src/boot/database.js +14 -5
- package/src/boot/loadCentral.js +5 -1
- package/src/index.js +12 -2
- package/src/modules/integrations/models.js +1 -0
- package/src/modules/integrations/registry.js +17 -0
- package/src/modules/integrations/routes.js +48 -0
- package/src/modules/integrations/runtime/inbox.js +16 -5
- package/src/modules/omie/client.js +252 -0
- package/src/modules/omie/definitions.js +206 -0
- package/src/modules/omie/index.js +27 -63
- package/src/modules/omie/models.js +308 -0
- package/src/modules/omie/provider.js +490 -0
package/package.json
CHANGED
package/src/boot/database.js
CHANGED
|
@@ -3,6 +3,7 @@ const registry = require("../core/registry");
|
|
|
3
3
|
const ctx = require("../core/context");
|
|
4
4
|
const { getIntegrationModels } = require("../modules/integrations/models");
|
|
5
5
|
const { listIntegrationProviders } = require("../modules/integrations/registry");
|
|
6
|
+
const { getOmieModels, omieEnabled } = require("../modules/omie");
|
|
6
7
|
|
|
7
8
|
function buildMongoUri() {
|
|
8
9
|
const cfg = {
|
|
@@ -72,16 +73,19 @@ async function syncDeclaredIndexes(entries = registry.listModels()) {
|
|
|
72
73
|
return results;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
async function
|
|
76
|
-
const
|
|
76
|
+
async function syncTechnicalIndexes() {
|
|
77
|
+
const integrationsEnabled = Boolean(
|
|
77
78
|
ctx.config.appManifest?.modules?.integrations
|
|
78
79
|
|| ctx.config.modules?.integrations
|
|
79
80
|
|| listIntegrationProviders().length,
|
|
80
81
|
);
|
|
81
|
-
if (!
|
|
82
|
+
if (!shouldSyncDeclaredIndexes() || !integrationsEnabled) return [];
|
|
83
|
+
|
|
84
|
+
const models = Object.values(getIntegrationModels());
|
|
85
|
+
if (omieEnabled(ctx.config)) models.push(...Object.values(getOmieModels()));
|
|
82
86
|
|
|
83
87
|
const results = [];
|
|
84
|
-
for (const model of
|
|
88
|
+
for (const model of models) {
|
|
85
89
|
const dropped = await model.syncIndexes();
|
|
86
90
|
const normalized = Array.isArray(dropped) ? dropped : [];
|
|
87
91
|
results.push({ model: model.modelName, dropped: normalized });
|
|
@@ -92,11 +96,15 @@ async function syncIntegrationIndexes() {
|
|
|
92
96
|
return results;
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
async function syncIntegrationIndexes() {
|
|
100
|
+
return syncTechnicalIndexes();
|
|
101
|
+
}
|
|
102
|
+
|
|
95
103
|
async function connectDB() {
|
|
96
104
|
const uri = buildMongoUri();
|
|
97
105
|
await mongoose.connect(uri, connectionOptions());
|
|
98
106
|
await syncDeclaredIndexes();
|
|
99
|
-
await
|
|
107
|
+
await syncTechnicalIndexes();
|
|
100
108
|
console.log(`Conectado ao MongoDB (${databaseLabel(uri)})`);
|
|
101
109
|
return mongoose.connection;
|
|
102
110
|
}
|
|
@@ -109,4 +117,5 @@ module.exports = {
|
|
|
109
117
|
shouldSyncDeclaredIndexes,
|
|
110
118
|
syncDeclaredIndexes,
|
|
111
119
|
syncIntegrationIndexes,
|
|
120
|
+
syncTechnicalIndexes,
|
|
112
121
|
};
|
package/src/boot/loadCentral.js
CHANGED
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
PROCESS_MANIFEST_FILENAME,
|
|
16
16
|
loadProcessManifest,
|
|
17
17
|
} = require("../manifest/processManifest");
|
|
18
|
+
const { ensureOmieProviderRegistered } = require("../modules/omie");
|
|
18
19
|
|
|
19
20
|
const DOMAIN_DIRS = [
|
|
20
21
|
"models",
|
|
@@ -91,6 +92,9 @@ function loadCentral(cwd = process.cwd()) {
|
|
|
91
92
|
if (fs.existsSync(full)) requireDir(full);
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
// O adaptador nativo é montado depois das declarações da Central. Assim,
|
|
96
|
+
// endpoints, mappings, listas e ações permanecem fora do pacote técnico.
|
|
97
|
+
ensureOmieProviderRegistered(ctx.config);
|
|
94
98
|
return ctx.config;
|
|
95
99
|
}
|
|
96
100
|
|
|
@@ -107,4 +111,4 @@ module.exports = {
|
|
|
107
111
|
DOMAIN_DIRS,
|
|
108
112
|
APP_CONFIG_KEYS,
|
|
109
113
|
assertConfigBoundary,
|
|
110
|
-
};
|
|
114
|
+
};
|
package/src/index.js
CHANGED
|
@@ -7,7 +7,7 @@ const define = require("./core/define");
|
|
|
7
7
|
const fields = require("./core/fields");
|
|
8
8
|
const registry = require("./core/registry");
|
|
9
9
|
const GenericError = require("./core/errors/GenericError");
|
|
10
|
-
const
|
|
10
|
+
const omieModule = require("./modules/omie");
|
|
11
11
|
const integrations = require("./modules/integrations");
|
|
12
12
|
const appManifest = require("./manifest/appManifest");
|
|
13
13
|
const domainManifest = require("./manifest/domainManifest");
|
|
@@ -71,7 +71,17 @@ module.exports = {
|
|
|
71
71
|
drainProcessJobs: processRuntime.drainProcessJobs,
|
|
72
72
|
|
|
73
73
|
fields,
|
|
74
|
-
omie,
|
|
74
|
+
omie: omieModule.omie,
|
|
75
|
+
omieModule,
|
|
76
|
+
createOmieClient: omieModule.createOmieClient,
|
|
77
|
+
OmieApiError: omieModule.OmieApiError,
|
|
78
|
+
enqueueOmieCall: omieModule.enqueueOmieCall,
|
|
79
|
+
ensureOmieProviderRegistered: omieModule.ensureOmieProviderRegistered,
|
|
80
|
+
collectOmieDefinitions: omieModule.collectOmieDefinitions,
|
|
81
|
+
describeOmieDefinitions: omieModule.describeOmieDefinitions,
|
|
82
|
+
saveOmieConfiguration: omieModule.saveOmieConfiguration,
|
|
83
|
+
listOmieConfigurations: omieModule.listOmieConfigurations,
|
|
84
|
+
testOmieConnection: omieModule.testOmieConnection,
|
|
75
85
|
integrations,
|
|
76
86
|
registerIntegrationProvider: integrations.registerIntegrationProvider,
|
|
77
87
|
enqueueIntegration: integrations.enqueueIntegration,
|
|
@@ -59,6 +59,7 @@ function inboxSchema() {
|
|
|
59
59
|
deduplicationKey: { type: String, required: true, unique: true },
|
|
60
60
|
payloadHash: { type: String, required: true, index: true },
|
|
61
61
|
payload: jsonField({}),
|
|
62
|
+
metadata: jsonField({}),
|
|
62
63
|
receivedAt: { type: Date, default: Date.now, index: true },
|
|
63
64
|
status: { type: String, enum: INBOX_STATUSES, default: "Pendente", index: true },
|
|
64
65
|
attempts: { type: Number, min: 0, default: 0 },
|
|
@@ -34,6 +34,22 @@ function normalizeResources(resources = []) {
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function publicConfiguration(configuration) {
|
|
38
|
+
if (!configuration) return null;
|
|
39
|
+
return {
|
|
40
|
+
enabled: configuration.enabled !== false,
|
|
41
|
+
multiInstance: configuration.multiInstance === true,
|
|
42
|
+
fields: (configuration.fields || []).map((field) => ({
|
|
43
|
+
key: field.key,
|
|
44
|
+
label: field.label || field.key,
|
|
45
|
+
type: field.type || "text",
|
|
46
|
+
readOnly: field.readOnly === true,
|
|
47
|
+
writeOnly: field.writeOnly === true,
|
|
48
|
+
options: field.options || undefined,
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
function metadataFor(provider) {
|
|
38
54
|
return {
|
|
39
55
|
id: provider.key,
|
|
@@ -45,6 +61,7 @@ function metadataFor(provider) {
|
|
|
45
61
|
? (provider.enabled() ? "ativo" : "inativo")
|
|
46
62
|
: (provider.enabled === false ? "inativo" : "ativo"),
|
|
47
63
|
capabilities: [...(provider.capabilities || [])],
|
|
64
|
+
configuration: publicConfiguration(provider.configuration),
|
|
48
65
|
resources: provider.resources.map((resource) => ({
|
|
49
66
|
key: resource.key,
|
|
50
67
|
label: resource.label,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const express = require("express");
|
|
4
|
+
const GenericError = require("../../core/errors/GenericError");
|
|
4
5
|
const auth = require("../../core/middlewares/auth");
|
|
5
6
|
const { rbac } = require("../../core/middlewares/rbac");
|
|
6
7
|
const { asyncHandler } = require("../../core/utils/helpers");
|
|
@@ -32,6 +33,15 @@ function pagination(query = {}) {
|
|
|
32
33
|
return { limit, page, skip: (page - 1) * limit };
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
function requireProviderExtension(provider, method, message) {
|
|
37
|
+
if (typeof provider[method] !== "function") {
|
|
38
|
+
throw new GenericError(message || `O provedor ${provider.label} não oferece ${method}.`, {
|
|
39
|
+
statusCode: 404,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return provider[method].bind(provider);
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
function buildIntegrationRouter() {
|
|
36
46
|
const router = express.Router();
|
|
37
47
|
|
|
@@ -57,6 +67,44 @@ function buildIntegrationRouter() {
|
|
|
57
67
|
const metadata = listIntegrationProviders().find((item) => item.key === provider.key);
|
|
58
68
|
res.json({ provider: provider.key, resources: metadata?.resources || [] });
|
|
59
69
|
});
|
|
70
|
+
router.get("/providers/:provider/configuration", rbac(READ_ROLES), asyncHandler(async (req, res) => {
|
|
71
|
+
const provider = getIntegrationProvider(req.params.provider);
|
|
72
|
+
const getConfiguration = requireProviderExtension(
|
|
73
|
+
provider,
|
|
74
|
+
"getConfiguration",
|
|
75
|
+
`O provedor ${provider.label} não possui configuração gerenciada pelo Core.`,
|
|
76
|
+
);
|
|
77
|
+
res.json({ provider: provider.key, instances: sanitizeIntegrationValue(await getConfiguration()) });
|
|
78
|
+
}));
|
|
79
|
+
router.put("/providers/:provider/configuration/:instanceId", rbac(WRITE_ROLES), asyncHandler(async (req, res) => {
|
|
80
|
+
const provider = getIntegrationProvider(req.params.provider);
|
|
81
|
+
const saveConfiguration = requireProviderExtension(provider, "saveConfiguration");
|
|
82
|
+
const saved = await saveConfiguration(req.params.instanceId, req.body || {});
|
|
83
|
+
res.json({ provider: provider.key, configuration: sanitizeIntegrationValue(saved) });
|
|
84
|
+
}));
|
|
85
|
+
router.post("/providers/:provider/configuration/:instanceId/test", rbac(WRITE_ROLES), asyncHandler(async (req, res) => {
|
|
86
|
+
const provider = getIntegrationProvider(req.params.provider);
|
|
87
|
+
const testConnection = requireProviderExtension(provider, "testConnection");
|
|
88
|
+
res.json(await testConnection(req.params.instanceId, {
|
|
89
|
+
input: req.body || {},
|
|
90
|
+
source: "api",
|
|
91
|
+
requestId: req.requestId || "",
|
|
92
|
+
}));
|
|
93
|
+
}));
|
|
94
|
+
router.get("/providers/:provider/mappings", rbac(READ_ROLES), asyncHandler(async (req, res) => {
|
|
95
|
+
const provider = getIntegrationProvider(req.params.provider);
|
|
96
|
+
const describe = requireProviderExtension(provider, "describe");
|
|
97
|
+
res.json({ provider: provider.key, ...sanitizeIntegrationValue(await describe()) });
|
|
98
|
+
}));
|
|
99
|
+
router.post("/providers/:provider/calls/:call", rbac(WRITE_ROLES), asyncHandler(async (req, res) => {
|
|
100
|
+
const provider = getIntegrationProvider(req.params.provider);
|
|
101
|
+
const executeCall = requireProviderExtension(provider, "executeCall");
|
|
102
|
+
res.json(await executeCall(req.params.call, req.body?.input || req.body || {}, {
|
|
103
|
+
instanceId: req.body?.instanceId || "default",
|
|
104
|
+
source: "api",
|
|
105
|
+
requestId: req.requestId || "",
|
|
106
|
+
}));
|
|
107
|
+
}));
|
|
60
108
|
router.get("/health", rbac(READ_ROLES), asyncHandler(async (_req, res) => {
|
|
61
109
|
res.json({ results: await integrationHealth() });
|
|
62
110
|
}));
|
|
@@ -10,18 +10,24 @@ const { DEFAULT_LEASE_MS, calculateBackoff, classifyError, isEnabled, payloadHas
|
|
|
10
10
|
|
|
11
11
|
async function receiveWebhook(input) {
|
|
12
12
|
const provider = getIntegrationProvider(input.provider);
|
|
13
|
+
let verificationMetadata = {};
|
|
13
14
|
if (typeof provider.verifyWebhook === "function") {
|
|
14
|
-
const
|
|
15
|
+
const verification = await provider.verifyWebhook({
|
|
15
16
|
token: input.token,
|
|
16
17
|
headers: input.headers || {},
|
|
17
18
|
payload: input.payload,
|
|
18
19
|
});
|
|
20
|
+
const accepted = verification === true || verification?.accepted === true;
|
|
19
21
|
if (!accepted) throw new GenericError("Webhook de integração não autorizado.", { statusCode: 401 });
|
|
22
|
+
verificationMetadata = sanitizeIntegrationValue(verification?.metadata || {});
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
const
|
|
25
|
+
const normalized = typeof provider.normalizeWebhook === "function"
|
|
26
|
+
? await provider.normalizeWebhook({ ...input, metadata: verificationMetadata })
|
|
27
|
+
: input;
|
|
28
|
+
const sanitizedPayload = sanitizeIntegrationValue(normalized.payload || {});
|
|
23
29
|
const hash = payloadHash(sanitizedPayload);
|
|
24
|
-
const externalEventId = String(
|
|
30
|
+
const externalEventId = String(normalized.externalEventId || "").trim();
|
|
25
31
|
const deduplicationKey = `${provider.key}:${externalEventId || hash}`;
|
|
26
32
|
const { Inbox } = getIntegrationModels();
|
|
27
33
|
return Inbox.findOneAndUpdate(
|
|
@@ -29,12 +35,16 @@ async function receiveWebhook(input) {
|
|
|
29
35
|
{
|
|
30
36
|
$setOnInsert: {
|
|
31
37
|
provider: provider.key,
|
|
32
|
-
resource: String(
|
|
33
|
-
eventType: String(
|
|
38
|
+
resource: String(normalized.resource || ""),
|
|
39
|
+
eventType: String(normalized.eventType || "unknown"),
|
|
34
40
|
externalEventId,
|
|
35
41
|
deduplicationKey,
|
|
36
42
|
payloadHash: hash,
|
|
37
43
|
payload: sanitizedPayload,
|
|
44
|
+
metadata: sanitizeIntegrationValue({
|
|
45
|
+
...verificationMetadata,
|
|
46
|
+
...(normalized.metadata || {}),
|
|
47
|
+
}),
|
|
38
48
|
receivedAt: new Date(),
|
|
39
49
|
status: "Pendente",
|
|
40
50
|
attempts: 0,
|
|
@@ -76,6 +86,7 @@ async function processWebhookInbox(options = {}) {
|
|
|
76
86
|
source: "inbox",
|
|
77
87
|
inboxId: String(event._id),
|
|
78
88
|
externalEventId: event.externalEventId || "",
|
|
89
|
+
...sanitizeIntegrationValue(event.metadata || {}),
|
|
79
90
|
},
|
|
80
91
|
runner: (trace) => handler(event, {
|
|
81
92
|
...options,
|
|
@@ -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
|
+
};
|