From fc25734689b65c17f84eb03496a729dba0945ea9 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Sun, 6 Aug 2023 10:03:45 +0200 Subject: [PATCH] :recycle: (webhook) Integrate webhook in typebot schema Closes #313 --- .../public/templates/basic-chat-gpt.json | 2 +- .../public/templates/chat-gpt-personas.json | 2 +- .../public/templates/customer-support.json | 1 + .../templates/digital-product-payment.json | 1 + .../public/templates/dog-insurance-offer.json | 2 +- apps/builder/public/templates/faq.json | 1 + .../builder/public/templates/lead-gen-ai.json | 2 +- apps/builder/public/templates/lead-gen.json | 1 + .../builder/public/templates/lead-magnet.json | 2 +- .../public/templates/lead-scoring.json | 1 + .../templates/movie-recommendation.json | 328 ++++++++++------- apps/builder/public/templates/nps.json | 2 +- apps/builder/public/templates/onboarding.json | 2 +- .../templates/product-recommendation.json | 2 +- apps/builder/public/templates/quiz.json | 1 + .../makeCom/components/MakeComContent.tsx | 2 +- .../makeCom/components/MakeComSettings.tsx | 24 +- .../components/PabblyConnectContent.tsx | 2 +- .../components/PabblyConnectSettings.tsx | 19 +- .../webhook/api/getResultExample.ts | 13 +- .../webhook/api/listWebhookBlocks.ts | 35 +- .../webhook/api/subscribeWebhook.ts | 62 +++- .../webhook/api/unsubscribeWebhook.ts | 43 ++- .../components/WebhookAdvancedConfigForm.tsx | 8 +- .../webhook/components/WebhookContent.tsx | 4 +- .../webhook/components/WebhookSettings.tsx | 18 +- .../integrations/webhook/webhook.spec.ts | 6 +- .../zapier/components/ZapierContent.tsx | 2 +- .../zapier/components/ZapierSettings.tsx | 24 +- .../blocks/logic/condition/condition.spec.ts | 6 +- .../features/dashboard/api/parseNewTypebot.ts | 2 +- .../dashboard/queries/importTypebotQuery.ts | 94 +++-- .../editor/providers/typebotActions/blocks.ts | 3 +- .../features/typebot/helpers/parseNewBlock.ts | 4 +- .../src/pages/api/typebots/[typebotId].ts | 14 +- apps/docs/openapi/builder/_spec_.json | 342 +++++++++++++++++- apps/docs/openapi/chat/_spec_.json | 336 ++++++++++++++++- apps/viewer/next.config.js | 39 +- .../webhook/executeWebhookBlock.ts | 10 +- .../integrations/webhook/webhook.spec.ts | 2 +- apps/viewer/src/features/chat/chat.spec.ts | 3 +- apps/viewer/src/pages/[[...publicId]].tsx | 2 +- .../blocks/[blockId]/executeWebhook.ts | 273 +++++++------- .../blocks/[blockId]/sampleResult.ts | 38 -- .../steps/[stepId]/executeWebhook.ts | 72 ---- .../[blockId]/steps/[stepId]/sampleResult.ts | 34 -- .../steps/[stepId]/subscribeWebhook.ts | 53 --- .../steps/[stepId]/unsubscribeWebhook.ts | 41 --- .../blocks/[blockId]/subscribeWebhook.ts | 52 --- .../blocks/[blockId]/unsubscribeWebhook.ts | 40 -- .../ConversationContainer.tsx | 1 - .../migrations/migrateTypebotFromV3ToV4.ts | 63 ++++ packages/lib/utils.ts | 10 - .../features/blocks/integrations/index.ts | 2 +- .../features/blocks/integrations/makeCom.ts | 7 +- .../blocks/integrations/pabblyConnect.ts | 7 +- .../features/blocks/integrations/webhook.ts | 45 --- .../blocks/integrations/webhook/enums.ts | 11 + .../blocks/integrations/webhook/index.ts | 2 + .../blocks/integrations/webhook/schemas.ts | 95 +++++ .../features/blocks/integrations/zapier.ts | 7 +- packages/schemas/features/chat.ts | 2 +- packages/schemas/features/publicTypebot.ts | 2 +- packages/schemas/features/typebot/typebot.ts | 2 +- packages/schemas/features/webhooks.ts | 48 --- packages/schemas/index.ts | 1 - 66 files changed, 1501 insertions(+), 876 deletions(-) delete mode 100644 apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/sampleResult.ts delete mode 100644 apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/executeWebhook.ts delete mode 100644 apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/sampleResult.ts delete mode 100644 apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/subscribeWebhook.ts delete mode 100644 apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/unsubscribeWebhook.ts delete mode 100644 apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/subscribeWebhook.ts delete mode 100644 apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/unsubscribeWebhook.ts create mode 100644 packages/lib/migrations/migrateTypebotFromV3ToV4.ts delete mode 100644 packages/schemas/features/blocks/integrations/webhook.ts create mode 100644 packages/schemas/features/blocks/integrations/webhook/enums.ts create mode 100644 packages/schemas/features/blocks/integrations/webhook/index.ts create mode 100644 packages/schemas/features/blocks/integrations/webhook/schemas.ts delete mode 100644 packages/schemas/features/webhooks.ts diff --git a/apps/builder/public/templates/basic-chat-gpt.json b/apps/builder/public/templates/basic-chat-gpt.json index 5a2653e54..c62b1bbb8 100644 --- a/apps/builder/public/templates/basic-chat-gpt.json +++ b/apps/builder/public/templates/basic-chat-gpt.json @@ -1,6 +1,6 @@ { "id": "clf6ov7hg00001ao6q02sb8re", - "version": "3", + "version": "4", "createdAt": "2023-03-13T10:35:44.933Z", "updatedAt": "2023-03-13T14:53:00.817Z", "icon": "🤖", diff --git a/apps/builder/public/templates/chat-gpt-personas.json b/apps/builder/public/templates/chat-gpt-personas.json index 1ae7dff4b..bff9befaf 100644 --- a/apps/builder/public/templates/chat-gpt-personas.json +++ b/apps/builder/public/templates/chat-gpt-personas.json @@ -1,6 +1,6 @@ { "id": "qcueq3ttys1ddagic7jsimp4", - "version": "3", + "version": "4", "createdAt": "2023-03-30T15:45:12.464Z", "updatedAt": "2023-03-30T15:45:12.464Z", "icon": "🎭", diff --git a/apps/builder/public/templates/customer-support.json b/apps/builder/public/templates/customer-support.json index 2609100c4..85229781b 100644 --- a/apps/builder/public/templates/customer-support.json +++ b/apps/builder/public/templates/customer-support.json @@ -1,5 +1,6 @@ { "id": "cl16la7p900990b1a72qjqbb3", + "version": "4", "createdAt": "2022-03-25T15:39:33.885Z", "updatedAt": "2022-03-25T15:42:12.544Z", "name": "Customer Support", diff --git a/apps/builder/public/templates/digital-product-payment.json b/apps/builder/public/templates/digital-product-payment.json index 410c9d8ed..96e5d0258 100644 --- a/apps/builder/public/templates/digital-product-payment.json +++ b/apps/builder/public/templates/digital-product-payment.json @@ -1,5 +1,6 @@ { "id": "cl3u43chu40824z1acel3iw1c", + "version": "4", "createdAt": "2022-05-31T12:04:12.930Z", "updatedAt": "2022-05-31T12:31:12.867Z", "icon": "đŸ–ŧī¸", diff --git a/apps/builder/public/templates/dog-insurance-offer.json b/apps/builder/public/templates/dog-insurance-offer.json index 8e9a3aff8..46d547681 100644 --- a/apps/builder/public/templates/dog-insurance-offer.json +++ b/apps/builder/public/templates/dog-insurance-offer.json @@ -1,6 +1,6 @@ { "id": "clh9fzu4b00031aotzr1ik4ba", - "version": "3", + "version": "4", "createdAt": "2023-05-04T18:10:07.548Z", "updatedAt": "2023-05-04T19:13:45.356Z", "icon": "đŸļ", diff --git a/apps/builder/public/templates/faq.json b/apps/builder/public/templates/faq.json index a4af3daba..219d4fca2 100644 --- a/apps/builder/public/templates/faq.json +++ b/apps/builder/public/templates/faq.json @@ -1,5 +1,6 @@ { "id": "cl96ns7zc000dky099ku4bmav", + "version": "4", "createdAt": "2022-10-13T06:07:11.976Z", "updatedAt": "2022-10-13T06:27:31.951Z", "icon": "đŸ’Ŧ", diff --git a/apps/builder/public/templates/lead-gen-ai.json b/apps/builder/public/templates/lead-gen-ai.json index 6d2f9c7c7..eab3ed48a 100644 --- a/apps/builder/public/templates/lead-gen-ai.json +++ b/apps/builder/public/templates/lead-gen-ai.json @@ -1,6 +1,6 @@ { "id": "pjkxzxkujkbk2oml3d35w6zz", - "version": "3", + "version": "4", "createdAt": "2023-04-20T12:29:18.633Z", "updatedAt": "2023-04-20T12:50:12.385Z", "icon": "đŸĻž", diff --git a/apps/builder/public/templates/lead-gen.json b/apps/builder/public/templates/lead-gen.json index c7c42179e..a4d714400 100644 --- a/apps/builder/public/templates/lead-gen.json +++ b/apps/builder/public/templates/lead-gen.json @@ -1,5 +1,6 @@ { "id": "qgMiLSr4W1ftXocFncin1G", + "version": "4", "createdAt": "2022-02-05T06:21:16.522Z", "updatedAt": "2022-02-05T06:21:16.522Z", "name": "Lead Generation", diff --git a/apps/builder/public/templates/lead-magnet.json b/apps/builder/public/templates/lead-magnet.json index 018700bde..f842ba58a 100644 --- a/apps/builder/public/templates/lead-magnet.json +++ b/apps/builder/public/templates/lead-magnet.json @@ -1,6 +1,6 @@ { "id": "clg69gafk00011ar14aryt8ej", - "version": "3", + "version": "4", "createdAt": "2023-04-07T08:03:57.008Z", "updatedAt": "2023-04-07T08:22:10.285Z", "icon": "🧲", diff --git a/apps/builder/public/templates/lead-scoring.json b/apps/builder/public/templates/lead-scoring.json index 5473808e2..da8e24235 100644 --- a/apps/builder/public/templates/lead-scoring.json +++ b/apps/builder/public/templates/lead-scoring.json @@ -1,5 +1,6 @@ { "id": "cl1seoz582103xk1at12y4ucb", + "version": "4", "createdAt": "2022-04-09T22:06:01.196Z", "updatedAt": "2022-04-09T22:35:22.449Z", "icon": "🏆", diff --git a/apps/builder/public/templates/movie-recommendation.json b/apps/builder/public/templates/movie-recommendation.json index 0a6b86537..e40b70b4e 100644 --- a/apps/builder/public/templates/movie-recommendation.json +++ b/apps/builder/public/templates/movie-recommendation.json @@ -1,41 +1,41 @@ { - "id": "clf17krhw00011am023muokc1", - "version": "3", - "createdAt": "2023-03-09T14:32:53.301Z", - "updatedAt": "2023-03-09T16:26:36.916Z", + "id": "d8z3y5ats5r0lyptw938re79", + "version": "4", + "createdAt": "2023-08-06T05:56:35.727Z", + "updatedAt": "2023-08-06T06:39:16.019Z", "icon": "đŸŋ", "name": "Movie recommendation", "folderId": null, "groups": [ { - "id": "kto4a8zwoqvagp14sjol0mqq", + "id": "u6lpjibfjhyoqij5wjf9kvnl", "title": "Start", "blocks": [ { - "id": "dn2vorjxq275ulqwd97sgfjp", + "id": "rha69fygov33vym1hf6z837p", "type": "start", "label": "Start", - "groupId": "kto4a8zwoqvagp14sjol0mqq", - "outgoingEdgeId": "ozfvttdxg70dy931m762ssc0" + "groupId": "u6lpjibfjhyoqij5wjf9kvnl", + "outgoingEdgeId": "wfec8f4e1jtden2wqna6nrso" } ], "graphCoordinates": { "x": 0, "y": 0 } }, { - "id": "vpsi7kfd4hiu38fd0txt3ndn", + "id": "mjnkukpkpvf4ha2g4n5m804v", "title": "Menu", "blocks": [ { - "id": "tmm2xkt5wqt5zmxzkyg2kx1q", + "id": "kjlf184vxf0uorniwje28iqb", "type": "Set variable", - "groupId": "vpsi7kfd4hiu38fd0txt3ndn", + "groupId": "mjnkukpkpvf4ha2g4n5m804v", "options": { "variableId": "vh5bxx07kl3016wr1undh2yb3", "expressionToEvaluate": "2f584d1ffe2b7fb082dd4e05038e9bd7" } }, { - "id": "pd22jxxptmw66xhwddj8toos", + "id": "f5rr5wi9zldun13tw79u9z2n", "type": "text", "content": { "richText": [ @@ -47,18 +47,18 @@ } ] }, - "groupId": "vpsi7kfd4hiu38fd0txt3ndn" + "groupId": "mjnkukpkpvf4ha2g4n5m804v" }, { - "id": "vwfnjnfnqnlvgraai2k4kl9e", + "id": "c7swi84rmdvrul0wz5kxtplm", "type": "image", "content": { "url": "https://media3.giphy.com/media/BElb9DVpHezcZufOhl/giphy-downsized.gif?cid=fe3852a3uwhsp1sc3j6avr625v5e94h1v8o3wfb1ii3xwswk&rid=giphy-downsized.gif&ct=g" }, - "groupId": "vpsi7kfd4hiu38fd0txt3ndn" + "groupId": "mjnkukpkpvf4ha2g4n5m804v" }, { - "id": "gr8qlv926y817febq85zl0y0", + "id": "nlihfc4ptxnxoktqblh6mcql", "type": "text", "content": { "richText": [ @@ -68,28 +68,28 @@ } ] }, - "groupId": "vpsi7kfd4hiu38fd0txt3ndn" + "groupId": "mjnkukpkpvf4ha2g4n5m804v" }, { - "id": "fm1894ok09bdyjpqhqth1p7q", + "id": "vr73urm54d9mq2oqg7ey1xh8", "type": "image", "content": { "url": "https://www.themoviedb.org/assets/2/v4/logos/v2/blue_square_1-5bdc75aaebeb75dc7ae79426ddd9be3b2be1e342510f8202baf6bffa71d7f5c4.svg" }, - "groupId": "vpsi7kfd4hiu38fd0txt3ndn" + "groupId": "mjnkukpkpvf4ha2g4n5m804v" }, { - "id": "qllnzuo1zxr5ef8bq9kjdjel", + "id": "g5yyuh9g75x7xa7fgqhhi1zz", "type": "text", "content": { "richText": [ { "type": "p", "children": [{ "text": "How can I we help?" }] } ] }, - "groupId": "vpsi7kfd4hiu38fd0txt3ndn" + "groupId": "mjnkukpkpvf4ha2g4n5m804v" }, { - "id": "sodsq9mcigwvogmwx0t4jvil", + "id": "tzf45bvd8iquoxz7qgta8v94", "type": "choice input", "items": [ { @@ -97,39 +97,52 @@ "type": 0, "blockId": "sodsq9mcigwvogmwx0t4jvil", "content": "Select a genre 💅", - "outgoingEdgeId": "o8n3df42rx0og3vlkzhskp7r" + "outgoingEdgeId": "t8qyjpigrz7cdl8gxl1wxlwj" }, { "id": "i8ls2f8inq2ovuijj6l7rbcq", "type": 0, "blockId": "sodsq9mcigwvogmwx0t4jvil", "content": "See what's trending 🔝", - "outgoingEdgeId": "cfwctrcqoucw6djnn0j2zsv0" + "outgoingEdgeId": "tjn2ljosqyd4aj9dk8mnifsu" } ], - "groupId": "vpsi7kfd4hiu38fd0txt3ndn", + "groupId": "mjnkukpkpvf4ha2g4n5m804v", "options": { "buttonLabel": "Send", "isMultipleChoice": false } } ], - "graphCoordinates": { "x": 255.24609375, "y": 173.00390625 } + "graphCoordinates": { "x": 255.25, "y": 172.89 } }, { - "id": "xjwiuplgl79ezx460xfuplgr", + "id": "kq1g5z6pz4buot7sawqdrr3s", "title": "Genre", "blocks": [ { - "id": "vqrsszjis5qtbalo8d0fwb1j", + "id": "ecwz96cghzp4ji3lyx7taxt1", "type": "text", "content": { "richText": [{ "type": "p", "children": [{ "text": "Sure!" }] }] }, - "groupId": "xjwiuplgl79ezx460xfuplgr" + "groupId": "kq1g5z6pz4buot7sawqdrr3s" }, { - "id": "lmoqmlovcsg2l0uzkm9sohor", + "id": "gd4lt2pcljer6zaf7v9hkr1k", "type": "Webhook", - "groupId": "xjwiuplgl79ezx460xfuplgr", + "groupId": "kq1g5z6pz4buot7sawqdrr3s", "options": { + "webhook": { + "id": "t4pht3ndfc8tu9geovi7czqm", + "url": "https://api.themoviedb.org/3/genre/movie/list", + "method": "GET", + "headers": [], + "queryParams": [ + { + "id": "gq6m7x2k20qzrj752qi2zpmu", + "key": "api_key", + "value": "{{API Key}}" + } + ] + }, "isCustomBody": false, "isAdvancedConfig": true, "variablesForTest": [ @@ -151,11 +164,10 @@ "variableId": "vwc00rydyp035vtb0nlaqyzwr" } ] - }, - "webhookId": "kbd4kqs5lk58nyeggctha9hc" + } }, { - "id": "v2516de17eigf6mbccy2t6dr", + "id": "qeyvu7uq5tkvo7uo8iaj87z5", "type": "text", "content": { "richText": [ @@ -165,10 +177,10 @@ } ] }, - "groupId": "xjwiuplgl79ezx460xfuplgr" + "groupId": "kq1g5z6pz4buot7sawqdrr3s" }, { - "id": "eqm19tzeh7kullwld8auoqy4", + "id": "nwuk2clo78hmnh4d0g31u9xg", "type": "choice input", "items": [ { @@ -178,7 +190,7 @@ "content": "Click to edit" } ], - "groupId": "xjwiuplgl79ezx460xfuplgr", + "groupId": "kq1g5z6pz4buot7sawqdrr3s", "options": { "variableId": "vkmbb3rb2hcfd2io1fhf7rz5x", "buttonLabel": "Send", @@ -187,36 +199,55 @@ } }, { - "id": "s27x4vixvakw62rwlsx61vw6", + "id": "j7pm34um4piuyabwlobjc356", "type": "Set variable", - "groupId": "xjwiuplgl79ezx460xfuplgr", + "groupId": "kq1g5z6pz4buot7sawqdrr3s", "options": { + "type": "Map item with same index", "isCode": true, "variableId": "vwewa4yugqch2sswdpneszk3i", + "mapListItemParams": { + "baseItemVariableId": "vkmbb3rb2hcfd2io1fhf7rz5x", + "baseListVariableId": "vx0bbqzug4vk3zpc31ly8k7al", + "targetListVariableId": "vwc00rydyp035vtb0nlaqyzwr" + }, "expressionToEvaluate": "{{Genre IDs}}.at({{Genres}}.indexOf({{Selected genre}}))" }, - "outgoingEdgeId": "vcuv47fbcqrtuelfy4i6ctqz" + "outgoingEdgeId": "tfuuwjnpn7mftd5s65mbhytd" } ], - "graphCoordinates": { "x": 630.84765625, "y": 333.68359375 } + "graphCoordinates": { "x": 630.85, "y": 333.68 } }, { - "id": "dewz832l9kse7xx1vhkihbk6", + "id": "d6v9lh83c7zuwrhf2mmo6nxo", "title": "Trending", "blocks": [ { - "id": "yqci070mkxfjjdok5yztj1nx", + "id": "edokvbp15ubqeuydw9n7wf4w", "type": "text", "content": { "richText": [{ "type": "p", "children": [{ "text": "Sure!" }] }] }, - "groupId": "dewz832l9kse7xx1vhkihbk6" + "groupId": "d6v9lh83c7zuwrhf2mmo6nxo" }, { - "id": "xkd9bmjx1xf6uglewaeds8d8", + "id": "pwxb57b8nc2bp764vcdstois", "type": "Webhook", - "groupId": "dewz832l9kse7xx1vhkihbk6", + "groupId": "d6v9lh83c7zuwrhf2mmo6nxo", "options": { + "webhook": { + "id": "a5oaijpqxo5a0mqrnjqg3tyt", + "url": "https://api.themoviedb.org/3/trending/movie/week", + "method": "GET", + "headers": [], + "queryParams": [ + { + "id": "x9nxr63itm2lvbe8dmi53mi7", + "key": "api_key", + "value": "{{API Key}}" + } + ] + }, "isCustomBody": false, "isAdvancedConfig": true, "variablesForTest": [ @@ -238,11 +269,10 @@ "variableId": "vcmybxcoaytd2geo5sqx7v8hw" } ] - }, - "webhookId": "ohc5k58i0bs4i2vc5ymjxqbx" + } }, { - "id": "u6q1nvxowaszfdu4g42nqz38", + "id": "ruhgtbpv18cy2g5ujavljkku", "type": "text", "content": { "richText": [ @@ -252,10 +282,10 @@ } ] }, - "groupId": "dewz832l9kse7xx1vhkihbk6" + "groupId": "d6v9lh83c7zuwrhf2mmo6nxo" }, { - "id": "stlcncbupmsjxjipi86s45hy", + "id": "krcvvncnqtn99v0qe1dzudrk", "type": "choice input", "items": [ { @@ -265,7 +295,7 @@ "content": "Click to edit" } ], - "groupId": "dewz832l9kse7xx1vhkihbk6", + "groupId": "d6v9lh83c7zuwrhf2mmo6nxo", "options": { "variableId": "vulnb1om2fk8mvkcesl8s15cr", "buttonLabel": "Send", @@ -274,25 +304,31 @@ } }, { - "id": "h0mbyxa1o6kyhpv2gwbweunu", + "id": "mgn6uuw2yebmengsukjramjx", "type": "Set variable", - "groupId": "dewz832l9kse7xx1vhkihbk6", + "groupId": "d6v9lh83c7zuwrhf2mmo6nxo", "options": { + "type": "Map item with same index", "isCode": true, "variableId": "vzslfw8oyo1f08uo5rpkegn0x", + "mapListItemParams": { + "baseItemVariableId": "vulnb1om2fk8mvkcesl8s15cr", + "baseListVariableId": "vkzk96oh1pmdjv2bt5ps60qc0", + "targetListVariableId": "vcmybxcoaytd2geo5sqx7v8hw" + }, "expressionToEvaluate": "{{Trending IDs}}.at({{Trending Movies}}.indexOf({{Selected Trending Movie}}))" }, - "outgoingEdgeId": "ctxcurho3eq3dkna3emyvpwr" + "outgoingEdgeId": "ual6xszx6tfcxqrnihc6zrvx" } ], - "graphCoordinates": { "x": 621.875, "y": 933.359375 } + "graphCoordinates": { "x": 628.03, "y": 965.53 } }, { - "id": "a1c99wep8eqambqjw8g8yeo8", + "id": "v35sky44jzz9fkwwul2qxufl", "title": "Movies by genre", "blocks": [ { - "id": "dbp1cdzqy7txabinmm5osi1a", + "id": "g2pgwx5yr1ou9vkoy6gdwuor", "type": "text", "content": { "richText": [ @@ -306,13 +342,31 @@ } ] }, - "groupId": "a1c99wep8eqambqjw8g8yeo8" + "groupId": "v35sky44jzz9fkwwul2qxufl" }, { - "id": "ym35vqr6euzhmtj8i03yzilz", + "id": "a2datk3pv8o6xgitwjsq61m2", "type": "Webhook", - "groupId": "a1c99wep8eqambqjw8g8yeo8", + "groupId": "v35sky44jzz9fkwwul2qxufl", "options": { + "webhook": { + "id": "em7huyvp98pd6hr25md0l6hb", + "url": "https://api.themoviedb.org/3/discover/movie", + "method": "GET", + "headers": [], + "queryParams": [ + { + "id": "mgwlp399a056o9jo93tjqp02", + "key": "api_key", + "value": "{{API Key}}" + }, + { + "id": "dv4wioynywqo57jq8lakq3yr", + "key": "with_genres", + "value": "{{Selected genre ID}}" + } + ] + }, "isCustomBody": false, "isAdvancedConfig": true, "variablesForTest": [ @@ -339,11 +393,10 @@ "variableId": "vhc2pc1sv4xc778r9od2ctooz" } ] - }, - "webhookId": "brjx15qipztmljhh31bsxj4u" + } }, { - "id": "dxp0gakw90f3ckahjuphx5ir", + "id": "tr5y76tx9ca336f8ob9odfa6", "type": "choice input", "items": [ { @@ -353,7 +406,7 @@ "content": "Click to edit" } ], - "groupId": "a1c99wep8eqambqjw8g8yeo8", + "groupId": "v35sky44jzz9fkwwul2qxufl", "options": { "variableId": "vyyr3j2pu76uzvf88laai8snl", "buttonLabel": "Send", @@ -362,38 +415,57 @@ } }, { - "id": "hufag2k15sttgg2v2y1mipxz", + "id": "vudr8jrv2k3x0ubemt39tv7a", "type": "Set variable", - "groupId": "a1c99wep8eqambqjw8g8yeo8", + "groupId": "v35sky44jzz9fkwwul2qxufl", "options": { + "type": "Map item with same index", "isCode": true, "variableId": "vzslfw8oyo1f08uo5rpkegn0x", + "mapListItemParams": { + "baseItemVariableId": "vyyr3j2pu76uzvf88laai8snl", + "baseListVariableId": "vad8vq3jfyybxo4la57hfd529", + "targetListVariableId": "vhc2pc1sv4xc778r9od2ctooz" + }, "expressionToEvaluate": "const movieIndex = {{Movies}}.indexOf({{Selected Movie}})\n\nreturn {{Movie IDs}}.at(movieIndex)" }, - "outgoingEdgeId": "wv1qglc5fmexvpaei0xbhtzm" + "outgoingEdgeId": "r4wyd2185zhen98r5pmx53g9" } ], - "graphCoordinates": { "x": 977.484375, "y": 330.8515625 } + "graphCoordinates": { "x": 977.48, "y": 330.85 } }, { - "id": "xutzzlxo0f5hs9q8ga1e8tl9", + "id": "uozlg88loeb8xegu6y4le6k8", "title": "Movie details", "blocks": [ { - "id": "tsferop3f80u2jq9p8ip964h", + "id": "ve9m7fromxw4tbm8558n8520", "type": "text", "content": { "richText": [ { "type": "p", "children": [{ "text": "Excellent choice đŸ”Ĩ" }] } ] }, - "groupId": "xutzzlxo0f5hs9q8ga1e8tl9" + "groupId": "uozlg88loeb8xegu6y4le6k8" }, { - "id": "sssp7cez39ndix4owjl8x09t", + "id": "xag9d5i5td40kdt3poyq5g4b", "type": "Webhook", - "groupId": "xutzzlxo0f5hs9q8ga1e8tl9", + "groupId": "uozlg88loeb8xegu6y4le6k8", "options": { + "webhook": { + "id": "ewrjdmbrm2yksvjrbp0z9duy", + "url": "https://api.themoviedb.org/3/movie/{{Selected Movie ID}}", + "method": "GET", + "headers": [], + "queryParams": [ + { + "id": "f33u3unbezibfdv54kbkien3", + "key": "api_key", + "value": "{{API Key}}" + } + ] + }, "isCustomBody": false, "isAdvancedConfig": true, "variablesForTest": [ @@ -425,13 +497,12 @@ "variableId": "vzf5ryexokpr4dihiur2spm8z" } ] - }, - "webhookId": "d0x75v11voy19hnrrqnjlwpv" + } }, { - "id": "e4h6b2vb409zkwa0jrpyjpt0", + "id": "d0rsus9shxj8iowczbcaw53i", "type": "Set variable", - "groupId": "xutzzlxo0f5hs9q8ga1e8tl9", + "groupId": "uozlg88loeb8xegu6y4le6k8", "options": { "isCode": false, "variableId": "vwitf3um5uweynypc0hxxwm14", @@ -439,23 +510,23 @@ } }, { - "id": "ze726gr9fs7elsdqekbijckv", + "id": "nqnry4c1z3wwcni8rwpduuhe", "type": "image", "content": { "url": "{{Poster URL}}" }, - "groupId": "xutzzlxo0f5hs9q8ga1e8tl9" + "groupId": "uozlg88loeb8xegu6y4le6k8" }, { - "id": "t21xt4nr48hyu2qk2ij0d2oe", + "id": "lhljw54rdykyqtjiuh6jsl5c", "type": "text", "content": { "richText": [ { "type": "p", "children": [{ "text": "{{Movie Overview}}" }] } ] }, - "groupId": "xutzzlxo0f5hs9q8ga1e8tl9" + "groupId": "uozlg88loeb8xegu6y4le6k8" }, { - "id": "nahhsfc6hsok5sufxcknopxr", + "id": "yifjhiamifo1y4ay7vols0mm", "type": "choice input", "items": [ { @@ -463,7 +534,7 @@ "type": 0, "blockId": "nahhsfc6hsok5sufxcknopxr", "content": "Watch the movie", - "outgoingEdgeId": "v2qrjwz43nej3nxbkbcq3kyd" + "outgoingEdgeId": "fteu5frsbj8wejfhhwzuv8t1" }, { "id": "j9d1pf2tndax0itezys7t73c", @@ -472,44 +543,44 @@ "content": "Find something else" } ], - "groupId": "xutzzlxo0f5hs9q8ga1e8tl9", + "groupId": "uozlg88loeb8xegu6y4le6k8", "options": { "buttonLabel": "Send", "isMultipleChoice": false }, - "outgoingEdgeId": "rddv20ku82g2yyagatt95t3r" + "outgoingEdgeId": "ntef5v9bt7vn4wg8s3dfm8yl" } ], - "graphCoordinates": { "x": 1198.4140625, "y": 846.53515625 } + "graphCoordinates": { "x": 1151.05, "y": 913.34 } }, { - "id": "vgulvp3bw1bi9em7yq6jfdrv", + "id": "x4d8cdsyzoqz6vzsurnb8twc", "title": "Redirect to IMDB", "blocks": [ { - "id": "y7z3nkawu4dwh1otgcwbenyz", + "id": "mw0e0bzwiokhndkkncp9niu2", "type": "Redirect", - "groupId": "vgulvp3bw1bi9em7yq6jfdrv", + "groupId": "x4d8cdsyzoqz6vzsurnb8twc", "options": { "url": "https://m.imdb.com/title/{{IMDB ID}}", "isNewTab": false } } ], - "graphCoordinates": { "x": 1693.453125, "y": 1324.9921875 } + "graphCoordinates": { "x": 1616.55, "y": 1411.44 } }, { - "id": "trfx6m1qiu7awvwjfnhfd712", + "id": "x9610wtdv125hg56wicm2qmv", "title": "Return to menu", "blocks": [ { - "id": "jumese0l44liwke0l5glv0fs", + "id": "efto9jivvcvomj3kltf57hbb", "type": "Jump", - "groupId": "trfx6m1qiu7awvwjfnhfd712", + "groupId": "x9610wtdv125hg56wicm2qmv", "options": { "blockId": "sodsq9mcigwvogmwx0t4jvil", - "groupId": "vpsi7kfd4hiu38fd0txt3ndn" + "groupId": "mjnkukpkpvf4ha2g4n5m804v" } } ], - "graphCoordinates": { "x": 1690.52734375, "y": 1501.76953125 } + "graphCoordinates": { "x": 1619.34, "y": 1586.5 } } ], "variables": [ @@ -532,70 +603,70 @@ ], "edges": [ { - "id": "ozfvttdxg70dy931m762ssc0", - "to": { "groupId": "vpsi7kfd4hiu38fd0txt3ndn" }, + "id": "wfec8f4e1jtden2wqna6nrso", + "to": { "groupId": "mjnkukpkpvf4ha2g4n5m804v" }, "from": { - "blockId": "dn2vorjxq275ulqwd97sgfjp", - "groupId": "kto4a8zwoqvagp14sjol0mqq" + "blockId": "rha69fygov33vym1hf6z837p", + "groupId": "u6lpjibfjhyoqij5wjf9kvnl" } }, { - "id": "o8n3df42rx0og3vlkzhskp7r", - "to": { "groupId": "xjwiuplgl79ezx460xfuplgr" }, + "id": "t8qyjpigrz7cdl8gxl1wxlwj", + "to": { "groupId": "kq1g5z6pz4buot7sawqdrr3s" }, "from": { "itemId": "kaimvzg9igdtktgou5m3s1bw", - "blockId": "sodsq9mcigwvogmwx0t4jvil", - "groupId": "vpsi7kfd4hiu38fd0txt3ndn" + "blockId": "tzf45bvd8iquoxz7qgta8v94", + "groupId": "mjnkukpkpvf4ha2g4n5m804v" } }, { - "id": "vcuv47fbcqrtuelfy4i6ctqz", - "to": { "groupId": "a1c99wep8eqambqjw8g8yeo8" }, + "id": "tfuuwjnpn7mftd5s65mbhytd", + "to": { "groupId": "v35sky44jzz9fkwwul2qxufl" }, "from": { - "blockId": "s27x4vixvakw62rwlsx61vw6", - "groupId": "xjwiuplgl79ezx460xfuplgr" + "blockId": "j7pm34um4piuyabwlobjc356", + "groupId": "kq1g5z6pz4buot7sawqdrr3s" } }, { - "id": "wv1qglc5fmexvpaei0xbhtzm", - "to": { "groupId": "xutzzlxo0f5hs9q8ga1e8tl9" }, + "id": "r4wyd2185zhen98r5pmx53g9", + "to": { "groupId": "uozlg88loeb8xegu6y4le6k8" }, "from": { - "blockId": "hufag2k15sttgg2v2y1mipxz", - "groupId": "a1c99wep8eqambqjw8g8yeo8" + "blockId": "vudr8jrv2k3x0ubemt39tv7a", + "groupId": "v35sky44jzz9fkwwul2qxufl" } }, { - "id": "v2qrjwz43nej3nxbkbcq3kyd", - "to": { "groupId": "vgulvp3bw1bi9em7yq6jfdrv" }, + "id": "fteu5frsbj8wejfhhwzuv8t1", + "to": { "groupId": "x4d8cdsyzoqz6vzsurnb8twc" }, "from": { "itemId": "n4818dnrb4arw1xh5v0ot8vz", - "blockId": "nahhsfc6hsok5sufxcknopxr", - "groupId": "xutzzlxo0f5hs9q8ga1e8tl9" + "blockId": "yifjhiamifo1y4ay7vols0mm", + "groupId": "uozlg88loeb8xegu6y4le6k8" } }, { - "id": "cfwctrcqoucw6djnn0j2zsv0", - "to": { "groupId": "dewz832l9kse7xx1vhkihbk6" }, + "id": "tjn2ljosqyd4aj9dk8mnifsu", + "to": { "groupId": "d6v9lh83c7zuwrhf2mmo6nxo" }, "from": { "itemId": "i8ls2f8inq2ovuijj6l7rbcq", - "blockId": "sodsq9mcigwvogmwx0t4jvil", - "groupId": "vpsi7kfd4hiu38fd0txt3ndn" + "blockId": "tzf45bvd8iquoxz7qgta8v94", + "groupId": "mjnkukpkpvf4ha2g4n5m804v" } }, { - "id": "rddv20ku82g2yyagatt95t3r", - "to": { "groupId": "trfx6m1qiu7awvwjfnhfd712" }, + "id": "ntef5v9bt7vn4wg8s3dfm8yl", + "to": { "groupId": "x9610wtdv125hg56wicm2qmv" }, "from": { - "blockId": "nahhsfc6hsok5sufxcknopxr", - "groupId": "xutzzlxo0f5hs9q8ga1e8tl9" + "blockId": "yifjhiamifo1y4ay7vols0mm", + "groupId": "uozlg88loeb8xegu6y4le6k8" } }, { - "id": "ctxcurho3eq3dkna3emyvpwr", - "to": { "groupId": "xutzzlxo0f5hs9q8ga1e8tl9" }, + "id": "ual6xszx6tfcxqrnihc6zrvx", + "to": { "groupId": "uozlg88loeb8xegu6y4le6k8" }, "from": { - "blockId": "h0mbyxa1o6kyhpv2gwbweunu", - "groupId": "dewz832l9kse7xx1vhkihbk6" + "blockId": "mgn6uuw2yebmengsukjramjx", + "groupId": "d6v9lh83c7zuwrhf2mmo6nxo" } } ], @@ -632,7 +703,6 @@ }, "publicId": null, "customDomain": null, - "workspaceId": "freeWorkspace", "resultsTablePreferences": null, "isArchived": false, "isClosed": false diff --git a/apps/builder/public/templates/nps.json b/apps/builder/public/templates/nps.json index 87d375b95..38f0447ac 100644 --- a/apps/builder/public/templates/nps.json +++ b/apps/builder/public/templates/nps.json @@ -1,6 +1,6 @@ { "id": "clesntjqu00011a4xkgffc3p0", - "version": "3", + "version": "4", "createdAt": "2023-03-03T14:57:41.430Z", "updatedAt": "2023-03-03T16:14:29.268Z", "icon": "⭐", diff --git a/apps/builder/public/templates/onboarding.json b/apps/builder/public/templates/onboarding.json index 897c62faf..cdc01f27e 100644 --- a/apps/builder/public/templates/onboarding.json +++ b/apps/builder/public/templates/onboarding.json @@ -1,6 +1,6 @@ { "id": "qkvenb8ur2y0ahlbckmx7law", - "version": "3", + "version": "4", "createdAt": "2023-02-22T14:26:28.592Z", "updatedAt": "2023-02-22T14:28:05.063Z", "icon": null, diff --git a/apps/builder/public/templates/product-recommendation.json b/apps/builder/public/templates/product-recommendation.json index faaaaccc1..4a8690206 100644 --- a/apps/builder/public/templates/product-recommendation.json +++ b/apps/builder/public/templates/product-recommendation.json @@ -1,6 +1,6 @@ { "id": "mdbjvgqhqypbsdd0airkzcwu", - "version": "3", + "version": "4", "createdAt": "2023-06-05T15:40:37.278Z", "updatedAt": "2023-06-05T15:45:43.049Z", "icon": "đŸĢ", diff --git a/apps/builder/public/templates/quiz.json b/apps/builder/public/templates/quiz.json index 4ab9e6a13..878c3066f 100644 --- a/apps/builder/public/templates/quiz.json +++ b/apps/builder/public/templates/quiz.json @@ -1,5 +1,6 @@ { "id": "cl1qz4luj04007w1ai7rk1j5q", + "version": "4", "createdAt": "2022-04-08T22:02:30.427Z", "updatedAt": "2022-04-08T23:03:34.726Z", "icon": "đŸ•šī¸", diff --git a/apps/builder/src/features/blocks/integrations/makeCom/components/MakeComContent.tsx b/apps/builder/src/features/blocks/integrations/makeCom/components/MakeComContent.tsx index 8a674a28d..d89f0528c 100644 --- a/apps/builder/src/features/blocks/integrations/makeCom/components/MakeComContent.tsx +++ b/apps/builder/src/features/blocks/integrations/makeCom/components/MakeComContent.tsx @@ -9,7 +9,7 @@ type Props = { export const MakeComContent = ({ block }: Props) => { const { webhooks } = useTypebot() - const webhook = webhooks.find(byId(block.webhookId)) + const webhook = block.options.webhook ?? webhooks.find(byId(block.webhookId)) if (isNotDefined(webhook?.body)) return Configure... diff --git a/apps/builder/src/features/blocks/integrations/makeCom/components/MakeComSettings.tsx b/apps/builder/src/features/blocks/integrations/makeCom/components/MakeComSettings.tsx index 4af1844f3..f3cd5bcc5 100644 --- a/apps/builder/src/features/blocks/integrations/makeCom/components/MakeComSettings.tsx +++ b/apps/builder/src/features/blocks/integrations/makeCom/components/MakeComSettings.tsx @@ -22,10 +22,17 @@ export const MakeComSettings = ({ const setLocalWebhook = useCallback( async (newLocalWebhook: Webhook) => { + if (options.webhook) { + onOptionsChange({ + ...options, + webhook: newLocalWebhook, + }) + return + } _setLocalWebhook(newLocalWebhook) await updateWebhook(newLocalWebhook.id, newLocalWebhook) }, - [updateWebhook] + [onOptionsChange, options, updateWebhook] ) useEffect(() => { @@ -33,20 +40,23 @@ export const MakeComSettings = ({ !localWebhook || localWebhook.url || !webhook?.url || - webhook.url === localWebhook.url + webhook.url === localWebhook.url || + options.webhook ) return setLocalWebhook({ ...localWebhook, url: webhook?.url, }) - }, [webhook, localWebhook, setLocalWebhook]) + }, [webhook, localWebhook, setLocalWebhook, options.webhook]) + + const url = options.webhook?.url ?? localWebhook?.url return ( - + - {localWebhook?.url ? ( + {url ? ( <>Your scenario is correctly configured 🚀 ) : ( @@ -62,10 +72,10 @@ export const MakeComSettings = ({ )} - {localWebhook && ( + {(localWebhook || options.webhook) && ( { const { webhooks } = useTypebot() - const webhook = webhooks.find(byId(block.webhookId)) + const webhook = block.options.webhook ?? webhooks.find(byId(block.webhookId)) if (isNotDefined(webhook?.body)) return Configure... diff --git a/apps/builder/src/features/blocks/integrations/pabbly/components/PabblyConnectSettings.tsx b/apps/builder/src/features/blocks/integrations/pabbly/components/PabblyConnectSettings.tsx index c7f1601b3..a3cb1a529 100644 --- a/apps/builder/src/features/blocks/integrations/pabbly/components/PabblyConnectSettings.tsx +++ b/apps/builder/src/features/blocks/integrations/pabbly/components/PabblyConnectSettings.tsx @@ -27,6 +27,13 @@ export const PabblyConnectSettings = ({ ) const setLocalWebhook = async (newLocalWebhook: Webhook) => { + if (options.webhook) { + onOptionsChange({ + ...options, + webhook: newLocalWebhook, + }) + return + } _setLocalWebhook(newLocalWebhook) await updateWebhook(newLocalWebhook.id, newLocalWebhook) } @@ -38,11 +45,13 @@ export const PabblyConnectSettings = ({ url, }) + const url = options.webhook?.url ?? localWebhook?.url + return ( - + - {localWebhook?.url ? ( + {url ? ( <>Your scenario is correctly configured 🚀 ) : ( @@ -60,15 +69,15 @@ export const PabblyConnectSettings = ({ - {localWebhook && ( + {(localWebhook || options.webhook) && ( & { - webhooks: Webhook[] - }) - | null + })) as Pick | null if (!typebot) throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' }) const block = typebot.groups - .flatMap((g) => g.blocks) - .find((s) => s.id === blockId) + .flatMap((group) => group.blocks) + .find((block) => block.id === blockId) if (!block) throw new TRPCError({ code: 'NOT_FOUND', message: 'Block not found' }) diff --git a/apps/builder/src/features/blocks/integrations/webhook/api/listWebhookBlocks.ts b/apps/builder/src/features/blocks/integrations/webhook/api/listWebhookBlocks.ts index 9d5ead8a2..ea63628d0 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/api/listWebhookBlocks.ts +++ b/apps/builder/src/features/blocks/integrations/webhook/api/listWebhookBlocks.ts @@ -2,9 +2,10 @@ import prisma from '@/lib/prisma' import { canReadTypebots } from '@/helpers/databaseRules' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' -import { Group, Typebot, Webhook, WebhookBlock } from '@typebot.io/schemas' +import { Group, IntegrationBlockType, Typebot } from '@typebot.io/schemas' import { byId, isWebhookBlock, parseGroupTitle } from '@typebot.io/lib' import { z } from 'zod' +import { Webhook } from '@typebot.io/prisma' export const listWebhookBlocks = authenticatedProcedure .meta({ @@ -28,6 +29,12 @@ export const listWebhookBlocks = authenticatedProcedure webhookBlocks: z.array( z.object({ id: z.string(), + type: z.enum([ + IntegrationBlockType.WEBHOOK, + IntegrationBlockType.ZAPIER, + IntegrationBlockType.MAKE_COM, + IntegrationBlockType.PABBLY_CONNECT, + ]), label: z.string(), url: z.string().optional(), }) @@ -46,17 +53,27 @@ export const listWebhookBlocks = authenticatedProcedure throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' }) const webhookBlocks = (typebot?.groups as Group[]).reduce< - { id: string; label: string; url: string | undefined }[] + { + id: string + label: string + url: string | undefined + type: + | IntegrationBlockType.WEBHOOK + | IntegrationBlockType.ZAPIER + | IntegrationBlockType.MAKE_COM + | IntegrationBlockType.PABBLY_CONNECT + }[] >((webhookBlocks, group) => { - const blocks = group.blocks.filter((block) => - isWebhookBlock(block) - ) as WebhookBlock[] + const blocks = group.blocks.filter(isWebhookBlock) return [ ...webhookBlocks, - ...blocks.map((b) => ({ - id: b.id, - label: `${parseGroupTitle(group.title)} > ${b.id}`, - url: typebot?.webhooks.find(byId(b.webhookId))?.url ?? undefined, + ...blocks.map((block) => ({ + id: block.id, + type: block.type, + label: `${parseGroupTitle(group.title)} > ${block.id}`, + url: block.options.webhook + ? block.options.webhook.url + : typebot?.webhooks.find(byId(block.webhookId))?.url ?? undefined, })), ] }, []) diff --git a/apps/builder/src/features/blocks/integrations/webhook/api/subscribeWebhook.ts b/apps/builder/src/features/blocks/integrations/webhook/api/subscribeWebhook.ts index 560c208f2..cdffab361 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/api/subscribeWebhook.ts +++ b/apps/builder/src/features/blocks/integrations/webhook/api/subscribeWebhook.ts @@ -2,9 +2,10 @@ import prisma from '@/lib/prisma' import { canWriteTypebots } from '@/helpers/databaseRules' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' -import { Typebot, Webhook, WebhookBlock } from '@typebot.io/schemas' +import { Typebot, WebhookBlock } from '@typebot.io/schemas' import { byId, isWebhookBlock } from '@typebot.io/lib' import { z } from 'zod' +import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums' export const subscribeWebhook = authenticatedProcedure .meta({ @@ -34,9 +35,8 @@ export const subscribeWebhook = authenticatedProcedure where: canWriteTypebots(typebotId, user), select: { groups: true, - webhooks: true, }, - })) as (Pick & { webhooks: Webhook[] }) | null + })) as Pick | null if (!typebot) throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' }) @@ -51,18 +51,50 @@ export const subscribeWebhook = authenticatedProcedure message: 'Webhook block not found', }) - await prisma.webhook.upsert({ - where: { id: webhookBlock.webhookId }, - update: { url, body: '{{state}}', method: 'POST' }, - create: { - url, - body: '{{state}}', - method: 'POST', - typebotId, - headers: [], - queryParams: [], - }, - }) + const newWebhook = { + id: webhookBlock.webhookId ?? webhookBlock.id, + url, + body: '{{state}}', + method: HttpMethod.POST, + headers: [], + queryParams: [], + } + + if (webhookBlock.webhookId) + await prisma.webhook.upsert({ + where: { id: webhookBlock.webhookId }, + update: { url, body: newWebhook.body, method: newWebhook.method }, + create: { + typebotId, + ...newWebhook, + }, + }) + else { + const updatedGroups = typebot.groups.map((group) => + group.id !== webhookBlock.groupId + ? group + : { + ...group, + blocks: group.blocks.map((block) => + block.id !== webhookBlock.id + ? block + : { + ...block, + options: { + ...webhookBlock.options, + webhook: newWebhook, + }, + } + ), + } + ) + await prisma.typebot.updateMany({ + where: { id: typebotId }, + data: { + groups: updatedGroups, + }, + }) + } return { id: blockId, diff --git a/apps/builder/src/features/blocks/integrations/webhook/api/unsubscribeWebhook.ts b/apps/builder/src/features/blocks/integrations/webhook/api/unsubscribeWebhook.ts index b4bf1b45c..2f4b4a208 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/api/unsubscribeWebhook.ts +++ b/apps/builder/src/features/blocks/integrations/webhook/api/unsubscribeWebhook.ts @@ -50,10 +50,45 @@ export const unsubscribeWebhook = authenticatedProcedure message: 'Webhook block not found', }) - await prisma.webhook.update({ - where: { id: webhookBlock.webhookId }, - data: { url: null }, - }) + if (webhookBlock.webhookId) + await prisma.webhook.update({ + where: { id: webhookBlock.webhookId }, + data: { url: null }, + }) + else { + if (!webhookBlock.options.webhook) + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Webhook block not found', + }) + const updatedGroups = typebot.groups.map((group) => + group.id !== webhookBlock.groupId + ? group + : { + ...group, + blocks: group.blocks.map((block) => + block.id !== webhookBlock.id + ? block + : { + ...block, + options: { + ...webhookBlock.options, + webhook: { + ...webhookBlock.options.webhook, + url: undefined, + }, + }, + } + ), + } + ) + await prisma.typebot.updateMany({ + where: { id: typebotId }, + data: { + groups: updatedGroups, + }, + }) + } return { id: blockId, diff --git a/apps/builder/src/features/blocks/integrations/webhook/components/WebhookAdvancedConfigForm.tsx b/apps/builder/src/features/blocks/integrations/webhook/components/WebhookAdvancedConfigForm.tsx index e776d2b25..91c874d2c 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/components/WebhookAdvancedConfigForm.tsx +++ b/apps/builder/src/features/blocks/integrations/webhook/components/WebhookAdvancedConfigForm.tsx @@ -16,7 +16,6 @@ import { Text, } from '@chakra-ui/react' import { - HttpMethod, KeyValue, VariableForTest, ResponseVariableMapping, @@ -31,6 +30,7 @@ import { QueryParamsInputs, HeadersInputs } from './KeyValueInputs' import { DataVariableInputs } from './ResponseMappingInputs' import { VariableForTestInputs } from './VariableForTestInputs' import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings' +import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums' type Props = { blockId: string @@ -78,9 +78,11 @@ export const WebhookAdvancedConfigForm = ({ onOptionsChange({ ...options, isCustomBody }) const executeTestRequest = async () => { - if (!typebot || !webhook) return + if (!typebot) return setIsTestResponseLoading(true) - await Promise.all([updateWebhook(webhook.id, webhook), save()]) + if (!options.webhook) + await Promise.all([updateWebhook(webhook.id, webhook), save()]) + else await save() const { data, error } = await executeWebhook( typebot.id, convertVariablesForTestToVariables( diff --git a/apps/builder/src/features/blocks/integrations/webhook/components/WebhookContent.tsx b/apps/builder/src/features/blocks/integrations/webhook/components/WebhookContent.tsx index 773c4d3ae..b6c76449a 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/components/WebhookContent.tsx +++ b/apps/builder/src/features/blocks/integrations/webhook/components/WebhookContent.tsx @@ -8,10 +8,10 @@ type Props = { block: WebhookBlock } -export const WebhookContent = ({ block: { webhookId, options } }: Props) => { +export const WebhookContent = ({ block: { options, webhookId } }: Props) => { const { typebot } = useTypebot() const { webhooks } = useTypebot() - const webhook = webhooks.find(byId(webhookId)) + const webhook = options.webhook ?? webhooks.find(byId(webhookId)) if (!webhook?.url) return Configure... return ( diff --git a/apps/builder/src/features/blocks/integrations/webhook/components/WebhookSettings.tsx b/apps/builder/src/features/blocks/integrations/webhook/components/WebhookSettings.tsx index 4386de7eb..d95a3e391 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/components/WebhookSettings.tsx +++ b/apps/builder/src/features/blocks/integrations/webhook/components/WebhookSettings.tsx @@ -21,25 +21,33 @@ export const WebhookSettings = ({ ) const setLocalWebhook = async (newLocalWebhook: Webhook) => { + if (options.webhook) { + onOptionsChange({ ...options, webhook: newLocalWebhook }) + return + } _setLocalWebhook(newLocalWebhook) await updateWebhook(newLocalWebhook.id, newLocalWebhook) } - const updateUrl = (url?: string) => - localWebhook && setLocalWebhook({ ...localWebhook, url: url ?? null }) + const updateUrl = (url: string) => { + if (options.webhook) + onOptionsChange({ ...options, webhook: { ...options.webhook, url } }) + else if (localWebhook) + setLocalWebhook({ ...localWebhook, url: url ?? undefined }) + } - if (!localWebhook) return + if (!localWebhook && !options.webhook) return return ( { ) await page.click('text=Test the request') await expect(page.locator('div[role="textbox"] >> nth=-1')).toContainText( - `"Group #1": "answer value", "Group #2": "20", "Group #2 (1)": "Yes"`, {timeout: 10000} + `"Group #1": "answer value", "Group #2": "20", "Group #2 (1)": "Yes"`, + { timeout: 10000 } ) }) @@ -126,6 +127,7 @@ test.describe('API', () => { expect(webhookBlocks[0]).toEqual({ id: 'webhookBlock', label: 'Webhook > webhookBlock', + type: 'Webhook', }) }) diff --git a/apps/builder/src/features/blocks/integrations/zapier/components/ZapierContent.tsx b/apps/builder/src/features/blocks/integrations/zapier/components/ZapierContent.tsx index f13ad83e2..07daf7e01 100644 --- a/apps/builder/src/features/blocks/integrations/zapier/components/ZapierContent.tsx +++ b/apps/builder/src/features/blocks/integrations/zapier/components/ZapierContent.tsx @@ -9,7 +9,7 @@ type Props = { export const ZapierContent = ({ block }: Props) => { const { webhooks } = useTypebot() - const webhook = webhooks.find(byId(block.webhookId)) + const webhook = block.options.webhook ?? webhooks.find(byId(block.webhookId)) if (isNotDefined(webhook?.body)) return Configure... diff --git a/apps/builder/src/features/blocks/integrations/zapier/components/ZapierSettings.tsx b/apps/builder/src/features/blocks/integrations/zapier/components/ZapierSettings.tsx index e4a3b3757..8210b6f32 100644 --- a/apps/builder/src/features/blocks/integrations/zapier/components/ZapierSettings.tsx +++ b/apps/builder/src/features/blocks/integrations/zapier/components/ZapierSettings.tsx @@ -22,10 +22,17 @@ export const ZapierSettings = ({ const setLocalWebhook = useCallback( async (newLocalWebhook: Webhook) => { + if (options.webhook) { + onOptionsChange({ + ...options, + webhook: newLocalWebhook, + }) + return + } _setLocalWebhook(newLocalWebhook) await updateWebhook(newLocalWebhook.id, newLocalWebhook) }, - [updateWebhook] + [onOptionsChange, options, updateWebhook] ) useEffect(() => { @@ -33,20 +40,23 @@ export const ZapierSettings = ({ !localWebhook || localWebhook.url || !webhook?.url || - webhook.url === localWebhook.url + webhook.url === localWebhook.url || + options.webhook ) return setLocalWebhook({ ...localWebhook, url: webhook?.url, }) - }, [webhook, localWebhook, setLocalWebhook]) + }, [webhook, localWebhook, setLocalWebhook, options.webhook]) + + const url = options.webhook?.url ?? localWebhook?.url return ( - + - {localWebhook?.url ? ( + {url ? ( <>Your zap is correctly configured 🚀 ) : ( @@ -62,10 +72,10 @@ export const ZapierSettings = ({ )} - {localWebhook && ( + {(localWebhook || options.webhook) && ( { await page.click('button:has-text("Age")') await page.click('button:has-text("Select an operator")') await page.click('button:has-text("Greater than")', { force: true }) - await page.fill('input[placeholder="Type a value..."]', '80') + await page.fill('input[placeholder="Type a number..."]', '80') await page.click('button:has-text("Add a comparison")') @@ -35,7 +35,7 @@ test.describe('Condition block', () => { await page.click('button:has-text("Select an operator")') await page.click('button:has-text("Less than")', { force: true }) await page.fill( - ':nth-match(input[placeholder="Type a value..."], 2)', + ':nth-match(input[placeholder="Type a number..."], 2)', '100' ) @@ -47,7 +47,7 @@ test.describe('Condition block', () => { await page.click('button:has-text("Age")') await page.click('button:has-text("Select an operator")') await page.click('button:has-text("Greater than")', { force: true }) - await page.fill('input[placeholder="Type a value..."]', '20') + await page.fill('input[placeholder="Type a number..."]', '20') await page.click('text=Preview') await page diff --git a/apps/builder/src/features/dashboard/api/parseNewTypebot.ts b/apps/builder/src/features/dashboard/api/parseNewTypebot.ts index fa3a21bc9..72a69bf6d 100644 --- a/apps/builder/src/features/dashboard/api/parseNewTypebot.ts +++ b/apps/builder/src/features/dashboard/api/parseNewTypebot.ts @@ -49,7 +49,7 @@ export const parseNewTypebot = ({ return { folderId, name, - version: '3', + version: '4', workspaceId, groups: [startGroup], edges: [], diff --git a/apps/builder/src/features/dashboard/queries/importTypebotQuery.ts b/apps/builder/src/features/dashboard/queries/importTypebotQuery.ts index b16640977..463d304f0 100644 --- a/apps/builder/src/features/dashboard/queries/importTypebotQuery.ts +++ b/apps/builder/src/features/dashboard/queries/importTypebotQuery.ts @@ -29,13 +29,19 @@ export const importTypebotQuery = async (typebot: Typebot, userPlan: Plan) => { const webhookBlocks = typebot.groups .flatMap((b) => b.blocks) .filter(isWebhookBlock) + .filter((block) => block.webhookId) await Promise.all( - webhookBlocks.map((s) => + webhookBlocks.map((webhookBlock) => duplicateWebhookQuery({ - existingIds: { typebotId: typebot.id, webhookId: s.webhookId }, + existingIds: { + typebotId: typebot.id, + webhookId: webhookBlock.webhookId as string, + }, newIds: { typebotId: newTypebot.id, - webhookId: webhookIdsMapping.get(s.webhookId) as string, + webhookId: webhookIdsMapping.get( + webhookBlock.webhookId as string + ) as string, }, }) ) @@ -51,84 +57,98 @@ const duplicateTypebot = ( webhookIdsMapping: Map } => { const groupIdsMapping = generateOldNewIdsMapping(typebot.groups) + const blockIdsMapping = generateOldNewIdsMapping( + typebot.groups.flatMap((group) => group.blocks) + ) const edgeIdsMapping = generateOldNewIdsMapping(typebot.edges) const webhookIdsMapping = generateOldNewIdsMapping( typebot.groups - .flatMap((b) => b.blocks) + .flatMap((group) => group.blocks) .filter(isWebhookBlock) - .map((s) => ({ id: s.webhookId })) + .map((block) => ({ + id: block.webhookId as string, + })) ) const id = createId() return { typebot: { ...typebot, - version: '3', id, name: `${typebot.name} copy`, publicId: null, customDomain: null, - groups: typebot.groups.map((b) => ({ - ...b, - id: groupIdsMapping.get(b.id) as string, - blocks: b.blocks.map((s) => { + groups: typebot.groups.map((group) => ({ + ...group, + id: groupIdsMapping.get(group.id) as string, + blocks: group.blocks.map((block) => { const newIds = { - groupId: groupIdsMapping.get(s.groupId) as string, - outgoingEdgeId: s.outgoingEdgeId - ? edgeIdsMapping.get(s.outgoingEdgeId) + id: blockIdsMapping.get(block.id) as string, + groupId: groupIdsMapping.get(block.groupId) as string, + outgoingEdgeId: block.outgoingEdgeId + ? edgeIdsMapping.get(block.outgoingEdgeId) : undefined, } if ( - s.type === LogicBlockType.TYPEBOT_LINK && - s.options.typebotId === 'current' && - isDefined(s.options.groupId) + block.type === LogicBlockType.TYPEBOT_LINK && + block.options.typebotId === 'current' && + isDefined(block.options.groupId) ) return { - ...s, + ...block, + ...newIds, options: { - ...s.options, - groupId: groupIdsMapping.get(s.options.groupId as string), + ...block.options, + groupId: groupIdsMapping.get(block.options.groupId as string), }, } - if (s.type === LogicBlockType.JUMP) + if (block.type === LogicBlockType.JUMP) return { - ...s, + ...block, + ...newIds, options: { - ...s.options, - groupId: groupIdsMapping.get(s.options.groupId as string), + ...block.options, + groupId: groupIdsMapping.get(block.options.groupId as string), } satisfies JumpBlock['options'], } - if (blockHasItems(s)) + if (blockHasItems(block)) return { - ...s, - items: s.items.map((item) => ({ + ...block, + ...newIds, + items: block.items.map((item) => ({ ...item, outgoingEdgeId: item.outgoingEdgeId ? (edgeIdsMapping.get(item.outgoingEdgeId) as string) : undefined, })), - ...newIds, } as ChoiceInputBlock | ConditionBlock - if (isWebhookBlock(s)) { + if (isWebhookBlock(block) && block.webhookId) { return { - ...s, - webhookId: webhookIdsMapping.get(s.webhookId) as string, + ...block, ...newIds, + webhookId: webhookIdsMapping.get(block.webhookId) as string, } } return { - ...s, + ...block, ...newIds, } }), })), - edges: typebot.edges.map((e) => ({ - ...e, - id: edgeIdsMapping.get(e.id) as string, + edges: typebot.edges.map((edge) => ({ + ...edge, + id: edgeIdsMapping.get(edge.id) as string, from: { - ...e.from, - groupId: groupIdsMapping.get(e.from.groupId) as string, + ...edge.from, + blockId: blockIdsMapping.get(edge.from.blockId) as string, + groupId: groupIdsMapping.get(edge.from.groupId) as string, + }, + to: { + ...edge.to, + blockId: edge.to.blockId + ? (blockIdsMapping.get(edge.to.blockId) as string) + : undefined, + groupId: groupIdsMapping.get(edge.to.groupId) as string, }, - to: { ...e.to, groupId: groupIdsMapping.get(e.to.groupId) as string }, })), settings: typebot.settings.general.isBrandingEnabled === false && diff --git a/apps/builder/src/features/editor/providers/typebotActions/blocks.ts b/apps/builder/src/features/editor/providers/typebotActions/blocks.ts index f9d991805..6a1907b0a 100644 --- a/apps/builder/src/features/editor/providers/typebotActions/blocks.ts +++ b/apps/builder/src/features/editor/providers/typebotActions/blocks.ts @@ -189,7 +189,8 @@ export const duplicateBlockDraft = } as Block if (isWebhookBlock(block)) { const newWebhookId = createId() - onWebhookBlockDuplicated(block.webhookId, newWebhookId) + if (block.webhookId) + onWebhookBlockDuplicated(block.webhookId, newWebhookId) return { ...block, groupId, diff --git a/apps/builder/src/features/typebot/helpers/parseNewBlock.ts b/apps/builder/src/features/typebot/helpers/parseNewBlock.ts index 58af86f85..a648f0bba 100644 --- a/apps/builder/src/features/typebot/helpers/parseNewBlock.ts +++ b/apps/builder/src/features/typebot/helpers/parseNewBlock.ts @@ -2,7 +2,6 @@ import { createId } from '@paralleldrive/cuid2' import { isBubbleBlockType, blockTypeHasOption, - blockTypeHasWebhook, blockTypeHasItems, } from '@typebot.io/lib' import { @@ -134,7 +133,7 @@ const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => { case IntegrationBlockType.PABBLY_CONNECT: case IntegrationBlockType.MAKE_COM: case IntegrationBlockType.WEBHOOK: - return defaultWebhookOptions + return defaultWebhookOptions(createId()) case IntegrationBlockType.EMAIL: return defaultSendEmailOptions case IntegrationBlockType.CHATWOOT: @@ -159,7 +158,6 @@ export const parseNewBlock = ( options: blockTypeHasOption(type) ? parseDefaultBlockOptions(type) : undefined, - webhookId: blockTypeHasWebhook(type) ? createId() : undefined, items: blockTypeHasItems(type) ? parseDefaultItems(type, id) : undefined, } as DraggableBlock } diff --git a/apps/builder/src/pages/api/typebots/[typebotId].ts b/apps/builder/src/pages/api/typebots/[typebotId].ts index d2ce038f0..f461c36e0 100644 --- a/apps/builder/src/pages/api/typebots/[typebotId].ts +++ b/apps/builder/src/pages/api/typebots/[typebotId].ts @@ -10,6 +10,7 @@ import { isReadTypebotForbidden } from '@/features/typebot/helpers/isReadTypebot import { removeTypebotOldProperties } from '@/features/typebot/helpers/removeTypebotOldProperties' import { roundGroupsCoordinate } from '@/features/typebot/helpers/roundGroupsCoordinate' import { archiveResults } from '@typebot.io/lib/api/helpers/archiveResults' +import { migrateTypebotFromV3ToV4 } from '@typebot.io/lib/migrations/migrateTypebotFromV3ToV4' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const user = await getAuthenticatedUser(req, res) @@ -37,9 +38,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { collaborators.find((c) => c.userId === user.id)?.type === CollaborationType.READ return res.send({ - typebot: roundGroupsCoordinate( - removeTypebotOldProperties(typebot) as Typebot - ), + typebot: await migrateTypebot(typebot as Typebot), publishedTypebot, isReadOnly, webhooks, @@ -95,7 +94,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const updates = { ...omit(data, 'id', 'createdAt', 'updatedAt'), - version: '3', theme: data.theme ?? undefined, settings: data.settings ?? undefined, resultsTablePreferences: data.resultsTablePreferences ?? undefined, @@ -142,4 +140,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return methodNotAllowed(res) } +const migrateTypebot = async (typebot: Typebot): Promise => { + if (typebot.version === '4') return typebot + const updatedTypebot = roundGroupsCoordinate( + removeTypebotOldProperties(typebot) as Typebot + ) + return migrateTypebotFromV3ToV4(prisma)(updatedTypebot) +} + export default handler diff --git a/apps/docs/openapi/builder/_spec_.json b/apps/docs/openapi/builder/_spec_.json index d524cd6cd..40963394c 100644 --- a/apps/docs/openapi/builder/_spec_.json +++ b/apps/docs/openapi/builder/_spec_.json @@ -2797,6 +2797,83 @@ }, "isExecutedOnClient": { "type": "boolean" + }, + "webhook": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "queryParams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "headers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "method": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "CONNECT", + "OPTIONS", + "TRACE" + ] + }, + "url": { + "type": "string" + }, + "body": { + "type": "string" + } + }, + "required": [ + "id", + "queryParams", + "headers", + "method" + ], + "additionalProperties": false } }, "required": [ @@ -2806,15 +2883,15 @@ "additionalProperties": false }, "webhookId": { - "type": "string" + "type": "string", + "description": "Deprecated, use webhook.id instead" } }, "required": [ "id", "groupId", "type", - "options", - "webhookId" + "options" ], "additionalProperties": false }, @@ -3135,6 +3212,83 @@ }, "isExecutedOnClient": { "type": "boolean" + }, + "webhook": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "queryParams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "headers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "method": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "CONNECT", + "OPTIONS", + "TRACE" + ] + }, + "url": { + "type": "string" + }, + "body": { + "type": "string" + } + }, + "required": [ + "id", + "queryParams", + "headers", + "method" + ], + "additionalProperties": false } }, "required": [ @@ -3144,15 +3298,15 @@ "additionalProperties": false }, "webhookId": { - "type": "string" + "type": "string", + "description": "Deprecated, use webhook.id instead" } }, "required": [ "id", "groupId", "type", - "options", - "webhookId" + "options" ], "additionalProperties": false }, @@ -3303,6 +3457,83 @@ }, "isExecutedOnClient": { "type": "boolean" + }, + "webhook": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "queryParams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "headers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "method": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "CONNECT", + "OPTIONS", + "TRACE" + ] + }, + "url": { + "type": "string" + }, + "body": { + "type": "string" + } + }, + "required": [ + "id", + "queryParams", + "headers", + "method" + ], + "additionalProperties": false } }, "required": [ @@ -3312,15 +3543,15 @@ "additionalProperties": false }, "webhookId": { - "type": "string" + "type": "string", + "description": "Deprecated, now integrated in webhook block options" } }, "required": [ "id", "groupId", "type", - "options", - "webhookId" + "options" ], "additionalProperties": false }, @@ -3395,6 +3626,83 @@ }, "isExecutedOnClient": { "type": "boolean" + }, + "webhook": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "queryParams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "headers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "method": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "CONNECT", + "OPTIONS", + "TRACE" + ] + }, + "url": { + "type": "string" + }, + "body": { + "type": "string" + } + }, + "required": [ + "id", + "queryParams", + "headers", + "method" + ], + "additionalProperties": false } }, "required": [ @@ -3404,15 +3712,15 @@ "additionalProperties": false }, "webhookId": { - "type": "string" + "type": "string", + "description": "Deprecated, use webhook.id instead" } }, "required": [ "id", "groupId", "type", - "options", - "webhookId" + "options" ], "additionalProperties": false }, @@ -4528,6 +4836,15 @@ "id": { "type": "string" }, + "type": { + "type": "string", + "enum": [ + "Webhook", + "Zapier", + "Make.com", + "Pabbly" + ] + }, "label": { "type": "string" }, @@ -4537,6 +4854,7 @@ }, "required": [ "id", + "type", "label" ], "additionalProperties": false diff --git a/apps/docs/openapi/chat/_spec_.json b/apps/docs/openapi/chat/_spec_.json index 8f40410d6..996529bb2 100644 --- a/apps/docs/openapi/chat/_spec_.json +++ b/apps/docs/openapi/chat/_spec_.json @@ -2365,6 +2365,83 @@ }, "isExecutedOnClient": { "type": "boolean" + }, + "webhook": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "queryParams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "headers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "method": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "CONNECT", + "OPTIONS", + "TRACE" + ] + }, + "url": { + "type": "string" + }, + "body": { + "type": "string" + } + }, + "required": [ + "id", + "queryParams", + "headers", + "method" + ], + "additionalProperties": false } }, "required": [ @@ -2374,15 +2451,15 @@ "additionalProperties": false }, "webhookId": { - "type": "string" + "type": "string", + "description": "Deprecated, use webhook.id instead" } }, "required": [ "id", "groupId", "type", - "options", - "webhookId" + "options" ], "additionalProperties": false }, @@ -2703,6 +2780,83 @@ }, "isExecutedOnClient": { "type": "boolean" + }, + "webhook": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "queryParams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "headers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "method": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "CONNECT", + "OPTIONS", + "TRACE" + ] + }, + "url": { + "type": "string" + }, + "body": { + "type": "string" + } + }, + "required": [ + "id", + "queryParams", + "headers", + "method" + ], + "additionalProperties": false } }, "required": [ @@ -2712,15 +2866,15 @@ "additionalProperties": false }, "webhookId": { - "type": "string" + "type": "string", + "description": "Deprecated, use webhook.id instead" } }, "required": [ "id", "groupId", "type", - "options", - "webhookId" + "options" ], "additionalProperties": false }, @@ -2871,6 +3025,83 @@ }, "isExecutedOnClient": { "type": "boolean" + }, + "webhook": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "queryParams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "headers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "method": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "CONNECT", + "OPTIONS", + "TRACE" + ] + }, + "url": { + "type": "string" + }, + "body": { + "type": "string" + } + }, + "required": [ + "id", + "queryParams", + "headers", + "method" + ], + "additionalProperties": false } }, "required": [ @@ -2880,15 +3111,15 @@ "additionalProperties": false }, "webhookId": { - "type": "string" + "type": "string", + "description": "Deprecated, now integrated in webhook block options" } }, "required": [ "id", "groupId", "type", - "options", - "webhookId" + "options" ], "additionalProperties": false }, @@ -2963,6 +3194,83 @@ }, "isExecutedOnClient": { "type": "boolean" + }, + "webhook": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "queryParams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "headers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "method": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "CONNECT", + "OPTIONS", + "TRACE" + ] + }, + "url": { + "type": "string" + }, + "body": { + "type": "string" + } + }, + "required": [ + "id", + "queryParams", + "headers", + "method" + ], + "additionalProperties": false } }, "required": [ @@ -2972,15 +3280,15 @@ "additionalProperties": false }, "webhookId": { - "type": "string" + "type": "string", + "description": "Deprecated, use webhook.id instead" } }, "required": [ "id", "groupId", "type", - "options", - "webhookId" + "options" ], "additionalProperties": false }, @@ -3524,7 +3832,7 @@ }, "isPreview": { "type": "boolean", - "description": "If set to `true`, it will start a Preview session with the unpublished bot and it won't be saved in the Results tab. You need to be authenticated for this to work." + "description": "If set to `true`, it will start a Preview session with the unpublished bot and it won't be saved in the Results tab. You need to be authenticated with a bearer token for this to work." }, "resultId": { "type": "string", @@ -5808,7 +6116,7 @@ } } }, - "/session/{sessionId}/updateTypebot": { + "/sessions/{sessionId}/updateTypebot": { "post": { "operationId": "updateTypebotInSession", "summary": "Update typebot in session", diff --git a/apps/viewer/next.config.js b/apps/viewer/next.config.js index 1d63c5dd5..5518859ce 100644 --- a/apps/viewer/next.config.js +++ b/apps/viewer/next.config.js @@ -76,11 +76,40 @@ const nextConfig = { })) ) : [] - ).concat({ - source: '/api/typebots/:typebotId/blocks/:blockId/storage/upload-url', - destination: - '/api/v1/typebots/:typebotId/blocks/:blockId/storage/upload-url', - }), + ).concat([ + { + source: '/api/typebots/:typebotId/blocks/:blockId/storage/upload-url', + destination: + '/api/v1/typebots/:typebotId/blocks/:blockId/storage/upload-url', + }, + { + source: + '/api/typebots/:typebotId/blocks/:blockId/steps/:stepId/sampleResult', + destination: `${process.env.NEXTAUTH_URL}/api/v1/typebots/:typebotId/webhookBlocks/:blockId/getResultExample`, + }, + { + source: '/api/typebots/:typebotId/blocks/:blockId/sampleResult', + destination: `${process.env.NEXTAUTH_URL}/api/v1/typebots/:typebotId/webhookBlocks/:blockId/getResultExample`, + }, + { + source: + '/api/typebots/:typebotId/blocks/:blockId/steps/:stepId/unsubscribeWebhook', + destination: `${process.env.NEXTAUTH_URL}/api/v1/typebots/:typebotId/webhookBlocks/:blockId/unsubscribe`, + }, + { + source: '/api/typebots/:typebotId/blocks/:blockId/unsubscribeWebhook', + destination: `${process.env.NEXTAUTH_URL}/api/v1/typebots/:typebotId/webhookBlocks/:blockId/unsubscribe`, + }, + { + source: + '/api/typebots/:typebotId/blocks/:blockId/steps/:stepId/subscribeWebhook', + destination: `${process.env.NEXTAUTH_URL}/api/v1/typebots/:typebotId/webhookBlocks/:blockId/subscribe`, + }, + { + source: '/api/typebots/:typebotId/blocks/:blockId/subscribeWebhook', + destination: `${process.env.NEXTAUTH_URL}/api/v1/typebots/:typebotId/webhookBlocks/:blockId/subscribe`, + }, + ]), } }, } diff --git a/apps/viewer/src/features/blocks/integrations/webhook/executeWebhookBlock.ts b/apps/viewer/src/features/blocks/integrations/webhook/executeWebhookBlock.ts index 99fa60519..5f852f891 100644 --- a/apps/viewer/src/features/blocks/integrations/webhook/executeWebhookBlock.ts +++ b/apps/viewer/src/features/blocks/integrations/webhook/executeWebhookBlock.ts @@ -11,7 +11,6 @@ import { WebhookResponse, WebhookOptions, defaultWebhookAttributes, - HttpMethod, PublicTypebot, KeyValue, ReplyLog, @@ -26,6 +25,7 @@ import { parseSampleResult } from './parseSampleResult' import { ExecuteIntegrationResponse } from '@/features/chat/types' import { parseVariables } from '@/features/variables/parseVariables' import { resumeWebhookExecution } from './resumeWebhookExecution' +import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums' type ParsedWebhook = ExecutableWebhook & { basicAuth: { username?: string; password?: string } @@ -38,9 +38,11 @@ export const executeWebhookBlock = async ( ): Promise => { const { typebot, result } = state const logs: ReplyLog[] = [] - const webhook = (await prisma.webhook.findUnique({ - where: { id: block.webhookId }, - })) as Webhook | null + const webhook = + block.options.webhook ?? + ((await prisma.webhook.findUnique({ + where: { id: block.webhookId }, + })) as Webhook | null) if (!webhook) { logs.push({ status: 'error', diff --git a/apps/viewer/src/features/blocks/integrations/webhook/webhook.spec.ts b/apps/viewer/src/features/blocks/integrations/webhook/webhook.spec.ts index ebe6c3972..fc34488b6 100644 --- a/apps/viewer/src/features/blocks/integrations/webhook/webhook.spec.ts +++ b/apps/viewer/src/features/blocks/integrations/webhook/webhook.spec.ts @@ -1,6 +1,6 @@ import test, { expect } from '@playwright/test' import { createId } from '@paralleldrive/cuid2' -import { HttpMethod } from '@typebot.io/schemas' +import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums' import { createWebhook, importTypebotInDatabase, diff --git a/apps/viewer/src/features/chat/chat.spec.ts b/apps/viewer/src/features/chat/chat.spec.ts index 7a0d26062..99f034c7e 100644 --- a/apps/viewer/src/features/chat/chat.spec.ts +++ b/apps/viewer/src/features/chat/chat.spec.ts @@ -2,13 +2,14 @@ import { getTestAsset } from '@/test/utils/playwright' import test, { expect } from '@playwright/test' import { createId } from '@paralleldrive/cuid2' import prisma from '@/lib/prisma' -import { HttpMethod, SendMessageInput } from '@typebot.io/schemas' +import { SendMessageInput } from '@typebot.io/schemas' import { createWebhook, deleteTypebots, deleteWebhooks, importTypebotInDatabase, } from '@typebot.io/lib/playwright/databaseActions' +import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums' test.afterEach(async () => { await deleteWebhooks(['chat-webhook-id']) diff --git a/apps/viewer/src/pages/[[...publicId]].tsx b/apps/viewer/src/pages/[[...publicId]].tsx index bb46f7afe..54a442610 100644 --- a/apps/viewer/src/pages/[[...publicId]].tsx +++ b/apps/viewer/src/pages/[[...publicId]].tsx @@ -162,7 +162,7 @@ const App = ({ return if (publishedTypebot.typebot.isClosed) return - return publishedTypebot.version === '3' ? ( + return publishedTypebot.version ? ( { const block = typebot.groups .flatMap((g) => g.blocks) .find(byId(blockId)) as WebhookBlock - const webhook = typebot.webhooks.find(byId(block.webhookId)) + const webhook = + block.options.webhook ?? typebot.webhooks.find(byId(block.webhookId)) if (!webhook) return res .status(404) @@ -84,116 +85,131 @@ const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body) export const executeWebhook = (typebot: Typebot) => - async ({ - webhook, - variables, - groupId, - resultValues, - resultId, - parentTypebotIds = [], - }: { - webhook: Webhook - variables: Variable[] - groupId: string - resultValues?: ResultValues - resultId?: string - parentTypebotIds: string[] - }): Promise => { - if (!webhook.url || !webhook.method) - return { - statusCode: 400, - data: { message: `Webhook doesn't have url or method` }, - } - const basicAuth: { username?: string; password?: string } = {} - const basicAuthHeaderIdx = webhook.headers.findIndex( - (h) => - h.key?.toLowerCase() === 'authorization' && - h.value?.toLowerCase()?.includes('basic') - ) - const isUsernamePasswordBasicAuth = - basicAuthHeaderIdx !== -1 && - webhook.headers[basicAuthHeaderIdx].value?.includes(':') - if (isUsernamePasswordBasicAuth) { - const [username, password] = - webhook.headers[basicAuthHeaderIdx].value?.slice(6).split(':') ?? [] - basicAuth.username = username - basicAuth.password = password - webhook.headers.splice(basicAuthHeaderIdx, 1) - } - const headers = convertKeyValueTableToObject(webhook.headers, variables) as - | Headers - | undefined - const queryParams = stringify( - convertKeyValueTableToObject(webhook.queryParams, variables) - ) - const contentType = headers ? headers['Content-Type'] : undefined - const linkedTypebotsParents = await fetchLinkedTypebots({ - isPreview: !('typebotId' in typebot), - typebotIds: parentTypebotIds, - }) - const linkedTypebotsChildren = await getPreviouslyLinkedTypebots({ - isPreview: !('typebotId' in typebot), - typebots: [typebot], - })([]) - const bodyContent = await getBodyContent(typebot, [ - ...linkedTypebotsParents, - ...linkedTypebotsChildren, - ])({ - body: webhook.body, - resultValues, - groupId, + async ({ + webhook, variables, - }) - const { data: body, isJson } = - bodyContent && webhook.method !== HttpMethod.GET - ? safeJsonParse( + groupId, + resultValues, + resultId, + parentTypebotIds = [], + }: { + webhook: Webhook + variables: Variable[] + groupId: string + resultValues?: ResultValues + resultId?: string + parentTypebotIds: string[] + }): Promise => { + if (!webhook.url || !webhook.method) + return { + statusCode: 400, + data: { message: `Webhook doesn't have url or method` }, + } + const basicAuth: { username?: string; password?: string } = {} + const basicAuthHeaderIdx = webhook.headers.findIndex( + (h) => + h.key?.toLowerCase() === 'authorization' && + h.value?.toLowerCase()?.includes('basic') + ) + const isUsernamePasswordBasicAuth = + basicAuthHeaderIdx !== -1 && + webhook.headers[basicAuthHeaderIdx].value?.includes(':') + if (isUsernamePasswordBasicAuth) { + const [username, password] = + webhook.headers[basicAuthHeaderIdx].value?.slice(6).split(':') ?? [] + basicAuth.username = username + basicAuth.password = password + webhook.headers.splice(basicAuthHeaderIdx, 1) + } + const headers = convertKeyValueTableToObject(webhook.headers, variables) as + | Headers + | undefined + const queryParams = stringify( + convertKeyValueTableToObject(webhook.queryParams, variables) + ) + const contentType = headers ? headers['Content-Type'] : undefined + const linkedTypebotsParents = await fetchLinkedTypebots({ + isPreview: !('typebotId' in typebot), + typebotIds: parentTypebotIds, + }) + const linkedTypebotsChildren = await getPreviouslyLinkedTypebots({ + isPreview: !('typebotId' in typebot), + typebots: [typebot], + })([]) + const bodyContent = await getBodyContent(typebot, [ + ...linkedTypebotsParents, + ...linkedTypebotsChildren, + ])({ + body: webhook.body, + resultValues, + groupId, + variables, + }) + const { data: body, isJson } = + bodyContent && webhook.method !== HttpMethod.GET + ? safeJsonParse( parseVariables(variables, { escapeForJson: !checkIfBodyIsAVariable(bodyContent), })(bodyContent) ) - : { data: undefined, isJson: false } + : { data: undefined, isJson: false } - const request = { - url: parseVariables(variables)( - webhook.url + (queryParams !== '' ? `?${queryParams}` : '') - ), - method: webhook.method as Method, - headers, - ...basicAuth, - json: - !contentType?.includes('x-www-form-urlencoded') && body && isJson - ? body - : undefined, - form: - contentType?.includes('x-www-form-urlencoded') && body - ? body - : undefined, - body: body && !isJson ? body : undefined, - } - try { - const response = await got(request.url, omit(request, 'url')) - await saveSuccessLog({ - resultId, - message: 'Webhook successfuly executed.', - details: { - statusCode: response.statusCode, - request, - response: safeJsonParse(response.body).data, - }, - }) - return { - statusCode: response.statusCode, - data: safeJsonParse(response.body).data, + const request = { + url: parseVariables(variables)( + webhook.url + (queryParams !== '' ? `?${queryParams}` : '') + ), + method: webhook.method as Method, + headers, + ...basicAuth, + json: + !contentType?.includes('x-www-form-urlencoded') && body && isJson + ? body + : undefined, + form: + contentType?.includes('x-www-form-urlencoded') && body + ? body + : undefined, + body: body && !isJson ? body : undefined, } - } catch (error) { - if (error instanceof HTTPError) { - const response = { - statusCode: error.response.statusCode, - data: safeJsonParse(error.response.body as string).data, + try { + const response = await got(request.url, omit(request, 'url')) + await saveSuccessLog({ + resultId, + message: 'Webhook successfuly executed.', + details: { + statusCode: response.statusCode, + request, + response: safeJsonParse(response.body).data, + }, + }) + return { + statusCode: response.statusCode, + data: safeJsonParse(response.body).data, } + } catch (error) { + if (error instanceof HTTPError) { + const response = { + statusCode: error.response.statusCode, + data: safeJsonParse(error.response.body as string).data, + } + await saveErrorLog({ + resultId, + message: 'Webhook returned an error', + details: { + request, + response, + }, + }) + return response + } + const response = { + statusCode: 500, + data: { message: `Error from Typebot server: ${error}` }, + } + console.error(error) await saveErrorLog({ resultId, - message: 'Webhook returned an error', + message: 'Webhook failed to execute', details: { request, response, @@ -201,51 +217,36 @@ export const executeWebhook = }) return response } - const response = { - statusCode: 500, - data: { message: `Error from Typebot server: ${error}` }, - } - console.error(error) - await saveErrorLog({ - resultId, - message: 'Webhook failed to execute', - details: { - request, - response, - }, - }) - return response } - } const getBodyContent = ( typebot: Pick, linkedTypebots: (Typebot | PublicTypebot)[] ) => - async ({ - body, - resultValues, - groupId, - variables, - }: { - body?: string | null - resultValues?: ResultValues - groupId: string - variables: Variable[] - }): Promise => { - if (!body) return - return body === '{{state}}' - ? JSON.stringify( + async ({ + body, + resultValues, + groupId, + variables, + }: { + body?: string | null + resultValues?: ResultValues + groupId: string + variables: Variable[] + }): Promise => { + if (!body) return + return body === '{{state}}' + ? JSON.stringify( resultValues ? parseAnswers(typebot, linkedTypebots)(resultValues) : await parseSampleResult(typebot, linkedTypebots)( - groupId, - variables - ) + groupId, + variables + ) ) - : body - } + : body + } const convertKeyValueTableToObject = ( keyValues: KeyValue[] | undefined, diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/sampleResult.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/sampleResult.ts deleted file mode 100644 index c09c2682e..000000000 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/sampleResult.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { authenticateUser } from '@/helpers/authenticateUser' -import prisma from '@/lib/prisma' -import { Typebot } from '@typebot.io/schemas' -import { NextApiRequest, NextApiResponse } from 'next' -import { methodNotAllowed } from '@typebot.io/lib/api' -import { getPreviouslyLinkedTypebots } from '@/features/blocks/logic/typebotLink/getPreviouslyLinkedTypebots' -import { parseSampleResult } from '@/features/blocks/integrations/webhook/parseSampleResult' - -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const user = await authenticateUser(req) - if (!user) return res.status(401).json({ message: 'Not authenticated' }) - if (req.method === 'GET') { - const typebotId = req.query.typebotId as string - const blockId = req.query.blockId as string - const typebot = (await prisma.typebot.findFirst({ - where: { - id: typebotId, - workspace: { members: { some: { userId: user.id } } }, - }, - })) as unknown as Typebot | undefined - if (!typebot) return res.status(400).send({ message: 'Typebot not found' }) - const block = typebot.groups - .flatMap((g) => g.blocks) - .find((s) => s.id === blockId) - if (!block) return res.status(404).send({ message: 'Group not found' }) - const linkedTypebots = await getPreviouslyLinkedTypebots({ - isPreview: true, - typebots: [typebot], - user, - })([]) - return res.send( - await parseSampleResult(typebot, linkedTypebots)(block.groupId, []) - ) - } - methodNotAllowed(res) -} - -export default handler diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/executeWebhook.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/executeWebhook.ts deleted file mode 100644 index c84d65408..000000000 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/executeWebhook.ts +++ /dev/null @@ -1,72 +0,0 @@ -import prisma from '@/lib/prisma' -import { - defaultWebhookAttributes, - ResultValues, - Typebot, - Variable, - Webhook, - WebhookOptions, - WebhookBlock, -} from '@typebot.io/schemas' -import { NextApiRequest, NextApiResponse } from 'next' -import { initMiddleware, methodNotAllowed, notFound } from '@typebot.io/lib/api' -import { byId } from '@typebot.io/lib' -import Cors from 'cors' -import { executeWebhook } from '../../executeWebhook' - -const cors = initMiddleware(Cors()) - -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - await cors(req, res) - if (req.method === 'POST') { - const typebotId = req.query.typebotId as string - const groupId = req.query.groupId as string - const blockId = req.query.blockId as string - const resultId = req.query.resultId as string | undefined - const { resultValues, variables, parentTypebotIds } = ( - typeof req.body === 'string' ? JSON.parse(req.body) : req.body - ) as { - resultValues: ResultValues - variables: Variable[] - parentTypebotIds: string[] - } - const typebot = (await prisma.typebot.findUnique({ - where: { id: typebotId }, - include: { webhooks: true }, - })) as unknown as (Typebot & { webhooks: Webhook[] }) | null - if (!typebot) return notFound(res) - const block = typebot.groups - .find(byId(groupId)) - ?.blocks.find(byId(blockId)) as WebhookBlock - const webhook = typebot.webhooks.find(byId(block.webhookId)) - if (!webhook) - return res - .status(404) - .send({ statusCode: 404, data: { message: `Couldn't find webhook` } }) - const preparedWebhook = prepareWebhookAttributes(webhook, block.options) - const result = await executeWebhook(typebot)({ - webhook: preparedWebhook, - variables, - groupId, - resultValues, - resultId, - parentTypebotIds, - }) - return res.status(200).send(result) - } - return methodNotAllowed(res) -} - -const prepareWebhookAttributes = ( - webhook: Webhook, - options: WebhookOptions -): Webhook => { - if (options.isAdvancedConfig === false) { - return { ...webhook, body: '{{state}}', ...defaultWebhookAttributes } - } else if (options.isCustomBody === false) { - return { ...webhook, body: '{{state}}' } - } - return webhook -} - -export default handler diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/sampleResult.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/sampleResult.ts deleted file mode 100644 index bd7f2af96..000000000 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/sampleResult.ts +++ /dev/null @@ -1,34 +0,0 @@ -import prisma from '@/lib/prisma' -import { Typebot } from '@typebot.io/schemas' -import { NextApiRequest, NextApiResponse } from 'next' -import { methodNotAllowed } from '@typebot.io/lib/api' -import { parseSampleResult } from '@/features/blocks/integrations/webhook/parseSampleResult' -import { getPreviouslyLinkedTypebots } from '@/features/blocks/logic/typebotLink/getPreviouslyLinkedTypebots' -import { authenticateUser } from '@/helpers/authenticateUser' - -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const user = await authenticateUser(req) - if (!user) return res.status(401).json({ message: 'Not authenticated' }) - if (req.method === 'GET') { - const typebotId = req.query.typebotId as string - const groupId = req.query.groupId as string - const typebot = (await prisma.typebot.findFirst({ - where: { - id: typebotId, - workspace: { members: { some: { userId: user.id } } }, - }, - })) as unknown as Typebot | undefined - if (!typebot) return res.status(400).send({ message: 'Typebot not found' }) - const linkedTypebots = await getPreviouslyLinkedTypebots({ - isPreview: true, - typebots: [typebot], - user, - })([]) - return res.send( - await parseSampleResult(typebot, linkedTypebots)(groupId, []) - ) - } - methodNotAllowed(res) -} - -export default handler diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/subscribeWebhook.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/subscribeWebhook.ts deleted file mode 100644 index ccf709096..000000000 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/subscribeWebhook.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Typebot, WebhookBlock } from '@typebot.io/schemas' -import { NextApiRequest, NextApiResponse } from 'next' -import { methodNotAllowed } from '@typebot.io/lib/api' -import { byId } from '@typebot.io/lib' -import prisma from '@/lib/prisma' -import { authenticateUser } from '@/helpers/authenticateUser' - -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const user = await authenticateUser(req) - if (!user) return res.status(401).json({ message: 'Not authenticated' }) - if (req.method === 'POST') { - const body = req.body as Record - if (!('url' in body)) - return res.status(403).send({ message: 'url is missing in body' }) - const { url } = body - const typebotId = req.query.typebotId as string - const groupId = req.query.groupId as string - const blockId = req.query.blockId as string - const typebot = (await prisma.typebot.findFirst({ - where: { - id: typebotId, - workspace: { members: { some: { userId: user.id } } }, - }, - })) as unknown as Typebot | undefined - if (!typebot) return res.status(400).send({ message: 'Typebot not found' }) - try { - const { webhookId } = typebot.groups - .find(byId(groupId)) - ?.blocks.find(byId(blockId)) as WebhookBlock - await prisma.webhook.upsert({ - where: { id: webhookId }, - update: { url, body: '{{state}}', method: 'POST' }, - create: { - url, - body: '{{state}}', - method: 'POST', - typebotId, - headers: [], - queryParams: [], - }, - }) - - return res.send({ message: 'success' }) - } catch (err) { - return res - .status(400) - .send({ message: "blockId doesn't point to a Webhook block" }) - } - } - return methodNotAllowed(res) -} - -export default handler diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/unsubscribeWebhook.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/unsubscribeWebhook.ts deleted file mode 100644 index 0e7dff4cc..000000000 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/unsubscribeWebhook.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Typebot, WebhookBlock } from '@typebot.io/schemas' -import { NextApiRequest, NextApiResponse } from 'next' -import { methodNotAllowed } from '@typebot.io/lib/api' -import { byId } from '@typebot.io/lib' -import { authenticateUser } from '@/helpers/authenticateUser' -import prisma from '@/lib/prisma' - -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const user = await authenticateUser(req) - if (!user) return res.status(401).json({ message: 'Not authenticated' }) - if (req.method === 'POST') { - const typebotId = req.query.typebotId as string - const groupId = req.query.groupId as string - const blockId = req.query.blockId as string - const typebot = (await prisma.typebot.findFirst({ - where: { - id: typebotId, - workspace: { members: { some: { userId: user.id } } }, - }, - })) as unknown as Typebot | undefined - if (!typebot) return res.status(400).send({ message: 'Typebot not found' }) - try { - const { webhookId } = typebot.groups - .find(byId(groupId)) - ?.blocks.find(byId(blockId)) as WebhookBlock - await prisma.webhook.updateMany({ - where: { id: webhookId }, - data: { url: null }, - }) - - return res.send({ message: 'success' }) - } catch (err) { - return res - .status(400) - .send({ message: "blockId doesn't point to a Webhook block" }) - } - } - return methodNotAllowed(res) -} - -export default handler diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/subscribeWebhook.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/subscribeWebhook.ts deleted file mode 100644 index 30e0c207f..000000000 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/subscribeWebhook.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { authenticateUser } from '@/helpers/authenticateUser' -import prisma from '@/lib/prisma' -import { Typebot, WebhookBlock } from '@typebot.io/schemas' -import { NextApiRequest, NextApiResponse } from 'next' -import { byId } from '@typebot.io/lib' -import { methodNotAllowed } from '@typebot.io/lib/api' - -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const user = await authenticateUser(req) - if (!user) return res.status(401).json({ message: 'Not authenticated' }) - if (req.method === 'POST') { - const body = req.body as Record - if (!('url' in body)) - return res.status(403).send({ message: 'url is missing in body' }) - const { url } = body - const typebotId = req.query.typebotId as string - const blockId = req.query.blockId as string - const typebot = (await prisma.typebot.findFirst({ - where: { - id: typebotId, - workspace: { members: { some: { userId: user.id } } }, - }, - })) as unknown as Typebot | undefined - if (!typebot) return res.status(400).send({ message: 'Typebot not found' }) - try { - const { webhookId } = typebot.groups - .flatMap((g) => g.blocks) - .find(byId(blockId)) as WebhookBlock - await prisma.webhook.upsert({ - where: { id: webhookId }, - update: { url, body: '{{state}}', method: 'POST' }, - create: { - url, - body: '{{state}}', - method: 'POST', - typebotId, - headers: [], - queryParams: [], - }, - }) - - return res.send({ message: 'success' }) - } catch (err) { - return res - .status(400) - .send({ message: "groupId doesn't point to a Webhook block" }) - } - } - return methodNotAllowed(res) -} - -export default handler diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/unsubscribeWebhook.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/unsubscribeWebhook.ts deleted file mode 100644 index 3cc12035b..000000000 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/unsubscribeWebhook.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { authenticateUser } from '@/helpers/authenticateUser' -import prisma from '@/lib/prisma' -import { Typebot, WebhookBlock } from '@typebot.io/schemas' -import { NextApiRequest, NextApiResponse } from 'next' -import { byId } from '@typebot.io/lib' -import { methodNotAllowed } from '@typebot.io/lib/api' - -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const user = await authenticateUser(req) - if (!user) return res.status(401).json({ message: 'Not authenticated' }) - if (req.method === 'POST') { - const typebotId = req.query.typebotId as string - const blockId = req.query.blockId as string - const typebot = (await prisma.typebot.findFirst({ - where: { - id: typebotId, - workspace: { members: { some: { userId: user.id } } }, - }, - })) as unknown as Typebot | undefined - if (!typebot) return res.status(400).send({ message: 'Typebot not found' }) - try { - const { webhookId } = typebot.groups - .flatMap((g) => g.blocks) - .find(byId(blockId)) as WebhookBlock - await prisma.webhook.updateMany({ - where: { id: webhookId }, - data: { url: null }, - }) - - return res.send({ message: 'success' }) - } catch (err) { - return res - .status(400) - .send({ message: "groupId doesn't point to a Webhook block" }) - } - } - return methodNotAllowed(res) -} - -export default handler diff --git a/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx b/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx index 3e13c6e13..abe640c66 100644 --- a/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx +++ b/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx @@ -105,7 +105,6 @@ export const ConversationContainer = (props: Props) => { }) const streamMessage = (content: string) => { - console.log('STREAM', content) setIsSending(false) const lastChunk = [...chatChunks()].pop() if (!lastChunk) return diff --git a/packages/lib/migrations/migrateTypebotFromV3ToV4.ts b/packages/lib/migrations/migrateTypebotFromV3ToV4.ts new file mode 100644 index 000000000..9e9bc3644 --- /dev/null +++ b/packages/lib/migrations/migrateTypebotFromV3ToV4.ts @@ -0,0 +1,63 @@ +import { PrismaClient, Webhook as WebhookFromDb } from '@typebot.io/prisma' +import { + Block, + Typebot, + Webhook, + defaultWebhookAttributes, +} from '@typebot.io/schemas' +import { isWebhookBlock } from '../utils' +import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums' + +export const migrateTypebotFromV3ToV4 = + (prisma: PrismaClient) => + async ( + typebot: Typebot + ): Promise & { version: '4' }> => { + if (typebot.version === '4') + return typebot as Omit & { version: '4' } + const webhookBlocks = typebot.groups + .flatMap((group) => group.blocks) + .filter(isWebhookBlock) + const webhooks = await prisma.webhook.findMany({ + where: { + id: { + in: webhookBlocks.map((block) => block.webhookId as string), + }, + }, + }) + return { + ...typebot, + version: '4', + groups: typebot.groups.map((group) => ({ + ...group, + blocks: group.blocks.map(migrateWebhookBlock(webhooks)), + })), + } + } + +const migrateWebhookBlock = + (webhooks: WebhookFromDb[]) => + (block: Block): Block => { + if (!isWebhookBlock(block)) return block + const webhook = webhooks.find((webhook) => webhook.id === block.webhookId) + return { + ...block, + webhookId: undefined, + options: { + ...block.options, + webhook: webhook + ? { + id: webhook.id, + url: webhook.url ?? undefined, + method: (webhook.method as Webhook['method']) ?? HttpMethod.POST, + headers: (webhook.headers as Webhook['headers']) ?? [], + queryParams: (webhook.queryParams as Webhook['headers']) ?? [], + body: webhook.body ?? undefined, + } + : { + ...defaultWebhookAttributes, + id: block.webhookId ?? '', + }, + }, + } + } diff --git a/packages/lib/utils.ts b/packages/lib/utils.ts index fec1e9ee5..adade3949 100644 --- a/packages/lib/utils.ts +++ b/packages/lib/utils.ts @@ -125,16 +125,6 @@ export const blockTypeHasOption = ( .concat(Object.values(IntegrationBlockType)) .includes(type) -export const blockTypeHasWebhook = ( - type: BlockType -): type is IntegrationBlockType.WEBHOOK => - Object.values([ - IntegrationBlockType.WEBHOOK, - IntegrationBlockType.ZAPIER, - IntegrationBlockType.MAKE_COM, - IntegrationBlockType.PABBLY_CONNECT, - ] as string[]).includes(type) - export const blockTypeHasItems = ( type: BlockType ): type is diff --git a/packages/schemas/features/blocks/integrations/index.ts b/packages/schemas/features/blocks/integrations/index.ts index 8fee9da55..04a5b113f 100644 --- a/packages/schemas/features/blocks/integrations/index.ts +++ b/packages/schemas/features/blocks/integrations/index.ts @@ -5,7 +5,7 @@ export * from './googleSheets' export * from './makeCom' export * from './pabblyConnect' export * from './sendEmail' -export * from './webhook' +export * from './webhook/schemas' export * from './zapier' export * from './pixel/schemas' export * from './pixel/constants' diff --git a/packages/schemas/features/blocks/integrations/makeCom.ts b/packages/schemas/features/blocks/integrations/makeCom.ts index 4650f21cc..b092a4164 100644 --- a/packages/schemas/features/blocks/integrations/makeCom.ts +++ b/packages/schemas/features/blocks/integrations/makeCom.ts @@ -1,13 +1,16 @@ import { z } from 'zod' import { blockBaseSchema } from '../baseSchemas' import { IntegrationBlockType } from './enums' -import { webhookOptionsSchema } from './webhook' +import { webhookOptionsSchema } from './webhook/schemas' export const makeComBlockSchema = blockBaseSchema.merge( z.object({ type: z.enum([IntegrationBlockType.MAKE_COM]), options: webhookOptionsSchema, - webhookId: z.string(), + webhookId: z + .string() + .describe('Deprecated, use webhook.id instead') + .optional(), }) ) diff --git a/packages/schemas/features/blocks/integrations/pabblyConnect.ts b/packages/schemas/features/blocks/integrations/pabblyConnect.ts index 45fc3105c..726f2cb70 100644 --- a/packages/schemas/features/blocks/integrations/pabblyConnect.ts +++ b/packages/schemas/features/blocks/integrations/pabblyConnect.ts @@ -1,13 +1,16 @@ import { z } from 'zod' import { blockBaseSchema } from '../baseSchemas' import { IntegrationBlockType } from './enums' -import { webhookOptionsSchema } from './webhook' +import { webhookOptionsSchema } from './webhook/schemas' export const pabblyConnectBlockSchema = blockBaseSchema.merge( z.object({ type: z.enum([IntegrationBlockType.PABBLY_CONNECT]), options: webhookOptionsSchema, - webhookId: z.string(), + webhookId: z + .string() + .describe('Deprecated, use webhook.id instead') + .optional(), }) ) diff --git a/packages/schemas/features/blocks/integrations/webhook.ts b/packages/schemas/features/blocks/integrations/webhook.ts deleted file mode 100644 index e7df30162..000000000 --- a/packages/schemas/features/blocks/integrations/webhook.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { z } from 'zod' -import { blockBaseSchema } from '../baseSchemas' -import { IntegrationBlockType } from './enums' - -const variableForTestSchema = z.object({ - id: z.string(), - variableId: z.string().optional(), - value: z.string().optional(), -}) - -const responseVariableMappingSchema = z.object({ - id: z.string(), - variableId: z.string().optional(), - bodyPath: z.string().optional(), -}) - -export const webhookOptionsSchema = z.object({ - variablesForTest: z.array(variableForTestSchema), - responseVariableMapping: z.array(responseVariableMappingSchema), - isAdvancedConfig: z.boolean().optional(), - isCustomBody: z.boolean().optional(), - isExecutedOnClient: z.boolean().optional(), -}) - -export const webhookBlockSchema = blockBaseSchema.merge( - z.object({ - type: z.enum([IntegrationBlockType.WEBHOOK]), - options: webhookOptionsSchema, - webhookId: z.string(), - }) -) - -export const defaultWebhookOptions: Omit = { - responseVariableMapping: [], - variablesForTest: [], - isAdvancedConfig: false, - isCustomBody: false, -} - -export type WebhookBlock = z.infer -export type WebhookOptions = z.infer -export type ResponseVariableMapping = z.infer< - typeof responseVariableMappingSchema -> -export type VariableForTest = z.infer diff --git a/packages/schemas/features/blocks/integrations/webhook/enums.ts b/packages/schemas/features/blocks/integrations/webhook/enums.ts new file mode 100644 index 000000000..c1a618095 --- /dev/null +++ b/packages/schemas/features/blocks/integrations/webhook/enums.ts @@ -0,0 +1,11 @@ +export enum HttpMethod { + POST = 'POST', + GET = 'GET', + PUT = 'PUT', + DELETE = 'DELETE', + PATCH = 'PATCH', + HEAD = 'HEAD', + CONNECT = 'CONNECT', + OPTIONS = 'OPTIONS', + TRACE = 'TRACE', +} diff --git a/packages/schemas/features/blocks/integrations/webhook/index.ts b/packages/schemas/features/blocks/integrations/webhook/index.ts new file mode 100644 index 000000000..47da4e4b5 --- /dev/null +++ b/packages/schemas/features/blocks/integrations/webhook/index.ts @@ -0,0 +1,2 @@ +export * from './enums' +export * from './schemas' diff --git a/packages/schemas/features/blocks/integrations/webhook/schemas.ts b/packages/schemas/features/blocks/integrations/webhook/schemas.ts new file mode 100644 index 000000000..522bea48c --- /dev/null +++ b/packages/schemas/features/blocks/integrations/webhook/schemas.ts @@ -0,0 +1,95 @@ +import { z } from 'zod' +import { blockBaseSchema } from '../../baseSchemas' +import { IntegrationBlockType } from '../enums' +import { HttpMethod } from './enums' + +const variableForTestSchema = z.object({ + id: z.string(), + variableId: z.string().optional(), + value: z.string().optional(), +}) + +const responseVariableMappingSchema = z.object({ + id: z.string(), + variableId: z.string().optional(), + bodyPath: z.string().optional(), +}) + +const keyValueSchema = z.object({ + id: z.string(), + key: z.string().optional(), + value: z.string().optional(), +}) + +export const webhookSchema = z.object({ + id: z.string(), + queryParams: keyValueSchema.array(), + headers: keyValueSchema.array(), + method: z.nativeEnum(HttpMethod), + url: z.string().optional(), + body: z.string().optional(), +}) + +export const webhookOptionsSchema = z.object({ + variablesForTest: z.array(variableForTestSchema), + responseVariableMapping: z.array(responseVariableMappingSchema), + isAdvancedConfig: z.boolean().optional(), + isCustomBody: z.boolean().optional(), + isExecutedOnClient: z.boolean().optional(), + webhook: webhookSchema.optional(), +}) + +export const webhookBlockSchema = blockBaseSchema.merge( + z.object({ + type: z.enum([IntegrationBlockType.WEBHOOK]), + options: webhookOptionsSchema, + webhookId: z + .string() + .describe('Deprecated, now integrated in webhook block options') + .optional(), + }) +) + +export const defaultWebhookAttributes: Omit< + Webhook, + 'id' | 'body' | 'url' | 'typebotId' | 'createdAt' | 'updatedAt' +> = { + method: HttpMethod.POST, + headers: [], + queryParams: [], +} + +export const defaultWebhookOptions = (webhookId: string): WebhookOptions => ({ + responseVariableMapping: [], + variablesForTest: [], + isAdvancedConfig: false, + isCustomBody: false, + webhook: { + id: webhookId, + ...defaultWebhookAttributes, + }, +}) + +export const executableWebhookSchema = z.object({ + url: z.string(), + headers: z.record(z.string()).optional(), + body: z.unknown().optional(), + method: z.nativeEnum(HttpMethod).optional(), +}) + +export type KeyValue = { id: string; key?: string; value?: string } + +export type WebhookResponse = { + statusCode: number + data?: unknown +} + +export type ExecutableWebhook = z.infer + +export type Webhook = z.infer +export type WebhookBlock = z.infer +export type WebhookOptions = z.infer +export type ResponseVariableMapping = z.infer< + typeof responseVariableMappingSchema +> +export type VariableForTest = z.infer diff --git a/packages/schemas/features/blocks/integrations/zapier.ts b/packages/schemas/features/blocks/integrations/zapier.ts index 470ab9e50..b75489e24 100644 --- a/packages/schemas/features/blocks/integrations/zapier.ts +++ b/packages/schemas/features/blocks/integrations/zapier.ts @@ -1,13 +1,16 @@ import { z } from 'zod' import { blockBaseSchema } from '../baseSchemas' import { IntegrationBlockType } from './enums' -import { webhookOptionsSchema } from './webhook' +import { webhookOptionsSchema } from './webhook/schemas' export const zapierBlockSchema = blockBaseSchema.merge( z.object({ type: z.enum([IntegrationBlockType.ZAPIER]), options: webhookOptionsSchema, - webhookId: z.string(), + webhookId: z + .string() + .describe('Deprecated, use webhook.id instead') + .optional(), }) ) diff --git a/packages/schemas/features/chat.ts b/packages/schemas/features/chat.ts index fcb11145c..07f3f326f 100644 --- a/packages/schemas/features/chat.ts +++ b/packages/schemas/features/chat.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import { + executableWebhookSchema, googleAnalyticsOptionsSchema, paymentInputRuntimeOptionsSchema, pixelOptionsSchema, @@ -19,7 +20,6 @@ import { answerSchema } from './answer' import { BubbleBlockType } from './blocks/bubbles/enums' import { inputBlockSchemas } from './blocks/schemas' import { chatCompletionMessageSchema } from './blocks/integrations/openai' -import { executableWebhookSchema } from './webhooks' const typebotInSessionStateSchema = publicTypebotSchema.pick({ id: true, diff --git a/packages/schemas/features/publicTypebot.ts b/packages/schemas/features/publicTypebot.ts index 6574c8d8c..478a96ac8 100644 --- a/packages/schemas/features/publicTypebot.ts +++ b/packages/schemas/features/publicTypebot.ts @@ -11,7 +11,7 @@ import { z } from 'zod' export const publicTypebotSchema = z.object({ id: z.string(), - version: z.enum(['3']).nullable(), + version: z.enum(['3', '4']).nullable(), createdAt: z.date(), updatedAt: z.date(), typebotId: z.string(), diff --git a/packages/schemas/features/typebot/typebot.ts b/packages/schemas/features/typebot/typebot.ts index 3a58b098a..5038ba2d6 100644 --- a/packages/schemas/features/typebot/typebot.ts +++ b/packages/schemas/features/typebot/typebot.ts @@ -39,7 +39,7 @@ const resultsTablePreferencesSchema = z.object({ }) export const typebotSchema = z.object({ - version: z.enum(['3']).nullable(), + version: z.enum(['3', '4']).nullable(), id: z.string(), name: z.string(), groups: z.array(groupSchema), diff --git a/packages/schemas/features/webhooks.ts b/packages/schemas/features/webhooks.ts deleted file mode 100644 index 8685767d5..000000000 --- a/packages/schemas/features/webhooks.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Webhook as WebhookFromPrisma } from '@typebot.io/prisma' -import { z } from 'zod' - -export enum HttpMethod { - POST = 'POST', - GET = 'GET', - PUT = 'PUT', - DELETE = 'DELETE', - PATCH = 'PATCH', - HEAD = 'HEAD', - CONNECT = 'CONNECT', - OPTIONS = 'OPTIONS', - TRACE = 'TRACE', -} - -export type KeyValue = { id: string; key?: string; value?: string } - -export type Webhook = Omit< - WebhookFromPrisma, - 'queryParams' | 'headers' | 'method' | 'createdAt' | 'updatedAt' -> & { - queryParams: KeyValue[] - headers: KeyValue[] - method: HttpMethod -} - -export type WebhookResponse = { - statusCode: number - data?: unknown -} - -export const defaultWebhookAttributes: Omit< - Webhook, - 'id' | 'body' | 'url' | 'typebotId' | 'createdAt' | 'updatedAt' -> = { - method: HttpMethod.POST, - headers: [], - queryParams: [], -} - -export const executableWebhookSchema = z.object({ - url: z.string(), - headers: z.record(z.string()).optional(), - body: z.unknown().optional(), - method: z.nativeEnum(HttpMethod).optional(), -}) - -export type ExecutableWebhook = z.infer diff --git a/packages/schemas/index.ts b/packages/schemas/index.ts index bcd884df2..4c298d0df 100644 --- a/packages/schemas/index.ts +++ b/packages/schemas/index.ts @@ -5,7 +5,6 @@ export * from './features/result' export * from './features/answer' export * from './features/utils' export * from './features/credentials' -export * from './features/webhooks' export * from './features/chat' export * from './features/workspace' export * from './features/items'