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

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.d.ts CHANGED
@@ -1000,6 +1000,257 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
1000
1000
  };
1001
1001
  dialect: "pg";
1002
1002
  }>;
1003
+ /**
1004
+ * Biblioteca de mídia do bot: arquivos que um nó `action` de `send_media` dispara ao chegar nele.
1005
+ *
1006
+ * Tabela separada de `documents` de propósito, e não por simetria: `documents` é o acervo DE UMA
1007
+ * CONVERSA (tem `sessionId` obrigatório) e tem índice único em (companyId, uploadId), porque lá o
1008
+ * mesmo binário chegando duas vezes é reentrega de job. Aqui é o oposto — o mesmo arquivo é
1009
+ * enviado para todo cliente que passar pelo nó, então aquele único bloquearia o segundo envio.
1010
+ *
1011
+ * Sem FK para `flow_graphs.id`: o vínculo natural é (companyId, flowKey), que é justamente o
1012
+ * índice único de lá. `nodeId` não tem como ser FK — nós vivem dentro do jsonb `nodes` — então
1013
+ * apagar um nó no editor deixa a linha órfã; quem lista sempre parte de um nó existente, e a
1014
+ * limpeza é passo de aplicação ao salvar o grafo.
1015
+ */
1016
+ declare const flowMedia: drizzle_orm_pg_core.PgTableWithColumns<{
1017
+ name: "flow_media";
1018
+ schema: "meta_whatsapp";
1019
+ columns: {
1020
+ id: drizzle_orm_pg_core.PgColumn<{
1021
+ name: "id";
1022
+ tableName: "flow_media";
1023
+ dataType: "string";
1024
+ columnType: "PgUUID";
1025
+ data: string;
1026
+ driverParam: string;
1027
+ notNull: true;
1028
+ hasDefault: true;
1029
+ isPrimaryKey: true;
1030
+ isAutoincrement: false;
1031
+ hasRuntimeDefault: false;
1032
+ enumValues: undefined;
1033
+ baseColumn: never;
1034
+ identity: undefined;
1035
+ generated: undefined;
1036
+ }, {}, {}>;
1037
+ companyId: drizzle_orm_pg_core.PgColumn<{
1038
+ name: "company_id";
1039
+ tableName: "flow_media";
1040
+ dataType: "string";
1041
+ columnType: "PgUUID";
1042
+ data: string;
1043
+ driverParam: string;
1044
+ notNull: true;
1045
+ hasDefault: false;
1046
+ isPrimaryKey: false;
1047
+ isAutoincrement: false;
1048
+ hasRuntimeDefault: false;
1049
+ enumValues: undefined;
1050
+ baseColumn: never;
1051
+ identity: undefined;
1052
+ generated: undefined;
1053
+ }, {}, {}>;
1054
+ flowKey: drizzle_orm_pg_core.PgColumn<{
1055
+ name: "flow_key";
1056
+ tableName: "flow_media";
1057
+ dataType: "string";
1058
+ columnType: "PgVarchar";
1059
+ data: string;
1060
+ driverParam: string;
1061
+ notNull: true;
1062
+ hasDefault: false;
1063
+ isPrimaryKey: false;
1064
+ isAutoincrement: false;
1065
+ hasRuntimeDefault: false;
1066
+ enumValues: [string, ...string[]];
1067
+ baseColumn: never;
1068
+ identity: undefined;
1069
+ generated: undefined;
1070
+ }, {}, {
1071
+ length: 64;
1072
+ }>;
1073
+ nodeId: drizzle_orm_pg_core.PgColumn<{
1074
+ name: "node_id";
1075
+ tableName: "flow_media";
1076
+ dataType: "string";
1077
+ columnType: "PgVarchar";
1078
+ data: string;
1079
+ driverParam: string;
1080
+ notNull: true;
1081
+ hasDefault: false;
1082
+ isPrimaryKey: false;
1083
+ isAutoincrement: false;
1084
+ hasRuntimeDefault: false;
1085
+ enumValues: [string, ...string[]];
1086
+ baseColumn: never;
1087
+ identity: undefined;
1088
+ generated: undefined;
1089
+ }, {}, {
1090
+ length: 64;
1091
+ }>;
1092
+ uploadId: drizzle_orm_pg_core.PgColumn<{
1093
+ name: "upload_id";
1094
+ tableName: "flow_media";
1095
+ dataType: "string";
1096
+ columnType: "PgVarchar";
1097
+ data: string;
1098
+ driverParam: string;
1099
+ notNull: true;
1100
+ hasDefault: false;
1101
+ isPrimaryKey: false;
1102
+ isAutoincrement: false;
1103
+ hasRuntimeDefault: false;
1104
+ enumValues: [string, ...string[]];
1105
+ baseColumn: never;
1106
+ identity: undefined;
1107
+ generated: undefined;
1108
+ }, {}, {
1109
+ length: 256;
1110
+ }>;
1111
+ filename: drizzle_orm_pg_core.PgColumn<{
1112
+ name: "filename";
1113
+ tableName: "flow_media";
1114
+ dataType: "string";
1115
+ columnType: "PgVarchar";
1116
+ data: string;
1117
+ driverParam: string;
1118
+ notNull: true;
1119
+ hasDefault: false;
1120
+ isPrimaryKey: false;
1121
+ isAutoincrement: false;
1122
+ hasRuntimeDefault: false;
1123
+ enumValues: [string, ...string[]];
1124
+ baseColumn: never;
1125
+ identity: undefined;
1126
+ generated: undefined;
1127
+ }, {}, {
1128
+ length: 512;
1129
+ }>;
1130
+ mimeType: drizzle_orm_pg_core.PgColumn<{
1131
+ name: "mime_type";
1132
+ tableName: "flow_media";
1133
+ dataType: "string";
1134
+ columnType: "PgVarchar";
1135
+ data: string;
1136
+ driverParam: string;
1137
+ notNull: true;
1138
+ hasDefault: false;
1139
+ isPrimaryKey: false;
1140
+ isAutoincrement: false;
1141
+ hasRuntimeDefault: false;
1142
+ enumValues: [string, ...string[]];
1143
+ baseColumn: never;
1144
+ identity: undefined;
1145
+ generated: undefined;
1146
+ }, {}, {
1147
+ length: 128;
1148
+ }>;
1149
+ sizeBytes: drizzle_orm_pg_core.PgColumn<{
1150
+ name: "size_bytes";
1151
+ tableName: "flow_media";
1152
+ dataType: "number";
1153
+ columnType: "PgInteger";
1154
+ data: number;
1155
+ driverParam: string | number;
1156
+ notNull: true;
1157
+ hasDefault: false;
1158
+ isPrimaryKey: false;
1159
+ isAutoincrement: false;
1160
+ hasRuntimeDefault: false;
1161
+ enumValues: undefined;
1162
+ baseColumn: never;
1163
+ identity: undefined;
1164
+ generated: undefined;
1165
+ }, {}, {}>;
1166
+ caption: drizzle_orm_pg_core.PgColumn<{
1167
+ name: "caption";
1168
+ tableName: "flow_media";
1169
+ dataType: "string";
1170
+ columnType: "PgText";
1171
+ data: string;
1172
+ driverParam: string;
1173
+ notNull: false;
1174
+ hasDefault: false;
1175
+ isPrimaryKey: false;
1176
+ isAutoincrement: false;
1177
+ hasRuntimeDefault: false;
1178
+ enumValues: [string, ...string[]];
1179
+ baseColumn: never;
1180
+ identity: undefined;
1181
+ generated: undefined;
1182
+ }, {}, {}>;
1183
+ sortOrder: drizzle_orm_pg_core.PgColumn<{
1184
+ name: "sort_order";
1185
+ tableName: "flow_media";
1186
+ dataType: "number";
1187
+ columnType: "PgInteger";
1188
+ data: number;
1189
+ driverParam: string | number;
1190
+ notNull: true;
1191
+ hasDefault: true;
1192
+ isPrimaryKey: false;
1193
+ isAutoincrement: false;
1194
+ hasRuntimeDefault: false;
1195
+ enumValues: undefined;
1196
+ baseColumn: never;
1197
+ identity: undefined;
1198
+ generated: undefined;
1199
+ }, {}, {}>;
1200
+ active: drizzle_orm_pg_core.PgColumn<{
1201
+ name: "active";
1202
+ tableName: "flow_media";
1203
+ dataType: "boolean";
1204
+ columnType: "PgBoolean";
1205
+ data: boolean;
1206
+ driverParam: boolean;
1207
+ notNull: true;
1208
+ hasDefault: true;
1209
+ isPrimaryKey: false;
1210
+ isAutoincrement: false;
1211
+ hasRuntimeDefault: false;
1212
+ enumValues: undefined;
1213
+ baseColumn: never;
1214
+ identity: undefined;
1215
+ generated: undefined;
1216
+ }, {}, {}>;
1217
+ createdAt: drizzle_orm_pg_core.PgColumn<{
1218
+ name: "created_at";
1219
+ tableName: "flow_media";
1220
+ dataType: "date";
1221
+ columnType: "PgTimestamp";
1222
+ data: Date;
1223
+ driverParam: string;
1224
+ notNull: true;
1225
+ hasDefault: true;
1226
+ isPrimaryKey: false;
1227
+ isAutoincrement: false;
1228
+ hasRuntimeDefault: false;
1229
+ enumValues: undefined;
1230
+ baseColumn: never;
1231
+ identity: undefined;
1232
+ generated: undefined;
1233
+ }, {}, {}>;
1234
+ updatedAt: drizzle_orm_pg_core.PgColumn<{
1235
+ name: "updated_at";
1236
+ tableName: "flow_media";
1237
+ dataType: "date";
1238
+ columnType: "PgTimestamp";
1239
+ data: Date;
1240
+ driverParam: string;
1241
+ notNull: true;
1242
+ hasDefault: true;
1243
+ isPrimaryKey: false;
1244
+ isAutoincrement: false;
1245
+ hasRuntimeDefault: false;
1246
+ enumValues: undefined;
1247
+ baseColumn: never;
1248
+ identity: undefined;
1249
+ generated: undefined;
1250
+ }, {}, {}>;
1251
+ };
1252
+ dialect: "pg";
1253
+ }>;
1003
1254
  declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{
1004
1255
  name: "settings";
1005
1256
  schema: "meta_whatsapp";
@@ -1159,6 +1410,8 @@ type FlowGraphRow = typeof flowGraphs.$inferSelect;
1159
1410
  type NewFlowGraphRow = typeof flowGraphs.$inferInsert;
1160
1411
  type DocumentRow = typeof documents.$inferSelect;
1161
1412
  type NewDocumentRow = typeof documents.$inferInsert;
1413
+ type FlowMediaRow = typeof flowMedia.$inferSelect;
1414
+ type NewFlowMediaRow = typeof flowMedia.$inferInsert;
1162
1415
 
1163
1416
  interface ListConversationsFilters {
1164
1417
  page?: number;
@@ -1757,6 +2010,57 @@ declare class FlowInterpreter {
1757
2010
  }): Promise<FlowRunResult>;
1758
2011
  }
