@adatechnology/meta-whatsapp-module 0.2.0-rc.13 → 0.2.0-rc.15

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/dist/index.cjs CHANGED
@@ -22,12 +22,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
24
  CreateFlowGraphUseCase: () => CreateFlowGraphUseCase,
25
+ DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS: () => DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS,
25
26
  DeleteConversationUseCase: () => DeleteConversationUseCase,
26
27
  DeleteFlowGraphUseCase: () => DeleteFlowGraphUseCase,
27
28
  DocumentRepository: () => DocumentRepository,
28
29
  ExportConversationUseCase: () => ExportConversationUseCase,
30
+ FlowGraphCache: () => FlowGraphCache,
29
31
  FlowGraphRepository: () => FlowGraphRepository,
30
32
  FlowInterpreter: () => FlowInterpreter,
33
+ FlowMediaRepository: () => FlowMediaRepository,
31
34
  GetFlowGraphUseCase: () => GetFlowGraphUseCase,
32
35
  GetLiveFlowPositionsUseCase: () => GetLiveFlowPositionsUseCase,
33
36
  IngestInboundMediaUseCase: () => IngestInboundMediaUseCase,
@@ -54,9 +57,11 @@ __export(index_exports, {
54
57
  WhatsAppChannelAdapter: () => WhatsAppChannelAdapter,
55
58
  claimWebhookDelivery: () => claimWebhookDelivery,
56
59
  createMetaWhatsAppModule: () => createMetaWhatsAppModule,
60
+ createSendMediaAction: () => createSendMediaAction,
57
61
  documents: () => documents,
58
62
  extractMediaDescriptor: () => extractMediaDescriptor,
59
63
  flowGraphs: () => flowGraphs,
64
+ flowMedia: () => flowMedia,
60
65
  issueSseTicket: () => issueSseTicket,
61
66
  messages: () => messages,
62
67
  metaWhatsAppMigrationsFolder: () => metaWhatsAppMigrationsFolder,
@@ -72,6 +77,7 @@ module.exports = __toCommonJS(index_exports);
72
77
 
73
78
  // src/createMetaWhatsAppModule.ts
74
79
  var import_meta_whatsapp_provider = require("@adatechnology/meta-whatsapp-provider");
80
+ var import_meta_whatsapp_contracts11 = require("@adatechnology/meta-whatsapp-contracts");
75
81
 
76
82
  // src/repositories/SessionRepository.ts
77
83
  var import_drizzle_orm2 = require("drizzle-orm");
@@ -259,6 +265,46 @@ var flowGraphs = metaWhatsAppSchema.table("flow_graphs", {
259
265
  }, (table) => [
260
266
  (0, import_pg_core.uniqueIndex)("idx_flow_graphs_company_key").on(table.companyId, table.key)
261
267
  ]);
268
+ var flowMedia = metaWhatsAppSchema.table("flow_media", {
269
+ id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
270
+ companyId: (0, import_pg_core.uuid)("company_id").notNull(),
271
+ flowKey: (0, import_pg_core.varchar)("flow_key", {
272
+ length: 64
273
+ }).notNull(),
274
+ nodeId: (0, import_pg_core.varchar)("node_id", {
275
+ length: 64
276
+ }).notNull(),
277
+ uploadId: (0, import_pg_core.varchar)("upload_id", {
278
+ length: 256
279
+ }).notNull(),
280
+ filename: (0, import_pg_core.varchar)("filename", {
281
+ length: 512
282
+ }).notNull(),
283
+ mimeType: (0, import_pg_core.varchar)("mime_type", {
284
+ length: 128
285
+ }).notNull(),
286
+ sizeBytes: (0, import_pg_core.integer)("size_bytes").notNull(),
287
+ // Legenda da mídia no WhatsApp. Por arquivo, não por nó: um nó que manda tabela de preços e
288
+ // um folder precisa de textos diferentes para cada um.
289
+ caption: (0, import_pg_core.text)("caption"),
290
+ // Ordem de envio dentro do nó — o cliente recebe as mensagens em sequência, e "tabela antes
291
+ // do folder" é decisão de quem edita, não do banco.
292
+ sortOrder: (0, import_pg_core.integer)("sort_order").notNull().default(0),
293
+ // Desligar sem desanexar: trocar o material da campanha é o caso comum, e apagar a linha
294
+ // perderia a ordem e a legenda já ajustadas.
295
+ active: (0, import_pg_core.boolean)("active").notNull().default(true),
296
+ createdAt: (0, import_pg_core.timestamp)("created_at", {
297
+ withTimezone: true
298
+ }).notNull().defaultNow(),
299
+ updatedAt: (0, import_pg_core.timestamp)("updated_at", {
300
+ withTimezone: true
301
+ }).notNull().defaultNow()
302
+ }, (table) => [
303
+ (0, import_pg_core.index)("idx_flow_media_node").on(table.companyId, table.flowKey, table.nodeId, table.sortOrder),
304
+ // O mesmo arquivo anexado duas vezes ao MESMO nó é erro de clique no editor, e o cliente
305
+ // receberia o documento repetido.
306
+ (0, import_pg_core.uniqueIndex)("idx_flow_media_node_upload").on(table.companyId, table.flowKey, table.nodeId, table.uploadId)
307
+ ]);
262
308
  var settings = metaWhatsAppSchema.table("settings", {
263
309
  companyId: (0, import_pg_core.uuid)("company_id").primaryKey(),
264
310
  templateName: (0, import_pg_core.varchar)("template_name", {
@@ -583,12 +629,21 @@ var FlowGraphRepository = class {
583
629
  __name(this, "FlowGraphRepository");
584
630
  }
585
631
  db;
586
- constructor(db) {
632
+ cache;
633
+ // Cache opcional: sem ele o repositório se comporta exatamente como antes, lendo sempre do
634
+ // banco. É o host que decide se quer cachear e com qual provedor (ver CacheInterface).
635
+ constructor(db, cache) {
587
636
  this.db = db;
637
+ this.cache = cache;
588
638
  }
589
639
  async get(companyId, key) {
640
+ const cached = await this.cache?.read(companyId, key);
641
+ if (cached) return cached;
590
642
  const [row] = await this.db.select().from(flowGraphs).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(flowGraphs.companyId, companyId), (0, import_drizzle_orm4.eq)(flowGraphs.key, key))).limit(1);
591
- return row ? toContractGraph(row) : void 0;
643
+ if (!row) return void 0;
644
+ const graph = toContractGraph(row);
645
+ await this.cache?.write(companyId, graph);
646
+ return graph;
592
647
  }
593
648
  async list(companyId) {
594
649
  const rows = await this.db.select().from(flowGraphs).where((0, import_drizzle_orm4.eq)(flowGraphs.companyId, companyId));
@@ -618,7 +673,9 @@ var FlowGraphRepository = class {
618
673
  showInMenu: graph.showInMenu ?? false,
619
674
  menuOptionLabel: graph.menuOptionLabel
620
675
  }).returning();
621
- return toContractGraph(created);
676
+ const createdGraph = toContractGraph(created);
677
+ await this.cache?.invalidate(companyId, createdGraph.key);
678
+ return createdGraph;
622
679
  }
623
680
  // Lock otimista: a escrita só aplica se `expectedVersion` ainda bater com o que está salvo —
624
681
  // senão, alguém mais salvou entretanto e o editor precisa recarregar (ver comentário no schema).
@@ -632,10 +689,12 @@ var FlowGraphRepository = class {
632
689
  updatedAt: /* @__PURE__ */ new Date()
633
690
  }).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(flowGraphs.companyId, companyId), (0, import_drizzle_orm4.eq)(flowGraphs.key, graph.key), (0, import_drizzle_orm4.eq)(flowGraphs.version, expectedVersion))).returning();
634
691
  if (rows.length === 0) throw new OptimisticLockError(graph.key);
692
+ await this.cache?.invalidate(companyId, graph.key);
635
693
  return toContractGraph(rows[0]);
636
694
  }
