( tableId: string, tableName: string, eventType: EventType, rows: TableRow[], oldRows: Map<string, RowData> | null, schema: TableSchema, requestId: string )
| 47 | * @param oldRows - Map of row ID to previous data. Pass null for inserts. |
| 48 | */ |
| 49 | export async function fireTableTrigger( |
| 50 | tableId: string, |
| 51 | tableName: string, |
| 52 | eventType: EventType, |
| 53 | rows: TableRow[], |
| 54 | oldRows: Map<string, RowData> | null, |
| 55 | schema: TableSchema, |
| 56 | requestId: string |
| 57 | ): Promise<void> { |
| 58 | try { |
| 59 | // Lazy: the webhook utils/processor pull in the executor + blocks stack. |
| 60 | // Eager imports would force every `lib/table/service` consumer (e.g. the |
| 61 | // dispatcher) to pay that cold-start even when no trigger fires. |
| 62 | const { fetchActiveWebhooks } = await import('@/lib/webhooks/polling/utils') |
| 63 | const webhooks = await fetchActiveWebhooks('table') |
| 64 | if (webhooks.length === 0) return |
| 65 | |
| 66 | const headers = schema.columns.map((c) => c.name) |
| 67 | // The webhook payload is name-keyed (the workflow author references columns |
| 68 | // by name); stored row data is id-keyed, so translate on the way out. |
| 69 | const nameById = buildNameById(schema) |
| 70 | |
| 71 | // Filter to webhooks watching this table with a matching event type |
| 72 | const matching = webhooks.filter((entry) => { |
| 73 | const config = entry.webhook.providerConfig as WebhookConfig | null |
| 74 | // Canonical key `tableId` first; `tableSelector`/`manualTableId` are a transitional |
| 75 | // basic-first fallback for configs deployed before the canonical key was written. |
| 76 | const configTableId = readCanonicalTriggerValue( |
| 77 | config?.tableId, |
| 78 | config?.tableSelector, |
| 79 | config?.manualTableId |
| 80 | ) |
| 81 | if (configTableId !== tableId) return false |
| 82 | |
| 83 | const configEventType = config?.eventType ?? 'insert' |
| 84 | return configEventType === eventType |
| 85 | }) |
| 86 | |
| 87 | if (matching.length === 0) return |
| 88 | |
| 89 | const { processPolledWebhookEvent } = await import('@/lib/webhooks/processor') |
| 90 | |
| 91 | logger.info( |
| 92 | `[${requestId}] Firing ${matching.length} trigger(s) for ${rows.length} ${eventType} event(s) in table ${tableId}` |
| 93 | ) |
| 94 | |
| 95 | for (const { webhook: webhookData, workflow: workflowData } of matching) { |
| 96 | const config = webhookData.providerConfig as WebhookConfig | null |
| 97 | const watchColumns = parseWatchColumns(config?.watchColumns) |
| 98 | const includeHeaders = config?.includeHeaders !== false |
| 99 | |
| 100 | for (const row of rows) { |
| 101 | const previousIdData = oldRows?.get(row.id) ?? null |
| 102 | // Translate id-keyed stored data → name-keyed for the external payload. |
| 103 | const rawRow = rowDataIdToName(row.data, nameById) |
| 104 | const previousRow = previousIdData ? rowDataIdToName(previousIdData, nameById) : null |
| 105 | const changedColumns = previousIdData |
| 106 | ? detectChangedColumns(previousIdData, row.data) |
no test coverage detected