1759
2012
 
2013
+ type FlowMediaLocation = {
2014
+ companyId: string;
2015
+ flowKey: string;
2016
+ nodeId: string;
2017
+ };
2018
+ type AttachFlowMediaParams = FlowMediaLocation & {
2019
+ uploadId: string;
2020
+ filename: string;
2021
+ mimeType: string;
2022
+ sizeBytes: number;
2023
+ caption?: string;
2024
+ sortOrder?: number;
2025
+ };
2026
+ type UpdateFlowMediaParams = {
2027
+ companyId: string;
2028
+ id: string;
2029
+ caption?: string | null;
2030
+ sortOrder?: number;
2031
+ active?: boolean;
2032
+ };
2033
+ declare class FlowMediaRepository {
2034
+ private readonly db;
2035
+ constructor(db: MetaWhatsAppDatabase);
2036
+ private locationFilter;
2037
+ listActive(location: FlowMediaLocation): Promise<FlowMediaRow[]>;
2038
+ listAll(location: FlowMediaLocation): Promise<FlowMediaRow[]>;
2039
+ /**
2040
+ * Anexa um arquivo já existente no storage ao nó.
2041
+ *
2042
+ * `onConflictDoUpdate` em vez de deixar estourar: reanexar o mesmo arquivo é clique repetido no
2043
+ * editor, e o esperado ali é atualizar a legenda/ordem — não um erro de índice único na cara de
2044
+ * quem está montando o fluxo.
2045
+ */
2046
+ attach(params: AttachFlowMediaParams): Promise<FlowMediaRow>;
2047
+ update(params: UpdateFlowMediaParams): Promise<FlowMediaRow | undefined>;
2048
+ /**
2049
+ * Desanexa do nó. Não toca no storage de propósito: o mesmo `uploadId` pode estar anexado a
2050
+ * outro nó ou a outro fluxo, e apagar o binário aqui quebraria os demais. Quem apaga objeto é o
2051
+ * host, que é dono da biblioteca de arquivos.
2052
+ */
2053
+ detach(params: {
2054
+ companyId: string;
2055
+ id: string;
2056
+ }): Promise<void>;
2057
+ detachRemovedNodes(params: {
2058
+ companyId: string;
2059
+ flowKey: string;
2060
+ existingNodeIds: string[];
2061
+ }): Promise<void>;
2062
+ }
2063
+
1760
2064
  interface NonceStoreInterface {
1761
2065
  setIfAbsent(key: string, ttlSeconds: number): Promise<boolean>;
1762
2066
  }