637
695
  async delete(companyId, key) {
638
696
  await this.db.delete(flowGraphs).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(flowGraphs.companyId, companyId), (0, import_drizzle_orm4.eq)(flowGraphs.key, key)));
697
+ await this.cache?.invalidate(companyId, key);
639
698
  }
640
699
  // T4.2 — GetLiveFlowPositions: agrega sessões ativas por (flowKey, currentNodeId), lendo as
641
700
  // colunas dedicadas gravadas por SessionRepository.setFlowPosition. Agrega no banco (GROUP BY,
@@ -655,6 +714,48 @@ var FlowGraphRepository = class {
655
714
  }
656
715
  };
657
716
 
717
+ // src/repositories/FlowGraphCache.ts
718
+ var KEY_PREFIX = "meta-whatsapp:flow-graph";
719
+ var DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS = 300;
720
+ var FlowGraphCache = class {
721
+ static {
722
+ __name(this, "FlowGraphCache");
723
+ }
724
+ provider;
725
+ ttlSeconds;
726
+ constructor(provider, ttlSeconds = DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS) {
727
+ this.provider = provider;
728
+ this.ttlSeconds = ttlSeconds;
729
+ }
730
+ // companyId na chave, e não só a flowKey: a chave do fluxo é escolhida por quem edita e se
731
+ // repete entre empresas — 'consorcio' existe em todas — então uma chave sem tenant serviria o
732
+ // grafo de uma empresa para a conversa de outra.
733
+ keyFor(companyId, flowKey) {
734
+ return `${KEY_PREFIX}:${companyId}:${flowKey}`;
735
+ }
736
+ async read(companyId, flowKey) {
737
+ try {
738
+ const cached = await this.provider.get(this.keyFor(companyId, flowKey));
739
+ if (!cached) return void 0;
740
+ return JSON.parse(cached);
741
+ } catch {
742
+ return void 0;
743
+ }
744
+ }
745
+ async write(companyId, graph) {
746
+ try {
747
+ await this.provider.set(this.keyFor(companyId, graph.key), JSON.stringify(graph), this.ttlSeconds);
748
+ } catch {
749
+ }
750
+ }
751
+ async invalidate(companyId, flowKey) {
752
+ try {
753
+ await this.provider.delete(this.keyFor(companyId, flowKey));
754
+ } catch {
755
+ }
756
+ }
757
+ };
758
+
658
759
  // src/repositories/SettingsRepository.ts
