n8n-nodes-aib 0.5.1 → 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.
@@ -28,18 +28,23 @@ class AibDigitalApi {
28
28
  description: "Gerada em Integrações → Chaves de API. Use uma chave de agência para escolher a conta dentro do nó.",
29
29
  },
30
30
  ];
31
- authenticate = {
32
- type: "generic",
33
- properties: {
34
- headers: {
35
- Authorization: "=Bearer {{$credentials.apiKey}}",
36
- },
37
- },
38
- };
31
+ // Sem `authenticate` de propósito. O n8n injeta um "Custom API Call" no
32
+ // fim de toda lista chamada `resource` ou `operation` quando a credencial
33
+ // define esse campo — a regra é literal, sem como desligar:
34
+ //
35
+ // if (credType.authenticate !== undefined) return true;
36
+ // — load-nodes-and-credentials.js, supportsProxyAuth()
37
+ //
38
+ // A opção caía no meio das operações de verdade, e quem a escolhesse via
39
+ // o n8n esconder todos os outros campos do nó. O nó nunca dependeu disso:
40
+ // ele monta o `Authorization` na mão em `api()`. O que se perde é a
41
+ // credencial aparecer como tipo predefinido no nó HTTP Request.
39
42
  test = {
40
43
  request: {
41
44
  baseURL: "={{$credentials.baseUrl}}",
42
45
  url: "/api/v1/ping",
46
+ // Com `authenticate` fora, o cabeçalho do teste vem daqui.
47
+ headers: { Authorization: "=Bearer {{$credentials.apiKey}}" },
43
48
  },
44
49
  };
45
50
  }
@@ -31,18 +31,45 @@ async function api(ctx, method, path, body, qs) {
31
31
  });
32
32
  }
33
33
  }
34
+ /**
35
+ * Lê um parâmetro que pode ser texto (nó v1) ou seletor de recurso (v2).
36
+ *
37
+ * O resourceLocator guarda `{ mode, value }` onde a v1 guardava a string
38
+ * crua. As duas versões convivem no mesmo `execute`, então quem lê
39
+ * normaliza aqui em vez de espalhar `typeof` pelo arquivo.
40
+ */
41
+ function idDe(valor) {
42
+ if (valor && typeof valor === "object" && "value" in valor) {
43
+ return String(valor.value ?? "");
44
+ }
45
+ return valor ? String(valor) : "";
46
+ }
34
47
  /** A conta escolhida no nó (vazio = a conta da própria chave). */
35
48
  function contaQs(ctx, i) {
36
- const conta = ctx.getNodeParameter("conta", i, "");
49
+ const conta = idDe(ctx.getNodeParameter("conta", i, ""));
37
50
  return conta ? { conta } : {};
38
51
  }