@@ -1895,6 +2199,7 @@ declare function createMetaWhatsAppModule(params: CreateMetaWhatsAppModuleParams
1895
2199
  delete: DeleteFlowGraphUseCase;
1896
2200
  livePositions: GetLiveFlowPositionsUseCase;
1897
2201
  repository: FlowGraphRepository;
2202
+ mediaRepository: FlowMediaRepository;
1898
2203
  } | undefined;
1899
2204
  catalog: CatalogPort | undefined;
1900
2205
  };
@@ -1978,4 +2283,31 @@ declare function redeemSseTicket(store: TicketStoreInterface, ticket: string): P
1978
2283
  whatsappNumber: string;
1979
2284
  } | null>;
1980
2285
 
1981
- export { type CompanyDocumentView, type CompanyDocumentsPage, type ConversationDocumentView, type ConversationDocumentsPage, type CreateFlowGraphParams, CreateFlowGraphUseCase, type CreateMetaWhatsAppModuleParams, type DeleteConversationParams, type DeleteConversationResult, DeleteConversationUseCase, DeleteFlowGraphUseCase, DocumentRepository, type DocumentRow, type DrizzleMigrateFunction, type ExportConversationParams, type ExportConversationResult, ExportConversationUseCase, FlowGraphRepository, type FlowGraphRow, FlowInterpreter, type FlowRunResult, type FlowStepInput, type FlowStepResult, GetFlowGraphUseCase, GetLiveFlowPositionsUseCase, type IngestInboundMediaParams, type IngestInboundMediaResult, IngestInboundMediaUseCase, type InsertMessageParams, InvalidFlowGraphError, type LinkDocumentParams, type ListCompanyDocumentsParams, ListCompanyDocumentsUseCase, type ListConversationDocumentsParams, ListConversationDocumentsUseCase, type ListConversationsFilters, type ListConversationsParams, ListConversationsUseCase, type ListDocumentsParams, type ListDocumentsResult, ListFlowGraphsUseCase, type ListMessagesParams, ListMessagesUseCase, type LogMessageParams, LogMessageUseCase, META_WHATSAPP_MIGRATIONS_TABLE, type MessageModerator, MessageRepository, type MessageRow, type MetaWhatsAppDatabase, type MetaWhatsAppModule, type MetaWhatsAppModuleConfig, type MetaWhatsAppModuleFeatures, type MetaWhatsAppModuleProviders, type NewDocumentRow, type NewFlowGraphRow, type NewMessageRow, type NewSessionRow, type NewSettingsRow, type NonceStoreInterface, OptimisticLockError, type PurgeExpiredDocumentsParams, type PurgeExpiredDocumentsResult, PurgeExpiredDocumentsUseCase, type RealtimeRelay, type ReceiveWebhookParams, type ReceiveWebhookResult, ReceiveWebhookUseCase, type ReleaseConversationParams, ReleaseConversationUseCase, type ListMessagesParams$1 as RepositoryListMessagesParams, type RunMetaWhatsAppMigrationsParams, type SaveFlowGraphParams, SaveFlowGraphUseCase, type SendMediaParams, SendMessageUseCase, type SendTemplateParams, type SendTextParams, SessionRepository, type SessionRow, SettingsRepository, type SettingsRow, SseHub, type SseListener, type TakeoverConversationParams, TakeoverConversationUseCase, type TicketStoreInterface, WEBHOOK_NONCE_TTL_SECONDS, WhatsAppChannelAdapter, claimWebhookDelivery, createMetaWhatsAppModule, documents, extractMediaDescriptor, flowGraphs, issueSseTicket, messages, metaWhatsAppMigrationsFolder, metaWhatsAppSchema, redeemSseTicket, runMetaWhatsAppMigrations, sessions, settings, verifyWebhookChallenge, verifyWebhookSignature };
2286
+ type CreateSendMediaActionParams = {
2287
+ flowMediaRepository: FlowMediaRepository;
2288
+ objectStorage: ObjectStorageInterface & {
2289
+ getObject: NonNullable<ObjectStorageInterface['getObject']>;
2290
+ };
2291
+ logMessage: LogMessageUseCase;
2292
+ startState: SessionState;
2293
+ onError?: (error: unknown, details: {
2294
+ flowKey: string;
2295
+ nodeId: string;
2296
+ uploadId: string;
2297
+ }) => void;
2298
+ };
2299
+ /**
2300
+ * Action `send_media`: ao passar pelo nó, envia os arquivos que a biblioteca tem anexados a ele.
2301
+ *
2302
+ * É a única action built-in que o módulo implementa de ponta a ponta — as outras
2303
+ * (`handoff`, `send_product_list`) dependem de regra de negócio do produto. Esta não: "mandar
2304
+ * estes arquivos ao chegar aqui" é comportamento de canal, e replicá-la em cada host seria
2305
+ * copiar o mesmo código com os mesmos bugs.
2306
+ *
2307
+ * O nó não guarda os arquivos em `actionParams` de propósito. Se guardasse, trocar o material
2308
+ * exigiria editar e republicar o grafo — e o ponto todo é o contrário: quem cuida do conteúdo
2309
+ * troca o arquivo na biblioteca e o fluxo continua igual.
2310
+ */
2311
+ declare function createSendMediaAction(params: CreateSendMediaActionParams): FlowActionHandler;
2312
+
2313
+ export { type AttachFlowMediaParams, type CompanyDocumentView, type CompanyDocumentsPage, type ConversationDocumentView, type ConversationDocumentsPage, type CreateFlowGraphParams, CreateFlowGraphUseCase, type CreateMetaWhatsAppModuleParams, type CreateSendMediaActionParams, type DeleteConversationParams, type DeleteConversationResult, DeleteConversationUseCase, DeleteFlowGraphUseCase, DocumentRepository, type DocumentRow, type DrizzleMigrateFunction, type ExportConversationParams, type ExportConversationResult, ExportConversationUseCase, FlowGraphRepository, type FlowGraphRow, FlowInterpreter, type FlowMediaLocation, FlowMediaRepository, type FlowMediaRow, type FlowRunResult, type FlowStepInput, type FlowStepResult, GetFlowGraphUseCase, GetLiveFlowPositionsUseCase, type IngestInboundMediaParams, type IngestInboundMediaResult, IngestInboundMediaUseCase, type InsertMessageParams, InvalidFlowGraphError, type LinkDocumentParams, type ListCompanyDocumentsParams, ListCompanyDocumentsUseCase, type ListConversationDocumentsParams, ListConversationDocumentsUseCase, type ListConversationsFilters, type ListConversationsParams, ListConversationsUseCase, type ListDocumentsParams, type ListDocumentsResult, ListFlowGraphsUseCase, type ListMessagesParams, ListMessagesUseCase, type LogMessageParams, LogMessageUseCase, META_WHATSAPP_MIGRATIONS_TABLE, type MessageModerator, MessageRepository, type MessageRow, type MetaWhatsAppDatabase, type MetaWhatsAppModule, type MetaWhatsAppModuleConfig, type MetaWhatsAppModuleFeatures, type MetaWhatsAppModuleProviders, type NewDocumentRow, type NewFlowGraphRow, type NewFlowMediaRow, type NewMessageRow, type NewSessionRow, type NewSettingsRow, type NonceStoreInterface, OptimisticLockError, type PurgeExpiredDocumentsParams, type PurgeExpiredDocumentsResult, PurgeExpiredDocumentsUseCase, type RealtimeRelay, type ReceiveWebhookParams, type ReceiveWebhookResult, ReceiveWebhookUseCase, type ReleaseConversationParams, ReleaseConversationUseCase, type ListMessagesParams$1 as RepositoryListMessagesParams, type RunMetaWhatsAppMigrationsParams, type SaveFlowGraphParams, SaveFlowGraphUseCase, type SendMediaParams, SendMessageUseCase, type SendTemplateParams, type SendTextParams, SessionRepository, type SessionRow, SettingsRepository, type SettingsRow, SseHub, type SseListener, type TakeoverConversationParams, TakeoverConversationUseCase, type TicketStoreInterface, type UpdateFlowMediaParams, WEBHOOK_NONCE_TTL_SECONDS, WhatsAppChannelAdapter, claimWebhookDelivery, createMetaWhatsAppModule, createSendMediaAction, documents, extractMediaDescriptor, flowGraphs, flowMedia, issueSseTicket, messages, metaWhatsAppMigrationsFolder, metaWhatsAppSchema, redeemSseTicket, runMetaWhatsAppMigrations, sessions, settings, verifyWebhookChallenge, verifyWebhookSignature };
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ var __dirname = /* @__PURE__ */ getDirname();
10
10
 
