@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oondemand/oon-core-back",
3
- "version": "0.3.57",
3
+ "version": "0.3.58",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oondemand/oon-platform.git",
@@ -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 syncIntegrationIndexes() {
76
- const enabled = Boolean(
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 (!enabled || !shouldSyncDeclaredIndexes()) return [];
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 Object.values(getIntegrationModels())) {
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 syncIntegrationIndexes();
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
  };
@@ -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 { omie } = require("./modules/omie");
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 accepted = await provider.verifyWebhook({
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 sanitizedPayload = sanitizeIntegrationValue(input.payload || {});
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(input.externalEventId || "").trim();
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(input.resource || ""),
33
- eventType: String(input.eventType || "unknown"),
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
+ };