11import { db } from '@sim/db'
22import { webhook } from '@sim/db/schema'
33import { createLogger } from '@sim/logger'
4- import { and , eq } from 'drizzle-orm'
4+ import { eq , inArray } from 'drizzle-orm'
55import { nanoid } from 'nanoid'
66import type { NextRequest } from 'next/server'
77import { getProviderIdFromServiceId } from '@/lib/oauth'
@@ -298,7 +298,7 @@ async function syncCredentialSetWebhooks(params: {
298298 return null
299299}
300300
301- async function upsertSingleWebhook ( params : {
301+ async function createWebhookForBlock ( params : {
302302 request : NextRequest
303303 workflowId : string
304304 workflow : Record < string , unknown >
@@ -321,66 +321,6 @@ async function upsertSingleWebhook(params: {
321321 requestId,
322322 } = params
323323
324- const existingWebhooks = await db
325- . select ( )
326- . from ( webhook )
327- . where ( and ( eq ( webhook . workflowId , workflowId ) , eq ( webhook . blockId , block . id ) ) )
328- . limit ( 1 )
329-
330- const existing = existingWebhooks [ 0 ]
331- if ( existing ) {
332- const existingConfig = ( existing . providerConfig as Record < string , unknown > ) || { }
333- let nextProviderConfig = providerConfig
334-
335- if (
336- shouldRecreateExternalWebhookSubscription ( {
337- previousProvider : existing . provider as string ,
338- nextProvider : provider ,
339- previousConfig : existingConfig ,
340- nextConfig : nextProviderConfig ,
341- } )
342- ) {
343- await cleanupExternalWebhook ( existing , workflow , requestId )
344- const result = await createExternalWebhookSubscription (
345- request ,
346- {
347- ...existing ,
348- provider,
349- path : triggerPath ,
350- providerConfig : nextProviderConfig ,
351- } ,
352- workflow ,
353- userId ,
354- requestId
355- )
356- nextProviderConfig = result . updatedProviderConfig as Record < string , unknown >
357- }
358-
359- const finalProviderConfig = {
360- ...nextProviderConfig ,
361- credentialId : nextProviderConfig . credentialId ?? existingConfig . credentialId ,
362- credentialSetId : nextProviderConfig . credentialSetId ?? existingConfig . credentialSetId ,
363- userId : nextProviderConfig . userId ?? existingConfig . userId ,
364- historyId : existingConfig . historyId ,
365- lastCheckedTimestamp : existingConfig . lastCheckedTimestamp ,
366- setupCompleted : existingConfig . setupCompleted ,
367- externalId : nextProviderConfig . externalId ?? existingConfig . externalId ,
368- }
369-
370- await db
371- . update ( webhook )
372- . set ( {
373- path : triggerPath ,
374- provider,
375- providerConfig : finalProviderConfig ,
376- isActive : true ,
377- updatedAt : new Date ( ) ,
378- } )
379- . where ( eq ( webhook . id , existing . id ) )
380-
381- return null
382- }
383-
384324 const webhookId = nanoid ( )
385325 const createPayload = {
386326 id : webhookId ,
@@ -434,6 +374,7 @@ async function upsertSingleWebhook(params: {
434374
435375/**
436376 * Saves trigger webhook configurations as part of workflow deployment.
377+ * Uses delete + create approach for changed/deleted webhooks.
437378 */
438379export async function saveTriggerWebhooksForDeploy ( {
439380 request,
@@ -444,22 +385,31 @@ export async function saveTriggerWebhooksForDeploy({
444385 requestId,
445386} : SaveTriggerWebhooksInput ) : Promise < TriggerSaveResult > {
446387 const triggerBlocks = Object . values ( blocks || { } ) . filter ( Boolean )
388+ const currentBlockIds = new Set ( triggerBlocks . map ( ( b ) => b . id ) )
447389
448- if ( triggerBlocks . length === 0 ) {
449- return { success : true }
450- }
390+ // 1. Get all existing webhooks for this workflow
391+ const existingWebhooks = await db . select ( ) . from ( webhook ) . where ( eq ( webhook . workflowId , workflowId ) )
392+
393+ const webhooksByBlockId = new Map (
394+ existingWebhooks . filter ( ( wh ) => wh . blockId ) . map ( ( wh ) => [ wh . blockId ! , wh ] )
395+ )
396+
397+ logger . info ( `[${ requestId } ] Starting webhook sync` , {
398+ workflowId,
399+ currentBlockIds : Array . from ( currentBlockIds ) ,
400+ existingWebhookBlockIds : Array . from ( webhooksByBlockId . keys ( ) ) ,
401+ } )
402+
403+ // 2. Determine which webhooks to delete (orphaned or config changed)
404+ const webhooksToDelete : typeof existingWebhooks = [ ]
405+ const blocksNeedingWebhook : BlockState [ ] = [ ]
451406
452407 for ( const block of triggerBlocks ) {
453408 const triggerId = resolveTriggerId ( block )
454- if ( ! triggerId ) continue
455-
456- if ( ! isTriggerValid ( triggerId ) ) {
457- continue
458- }
409+ if ( ! triggerId || ! isTriggerValid ( triggerId ) ) continue
459410
460411 const triggerDef = getTrigger ( triggerId )
461412 const provider = triggerDef . provider
462-
463413 const { providerConfig, missingFields, triggerPath } = buildProviderConfig (
464414 block ,
465415 triggerId ,
@@ -475,8 +425,69 @@ export async function saveTriggerWebhooksForDeploy({
475425 } ,
476426 }
477427 }
428+ // Store config for later use
429+
430+ ; ( block as any ) . _webhookConfig = { provider, providerConfig, triggerPath, triggerDef }
431+
432+ const existingWh = webhooksByBlockId . get ( block . id )
433+ if ( ! existingWh ) {
434+ // No existing webhook - needs creation
435+ blocksNeedingWebhook . push ( block )
436+ } else {
437+ // Check if config changed
438+ const existingConfig = ( existingWh . providerConfig as Record < string , unknown > ) || { }
439+ if (
440+ shouldRecreateExternalWebhookSubscription ( {
441+ previousProvider : existingWh . provider as string ,
442+ nextProvider : provider ,
443+ previousConfig : existingConfig ,
444+ nextConfig : providerConfig ,
445+ } )
446+ ) {
447+ // Config changed - delete and recreate
448+ webhooksToDelete . push ( existingWh )
449+ blocksNeedingWebhook . push ( block )
450+ logger . info ( `[${ requestId } ] Webhook config changed for block ${ block . id } , will recreate` )
451+ }
452+ // else: config unchanged, keep existing webhook
453+ }
454+ }
455+
456+ // Add orphaned webhooks (block no longer exists)
457+ for ( const wh of existingWebhooks ) {
458+ if ( wh . blockId && ! currentBlockIds . has ( wh . blockId ) ) {
459+ webhooksToDelete . push ( wh )
460+ logger . info ( `[${ requestId } ] Webhook orphaned (block deleted): ${ wh . blockId } ` )
461+ }
462+ }
463+
464+ // 3. Delete webhooks that need deletion
465+ if ( webhooksToDelete . length > 0 ) {
466+ logger . info ( `[${ requestId } ] Deleting ${ webhooksToDelete . length } webhook(s)` , {
467+ webhookIds : webhooksToDelete . map ( ( wh ) => wh . id ) ,
468+ } )
469+
470+ for ( const wh of webhooksToDelete ) {
471+ try {
472+ await cleanupExternalWebhook ( wh , workflow , requestId )
473+ } catch ( cleanupError ) {
474+ logger . warn ( `[${ requestId } ] Failed to cleanup external webhook ${ wh . id } ` , cleanupError )
475+ }
476+ }
477+
478+ const idsToDelete = webhooksToDelete . map ( ( wh ) => wh . id )
479+ await db . delete ( webhook ) . where ( inArray ( webhook . id , idsToDelete ) )
480+ }
481+
482+ // 4. Create webhooks for blocks that need them
483+ for ( const block of blocksNeedingWebhook ) {
484+ const config = ( block as any ) . _webhookConfig
485+ if ( ! config ) continue
486+
487+ const { provider, providerConfig, triggerPath } = config
478488
479489 try {
490+ // Handle credential sets
480491 const credentialSetError = await syncCredentialSetWebhooks ( {
481492 workflowId,
482493 blockId : block . id ,
@@ -494,7 +505,7 @@ export async function saveTriggerWebhooksForDeploy({
494505 continue
495506 }
496507
497- const upsertError = await upsertSingleWebhook ( {
508+ const createError = await createWebhookForBlock ( {
498509 request,
499510 workflowId,
500511 workflow,
@@ -506,11 +517,11 @@ export async function saveTriggerWebhooksForDeploy({
506517 requestId,
507518 } )
508519
509- if ( upsertError ) {
510- return { success : false , error : upsertError }
520+ if ( createError ) {
521+ return { success : false , error : createError }
511522 }
512523 } catch ( error : any ) {
513- logger . error ( `[${ requestId } ] Failed to save trigger config for ${ block . id } ` , error )
524+ logger . error ( `[${ requestId } ] Failed to create webhook for ${ block . id } ` , error )
514525 return {
515526 success : false ,
516527 error : {
@@ -521,5 +532,10 @@ export async function saveTriggerWebhooksForDeploy({
521532 }
522533 }
523534
535+ // Clean up temp config
536+ for ( const block of triggerBlocks ) {
537+ ; ( block as any ) . _webhookConfig = undefined
538+ }
539+
524540 return { success : true }
525541}
0 commit comments