11
11
  // src/createMetaWhatsAppModule.ts
12
12
  import { WhatsAppMessageProvider } from "@adatechnology/meta-whatsapp-provider";
13
+ import { FLOW_ACTION_KIND } from "@adatechnology/meta-whatsapp-contracts";
13
14
 
14
15
  // src/repositories/SessionRepository.ts
15
16
  import { and, desc, eq, sql as sql2 } from "drizzle-orm";
@@ -197,6 +198,46 @@ var flowGraphs = metaWhatsAppSchema.table("flow_graphs", {
197
198
  }, (table) => [
198
199
  uniqueIndex("idx_flow_graphs_company_key").on(table.companyId, table.key)
199
200
  ]);
201
+ var flowMedia = metaWhatsAppSchema.table("flow_media", {
202
+ id: uuid("id").primaryKey().defaultRandom(),
203
+ companyId: uuid("company_id").notNull(),
204
+ flowKey: varchar("flow_key", {
205
+ length: 64
206
+ }).notNull(),
207
+ nodeId: varchar("node_id", {
208
+ length: 64
209
+ }).notNull(),
210
+ uploadId: varchar("upload_id", {
211
+ length: 256
212
+ }).notNull(),
213
+ filename: varchar("filename", {
214
+ length: 512
215
+ }).notNull(),
216
+ mimeType: varchar("mime_type", {
217
+ length: 128
218
+ }).notNull(),
219
+ sizeBytes: integer("size_bytes").notNull(),
220
+ // Legenda da mídia no WhatsApp. Por arquivo, não por nó: um nó que manda tabela de preços e
221
+ // um folder precisa de textos diferentes para cada um.
222
+ caption: text("caption"),
223
+ // Ordem de envio dentro do nó — o cliente recebe as mensagens em sequência, e "tabela antes
224
+ // do folder" é decisão de quem edita, não do banco.
225
+ sortOrder: integer("sort_order").notNull().default(0),
226
+ // Desligar sem desanexar: trocar o material da campanha é o caso comum, e apagar a linha
227
+ // perderia a ordem e a legenda já ajustadas.
228
+ active: boolean("active").notNull().default(true),
229
+ createdAt: timestamp("created_at", {
230
+ withTimezone: true
231
+ }).notNull().defaultNow(),
232
+ updatedAt: timestamp("updated_at", {
233
+ withTimezone: true
234
+ }).notNull().defaultNow()
235
+ }, (table) => [
236
+ index("idx_flow_media_node").on(table.companyId, table.flowKey, table.nodeId, table.sortOrder),
237
+ // O mesmo arquivo anexado duas vezes ao MESMO nó é erro de clique no editor, e o cliente
238
+ // receberia o documento repetido.
239
+ uniqueIndex("idx_flow_media_node_upload").on(table.companyId, table.flowKey, table.nodeId, table.uploadId)
240
+ ]);
200
241
  var settings = metaWhatsAppSchema.table("settings", {
201
242
  companyId: uuid("company_id").primaryKey(),
202
243
  templateName: varchar("template_name", {
@@ -1489,6 +1530,151 @@ var FlowInterpreter = class {
1489
1530
  }
1490
1531
  };
1491
1532
 
1533
+ // src/flows/createSendMediaAction.ts
1534
+ function mediaTypeFor2(mimeType) {
1535
+ if (mimeType.startsWith("image/")) return "image";
1536
+ if (mimeType.startsWith("audio/")) return "audio";
1537
+ if (mimeType.startsWith("video/")) return "video";
1538
+ return "document";
1539
+ }
1540
+ __name(mediaTypeFor2, "mediaTypeFor");
1541
+ function createSendMediaAction(params) {
1542
+ return async ({ node, session, channel }) => {
1543
+ if (!session.flowKey) return;
1544
+ const location = {
1545
+ companyId: session.companyId,
1546
+ flowKey: session.flowKey,
1547
+ nodeId: node.id
1548
+ };
1549
+ const attachments = await params.flowMediaRepository.listActive(location);
1550
+ for (const attachment of attachments) {
1551
+ try {
1552
+ const buffer = await params.objectStorage.getObject(attachment.uploadId);
1553
+ const { externalMessageId } = await channel.sendMedia({
1554
+ to: session.whatsappNumber,
1555
+ buffer,
1556
+ mimeType: attachment.mimeType,
1557
+ filename: attachment.filename,
1558
+ caption: attachment.caption ?? void 0
1559
+ });
1560
+ await params.logMessage.execute({
1561
+ companyId: session.companyId,
1562
+ whatsappNumber: session.whatsappNumber,
1563
+ direction: "outbound",
1564
+ sender: "bot",
1565
+ agentUserId: null,
1566
+ type: mediaTypeFor2(attachment.mimeType),
1567
+ content: attachment.caption ?? attachment.filename,
1568
+ payload: {
1569
+ filename: attachment.filename,
1570
+ mimeType: attachment.mimeType,
1571
+ uploadId: attachment.uploadId,
1572
+ flowMediaId: attachment.id
1573
+ },
1574
+ waMessageId: externalMessageId,
1575
+ status: "sent",
1576
+ startState: params.startState
1577
+ });
1578
+ } catch (error) {
1579
+ params.onError?.(error, {
1580
+ flowKey: location.flowKey,
1581
+ nodeId: node.id,
1582
+ uploadId: attachment.uploadId
1583
+ });
1584
+ }
1585
+ }
1586
+ };
1587
+ }
1588
+ __name(createSendMediaAction, "createSendMediaAction");
1589
+
1590
+ // src/repositories/FlowMediaRepository.ts
1591
+ import { and as and5, asc as asc2, eq as eq6, sql as sql4 } from "drizzle-orm";
1592
+ var FlowMediaRepository = class {
1593
+ static {
1594
+ __name(this, "FlowMediaRepository");
1595
+ }
1596
+ db;
1597
+ constructor(db) {
1598
+ this.db = db;
1599
+ }
1600
+ locationFilter(location) {
1601
+ return and5(eq6(flowMedia.companyId, location.companyId), eq6(flowMedia.flowKey, location.flowKey), eq6(flowMedia.nodeId, location.nodeId));
1602
+ }
1603
+ // O que o nó realmente envia, na ordem de envio. `active` filtrado aqui e não no chamador:
1604
+ // é a razão de a coluna existir, e um chamador que esquecesse do filtro mandaria ao cliente
1605
+ // justamente o material que alguém desligou.
1606
+ async listActive(location) {
1607
+ return this.db.select().from(flowMedia).where(and5(this.locationFilter(location), eq6(flowMedia.active, true))).orderBy(asc2(flowMedia.sortOrder), asc2(flowMedia.createdAt));
1608
+ }
1609
+ // Inclui os desligados — é a visão do editor, onde desligar precisa continuar visível para
1610
+ // poder ser religado.
1611
+ async listAll(location) {
1612
+ return this.db.select().from(flowMedia).where(this.locationFilter(location)).orderBy(asc2(flowMedia.sortOrder), asc2(flowMedia.createdAt));
1613
+ }
1614
+ /**
1615
+ * Anexa um arquivo já existente no storage ao nó.
1616
+ *
1617
+ * `onConflictDoUpdate` em vez de deixar estourar: reanexar o mesmo arquivo é clique repetido no
1618
+ * editor, e o esperado ali é atualizar a legenda/ordem — não um erro de índice único na cara de
1619
+ * quem está montando o fluxo.
1620
+ */
1621
+ async attach(params) {
1622
+ const [row] = await this.db.insert(flowMedia).values({
1623
+ companyId: params.companyId,
1624
+ flowKey: params.flowKey,
1625
+ nodeId: params.nodeId,
1626
+ uploadId: params.uploadId,
1627
+ filename: params.filename,
1628
+ mimeType: params.mimeType,
1629
+ sizeBytes: params.sizeBytes,
1630
+ caption: params.caption ?? null,
1631
+ sortOrder: params.sortOrder ?? 0
1632
+ }).onConflictDoUpdate({
1633
+ target: [
1634
+ flowMedia.companyId,
1635
+ flowMedia.flowKey,
1636
+ flowMedia.nodeId,
1637
+ flowMedia.uploadId
1638
+ ],
1639
+ set: {
1640
+ caption: params.caption ?? null,
1641
+ sortOrder: params.sortOrder ?? 0,
1642
+ active: true,
1643
+ updatedAt: sql4`now()`
1644
+ }
1645
+ }).returning();
1646
+ return row;
1647
+ }
1648
+ async update(params) {
1649
+ const [row] = await this.db.update(flowMedia).set({
1650
+ ...params.caption !== void 0 ? {
1651
+ caption: params.caption
1652
+ } : {},
1653
+ ...params.sortOrder !== void 0 ? {
1654
+ sortOrder: params.sortOrder
1655
+ } : {},
1656
+ ...params.active !== void 0 ? {
1657
+ active: params.active
1658
+ } : {},
1659
+ updatedAt: sql4`now()`
1660
+ }).where(and5(eq6(flowMedia.companyId, params.companyId), eq6(flowMedia.id, params.id))).returning();
1661
+ return row;
1662
+ }
1663
+ /**
1664
+ * Desanexa do nó. Não toca no storage de propósito: o mesmo `uploadId` pode estar anexado a
1665
+ * outro nó ou a outro fluxo, e apagar o binário aqui quebraria os demais. Quem apaga objeto é o
1666
+ * host, que é dono da biblioteca de arquivos.
1667
+ */
1668
+ async detach(params) {
1669
+ await this.db.delete(flowMedia).where(and5(eq6(flowMedia.companyId, params.companyId), eq6(flowMedia.id, params.id)));
1670
+ }
1671
+ // Chamado ao salvar o grafo: nós apagados no editor deixam linhas que nada mais alcança.
1672
+ async detachRemovedNodes(params) {
1673
+ const condition = params.existingNodeIds.length === 0 ? void 0 : sql4`${flowMedia.nodeId} NOT IN ${params.existingNodeIds}`;
1674
+ await this.db.delete(flowMedia).where(and5(eq6(flowMedia.companyId, params.companyId), eq6(flowMedia.flowKey, params.flowKey), condition));
1675
+ }
1676
+ };
1677
+
1492
1678
  // src/channel/WhatsAppChannelAdapter.ts
1493
1679
  import { WhatsAppWindowExpiredError as ProviderWindowExpiredError } from "@adatechnology/meta-graph-core";
1494
1680
  import { WindowExpiredError as WindowExpiredError2 } from "@adatechnology/meta-whatsapp-contracts";
@@ -1585,7 +1771,7 @@ async function claimWebhookDelivery(params) {
1585
1771
  __name(claimWebhookDelivery, "claimWebhookDelivery");
1586
1772
 
1587
1773
  // src/channel/IngestInboundMedia.use-case.ts
1588
- import { eq as eq6, and as and5 } from "drizzle-orm";
1774
+ import { eq as eq7, and as and6 } from "drizzle-orm";
1589
1775
  var IngestInboundMediaUseCase = class {
1590
1776
  static {
1591
1777
  __name(this, "IngestInboundMediaUseCase");
@@ -1601,7 +1787,7 @@ var IngestInboundMediaUseCase = class {
1601
1787
  this.documentRepository = documentRepository;
1602
1788
  }
1603
1789
  async execute(params) {
1604
- const [message] = await this.db.select().from(messages).where(and5(eq6(messages.companyId, params.companyId), eq6(messages.id, params.messageId))).limit(1);
1790
+ const [message] = await this.db.select().from(messages).where(and6(eq7(messages.companyId, params.companyId), eq7(messages.id, params.messageId))).limit(1);
1605
1791
  if (!message) throw new Error(`Mensagem ${params.messageId} n\xE3o encontrada para ingest\xE3o de m\xEDdia`);
1606
1792
  const payload = message.payload ?? {};
1607
1793
  if (payload["uploadId"] && payload["sourceMediaId"] === params.sourceMediaId) {
@@ -1622,13 +1808,17 @@ var IngestInboundMediaUseCase = class {
1622
1808
  uploadId,
1623
1809
  sourceMediaId: params.sourceMediaId,
1624
1810
  mimeType: mimeType || params.mimeType,
1811
+ // O tamanho já está na mão (é o buffer que acabou de ser copiado) e a bolha de documento o
1812
+ // exibe ao lado do tipo. Sem gravar aqui, a UI mostraria "PDF" sem o "· 180 KB", e buscá-lo
1813
+ // depois custaria uma consulta à tabela de documentos por mensagem renderizada.
1814
+ sizeBytes: buffer.length,
1625
1815
  ...params.filename ? {
1626
1816
  filename: params.filename
1627
1817
  } : {}
1628
1818
  };
1629
1819
  await this.db.update(messages).set({
1630
1820
  payload: updatedPayload
1631
- }).where(and5(eq6(messages.companyId, params.companyId), eq6(messages.id, params.messageId)));
1821
+ }).where(and6(eq7(messages.companyId, params.companyId), eq7(messages.id, params.messageId)));
1632
1822
  await this.documentRepository?.link({
1633
1823
  companyId: params.companyId,
1634
1824
  sessionId: message.sessionId,
@@ -1830,6 +2020,7 @@ function createMetaWhatsAppModule(params) {
1830
2020
  const settingsRepository = new SettingsRepository(db);
1831
2021
  const flowGraphRepository = new FlowGraphRepository(db);
1832
2022
  const documentRepository = new DocumentRepository(db);
2023
+ const flowMediaRepository = new FlowMediaRepository(db);
1833
2024
  const logMessage = new LogMessageUseCase(sessionRepository, messageRepository, providers.realtime, providers.moderator);
1834
2025
  const sendMessage = new SendMessageUseCase(channel, sessionRepository, logMessage, providers.objectStorage, documentRepository);
1835
2026
  const receiveWebhook = new ReceiveWebhookUseCase({
@@ -1843,6 +2034,15 @@ function createMetaWhatsAppModule(params) {
1843
2034
  realtime: providers.realtime
1844
2035
  });
1845
2036
  const flowInterpreter = flowEngineEnabled ? new FlowInterpreter() : void 0;
2037
+ if (flowInterpreter && providers.objectStorage?.getObject) {
2038
+ flowInterpreter.registerFlowAction(FLOW_ACTION_KIND.SEND_MEDIA, createSendMediaAction({
2039
+ flowMediaRepository,
2040
+ objectStorage: providers.objectStorage,
2041
+ logMessage,
2042
+ startState,
2043
+ onError: hooks?.onFlowMediaError
2044
+ }));
2045
+ }
1846
2046
  const ingestInboundMedia = providers.objectStorage ? new IngestInboundMediaUseCase(db, channel, providers.objectStorage, documentRepository) : void 0;
1847
2047
  const listDocuments = new ListConversationDocumentsUseCase(sessionRepository, documentRepository);
1848
2048
  return {
@@ -1886,7 +2086,11 @@ function createMetaWhatsAppModule(params) {
1886
2086
  save: new SaveFlowGraphUseCase(flowGraphRepository),
1887
2087
  delete: new DeleteFlowGraphUseCase(flowGraphRepository),
1888
2088
  livePositions: new GetLiveFlowPositionsUseCase(flowGraphRepository),
1889
- repository: flowGraphRepository
2089
+ repository: flowGraphRepository,
2090
+ // Biblioteca de mídia dos nós `send_media` — o host liga nas rotas do editor
2091
+ // (anexar/reordenar/desligar). Existe mesmo sem storage injetado: gerenciar anexos é
2092
+ // consultar a tabela, e só o ENVIO precisa dos bytes.
2093
+ mediaRepository: flowMediaRepository
1890
2094
  } : void 0,
1891
2095
  catalog: providers.catalog
1892
2096
  };
@@ -1991,6 +2195,7 @@ export {
1991
2195
  ExportConversationUseCase,
1992
2196
  FlowGraphRepository,
1993
2197
  FlowInterpreter,
2198
+ FlowMediaRepository,
1994
2199
  GetFlowGraphUseCase,
1995
2200
  GetLiveFlowPositionsUseCase,
1996
2201
  IngestInboundMediaUseCase,
@@ -2017,9 +2222,11 @@ export {
2017
2222
  WhatsAppChannelAdapter,
2018
2223
  claimWebhookDelivery,
2019
2224
  createMetaWhatsAppModule,
2225
+ createSendMediaAction,
2020
2226
  documents,
2021
2227
  extractMediaDescriptor,
2022
2228
  flowGraphs,
2229
+ flowMedia,
2023
2230
  issueSseTicket,
2024
2231
  messages,
2025
2232
  metaWhatsAppMigrationsFolder,