Skip to content

Commit e64129c

Browse files
authored
feat(triggers): modify triggers to use existing subblock system, webhook order of operations improvements (#1774)
* feat(triggers): make triggers use existing subblock system, need to still fix webhook URL on multiselect and add script in text subblock for google form * minimize added subblocks, cleanup code, make triggers first-class subblock users * remove multi select dropdown and add props to existing dropdown instead * cleanup dropdown * add socket op to delete external webhook connections on block delete * establish external webhook before creating webhook DB record, surface better errors for ones that require external connections * fix copy button in short-input * revert environment.ts, cleanup * add triggers registry, update copilot tool to reflect new trigger setup * update trigger-save subblock * clean * cleanup * remove unused subblock store op, update search modal to reflect list of triggers * add init from workflow to subblock store to populate new subblock format from old triggers * fix mapping of old names to new ones * added debug logging * remove all extraneous debug logging and added mapping for triggerConfig field names that were changed * fix trigger config for triggers w/ multiple triggers * edge cases for effectiveTriggerId * cleaned up * fix dropdown multiselect * fix multiselect * updated short-input copy button * duplicate blocks in trigger mode * ack PR comments
1 parent 70ff539 commit e64129c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+3972
-3483
lines changed

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

Lines changed: 2 additions & 214 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ import { type NextRequest, NextResponse } from 'next/server'
55
import { getSession } from '@/lib/auth'
66
import { createLogger } from '@/lib/logs/console/logger'
77
import { getUserEntityPermissions } from '@/lib/permissions/utils'
8-
import { getBaseUrl } from '@/lib/urls/utils'
98
import { generateRequestId } from '@/lib/utils'
10-
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
119

1210
const logger = createLogger('WebhookAPI')
1311

@@ -245,219 +243,9 @@ export async function DELETE(
245243

246244
const foundWebhook = webhookData.webhook
247245

248-
// If it's an Airtable webhook, delete it from Airtable first
249-
if (foundWebhook.provider === 'airtable') {
250-
try {
251-
const { baseId, externalId } = (foundWebhook.providerConfig || {}) as {
252-
baseId?: string
253-
externalId?: string
254-
}
255-
256-
if (!baseId) {
257-
logger.warn(`[${requestId}] Missing baseId for Airtable webhook deletion.`, {
258-
webhookId: id,
259-
})
260-
return NextResponse.json(
261-
{ error: 'Missing baseId for Airtable webhook deletion' },
262-
{ status: 400 }
263-
)
264-
}
265-
266-
// Get access token for the workflow owner
267-
const userIdForToken = webhookData.workflow.userId
268-
const accessToken = await getOAuthToken(userIdForToken, 'airtable')
269-
if (!accessToken) {
270-
logger.warn(
271-
`[${requestId}] Could not retrieve Airtable access token for user ${userIdForToken}. Cannot delete webhook in Airtable.`,
272-
{ webhookId: id }
273-
)
274-
return NextResponse.json(
275-
{ error: 'Airtable access token not found for webhook deletion' },
276-
{ status: 401 }
277-
)
278-
}
279-
280-
// Resolve externalId if missing by listing webhooks and matching our notificationUrl
281-
let resolvedExternalId: string | undefined = externalId
282-
283-
if (!resolvedExternalId) {
284-
try {
285-
const expectedNotificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${foundWebhook.path}`
286-
287-
const listUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks`
288-
const listResp = await fetch(listUrl, {
289-
headers: {
290-
Authorization: `Bearer ${accessToken}`,
291-
},
292-
})
293-
const listBody = await listResp.json().catch(() => null)
294-
295-
if (listResp.ok && listBody && Array.isArray(listBody.webhooks)) {
296-
const match = listBody.webhooks.find((w: any) => {
297-
const url: string | undefined = w?.notificationUrl
298-
if (!url) return false
299-
// Prefer exact match; fallback to suffix match to handle origin/host remaps
300-
return (
301-
url === expectedNotificationUrl ||
302-
url.endsWith(`/api/webhooks/trigger/${foundWebhook.path}`)
303-
)
304-
})
305-
if (match?.id) {
306-
resolvedExternalId = match.id as string
307-
// Persist resolved externalId for future operations
308-
try {
309-
await db
310-
.update(webhook)
311-
.set({
312-
providerConfig: {
313-
...(foundWebhook.providerConfig || {}),
314-
externalId: resolvedExternalId,
315-
},
316-
updatedAt: new Date(),
317-
})
318-
.where(eq(webhook.id, id))
319-
} catch {
320-
// non-fatal persistence error
321-
}
322-
logger.info(`[${requestId}] Resolved Airtable externalId by listing webhooks`, {
323-
baseId,
324-
externalId: resolvedExternalId,
325-
})
326-
} else {
327-
logger.warn(`[${requestId}] Could not resolve Airtable externalId from list`, {
328-
baseId,
329-
expectedNotificationUrl,
330-
})
331-
}
332-
} else {
333-
logger.warn(`[${requestId}] Failed to list Airtable webhooks to resolve externalId`, {
334-
baseId,
335-
status: listResp.status,
336-
body: listBody,
337-
})
338-
}
339-
} catch (e: any) {
340-
logger.warn(`[${requestId}] Error attempting to resolve Airtable externalId`, {
341-
error: e?.message,
342-
})
343-
}
344-
}
345-
346-
// If still not resolvable, skip remote deletion but proceed with local delete
347-
if (!resolvedExternalId) {
348-
logger.info(
349-
`[${requestId}] Airtable externalId not found; skipping remote deletion and proceeding to remove local record`,
350-
{ baseId }
351-
)
352-
}
353-
354-
if (resolvedExternalId) {
355-
const airtableDeleteUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks/${resolvedExternalId}`
356-
const airtableResponse = await fetch(airtableDeleteUrl, {
357-
method: 'DELETE',
358-
headers: {
359-
Authorization: `Bearer ${accessToken}`,
360-
},
361-
})
362-
363-
// Attempt to parse error body for better diagnostics
364-
if (!airtableResponse.ok) {
365-
let responseBody: any = null
366-
try {
367-
responseBody = await airtableResponse.json()
368-
} catch {
369-
// ignore parse errors
370-
}
371-
372-
logger.error(
373-
`[${requestId}] Failed to delete Airtable webhook in Airtable. Status: ${airtableResponse.status}`,
374-
{ baseId, externalId: resolvedExternalId, response: responseBody }
375-
)
376-
return NextResponse.json(
377-
{
378-
error: 'Failed to delete webhook from Airtable',
379-
details:
380-
(responseBody && (responseBody.error?.message || responseBody.error)) ||
381-
`Status ${airtableResponse.status}`,
382-
},
383-
{ status: 500 }
384-
)
385-
}
386-
387-
logger.info(`[${requestId}] Successfully deleted Airtable webhook in Airtable`, {
388-
baseId,
389-
externalId: resolvedExternalId,
390-
})
391-
}
392-
} catch (error: any) {
393-
logger.error(`[${requestId}] Error deleting Airtable webhook`, {
394-
webhookId: id,
395-
error: error.message,
396-
stack: error.stack,
397-
})
398-
return NextResponse.json(
399-
{ error: 'Failed to delete webhook from Airtable', details: error.message },
400-
{ status: 500 }
401-
)
402-
}
403-
}
404-
405-
// Delete Microsoft Teams subscription if applicable
406-
if (foundWebhook.provider === 'microsoftteams') {
407-
const { deleteTeamsSubscription } = await import('@/lib/webhooks/webhook-helpers')
408-
logger.info(`[${requestId}] Deleting Teams subscription for webhook ${id}`)
409-
await deleteTeamsSubscription(foundWebhook, webhookData.workflow, requestId)
410-
// Don't fail webhook deletion if subscription cleanup fails
411-
}
412-
413-
// Delete Telegram webhook if applicable
414-
if (foundWebhook.provider === 'telegram') {
415-
try {
416-
const { botToken } = (foundWebhook.providerConfig || {}) as { botToken?: string }
417-
418-
if (!botToken) {
419-
logger.warn(`[${requestId}] Missing botToken for Telegram webhook deletion.`, {
420-
webhookId: id,
421-
})
422-
return NextResponse.json(
423-
{ error: 'Missing botToken for Telegram webhook deletion' },
424-
{ status: 400 }
425-
)
426-
}
427-
428-
const telegramApiUrl = `https://api.telegram.org/bot${botToken}/deleteWebhook`
429-
const telegramResponse = await fetch(telegramApiUrl, {
430-
method: 'POST',
431-
headers: { 'Content-Type': 'application/json' },
432-
})
433-
434-
const responseBody = await telegramResponse.json()
435-
if (!telegramResponse.ok || !responseBody.ok) {
436-
const errorMessage =
437-
responseBody.description ||
438-
`Failed to delete Telegram webhook. Status: ${telegramResponse.status}`
439-
logger.error(`[${requestId}] ${errorMessage}`, { response: responseBody })
440-
return NextResponse.json(
441-
{ error: 'Failed to delete webhook from Telegram', details: errorMessage },
442-
{ status: 500 }
443-
)
444-
}
445-
446-
logger.info(`[${requestId}] Successfully deleted Telegram webhook for webhook ${id}`)
447-
} catch (error: any) {
448-
logger.error(`[${requestId}] Error deleting Telegram webhook`, {
449-
webhookId: id,
450-
error: error.message,
451-
stack: error.stack,
452-
})
453-
return NextResponse.json(
454-
{ error: 'Failed to delete webhook from Telegram', details: error.message },
455-
{ status: 500 }
456-
)
457-
}
458-
}
246+
const { cleanupExternalWebhook } = await import('@/lib/webhooks/webhook-helpers')
247+
await cleanupExternalWebhook(foundWebhook, webhookData.workflow, requestId)
459248

460-
// Delete the webhook from the database
461249
await db.delete(webhook).where(eq(webhook.id, id))
462250

463251
logger.info(`[${requestId}] Successfully deleted webhook: ${id}`)

0 commit comments

Comments
 (0)