659
760
  var import_drizzle_orm5 = require("drizzle-orm");
660
761
  function toContractSettings(row) {
@@ -1551,6 +1652,151 @@ var FlowInterpreter = class {
1551
1652
  }
1552
1653
  };
1553
1654
 
1655
+ // src/flows/createSendMediaAction.ts
1656
+ function mediaTypeFor2(mimeType) {
1657
+ if (mimeType.startsWith("image/")) return "image";
1658
+ if (mimeType.startsWith("audio/")) return "audio";
1659
+ if (mimeType.startsWith("video/")) return "video";
1660
+ return "document";
1661
+ }
1662
+ __name(mediaTypeFor2, "mediaTypeFor");
1663
+ function createSendMediaAction(params) {
1664
+ return async ({ node, session, channel }) => {
1665
+ if (!session.flowKey) return;
1666
+ const location = {
1667
+ companyId: session.companyId,
1668
+ flowKey: session.flowKey,
1669
+ nodeId: node.id
1670
+ };
1671
+ const attachments = await params.flowMediaRepository.listActive(location);
1672
+ for (const attachment of attachments) {
1673
+ try {
1674
+ const buffer = await params.objectStorage.getObject(attachment.uploadId);
1675
+ const { externalMessageId } = await channel.sendMedia({
1676
+ to: session.whatsappNumber,
1677
+ buffer,
1678
+ mimeType: attachment.mimeType,
1679
+ filename: attachment.filename,
1680
+ caption: attachment.caption ?? void 0
1681
+ });
1682
+ await params.logMessage.execute({
1683
+ companyId: session.companyId,
1684
+ whatsappNumber: session.whatsappNumber,
1685
+ direction: "outbound",
1686
+ sender: "bot",
1687
+ agentUserId: null,
1688
+ type: mediaTypeFor2(attachment.mimeType),
1689
+ content: attachment.caption ?? attachment.filename,
1690
+ payload: {
1691
+ filename: attachment.filename,
1692
+ mimeType: attachment.mimeType,
1693
+ uploadId: attachment.uploadId,
1694
+ flowMediaId: attachment.id
1695
+ },
1696
+ waMessageId: externalMessageId,
1697
+ status: "sent",
1698
+ startState: params.startState
1699
+ });
1700
+ } catch (error) {
1701
+ params.onError?.(error, {
1702
+ flowKey: location.flowKey,
1703
+ nodeId: node.id,
1704
+ uploadId: attachment.uploadId
1705
+ });
1706
+ }
1707
+ }
1708
+ };
1709
+ }
1710
+ __name(createSendMediaAction, "createSendMediaAction");
1711
+
1712
+ // src/repositories/FlowMediaRepository.ts
1713
+ var import_drizzle_orm7 = require("drizzle-orm");
1714
+ var FlowMediaRepository = class {
1715
+ static {
1716
+ __name(this, "FlowMediaRepository");
1717
+ }
1718
+ db;
1719
+ constructor(db) {
1720
+ this.db = db;
1721
+ }
1722
+ locationFilter(location) {
1723
+ return (0, import_drizzle_orm7.and)((0, import_drizzle_orm7.eq)(flowMedia.companyId, location.companyId), (0, import_drizzle_orm7.eq)(flowMedia.flowKey, location.flowKey), (0, import_drizzle_orm7.eq)(flowMedia.nodeId, location.nodeId));
1724
+ }
1725
+ // O que o nó realmente envia, na ordem de envio. `active` filtrado aqui e não no chamador:
1726
+ // é a razão de a coluna existir, e um chamador que esquecesse do filtro mandaria ao cliente
1727
+ // justamente o material que alguém desligou.
1728
+ async listActive(location) {
1729
+ return this.db.select().from(flowMedia).where((0, import_drizzle_orm7.and)(this.locationFilter(location), (0, import_drizzle_orm7.eq)(flowMedia.active, true))).orderBy((0, import_drizzle_orm7.asc)(flowMedia.sortOrder), (0, import_drizzle_orm7.asc)(flowMedia.createdAt));
1730
+ }
1731
+ // Inclui os desligados — é a visão do editor, onde desligar precisa continuar visível para
1732
+ // poder ser religado.
1733
+ async listAll(location) {
1734
+ return this.db.select().from(flowMedia).where(this.locationFilter(location)).orderBy((0, import_drizzle_orm7.asc)(flowMedia.sortOrder), (0, import_drizzle_orm7.asc)(flowMedia.createdAt));
1735
+ }
1736
+ /**
1737
+ * Anexa um arquivo já existente no storage ao nó.
1738
+ *
1739
+ * `onConflictDoUpdate` em vez de deixar estourar: reanexar o mesmo arquivo é clique repetido no
1740
+ * editor, e o esperado ali é atualizar a legenda/ordem — não um erro de índice único na cara de
1741
+ * quem está montando o fluxo.
1742
+ */
1743
+ async attach(params) {
1744
+ const [row] = await this.db.insert(flowMedia).values({
1745
+ companyId: params.companyId,
1746
+ flowKey: params.flowKey,
1747
+ nodeId: params.nodeId,
1748
+ uploadId: params.uploadId,
1749
+ filename: params.filename,
1750
+ mimeType: params.mimeType,
1751
+ sizeBytes: params.sizeBytes,
1752
+ caption: params.caption ?? null,
1753
+ sortOrder: params.sortOrder ?? 0
1754
+ }).onConflictDoUpdate({
1755
+ target: [
1756
+ flowMedia.companyId,
1757
+ flowMedia.flowKey,
1758
+ flowMedia.nodeId,
1759
+ flowMedia.uploadId
1760
+ ],
1761
+ set: {
1762
+ caption: params.caption ?? null,
1763
+ sortOrder: params.sortOrder ?? 0,
1764
+ active: true,
1765
+ updatedAt: import_drizzle_orm7.sql`now()`
1766
+ }
1767
+ }).returning();
1768
+ return row;
1769
+ }
1770
+ async update(params) {
1771
+ const [row] = await this.db.update(flowMedia).set({
1772
+ ...params.caption !== void 0 ? {
1773
+ caption: params.caption
1774
+ } : {},
1775
+ ...params.sortOrder !== void 0 ? {
1776
+ sortOrder: params.sortOrder
1777
+ } : {},
1778
+ ...params.active !== void 0 ? {
1779
+ active: params.active
1780
+ } : {},
1781
+ updatedAt: import_drizzle_orm7.sql`now()`
1782
+ }).where((0, import_drizzle_orm7.and)((0, import_drizzle_orm7.eq)(flowMedia.companyId, params.companyId), (0, import_drizzle_orm7.eq)(flowMedia.id, params.id))).returning();
1783
+ return row;
1784
+ }
1785
+ /**
1786
+ * Desanexa do nó. Não toca no storage de propósito: o mesmo `uploadId` pode estar anexado a
1787
+ * outro nó ou a outro fluxo, e apagar o binário aqui quebraria os demais. Quem apaga objeto é o
1788
+ * host, que é dono da biblioteca de arquivos.
1789
+ */
1790
+ async detach(params) {
1791
+ await this.db.delete(flowMedia).where((0, import_drizzle_orm7.and)((0, import_drizzle_orm7.eq)(flowMedia.companyId, params.companyId), (0, import_drizzle_orm7.eq)(flowMedia.id, params.id)));
1792
+ }
1793
+ // Chamado ao salvar o grafo: nós apagados no editor deixam linhas que nada mais alcança.
1794
+ async detachRemovedNodes(params) {
1795
+ const condition = params.existingNodeIds.length === 0 ? void 0 : import_drizzle_orm7.sql`${flowMedia.nodeId} NOT IN ${params.existingNodeIds}`;
1796
+ await this.db.delete(flowMedia).where((0, import_drizzle_orm7.and)((0, import_drizzle_orm7.eq)(flowMedia.companyId, params.companyId), (0, import_drizzle_orm7.eq)(flowMedia.flowKey, params.flowKey), condition));
1797
+ }
1798
+ };
1799
+
1554
1800
  // src/channel/WhatsAppChannelAdapter.ts
