Skip to content

Commit 520f922

Browse files
Adam GoughAdam Gough
authored andcommitted
edited airtable payload and webhook deletion
1 parent 68936ad commit 520f922

4 files changed

Lines changed: 228 additions & 4 deletions

File tree

apps/sim/app/api/webhooks/[id]/route.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { eq } from 'drizzle-orm'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { getSession } from '@/lib/auth'
4+
import { env } from '@/lib/env'
45
import { createLogger } from '@/lib/logs/console/logger'
56
import { getUserEntityPermissions } from '@/lib/permissions/utils'
7+
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
68
import { db } from '@/db'
79
import { webhook, workflow } from '@/db/schema'
810

@@ -242,6 +244,167 @@ export async function DELETE(
242244

243245
const foundWebhook = webhookData.webhook
244246

247+
// If it's an Airtable webhook, delete it from Airtable first
248+
if (foundWebhook.provider === 'airtable') {
249+
try {
250+
const { baseId, externalId } = (foundWebhook.providerConfig || {}) as {
251+
baseId?: string
252+
externalId?: string
253+
}
254+
255+
if (!baseId) {
256+
logger.warn(`[${requestId}] Missing baseId for Airtable webhook deletion.`, {
257+
webhookId: id,
258+
})
259+
return NextResponse.json(
260+
{ error: 'Missing baseId for Airtable webhook deletion' },
261+
{ status: 400 }
262+
)
263+
}
264+
265+
// Get access token for the workflow owner
266+
const userIdForToken = webhookData.workflow.userId
267+
const accessToken = await getOAuthToken(userIdForToken, 'airtable')
268+
if (!accessToken) {
269+
logger.warn(
270+
`[${requestId}] Could not retrieve Airtable access token for user ${userIdForToken}. Cannot delete webhook in Airtable.`,
271+
{ webhookId: id }
272+
)
273+
return NextResponse.json(
274+
{ error: 'Airtable access token not found for webhook deletion' },
275+
{ status: 401 }
276+
)
277+
}
278+
279+
// Resolve externalId if missing by listing webhooks and matching our notificationUrl
280+
let resolvedExternalId: string | undefined = externalId
281+
282+
if (!resolvedExternalId) {
283+
try {
284+
const requestOrigin = new URL(request.url).origin
285+
const effectiveOrigin = requestOrigin.includes('localhost')
286+
? env.NEXT_PUBLIC_APP_URL || requestOrigin
287+
: requestOrigin
288+
const expectedNotificationUrl = `${effectiveOrigin}/api/webhooks/trigger/${foundWebhook.path}`
289+
290+
const listUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks`
291+
const listResp = await fetch(listUrl, {
292+
headers: {
293+
Authorization: `Bearer ${accessToken}`,
294+
},
295+
})
296+
const listBody = await listResp.json().catch(() => null)
297+
298+
if (listResp.ok && listBody && Array.isArray(listBody.webhooks)) {
299+
const match = listBody.webhooks.find((w: any) => {
300+
const url: string | undefined = w?.notificationUrl
301+
if (!url) return false
302+
// Prefer exact match; fallback to suffix match to handle origin/host remaps
303+
return (
304+
url === expectedNotificationUrl ||
305+
url.endsWith(`/api/webhooks/trigger/${foundWebhook.path}`)
306+
)
307+
})
308+
if (match?.id) {
309+
resolvedExternalId = match.id as string
310+
// Persist resolved externalId for future operations
311+
try {
312+
await db
313+
.update(webhook)
314+
.set({
315+
providerConfig: {
316+
...(foundWebhook.providerConfig || {}),
317+
externalId: resolvedExternalId,
318+
},
319+
updatedAt: new Date(),
320+
})
321+
.where(eq(webhook.id, id))
322+
} catch {
323+
// non-fatal persistence error
324+
}
325+
logger.info(`[${requestId}] Resolved Airtable externalId by listing webhooks`, {
326+
baseId,
327+
externalId: resolvedExternalId,
328+
})
329+
} else {
330+
logger.warn(`[${requestId}] Could not resolve Airtable externalId from list`, {
331+
baseId,
332+
expectedNotificationUrl,
333+
})
334+
}
335+
} else {
336+
logger.warn(`[${requestId}] Failed to list Airtable webhooks to resolve externalId`, {
337+
baseId,
338+
status: listResp.status,
339+
body: listBody,
340+
})
341+
}
342+
} catch (e: any) {
343+
logger.warn(`[${requestId}] Error attempting to resolve Airtable externalId`, {
344+
error: e?.message,
345+
})
346+
}
347+
}
348+
349+
// If still not resolvable, skip remote deletion but proceed with local delete
350+
if (!resolvedExternalId) {
351+
logger.info(
352+
`[${requestId}] Airtable externalId not found; skipping remote deletion and proceeding to remove local record`,
353+
{ baseId }
354+
)
355+
}
356+
357+
if (resolvedExternalId) {
358+
const airtableDeleteUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks/${resolvedExternalId}`
359+
const airtableResponse = await fetch(airtableDeleteUrl, {
360+
method: 'DELETE',
361+
headers: {
362+
Authorization: `Bearer ${accessToken}`,
363+
},
364+
})
365+
366+
// Attempt to parse error body for better diagnostics
367+
if (!airtableResponse.ok) {
368+
let responseBody: any = null
369+
try {
370+
responseBody = await airtableResponse.json()
371+
} catch {
372+
// ignore parse errors
373+
}
374+
375+
logger.error(
376+
`[${requestId}] Failed to delete Airtable webhook in Airtable. Status: ${airtableResponse.status}`,
377+
{ baseId, externalId: resolvedExternalId, response: responseBody }
378+
)
379+
return NextResponse.json(
380+
{
381+
error: 'Failed to delete webhook from Airtable',
382+
details:
383+
(responseBody && (responseBody.error?.message || responseBody.error)) ||
384+
`Status ${airtableResponse.status}`,
385+
},
386+
{ status: 500 }
387+
)
388+
}
389+
390+
logger.info(`[${requestId}] Successfully deleted Airtable webhook in Airtable`, {
391+
baseId,
392+
externalId: resolvedExternalId,
393+
})
394+
}
395+
} catch (error: any) {
396+
logger.error(`[${requestId}] Error deleting Airtable webhook`, {
397+
webhookId: id,
398+
error: error.message,
399+
stack: error.stack,
400+
})
401+
return NextResponse.json(
402+
{ error: 'Failed to delete webhook from Airtable', details: error.message },
403+
{ status: 500 }
404+
)
405+
}
406+
}
407+
245408
// If it's a Telegram webhook, delete it from Telegram first
246409
if (foundWebhook.provider === 'telegram') {
247410
try {

apps/sim/executor/handlers/trigger/trigger-handler.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,12 @@ export class TriggerBlockHandler implements BlockHandler {
6363
}
6464
}
6565

66-
// Extract the flattened properties that should be at root level (non-GitHub)
66+
// Provider-specific early return for Airtable: preserve raw shape entirely
67+
if (provider === 'airtable') {
68+
return starterOutput
69+
}
70+
71+
// Extract the flattened properties that should be at root level (non-GitHub/Airtable)
6772
const result: any = {
6873
// Always keep the input at root level
6974
input: starterOutput.input,

apps/sim/lib/webhooks/utils.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,8 @@ export async function fetchAndProcessAirtablePayloads(
825825
let apiCallCount = 0
826826
// Use a Map to consolidate changes per record ID
827827
const consolidatedChangesMap = new Map<string, AirtableChange>()
828+
// Capture raw payloads from Airtable for exposure to workflows
829+
const allPayloads: any[] = []
828830
const localProviderConfig = {
829831
...((webhookData.providerConfig as Record<string, any>) || {}),
830832
} // Local copy
@@ -1021,6 +1023,10 @@ export async function fetchAndProcessAirtablePayloads(
10211023
// --- Process and Consolidate Changes ---
10221024
if (receivedPayloads.length > 0) {
10231025
payloadsFetched += receivedPayloads.length
1026+
// Keep the raw payloads for later exposure to the workflow
1027+
for (const p of receivedPayloads) {
1028+
allPayloads.push(p)
1029+
}
10241030
let changeCount = 0
10251031
for (const payload of receivedPayloads) {
10261032
if (payload.changedTablesById) {
@@ -1186,10 +1192,25 @@ export async function fetchAndProcessAirtablePayloads(
11861192
)
11871193

11881194
// --- Execute Workflow if we have changes (simplified - no lock check) ---
1189-
if (finalConsolidatedChanges.length > 0) {
1195+
if (finalConsolidatedChanges.length > 0 || allPayloads.length > 0) {
11901196
try {
1191-
// Format the input for the executor using the consolidated changes
1192-
const input = { airtableChanges: finalConsolidatedChanges } // Use the consolidated array
1197+
// Build input exposing raw payloads and consolidated changes
1198+
const latestPayload = allPayloads.length > 0 ? allPayloads[allPayloads.length - 1] : null
1199+
const input: any = {
1200+
// Raw Airtable payloads as received from the API
1201+
payloads: allPayloads,
1202+
latestPayload,
1203+
// Consolidated, simplified changes for convenience
1204+
airtableChanges: finalConsolidatedChanges,
1205+
// Include webhook metadata for resolver fallbacks
1206+
webhook: {
1207+
data: {
1208+
provider: 'airtable',
1209+
providerConfig: webhookData.providerConfig,
1210+
payload: latestPayload,
1211+
},
1212+
},
1213+
}
11931214

11941215
// CRITICAL EXECUTION TRACE POINT
11951216
logger.info(
@@ -1206,6 +1227,7 @@ export async function fetchAndProcessAirtablePayloads(
12061227
logger.info(`[${requestId}] CRITICAL_TRACE: Airtable changes processed, returning input`, {
12071228
workflowId: workflowData.id,
12081229
recordCount: finalConsolidatedChanges.length,
1230+
rawPayloadCount: allPayloads.length,
12091231
timestamp: new Date().toISOString(),
12101232
})
12111233

apps/sim/triggers/airtable/webhook.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,40 @@ export const airtableWebhookTrigger: TriggerConfig = {
3838
},
3939

4040
outputs: {
41+
payloads: {
42+
type: 'array',
43+
description: 'The payloads of the Airtable changes',
44+
},
45+
latestPayload: {
46+
timestamp: {
47+
type: 'string',
48+
description: 'The timestamp of the Airtable change',
49+
},
50+
payloadFormat: {
51+
type: 'object',
52+
description: 'The format of the Airtable change',
53+
},
54+
actionMetadata: {
55+
source: {
56+
type: 'string',
57+
description: 'The source of the Airtable change',
58+
},
59+
sourceMetadata: {
60+
pageId: {
61+
type: 'string',
62+
description: 'The ID of the page that triggered the Airtable change',
63+
},
64+
},
65+
changedTablesById: {
66+
type: 'object',
67+
description: 'The tables that were changed',
68+
},
69+
baseTransactionNumber: {
70+
type: 'number',
71+
description: 'The transaction number of the Airtable change',
72+
},
73+
},
74+
},
4175
airtableChanges: {
4276
type: 'array',
4377
description: 'Changes made to the Airtable table',

0 commit comments

Comments
 (0)