52
+ /** A conta escolhida, do ponto de vista dos carregadores de lista. */
53
+ function contaDeLista(ctx) {
54
+ try {
55
+ return idDe(ctx.getNodeParameter("conta", ""));
56
+ }
57
+ catch {
58
+ return "";
59
+ }
60
+ }
39
61
  class AibDigital {
40
62
  description = {
41
63
  displayName: "AIB Digital",
42
64
  icon: "file:aibdigital.svg",
43
65
  name: "aibDigital",
44
66
  group: ["output"],
45
- version: 1,
67
+ // v2 troca os menus de Conta/Ambiente pelo seletor de recurso do n8n
68
+ // (o "Da lista | Por ID", com busca) e ganha a Caixa de entrada. O que
69
+ // ele guarda muda de texto para `{ mode, value }`, então a v1 fica de
70
+ // pé: fluxo antigo continua na v1, nó novo nasce na v2.
71
+ version: [1, 2],
72
+ defaultVersion: 2,
46
73
  subtitle: '={{$parameter["operation"]}}',
47
74
  description: "Opera o Sistema AIB Digital: conversas, mensagens e funil",
48
75
  defaults: { name: "AIB Digital" },
@@ -58,7 +85,30 @@ class AibDigital {
58
85
  type: "options",
59
86
  typeOptions: { loadOptionsMethod: "getContas" },
60
87
  default: "",
88
+ displayOptions: { show: { "@version": [1] } },
89
+ description: "O cliente. Com chave de agência, escolha aqui; com chave de conta, deixe vazio.",
90
+ },
91
+ {
92
+ displayName: "Conta",
93
+ name: "conta",
94
+ type: "resourceLocator",
95
+ default: { mode: "list", value: "" },
96
+ displayOptions: { show: { "@version": [2] } },
61
97
  description: "O cliente. Com chave de agência, escolha aqui; com chave de conta, deixe vazio.",
98
+ modes: [
99
+ {
100
+ displayName: "Da lista",
101
+ name: "list",
102
+ type: "list",
103
+ typeOptions: { searchListMethod: "buscarContas", searchable: true },
104
+ },
105
+ {
106
+ displayName: "Por ID",
107
+ name: "id",
108
+ type: "string",
109
+ hint: "O UUID da conta. Vazio = a conta da própria chave.",
110
+ },
111
+ ],
62
112
  },
63
113
  // ── Recurso → Operação, como no nó do Chatwoot ──────────────
64
114
  {
@@ -265,8 +315,49 @@ class AibDigital {
265
315
  type: "options",
266
316
  typeOptions: { loadOptionsMethod: "getAmbientes", loadOptionsDependsOn: ["conta"] },
267
317
  default: "",
268
- displayOptions: { show: { operation: ["selecionarConta", "criarFunil", "criarEtiqueta"] } },
318
+ displayOptions: {
319
+ show: { "@version": [1], operation: ["selecionarConta", "criarFunil", "criarEtiqueta"] },
320
+ },
321
+ description: "Onde o funil/dado vai nascer. Conta com 2+ ambientes exige escolher aqui.",
322
+ },
323
+ {
324
+ displayName: "Ambiente",
325
+ name: "ambienteId",
326
+ type: "resourceLocator",
327
+ default: { mode: "list", value: "" },
328
+ displayOptions: {
329
+ show: { "@version": [2], operation: ["selecionarConta", "criarFunil", "criarEtiqueta"] },
330
+ },
269
331
  description: "Onde o funil/dado vai nascer. Conta com 2+ ambientes exige escolher aqui.",
332
+ modes: [
333
+ {
334
+ displayName: "Da lista",
335
+ name: "list",
336
+ type: "list",
337
+ typeOptions: { searchListMethod: "buscarAmbientes", searchable: true },
338
+ },
339
+ { displayName: "Por ID", name: "id", type: "string" },
340
+ ],
341
+ },
342
+ {
343
+ // A outra metade do que o atendimento precisa saber antes de agir:
344
+ // escolhida a conta, escolher o WhatsApp em que se opera. Fica na
345
+ // âncora do fluxo, para os nós seguintes lerem `.caixa_id`.
346
+ displayName: "Caixa de entrada",
347
+ name: "caixaId",
348
+ type: "resourceLocator",
349
+ default: { mode: "list", value: "" },
350
+ displayOptions: { show: { "@version": [2], operation: ["selecionarConta"] } },
351
+ description: "O WhatsApp da conta. Vazio = o fluxo não se prende a uma caixa.",
352
+ modes: [
353
+ {
354
+ displayName: "Da lista",
355
+ name: "list",
356
+ type: "list",
357
+ typeOptions: { searchListMethod: "buscarCaixas", searchable: true },
358
+ },
359
+ { displayName: "Por ID", name: "id", type: "string" },
360
+ ],
270
361
  },
271
362
  {
272
363
  displayName: "Funil",
@@ -354,6 +445,80 @@ class AibDigital {
354
445
  ],
355
446
  };
356
447
  methods = {
448
+ // O seletor de recurso da v2. Devolve `{ results }` e filtra pelo que a
449
+ // pessoa digita — é o que dá a busca que o menu simples não tinha.
450
+ listSearch: {
451
+ async buscarContas(filtro) {
452
+ let contas = [];
453
+ try {
454
+ const res = await api(this, "GET", "/api/v1/contas");
455
+ contas = (res?.contas ?? []);
456
+ }
457
+ catch {
458
+ // Chave de conta não lista contas (403): sobra ela mesma, que é
459
+ // o que o valor vazio já significa.
460
+ return { results: [{ name: "— A conta da chave —", value: "" }] };
461
+ }
462
+ const cred = (await this.getCredentials("aibDigitalApi"));
463
+ const base = String(cred?.baseUrl ?? "").replace(/\/+$/, "");
464
+ const alvo = (filtro ?? "").toLowerCase();
465
+ const results = [
466
+ { name: "— A conta da chave —", value: "" },
467
+ ...contas
468
+ .filter((c) => !alvo || c.nome.toLowerCase().includes(alvo))
469
+ .map((c) => ({
470
+ name: ["ativa", "ativo", "active"].includes(c.status) ? c.nome : `${c.nome} (suspensa)`,
471
+ value: c.id,
472
+ url: base ? `${base}/inbox` : undefined,
473
+ })),
474
+ ];
475
+ return { results };
476
+ },
477
+ async buscarAmbientes(filtro) {
478
+ const conta = contaDeLista(this);
479
+ try {
480
+ const res = await api(this, "GET", "/api/v1/ambientes", undefined, conta ? { conta } : undefined);
481
+ const ambientes = (res?.ambientes ?? []);
482
+ if (ambientes.length <= 1) {
483
+ return {
484
+ results: [{ name: "— Único ambiente da conta —", value: ambientes[0]?.id ?? "" }],
485
+ };
486
+ }
487
+ const alvo = (filtro ?? "").toLowerCase();
488
+ return {
489
+ results: ambientes
490
+ .filter((a) => !alvo || a.nome.toLowerCase().includes(alvo))
491
+ .map((a) => ({ name: a.nome, value: a.id })),
492
+ };
493
+ }
494
+ catch {
495
+ return { results: [{ name: "— Único ambiente da conta —", value: "" }] };
496
+ }
497
+ },
498
+ async buscarCaixas(filtro) {
499
+ const conta = contaDeLista(this);
500
+ try {
501
+ const res = await api(this, "GET", "/api/v1/caixas", undefined, conta ? { conta } : undefined);
502
+ const caixas = (res?.caixas ?? []);
503
+ const alvo = (filtro ?? "").toLowerCase();
504
+ return {
505
+ results: caixas
506
+ .filter((c) => !alvo ||
507
+ c.nome?.toLowerCase().includes(alvo) ||
508
+ (c.telefone ?? "").includes(alvo))
509
+ // O telefone entra no rótulo porque é ele que distingue duas
510
+ // caixas com nome parecido — "Comercial" e "Comercial 2".
511
+ .map((c) => ({
512
+ name: c.telefone ? `${c.nome} · ${c.telefone}` : c.nome,
513
+ value: c.id,
514
+ })),
515
+ };
516
+ }
517
+ catch {
518
+ return { results: [] };
519
+ }
520
+ },
521
+ },
357
522
  loadOptions: {
358
523
  async getContas() {
359
524
  try {
@@ -373,7 +538,7 @@ class AibDigital {
373
538
  }
374
539
  },
375
540
  async getAmbientes() {
376
- const conta = this.getNodeParameter("conta", "");
541
+ const conta = contaDeLista(this);
377
542
  try {
378
543
  const res = await api(this, "GET", "/api/v1/ambientes", undefined, conta ? { conta } : undefined);
379
544
  const ambientes = (res?.ambientes ?? []);
@@ -387,13 +552,13 @@ class AibDigital {
387
552
  }
388
553
  },
389
554
  async getFunis() {
390
- const conta = this.getNodeParameter("conta", "");
555
+ const conta = contaDeLista(this);
391
556
  const res = await api(this, "GET", "/api/v1/funis", undefined, conta ? { conta } : undefined);
392
557
  const funis = (res?.funis ?? []);
393
558
  return funis.map((f) => ({ name: f.nome, value: f.id }));
394
559
  },
395
560
  async getEtapas() {
396
- const conta = this.getNodeParameter("conta", "");
561
+ const conta = contaDeLista(this);
397
562
  const res = await api(this, "GET", "/api/v1/funis", undefined, conta ? { conta } : undefined);
398
563
  const funis = (res?.funis ?? []);
399
564
  return funis.flatMap((f) => f.etapas.map((e) => ({ name: `${f.nome} → ${e.nome}`, value: e.id })));
@@ -404,9 +569,10 @@ class AibDigital {
404
569
  const items = this.getInputData();
405
570
  const out = [];
406
571
  let operation = this.getNodeParameter("operation", 0);
407
- // Autocorreção: ao atualizar o pacote, o n8n às vezes perde a operação
408
- // salva e injeta o "__CUSTOM_API_CALL__" da auth genérica. Em vez de
409
- // falhar, assumimos a operação natural do recurso escolhido.
572
+ // Rede para o que ficou salvo antes: a credencial não declara mais
573
+ // `authenticate`, então o n8n parou de injetar o "__CUSTOM_API_CALL__"
574
+ // nas listas — mas um nó gravado com esse valor continua no banco do
575
+ // cliente. Em vez de falhar, assume a operação natural do recurso.
410
576
  if (operation === "__CUSTOM_API_CALL__") {
411
577
  const recurso = this.getNodeParameter("recurso", 0, "conversa");
412
578
  const padrao = {
@@ -473,7 +639,7 @@ class AibDigital {
473
639
  }
474
640
  else if (operation === "criarFunil") {
475
641
  const nome = this.getNodeParameter("funilNome", i);
476
- const ambienteId = this.getNodeParameter("ambienteId", i, "");
642
+ const ambienteId = idDe(this.getNodeParameter("ambienteId", i, ""));
477
643
  const bruto = this.getNodeParameter("funilEtapas", i, "[]");
478
644
  const etapas = typeof bruto === "string" ? JSON.parse(bruto || "[]") : bruto;
479
645
  res = await api(this, "POST", "/api/v1/funis", {
@@ -497,7 +663,7 @@ class AibDigital {
497
663
  res = await api(this, "PATCH", `/api/v1/contatos/${contatoId}`, campos, qs);
498
664
  }
499
665
  else if (operation === "criarEtiqueta") {
500
- const ambienteSel = this.getNodeParameter("ambienteId", i, "");
666
+ const ambienteSel = idDe(this.getNodeParameter("ambienteId", i, ""));
501
667
  res = await api(this, "POST", "/api/v1/etiquetas", {
502
668
  nome: this.getNodeParameter("etiquetaNome", i),
503
669
  ...(this.getNodeParameter("etiquetaCor", i, "") ? { cor: this.getNodeParameter("etiquetaCor", i, "") } : {}),
@@ -509,11 +675,13 @@ class AibDigital {
509
675
  res = await api(this, "GET", "/api/v1/etiquetas", undefined, qs);
510
676
  }
511
677
  else if (operation === "selecionarConta") {
512
- // A âncora do fluxo: devolve a conta E o ambiente escolhidos para os
513
- // nós seguintes referenciarem ($('Selecionar conta').item.json.id e
514
- // .ambiente_id). É onde o funil vai nascer.
515
- const contaSel = this.getNodeParameter("conta", i, "");
516
- const ambienteSel = this.getNodeParameter("ambienteId", i, "");
678
+ // A âncora do fluxo: devolve a conta, o ambiente E a caixa escolhidos
679
+ // para os nós seguintes referenciarem ($('Selecionar conta').item.json.id,
680
+ // .ambiente_id e .caixa_id). É onde o funil vai nascer e por onde o
681
+ // atendimento fala.
682
+ const contaSel = idDe(this.getNodeParameter("conta", i, ""));
683
+ const ambienteSel = idDe(this.getNodeParameter("ambienteId", i, ""));
684
+ const caixaSel = idDe(this.getNodeParameter("caixaId", i, ""));
517
685
  let base;
518
686
  if (contaSel) {
519
687
  const r = await api(this, "GET", "/api/v1/contas");
@@ -527,6 +695,7 @@ class AibDigital {
527
695
  base = { id: "", nome: "— a conta da chave —", escopo: ping?.escopo };
528
696
  }
529
697
  base.ambiente_id = ambienteSel || null;
698
+ base.caixa_id = caixaSel || null;
530
699
  res = base;
531
700
  }
532
701
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-aib",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Nós do Sistema AIB Digital para n8n: gatilho de mensagens do WhatsApp (via AIB Inbox) e ações — enviar mensagem, mover card no funil, pausar IA — escolhendo conta e caixa por dropdown.",
5
5
  "license": "MIT",
6
6
  "author": "AIB Digital",