@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.
@@ -1,529 +0,0 @@
1
- "use strict";
2
-
3
- const coreRegistry = require("../../core/registry");
4
- const GenericError = require("../../core/errors/GenericError");
5
- const { registerIntegrationProvider } = require("../integrations/registry");
6
- const { enqueueIntegration, runTrackedOperation } = require("../integrations/runtime");
7
- const { sanitizeIntegrationError, sanitizeIntegrationValue } = require("../integrations/sanitizer");
8
- const { createOmieClient, getPath, resolveTemplate, OmieApiError } = require("./client");
9
- const { collectOmieDefinitions, describeOmieDefinitions } = require("./definitions");
10
- const {
11
- ensureOmieWebhookAccess,
12
- getOmieModels,
13
- listOmieConfigurations,
14
- normalizeInstanceId,
15
- resolveInstanceByWebhookToken,
16
- resolveOmieConfiguration,
17
- rotateOmieWebhookToken,
18
- saveOmieConfiguration,
19
- updateConnectionStatus,
20
- } = require("./models");
21
-
22
- function mapRecord(item, mapping = {}, scope = {}) {
23
- if (typeof mapping === "function") return mapping(item, scope);
24
- return Object.fromEntries(Object.entries(mapping).map(([field, source]) => {
25
- if (typeof source === "string" && !source.startsWith("$")) {
26
- return [field, getPath(item, source)];
27
- }
28
- return [field, resolveTemplate(source, { ...scope, item })];
29
- }));
30
- }
31
-
32
- function assertModel(list) {
33
- const entry = coreRegistry.getModel(list.target.model);
34
- if (!entry?.mongooseModel) {
35
- throw new GenericError(
36
- `A lista Omie ${list.key} referencia a model não registrada ${list.target.model}.`,
37
- { statusCode: 422 },
38
- );
39
- }
40
- return entry.mongooseModel;
41
- }
42
-
43
- async function clientFor(instanceId, options = {}) {
44
- const configuration = await resolveOmieConfiguration(instanceId, { includeSecrets: true });
45
- if (!configuration.active) {
46
- throw new GenericError(`A instância Omie ${configuration.label} está desativada.`, {
47
- statusCode: 503,
48
- });
49
- }
50
- return {
51
- configuration,
52
- client: createOmieClient({
53
- credentials: {
54
- appKey: configuration.appKey,
55
- appSecret: configuration.appSecret,
56
- baseUrl: configuration.baseUrl,
57
- },
58
- timeoutMs: options.timeoutMs,
59
- maxAttempts: options.maxAttempts,
60
- retryInitialMs: options.retryInitialMs,
61
- transport: options.transport,
62
- }),
63
- };
64
- }
65
-
66
- async function executeConfiguredCall(definitions, callKey, input = {}, options = {}) {
67
- const key = String(callKey || "").trim().toLowerCase();
68
- let definition = definitions.calls[key];
69
- if (!definition && definitions.legacy?.[key]) {
70
- const request = await definitions.legacy[key].toOmie(input);
71
- if (!request?.endpoint || !request?.call) {
72
- throw new GenericError(
73
- `O mapping Omie legado ${key} deve retornar endpoint e call.`,
74
- { statusCode: 422 },
75
- );
76
- }
77
- definition = { ...request, key, label: definitions.legacy[key].label };
78
- }
79
- if (!definition) {
80
- throw new GenericError(`Chamada Omie não declarada pela Central: ${key || "não informada"}.`, {
81
- statusCode: 404,
82
- });
83
- }
84
- const instanceId = normalizeInstanceId(options.instanceId || input.instanceId || "default");
85
- const { client, configuration } = await clientFor(instanceId, options);
86
- const context = {
87
- ...(options.context || {}),
88
- instanceId,
89
- cursor: options.cursor,
90
- };
91
- const result = definition.pagination && options.paginate !== false
92
- ? await client.paginate(definition, input, context)
93
- : await client.execute(definition, input, context);
94
- return {
95
- instanceId,
96
- call: key,
97
- configuration: {
98
- instanceId: configuration.instanceId,
99
- label: configuration.label,
100
- environment: configuration.environment,
101
- },
102
- ...result,
103
- };
104
- }
105
-
106
- function effectiveListMode(list, payload = {}) {
107
- const requested = String(payload.mode || "").toLowerCase();
108
- return requested || list.mode;
109
- }
110
-
111
- async function evaluateListFilter(item, filter, scope = {}) {
112
- if (typeof filter !== "function") return { include: true, reason: "" };
113
- const result = await filter(item, scope);
114
- if (result && typeof result === "object" && Object.prototype.hasOwnProperty.call(result, "include")) {
115
- return {
116
- include: result.include !== false,
117
- reason: String(result.reason || "filtered"),
118
- };
119
- }
120
- return {
121
- include: result !== false,
122
- reason: result === false ? "filtered" : "",
123
- };
124
- }
125
-
126
- async function synchronizeOmieList(definitions, list, event, context = {}) {
127
- const mode = effectiveListMode(list, event.payload || {});
128
- if (!["import", "full", "incremental", "individual"].includes(mode)) {
129
- throw new GenericError(`Modo de execução inválido para ${list.label}: ${mode}.`, {
130
- statusCode: 422,
131
- });
132
- }
133
- if (!list.direction || list.direction === "outbound") {
134
- throw new GenericError(`A lista ${list.label} não permite sincronização de entrada.`, {
135
- statusCode: 422,
136
- });
137
- }
138
-
139
- const Model = assertModel(list);
140
- const instanceId = normalizeInstanceId(event.payload?.instanceId || "default");
141
- const { ListState } = getOmieModels();
142
- const stateQuery = { provider: "omie", instanceId, listKey: list.key };
143
- const state = await ListState.findOneAndUpdate(
144
- stateQuery,
145
- { $set: { status: "running", lastStartedAt: new Date(), diagnostic: {} } },
146
- { upsert: true, new: true, setDefaultsOnInsert: true },
147
- ).lean();
148
-
149
- const input = {
150
- ...(event.payload || {}),
151
- instanceId,
152
- pageSize: Number(
153
- event.payload?.pageSize
154
- || list.batchSize
155
- || definitions.calls[list.call]?.pagination?.pageSize
156
- || 100,
157
- ),
158
- cursor: mode === "incremental" ? state.cursor : undefined,
159
- };
160
- const callResult = await executeConfiguredCall(definitions, list.call, input, {
161
- instanceId,
162
- cursor: input.cursor,
163
- paginate: mode !== "individual",
164
- context,
165
- transport: context.transport,
166
- timeoutMs: context.timeoutMs,
167
- maxAttempts: context.maxAttempts,
168
- retryInitialMs: context.retryInitialMs,
169
- });
170
- const callDefinition = definitions.calls[list.call];
171
- const items = Array.isArray(callResult.items)
172
- ? callResult.items
173
- : getPath(callResult.data, list.itemsPath || callDefinition.itemsPath || callDefinition.pagination?.itemsPath, []);
174
- if (!Array.isArray(items)) {
175
- throw new GenericError(`A chamada ${list.call} não retornou uma lista para ${list.label}.`, {
176
- statusCode: 422,
177
- });
178
- }
179
-
180
- const summary = {
181
- mode,
182
- received: items.length,
183
- created: 0,
184
- updated: 0,
185
- skipped: 0,
186
- inactivated: 0,
187
- errors: 0,
188
- };
189
- const seen = [];
190
- const failures = [];
191
-
192
- for (const [index, item] of items.entries()) {
193
- let externalValue;
194
- try {
195
- const scope = {
196
- input,
197
- response: callResult.response || callResult.data,
198
- instanceId,
199
- index,
200
- };
201
- const filterResult = await evaluateListFilter(item, list.filter, scope);
202
- if (!filterResult.include) {
203
- summary.skipped += 1;
204
- context.recordItem?.({
205
- index,
206
- status: "skipped",
207
- reason: filterResult.reason || "filtered",
208
- });
209
- continue;
210
- }
211
- const mapped = mapRecord(item, list.mapping || {}, scope);
212
- const externalField = list.target.externalKey;
213
- externalValue = mapped[externalField]
214
- ?? getPath(item, list.sourceExternalKey || externalField);
215
- if (externalValue === undefined || externalValue === null || externalValue === "") {
216
- throw new GenericError(`Item sem chave externa ${externalField}.`, { statusCode: 422 });
217
- }
218
- mapped[externalField] = externalValue;
219
- seen.push(externalValue);
220
- const current = await Model.findOne({ [externalField]: externalValue }).lean();
221
- if (current && list.policies.conflict === "skip") {
222
- summary.skipped += 1;
223
- continue;
224
- }
225
- if (current && list.policies.conflict === "local-wins") {
226
- const remoteDate = getPath(item, list.remoteUpdatedAtPath);
227
- if (!remoteDate || new Date(current.updatedAt || 0) > new Date(remoteDate)) {
228
- summary.skipped += 1;
229
- continue;
230
- }
231
- }
232
- if (current && list.policies.conflict === "error") {
233
- throw new GenericError(`Conflito com registro local ${externalValue}.`, { statusCode: 409 });
234
- }
235
- if (!current && !list.policies.create) {
236
- summary.skipped += 1;
237
- continue;
238
- }
239
- if (current && !list.policies.update) {
240
- summary.skipped += 1;
241
- continue;
242
- }
243
- await Model.findOneAndUpdate(
244
- { [externalField]: externalValue },
245
- { $set: mapped },
246
- {
247
- upsert: !current,
248
- new: true,
249
- runValidators: true,
250
- setDefaultsOnInsert: true,
251
- context: { integrationOrigin: { provider: "omie", instanceId, list: list.key } },
252
- },
253
- );
254
- if (current) summary.updated += 1;
255
- else summary.created += 1;
256
- context.recordItem?.({ index, externalKey: externalValue, status: current ? "updated" : "created" });
257
- } catch (error) {
258
- summary.errors += 1;
259
- const failure = {
260
- index,
261
- ...(externalValue !== undefined && externalValue !== null && externalValue !== ""
262
- ? { externalKey: sanitizeIntegrationValue(externalValue) }
263
- : {}),
264
- error: sanitizeIntegrationError(error),
265
- };
266
- failures.push(failure);
267
- context.recordItem?.(failure);
268
- context.recordError?.(failure);
269
- }
270
- }
271
-
272
- if (mode === "full" && list.policies.inactivate) {
273
- const activeField = list.target.activeField || "active";
274
- const inactiveValue = list.target.inactiveValue ?? false;
275
- const result = await Model.updateMany(
276
- { [list.target.externalKey]: { $nin: seen } },
277
- { $set: { [activeField]: inactiveValue } },
278
- { runValidators: true },
279
- );
280
- summary.inactivated = Number(result.modifiedCount || 0);
281
- }
282
-
283
- const cursorPath = list.cursorPath || callDefinition.cursorPath || callDefinition.pagination?.cursorPath;
284
- const nextCursor = cursorPath
285
- ? getPath(callResult.response || callResult.data, cursorPath, state.cursor)
286
- : state.cursor;
287
- await ListState.updateOne(stateQuery, {
288
- $set: {
289
- status: summary.errors ? "completed-with-errors" : "completed",
290
- cursor: nextCursor,
291
- lastCompletedAt: new Date(),
292
- summary: sanitizeIntegrationValue(summary),
293
- diagnostic: sanitizeIntegrationValue({ failures }),
294
- },
295
- });
296
- return { ...summary, failures };
297
- }
298
-
299
- async function testOmieConnection(definitions, instanceId = "default", options = {}) {
300
- const call = Object.values(definitions.calls).find((definition) => definition.connectionTest);
301
- if (!call) {
302
- throw new GenericError(
303
- "A Central deve marcar uma chamada Omie com connectionTest: true.",
304
- { statusCode: 422 },
305
- );
306
- }
307
- try {
308
- const result = await executeConfiguredCall(definitions, call.key, options.input || {}, {
309
- ...options,
310
- instanceId,
311
- paginate: false,
312
- });
313
- await updateConnectionStatus(instanceId, { ok: true });
314
- return {
315
- ok: true,
316
- instanceId: normalizeInstanceId(instanceId),
317
- call: call.key,
318
- diagnostic: sanitizeIntegrationValue({
319
- httpStatus: result.trace?.httpStatus,
320
- durationMs: result.trace?.durationMs,
321
- }),
322
- };
323
- } catch (error) {
324
- await updateConnectionStatus(instanceId, { ok: false, error: error?.message });
325
- throw error;
326
- }
327
- }
328
-
329
- async function handleOmieWebhook(definitions, event, context = {}) {
330
- const mapping = definitions.webhooks.find((candidate) => candidate.eventType === event.eventType)
331
- || definitions.webhooks.find((candidate) => candidate.eventType === "*");
332
- if (!mapping) {
333
- context.recordItem?.({
334
- status: "ignored",
335
- reason: "mapping-not-found",
336
- eventType: event.eventType,
337
- });
338
- return { ignored: true, reason: "mapping-not-found", eventType: event.eventType };
339
- }
340
- const instanceId = normalizeInstanceId(event.metadata?.instanceId || "default");
341
- const actions = [];
342
- for (const [index, action] of mapping.actions.entries()) {
343
- const payload = resolveTemplate(action.payload || { $path: "$payload" }, {
344
- payload: event.payload,
345
- event,
346
- metadata: event.metadata || {},
347
- instanceId,
348
- });
349
- const aggregateId = action.aggregateIdPath
350
- ? getPath(event.payload, action.aggregateIdPath, "")
351
- : String(action.aggregateId || "");
352
- const ticket = await enqueueIntegration({
353
- provider: "omie",
354
- handler: action.handler,
355
- resource: mapping.resource || event.resource || "",
356
- operation: "webhook-action",
357
- aggregateType: action.aggregateType || "",
358
- aggregateId,
359
- idempotencyKey: `omie:webhook:${event.deduplicationKey}:${index}:${action.handler}`,
360
- payload: sanitizeIntegrationValue({
361
- ...payload,
362
- instanceId,
363
- __oonIntegrationOrigin: {
364
- provider: "omie",
365
- kind: "webhook",
366
- eventId: String(event._id),
367
- },
368
- }),
369
- });
370
- actions.push({ handler: action.handler, ticketId: String(ticket._id) });
371
- }
372
- return { ignored: false, mapping: mapping.eventType, actions };
373
- }
374
-
375
- function shouldPreventLoop(input = {}) {
376
- return input.__oonIntegrationOrigin?.provider === "omie";
377
- }
378
-
379
- async function enqueueOmieCall(input = {}) {
380
- if (shouldPreventLoop(input.payload || input)) {
381
- return { skipped: true, reason: "bidirectional-loop-prevented" };
382
- }
383
- const call = String(input.call || input.mapping || "").trim().toLowerCase();
384
- if (!call) throw new GenericError("Informe a chamada Omie declarada pela Central.", { statusCode: 422 });
385
- const instanceId = normalizeInstanceId(input.instanceId || "default");
386
- return enqueueIntegration({
387
- provider: "omie",
388
- handler: "OMIE_CALL_EXECUTE",
389
- resource: String(input.resource || call),
390
- operation: String(input.operation || "call"),
391
- aggregateType: String(input.aggregateType || ""),
392
- aggregateId: String(input.aggregateId || ""),
393
- idempotencyKey: String(
394
- input.idempotencyKey
395
- || `omie:${instanceId}:${call}:${input.aggregateType || "call"}:${input.aggregateId || Date.now()}`,
396
- ),
397
- payload: sanitizeIntegrationValue({
398
- instanceId,
399
- call,
400
- input: input.payload || input.input || {},
401
- }),
402
- });
403
- }
404
-
405
- function omieEnabled(config = {}) {
406
- return Boolean(
407
- config.appManifest?.modules?.omie
408
- || config.modules?.omie
409
- || coreRegistry.listOmieMappings().length,
410
- );
411
- }
412
-
413
- function ensureOmieProviderRegistered(config = {}) {
414
- if (!omieEnabled(config)) return null;
415
- const definitions = collectOmieDefinitions();
416
- const resources = definitions.lists.map((list) => ({
417
- key: list.key,
418
- label: list.label,
419
- description: list.description,
420
- syncHandler: "OMIE_LIST_SYNC",
421
- includeInFullSync: list.includeInFullSync,
422
- order: list.order,
423
- operations: ["sync", list.mode, list.direction],
424
- }));
425
- const handlers = {
426
- OMIE_CALL_EXECUTE: async (event, context) => {
427
- const call = event.payload?.call;
428
- return executeConfiguredCall(definitions, call, event.payload?.input || {}, {
429
- instanceId: event.payload?.instanceId,
430
- context,
431
- });
432
- },
433
- OMIE_LIST_SYNC: async (event, context) => {
434
- const list = definitions.lists.find((candidate) => candidate.key === event.resource);
435
- if (!list) throw new GenericError(`Lista Omie não declarada: ${event.resource}.`, { statusCode: 404 });
436
- return synchronizeOmieList(definitions, list, event, context);
437
- },
438
- ...definitions.handlers,
439
- };
440
-
441
- return registerIntegrationProvider({
442
- key: "omie",
443
- replace: true,
444
- label: "Omie",
445
- description: "Infraestrutura Omie nativa; chamadas, listas, mappings e ações são declarados pela Central.",
446
- enabled: true,
447
- capabilities: ["connection-test", "declarative-calls", "listas-omie", "webhooks"],
448
- maxAttempts: 5,
449
- retryPolicy: { strategy: "exponential", initialMs: 60_000, maxMs: 21_600_000, jitter: true },
450
- classifyError(error) {
451
- return {
452
- retryable: error instanceof OmieApiError ? error.retryable : error?.retryable !== false,
453
- code: error?.code,
454
- };
455
- },
456
- resources,
457
- handlers,
458
- webhooks: {
459
- "*": (event, context) => handleOmieWebhook(definitions, event, context),
460
- },
461
- async verifyWebhook({ token }) {
462
- const instanceId = await resolveInstanceByWebhookToken(token);
463
- return instanceId ? { accepted: true, metadata: { instanceId } } : false;
464
- },
465
- configuration: {
466
- enabled: true,
467
- multiInstance: true,
468
- fields: [
469
- { key: "instanceId", label: "Instância", readOnly: true },
470
- { key: "label", label: "Nome" },
471
- { key: "active", label: "Ativa", type: "boolean" },
472
- { key: "environment", label: "Ambiente", type: "select", options: ["production", "homologation"] },
473
- { key: "baseUrl", label: "URL base" },
474
- { key: "appKey", label: "App Key", type: "password", writeOnly: true },
475
- { key: "appSecret", label: "App Secret", type: "password", writeOnly: true },
476
- { key: "webhookToken", label: "Token do webhook", type: "password", writeOnly: true },
477
- ],
478
- },
479
- getConfiguration: listOmieConfigurations,
480
- saveConfiguration: saveOmieConfiguration,
481
- testConnection: (instanceId, options) => testOmieConnection(definitions, instanceId, options),
482
- getWebhookAccess: (instanceId) => ensureOmieWebhookAccess(instanceId),
483
- rotateWebhookToken: (instanceId) => rotateOmieWebhookToken(instanceId),
484
- executeCall: (callKey, input, options) => runTrackedOperation({
485
- provider: "omie",
486
- resource: callKey,
487
- operation: "call",
488
- title: `Omie: ${callKey}`,
489
- metadata: { source: options?.source || "api", instanceId: options?.instanceId || "default" },
490
- runner: (context) => executeConfiguredCall(definitions, callKey, input, { ...options, context }),
491
- }),
492
- describe: async () => {
493
- const descriptor = describeOmieDefinitions(definitions);
494
- const { ListState } = getOmieModels();
495
- const states = await ListState.find({ provider: "omie" }).lean();
496
- return {
497
- ...descriptor,
498
- lists: descriptor.lists.map((list) => ({
499
- ...list,
500
- executions: states
501
- .filter((state) => state.listKey === list.key)
502
- .map((state) => sanitizeIntegrationValue({
503
- instanceId: state.instanceId,
504
- status: state.status,
505
- cursor: state.cursor,
506
- lastStartedAt: state.lastStartedAt,
507
- lastCompletedAt: state.lastCompletedAt,
508
- summary: state.summary,
509
- diagnostic: state.diagnostic,
510
- })),
511
- })),
512
- };
513
- },
514
- });
515
- }
516
-
517
- module.exports = {
518
- clientFor,
519
- enqueueOmieCall,
520
- ensureOmieProviderRegistered,
521
- evaluateListFilter,
522
- executeConfiguredCall,
523
- handleOmieWebhook,
524
- mapRecord,
525
- omieEnabled,
526
- shouldPreventLoop,
527
- synchronizeOmieList,
528
- testOmieConnection,
529
- };