1555
1801
  var import_meta_graph_core = require("@adatechnology/meta-graph-core");
1556
1802
  var import_meta_whatsapp_contracts8 = require("@adatechnology/meta-whatsapp-contracts");
@@ -1647,7 +1893,7 @@ async function claimWebhookDelivery(params) {
1647
1893
  __name(claimWebhookDelivery, "claimWebhookDelivery");
1648
1894
 
1649
1895
  // src/channel/IngestInboundMedia.use-case.ts
1650
- var import_drizzle_orm7 = require("drizzle-orm");
1896
+ var import_drizzle_orm8 = require("drizzle-orm");
1651
1897
  var IngestInboundMediaUseCase = class {
1652
1898
  static {
1653
1899
  __name(this, "IngestInboundMediaUseCase");
@@ -1663,7 +1909,7 @@ var IngestInboundMediaUseCase = class {
1663
1909
  this.documentRepository = documentRepository;
1664
1910
  }
1665
1911
  async execute(params) {
1666
- const [message] = await this.db.select().from(messages).where((0, import_drizzle_orm7.and)((0, import_drizzle_orm7.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm7.eq)(messages.id, params.messageId))).limit(1);
1912
+ const [message] = await this.db.select().from(messages).where((0, import_drizzle_orm8.and)((0, import_drizzle_orm8.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm8.eq)(messages.id, params.messageId))).limit(1);
1667
1913
  if (!message) throw new Error(`Mensagem ${params.messageId} n\xE3o encontrada para ingest\xE3o de m\xEDdia`);
1668
1914
  const payload = message.payload ?? {};
1669
1915
  if (payload["uploadId"] && payload["sourceMediaId"] === params.sourceMediaId) {
@@ -1684,13 +1930,17 @@ var IngestInboundMediaUseCase = class {
1684
1930
  uploadId,
1685
1931
  sourceMediaId: params.sourceMediaId,
1686
1932
  mimeType: mimeType || params.mimeType,
1933
+ // O tamanho já está na mão (é o buffer que acabou de ser copiado) e a bolha de documento o
1934
+ // exibe ao lado do tipo. Sem gravar aqui, a UI mostraria "PDF" sem o "· 180 KB", e buscá-lo
1935
+ // depois custaria uma consulta à tabela de documentos por mensagem renderizada.
1936
+ sizeBytes: buffer.length,
1687
1937
  ...params.filename ? {
1688
1938
  filename: params.filename
1689
1939
  } : {}
1690
1940
  };
1691
1941
  await this.db.update(messages).set({
1692
1942
  payload: updatedPayload
1693
- }).where((0, import_drizzle_orm7.and)((0, import_drizzle_orm7.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm7.eq)(messages.id, params.messageId)));
1943
+ }).where((0, import_drizzle_orm8.and)((0, import_drizzle_orm8.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm8.eq)(messages.id, params.messageId)));
1694
1944
  await this.documentRepository?.link({
1695
1945
  companyId: params.companyId,
1696
1946
  sessionId: message.sessionId,
@@ -1890,8 +2140,11 @@ function createMetaWhatsAppModule(params) {
1890
2140
  const sessionRepository = new SessionRepository(db);
1891
2141
  const messageRepository = new MessageRepository(db);
1892
2142
  const settingsRepository = new SettingsRepository(db);
1893
- const flowGraphRepository = new FlowGraphRepository(db);
2143
+ const flowGraphCacheFeature = params.features?.flowGraphCache ?? false;
2144
+ const flowGraphCache = flowGraphCacheFeature && providers.cache ? new FlowGraphCache(providers.cache, typeof flowGraphCacheFeature === "object" ? flowGraphCacheFeature.ttlSeconds ?? DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS : DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS) : void 0;
2145
+ const flowGraphRepository = new FlowGraphRepository(db, flowGraphCache);
1894
2146
  const documentRepository = new DocumentRepository(db);
2147
+ const flowMediaRepository = new FlowMediaRepository(db);
1895
2148
  const logMessage = new LogMessageUseCase(sessionRepository, messageRepository, providers.realtime, providers.moderator);
1896
2149
  const sendMessage = new SendMessageUseCase(channel, sessionRepository, logMessage, providers.objectStorage, documentRepository);
1897
2150
  const receiveWebhook = new ReceiveWebhookUseCase({
@@ -1905,6 +2158,15 @@ function createMetaWhatsAppModule(params) {
1905
2158
  realtime: providers.realtime
1906
2159
  });
1907
2160
  const flowInterpreter = flowEngineEnabled ? new FlowInterpreter() : void 0;
2161
+ if (flowInterpreter && providers.objectStorage?.getObject) {
2162
+ flowInterpreter.registerFlowAction(import_meta_whatsapp_contracts11.FLOW_ACTION_KIND.SEND_MEDIA, createSendMediaAction({
2163
+ flowMediaRepository,
2164
+ objectStorage: providers.objectStorage,
2165
+ logMessage,
2166
+ startState,
2167
+ onError: hooks?.onFlowMediaError
2168
+ }));
2169
+ }
1908
2170
  const ingestInboundMedia = providers.objectStorage ? new IngestInboundMediaUseCase(db, channel, providers.objectStorage, documentRepository) : void 0;
1909
2171
  const listDocuments = new ListConversationDocumentsUseCase(sessionRepository, documentRepository);
1910
2172
  return {
@@ -1948,7 +2210,11 @@ function createMetaWhatsAppModule(params) {
1948
2210
  save: new SaveFlowGraphUseCase(flowGraphRepository),
1949
2211
  delete: new DeleteFlowGraphUseCase(flowGraphRepository),
1950
2212
  livePositions: new GetLiveFlowPositionsUseCase(flowGraphRepository),
1951
- repository: flowGraphRepository
2213
+ repository: flowGraphRepository,
2214
+ // Biblioteca de mídia dos nós `send_media` — o host liga nas rotas do editor
2215
+ // (anexar/reordenar/desligar). Existe mesmo sem storage injetado: gerenciar anexos é
2216
+ // consultar a tabela, e só o ENVIO precisa dos bytes.
2217
+ mediaRepository: flowMediaRepository
1952
2218
  } : void 0,
1953
2219
  catalog: providers.catalog
1954
2220
  };
@@ -2048,12 +2314,15 @@ __name(redeemSseTicket, "redeemSseTicket");
2048
2314
  // Annotate the CommonJS export names for ESM import in node:
2049
2315
  0 && (module.exports = {
2050
2316
  CreateFlowGraphUseCase,
2317
+ DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS,
2051
2318
  DeleteConversationUseCase,
2052
2319
  DeleteFlowGraphUseCase,
2053
2320
  DocumentRepository,
2054
2321
  ExportConversationUseCase,
2322
+ FlowGraphCache,
2055
2323
  FlowGraphRepository,
2056
2324
  FlowInterpreter,
2325
+ FlowMediaRepository,
2057
2326
  GetFlowGraphUseCase,
2058
2327
  GetLiveFlowPositionsUseCase,
2059
2328
  IngestInboundMediaUseCase,
@@ -2080,9 +2349,11 @@ __name(redeemSseTicket, "redeemSseTicket");
2080
2349
  WhatsAppChannelAdapter,
2081
2350
  claimWebhookDelivery,
2082
2351
  createMetaWhatsAppModule,
2352
+ createSendMediaAction,
2083
2353
  documents,
2084
2354
  extractMediaDescriptor,
2085
2355
  flowGraphs,
2356
+ flowMedia,
2086
2357
  issueSseTicket,
2087
2358
  messages,
2088
2359
  metaWhatsAppMigrationsFolder,