diff --git a/backend/.sqlx/query-23419adcd74c326d716527293eff518b42f4cdb33e034441015494bd26c172d2.json b/backend/.sqlx/query-23419adcd74c326d716527293eff518b42f4cdb33e034441015494bd26c172d2.json index 5b810738b3..1cb68e3acb 100644 --- a/backend/.sqlx/query-23419adcd74c326d716527293eff518b42f4cdb33e034441015494bd26c172d2.json +++ b/backend/.sqlx/query-23419adcd74c326d716527293eff518b42f4cdb33e034441015494bd26c172d2.json @@ -38,7 +38,8 @@ "postgres", "sqs", "mqtt", - "gcp" + "gcp", + "default_email" ] } } diff --git a/backend/.sqlx/query-23fb2099fe211c9c5388f28097ed8635198a4144a3415121800f5df52b2a133f.json b/backend/.sqlx/query-23fb2099fe211c9c5388f28097ed8635198a4144a3415121800f5df52b2a133f.json new file mode 100644 index 0000000000..629531be93 --- /dev/null +++ b/backend/.sqlx/query-23fb2099fe211c9c5388f28097ed8635198a4144a3415121800f5df52b2a133f.json @@ -0,0 +1,54 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT script_path, is_flow, workspace_id, edited_by, email, path FROM email_trigger WHERE workspace_id = $1 AND local_part = $2 AND (workspaced_local_part = TRUE OR $3 IS TRUE)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "script_path", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "is_flow", + "type_info": "Bool" + }, + { + "ordinal": 2, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "edited_by", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "path", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Bool" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "23fb2099fe211c9c5388f28097ed8635198a4144a3415121800f5df52b2a133f" +} diff --git a/backend/.sqlx/query-28a5caa6e8a8a95d228908cc83e9109986a0803d50164a259aabcdee81014d07.json b/backend/.sqlx/query-28a5caa6e8a8a95d228908cc83e9109986a0803d50164a259aabcdee81014d07.json new file mode 100644 index 0000000000..6e58d3d386 --- /dev/null +++ b/backend/.sqlx/query-28a5caa6e8a8a95d228908cc83e9109986a0803d50164a259aabcdee81014d07.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT EXISTS(\n SELECT 1 \n FROM email_trigger \n WHERE \n ((workspaced_local_part IS TRUE AND workspace_id || '-' || local_part = $1) \n OR (workspaced_local_part IS FALSE AND local_part = $1))\n AND ($2::TEXT IS NULL OR path != $2)\n )\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "28a5caa6e8a8a95d228908cc83e9109986a0803d50164a259aabcdee81014d07" +} diff --git a/backend/.sqlx/query-3700706bb0408d6593be7f15f8aee4d6a023109e5dcfee7009a25ddf5db5e28d.json b/backend/.sqlx/query-3700706bb0408d6593be7f15f8aee4d6a023109e5dcfee7009a25ddf5db5e28d.json new file mode 100644 index 0000000000..8ccffb9455 --- /dev/null +++ b/backend/.sqlx/query-3700706bb0408d6593be7f15f8aee4d6a023109e5dcfee7009a25ddf5db5e28d.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT script_path, is_flow, workspace_id, edited_by, email, path FROM email_trigger WHERE local_part = $1 AND workspaced_local_part = FALSE", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "script_path", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "is_flow", + "type_info": "Bool" + }, + { + "ordinal": 2, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "edited_by", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "path", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "3700706bb0408d6593be7f15f8aee4d6a023109e5dcfee7009a25ddf5db5e28d" +} diff --git a/backend/.sqlx/query-42b4b73e9d60348e2d90fcade9dcad6d8995242dc20a4e14c1a8fae4fc6a9fd2.json b/backend/.sqlx/query-42b4b73e9d60348e2d90fcade9dcad6d8995242dc20a4e14c1a8fae4fc6a9fd2.json index c1a38be77e..52a2d901ec 100644 --- a/backend/.sqlx/query-42b4b73e9d60348e2d90fcade9dcad6d8995242dc20a4e14c1a8fae4fc6a9fd2.json +++ b/backend/.sqlx/query-42b4b73e9d60348e2d90fcade9dcad6d8995242dc20a4e14c1a8fae4fc6a9fd2.json @@ -22,7 +22,8 @@ "postgres", "sqs", "mqtt", - "gcp" + "gcp", + "default_email" ] } } diff --git a/backend/.sqlx/query-4c70aac8f83e9b76628a65df351c16cb630aa963d20898b084b043a59bfc4ba8.json b/backend/.sqlx/query-4c70aac8f83e9b76628a65df351c16cb630aa963d20898b084b043a59bfc4ba8.json new file mode 100644 index 0000000000..cc799822e5 --- /dev/null +++ b/backend/.sqlx/query-4c70aac8f83e9b76628a65df351c16cb630aa963d20898b084b043a59bfc4ba8.json @@ -0,0 +1,41 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT \n owner, \n email,\n path,\n is_flow\n FROM \n capture_config\n WHERE \n workspace_id = $1\n AND trigger_config->>'local_part' = $2\n AND trigger_kind = 'email'::TRIGGER_KIND \n AND last_client_ping > NOW() - INTERVAL '10 seconds'\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "owner", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "path", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "is_flow", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "4c70aac8f83e9b76628a65df351c16cb630aa963d20898b084b043a59bfc4ba8" +} diff --git a/backend/.sqlx/query-4f547c0fd54f3bc57212ce87810e35adf640d44d607e62a1fb296e38ac3fdd36.json b/backend/.sqlx/query-4f547c0fd54f3bc57212ce87810e35adf640d44d607e62a1fb296e38ac3fdd36.json index 15b107811b..cb53599789 100644 --- a/backend/.sqlx/query-4f547c0fd54f3bc57212ce87810e35adf640d44d607e62a1fb296e38ac3fdd36.json +++ b/backend/.sqlx/query-4f547c0fd54f3bc57212ce87810e35adf640d44d607e62a1fb296e38ac3fdd36.json @@ -30,7 +30,8 @@ "postgres", "sqs", "mqtt", - "gcp" + "gcp", + "default_email" ] } } @@ -66,7 +67,8 @@ "postgres", "sqs", "mqtt", - "gcp" + "gcp", + "default_email" ] } } diff --git a/backend/.sqlx/query-54ee00c98dc50ca3da0e2195a78e6df77fd1e8c969667b267aed3347335c91ab.json b/backend/.sqlx/query-54ee00c98dc50ca3da0e2195a78e6df77fd1e8c969667b267aed3347335c91ab.json new file mode 100644 index 0000000000..7be240422d --- /dev/null +++ b/backend/.sqlx/query-54ee00c98dc50ca3da0e2195a78e6df77fd1e8c969667b267aed3347335c91ab.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT EXISTS(\n SELECT 1 \n FROM email_trigger \n WHERE \n workspace_id = $1\n AND local_part = $2 \n AND ($3::TEXT IS NULL OR path != $3)\n )\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "54ee00c98dc50ca3da0e2195a78e6df77fd1e8c969667b267aed3347335c91ab" +} diff --git a/backend/.sqlx/query-6fafc23924eded970689040bd4a94d4d23ebee4f2b7d37bb54c47edb7720be00.json b/backend/.sqlx/query-6fafc23924eded970689040bd4a94d4d23ebee4f2b7d37bb54c47edb7720be00.json new file mode 100644 index 0000000000..706bcacdb2 --- /dev/null +++ b/backend/.sqlx/query-6fafc23924eded970689040bd4a94d4d23ebee4f2b7d37bb54c47edb7720be00.json @@ -0,0 +1,25 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE email_trigger \n SET \n script_path = $1,\n path = $2,\n is_flow = $3,\n local_part = $4,\n workspaced_local_part = $5,\n edited_by = $6,\n email = $7,\n edited_at = now(),\n error_handler_path = $8,\n error_handler_args = $9,\n retry = $10\n WHERE \n workspace_id = $11 AND path = $12\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Bool", + "Varchar", + "Bool", + "Varchar", + "Varchar", + "Varchar", + "Jsonb", + "Jsonb", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "6fafc23924eded970689040bd4a94d4d23ebee4f2b7d37bb54c47edb7720be00" +} diff --git a/backend/.sqlx/query-7fbf72d9059fcd77e4c1112fa4fa22e4276c1da653475628889ce17dc904fbaa.json b/backend/.sqlx/query-7fbf72d9059fcd77e4c1112fa4fa22e4276c1da653475628889ce17dc904fbaa.json index ca02b5c648..693785ebd0 100644 --- a/backend/.sqlx/query-7fbf72d9059fcd77e4c1112fa4fa22e4276c1da653475628889ce17dc904fbaa.json +++ b/backend/.sqlx/query-7fbf72d9059fcd77e4c1112fa4fa22e4276c1da653475628889ce17dc904fbaa.json @@ -25,7 +25,8 @@ "postgres", "sqs", "mqtt", - "gcp" + "gcp", + "default_email" ] } } diff --git a/backend/.sqlx/query-87564a196a1662f524407d853db506bf08c28efe82b68b3d44bafbd3d0e91c29.json b/backend/.sqlx/query-87564a196a1662f524407d853db506bf08c28efe82b68b3d44bafbd3d0e91c29.json index e2acac048f..36927a7fd7 100644 --- a/backend/.sqlx/query-87564a196a1662f524407d853db506bf08c28efe82b68b3d44bafbd3d0e91c29.json +++ b/backend/.sqlx/query-87564a196a1662f524407d853db506bf08c28efe82b68b3d44bafbd3d0e91c29.json @@ -33,7 +33,8 @@ "postgres", "sqs", "mqtt", - "gcp" + "gcp", + "default_email" ] } } diff --git a/backend/.sqlx/query-9c50e3a136a8ee3ec56e083f26d3a960b89e02ec40b292f3b5198baf2a1d3dbf.json b/backend/.sqlx/query-9c50e3a136a8ee3ec56e083f26d3a960b89e02ec40b292f3b5198baf2a1d3dbf.json index c21c30c012..94bffa26d0 100644 --- a/backend/.sqlx/query-9c50e3a136a8ee3ec56e083f26d3a960b89e02ec40b292f3b5198baf2a1d3dbf.json +++ b/backend/.sqlx/query-9c50e3a136a8ee3ec56e083f26d3a960b89e02ec40b292f3b5198baf2a1d3dbf.json @@ -30,7 +30,8 @@ "postgres", "sqs", "mqtt", - "gcp" + "gcp", + "default_email" ] } } diff --git a/backend/.sqlx/query-6ca3ae85ddeb2334c9ae4bd7682daef744a1b10b4f47bc7355a97011bc366aae.json b/backend/.sqlx/query-a7a23229d6915d7fdeea8073d31be6e7d9f7a8581bdbb5914f2c5b49f37dbc36.json similarity index 54% rename from backend/.sqlx/query-6ca3ae85ddeb2334c9ae4bd7682daef744a1b10b4f47bc7355a97011bc366aae.json rename to backend/.sqlx/query-a7a23229d6915d7fdeea8073d31be6e7d9f7a8581bdbb5914f2c5b49f37dbc36.json index 658b99f032..b34c87ca4c 100644 --- a/backend/.sqlx/query-6ca3ae85ddeb2334c9ae4bd7682daef744a1b10b4f47bc7355a97011bc366aae.json +++ b/backend/.sqlx/query-a7a23229d6915d7fdeea8073d31be6e7d9f7a8581bdbb5914f2c5b49f37dbc36.json @@ -1,11 +1,10 @@ { "db_name": "PostgreSQL", - "query": "\n UPDATE \n http_trigger \n SET \n workspaced_route = $1,\n wrap_body = $2,\n raw_string = $3,\n authentication_resource_path = $4,\n script_path = $5, \n path = $6, \n is_flow = $7, \n http_method = $8, \n static_asset_config = $9, \n edited_by = $10, \n email = $11, \n is_async = $12, \n authentication_method = $13, \n summary = $14,\n description = $15,\n edited_at = now(), \n is_static_website = $16,\n error_handler_path = $17,\n error_handler_args = $18,\n retry = $19\n WHERE \n workspace_id = $20 AND \n path = $21\n ", + "query": "\n UPDATE \n http_trigger \n SET \n wrap_body = $1,\n raw_string = $2,\n authentication_resource_path = $3,\n script_path = $4, \n path = $5, \n is_flow = $6, \n http_method = $7, \n static_asset_config = $8, \n edited_by = $9, \n email = $10, \n is_async = $11, \n authentication_method = $12, \n summary = $13,\n description = $14,\n edited_at = now(), \n is_static_website = $15,\n error_handler_path = $16,\n error_handler_args = $17,\n retry = $18\n WHERE \n workspace_id = $19 AND \n path = $20\n ", "describe": { "columns": [], "parameters": { "Left": [ - "Bool", "Bool", "Bool", "Varchar", @@ -57,5 +56,5 @@ }, "nullable": [] }, - "hash": "6ca3ae85ddeb2334c9ae4bd7682daef744a1b10b4f47bc7355a97011bc366aae" + "hash": "a7a23229d6915d7fdeea8073d31be6e7d9f7a8581bdbb5914f2c5b49f37dbc36" } diff --git a/backend/.sqlx/query-acb094aef60bba9083087264d65034fce38417099f15e8312be72a386f10bc1f.json b/backend/.sqlx/query-acb094aef60bba9083087264d65034fce38417099f15e8312be72a386f10bc1f.json new file mode 100644 index 0000000000..805e5f0e79 --- /dev/null +++ b/backend/.sqlx/query-acb094aef60bba9083087264d65034fce38417099f15e8312be72a386f10bc1f.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE email_trigger \n SET \n script_path = $1,\n path = $2,\n is_flow = $3,\n edited_by = $4,\n email = $5,\n edited_at = now(),\n error_handler_path = $6,\n error_handler_args = $7,\n retry = $8\n WHERE \n workspace_id = $9 AND path = $10\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Bool", + "Varchar", + "Varchar", + "Varchar", + "Jsonb", + "Jsonb", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "acb094aef60bba9083087264d65034fce38417099f15e8312be72a386f10bc1f" +} diff --git a/backend/.sqlx/query-b3f0595cacba194e08b9a3e244d9e637e9e156cd85b69126c87dfff89a47711d.json b/backend/.sqlx/query-b3f0595cacba194e08b9a3e244d9e637e9e156cd85b69126c87dfff89a47711d.json index a20f4e47ad..d55d9a0a43 100644 --- a/backend/.sqlx/query-b3f0595cacba194e08b9a3e244d9e637e9e156cd85b69126c87dfff89a47711d.json +++ b/backend/.sqlx/query-b3f0595cacba194e08b9a3e244d9e637e9e156cd85b69126c87dfff89a47711d.json @@ -22,7 +22,8 @@ "postgres", "sqs", "mqtt", - "gcp" + "gcp", + "default_email" ] } } diff --git a/backend/.sqlx/query-aa0215d4174c1aeda8631bcd582c895329d2daf722d360fbcbdef6f04bb1400f.json b/backend/.sqlx/query-d1d9a3184f3470949840cc07414d08641527a4fe329204c65c49ab82d9ee4afb.json similarity index 85% rename from backend/.sqlx/query-aa0215d4174c1aeda8631bcd582c895329d2daf722d360fbcbdef6f04bb1400f.json rename to backend/.sqlx/query-d1d9a3184f3470949840cc07414d08641527a4fe329204c65c49ab82d9ee4afb.json index 9526b72f55..2d3fc8b5b7 100644 --- a/backend/.sqlx/query-aa0215d4174c1aeda8631bcd582c895329d2daf722d360fbcbdef6f04bb1400f.json +++ b/backend/.sqlx/query-d1d9a3184f3470949840cc07414d08641527a4fe329204c65c49ab82d9ee4afb.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) AS \"websocket_used!\",\n EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) AS \"http_routes_used!\",\n EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as \"kafka_used!\",\n EXISTS(SELECT 1 FROM nats_trigger WHERE workspace_id = $1) as \"nats_used!\",\n EXISTS(SELECT 1 FROM postgres_trigger WHERE workspace_id = $1) AS \"postgres_used!\",\n EXISTS(SELECT 1 FROM mqtt_trigger WHERE workspace_id = $1) AS \"mqtt_used!\",\n EXISTS(SELECT 1 FROM sqs_trigger WHERE workspace_id = $1) AS \"sqs_used!\",\n EXISTS(SELECT 1 FROM gcp_trigger WHERE workspace_id = $1) AS \"gcp_used!\"\n ", + "query": "\n SELECT\n EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) AS \"websocket_used!\",\n EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) AS \"http_routes_used!\",\n EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as \"kafka_used!\",\n EXISTS(SELECT 1 FROM nats_trigger WHERE workspace_id = $1) as \"nats_used!\",\n EXISTS(SELECT 1 FROM postgres_trigger WHERE workspace_id = $1) AS \"postgres_used!\",\n EXISTS(SELECT 1 FROM mqtt_trigger WHERE workspace_id = $1) AS \"mqtt_used!\",\n EXISTS(SELECT 1 FROM sqs_trigger WHERE workspace_id = $1) AS \"sqs_used!\",\n EXISTS(SELECT 1 FROM gcp_trigger WHERE workspace_id = $1) AS \"gcp_used!\",\n EXISTS(SELECT 1 FROM email_trigger WHERE workspace_id = $1) AS \"email_used!\"\n ", "describe": { "columns": [ { @@ -42,6 +42,11 @@ "ordinal": 7, "name": "gcp_used!", "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "email_used!", + "type_info": "Bool" } ], "parameters": { @@ -57,8 +62,9 @@ null, null, null, + null, null ] }, - "hash": "aa0215d4174c1aeda8631bcd582c895329d2daf722d360fbcbdef6f04bb1400f" + "hash": "d1d9a3184f3470949840cc07414d08641527a4fe329204c65c49ab82d9ee4afb" } diff --git a/backend/.sqlx/query-d328d00e5393b0e8d2c4b3674221fabbae580fc404e0d4481fd8d7fb51272c94.json b/backend/.sqlx/query-d328d00e5393b0e8d2c4b3674221fabbae580fc404e0d4481fd8d7fb51272c94.json new file mode 100644 index 0000000000..474ce7b688 --- /dev/null +++ b/backend/.sqlx/query-d328d00e5393b0e8d2c4b3674221fabbae580fc404e0d4481fd8d7fb51272c94.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO email_trigger (\n workspace_id,\n path,\n script_path,\n is_flow,\n local_part,\n workspaced_local_part,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, now(), $9, $10, $11\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Bool", + "Varchar", + "Bool", + "Varchar", + "Varchar", + "Varchar", + "Jsonb", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "d328d00e5393b0e8d2c4b3674221fabbae580fc404e0d4481fd8d7fb51272c94" +} diff --git a/backend/.sqlx/query-eac595e19e5c8e70f1514ef29dec35c7342ac9a814c73f6290e1d6ebd3a55423.json b/backend/.sqlx/query-eac595e19e5c8e70f1514ef29dec35c7342ac9a814c73f6290e1d6ebd3a55423.json index 8890104678..c4ee4c1a48 100644 --- a/backend/.sqlx/query-eac595e19e5c8e70f1514ef29dec35c7342ac9a814c73f6290e1d6ebd3a55423.json +++ b/backend/.sqlx/query-eac595e19e5c8e70f1514ef29dec35c7342ac9a814c73f6290e1d6ebd3a55423.json @@ -22,7 +22,8 @@ "postgres", "sqs", "mqtt", - "gcp" + "gcp", + "default_email" ] } } diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 120e3bf047..636d631e65 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -28adb8c6253e798056fceedeed5dac3eaf64c568 \ No newline at end of file +29e2930269c744fe6136334f8b159afc9dd26bab \ No newline at end of file diff --git a/backend/migrations/20250905153025_add_email_triggers.down.sql b/backend/migrations/20250905153025_add_email_triggers.down.sql new file mode 100644 index 0000000000..aaf5132dd7 --- /dev/null +++ b/backend/migrations/20250905153025_add_email_triggers.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +DROP TABLE email_trigger; diff --git a/backend/migrations/20250905153025_add_email_triggers.up.sql b/backend/migrations/20250905153025_add_email_triggers.up.sql new file mode 100644 index 0000000000..3939a6351f --- /dev/null +++ b/backend/migrations/20250905153025_add_email_triggers.up.sql @@ -0,0 +1,68 @@ +-- Add up migration script here + +CREATE TABLE email_trigger ( + path VARCHAR(255) NOT NULL, + local_part VARCHAR(255) NOT NULL, + workspaced_local_part BOOLEAN NOT NULL, + script_path VARCHAR(255) NOT NULL, + is_flow BOOLEAN NOT NULL, + workspace_id VARCHAR(50) NOT NULL, + edited_by VARCHAR(50) NOT NULL, + email VARCHAR(255) NOT NULL, + edited_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + extra_perms JSONB NOT NULL DEFAULT '{}', + error_handler_path VARCHAR(255) NULL, + error_handler_args JSONB NULL, + retry JSONB NULL, + PRIMARY KEY (path, workspace_id) +); + +GRANT ALL ON email_trigger TO windmill_user; +GRANT ALL ON email_trigger TO windmill_admin; + +ALTER TABLE email_trigger ENABLE ROW LEVEL SECURITY; + +CREATE POLICY admin_policy ON email_trigger FOR ALL TO windmill_admin USING (true); + +CREATE POLICY see_folder_extra_perms_user_select ON email_trigger FOR SELECT TO windmill_user +USING (SPLIT_PART(email_trigger.path, '/', 1) = 'f' AND SPLIT_PART(email_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[])); +CREATE POLICY see_folder_extra_perms_user_insert ON email_trigger FOR INSERT TO windmill_user +WITH CHECK (SPLIT_PART(email_trigger.path, '/', 1) = 'f' AND SPLIT_PART(email_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[])); +CREATE POLICY see_folder_extra_perms_user_update ON email_trigger FOR UPDATE TO windmill_user +USING (SPLIT_PART(email_trigger.path, '/', 1) = 'f' AND SPLIT_PART(email_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[])); +CREATE POLICY see_folder_extra_perms_user_delete ON email_trigger FOR DELETE TO windmill_user +USING (SPLIT_PART(email_trigger.path, '/', 1) = 'f' AND SPLIT_PART(email_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[])); + +CREATE POLICY see_own ON email_trigger FOR ALL TO windmill_user +USING (SPLIT_PART(email_trigger.path, '/', 1) = 'u' AND SPLIT_PART(email_trigger.path, '/', 2) = current_setting('session.user')); +CREATE POLICY see_member ON email_trigger FOR ALL TO windmill_user +USING (SPLIT_PART(email_trigger.path, '/', 1) = 'g' AND SPLIT_PART(email_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[])); + +CREATE POLICY see_extra_perms_user_select ON email_trigger FOR SELECT TO windmill_user +USING (extra_perms ? CONCAT('u/', current_setting('session.user'))); +CREATE POLICY see_extra_perms_user_insert ON email_trigger FOR INSERT TO windmill_user +WITH CHECK ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean); +CREATE POLICY see_extra_perms_user_update ON email_trigger FOR UPDATE TO windmill_user +USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean); +CREATE POLICY see_extra_perms_user_delete ON email_trigger FOR DELETE TO windmill_user +USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean); + +CREATE POLICY see_extra_perms_groups_select ON email_trigger FOR SELECT TO windmill_user +USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]); +CREATE POLICY see_extra_perms_groups_insert ON email_trigger FOR INSERT TO windmill_user +WITH CHECK (exists( + SELECT key, value FROM jsonb_each_text(extra_perms) + WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]) + AND value::boolean)); +CREATE POLICY see_extra_perms_groups_update ON email_trigger FOR UPDATE TO windmill_user +USING (exists( + SELECT key, value FROM jsonb_each_text(extra_perms) + WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]) + AND value::boolean)); +CREATE POLICY see_extra_perms_groups_delete ON email_trigger FOR DELETE TO windmill_user +USING (exists( + SELECT key, value FROM jsonb_each_text(extra_perms) + WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]) + AND value::boolean)); + +ALTER TYPE TRIGGER_KIND ADD VALUE IF NOT EXISTS 'default_email'; \ No newline at end of file diff --git a/backend/migrations/20250909102627_change_type_of_default_email_captures.down.sql b/backend/migrations/20250909102627_change_type_of_default_email_captures.down.sql new file mode 100644 index 0000000000..04b630eb5f --- /dev/null +++ b/backend/migrations/20250909102627_change_type_of_default_email_captures.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here +UPDATE capture SET trigger_kind = 'email' WHERE trigger_kind = 'default_email'; +UPDATE capture_config SET trigger_kind = 'email' WHERE trigger_kind = 'default_email'; \ No newline at end of file diff --git a/backend/migrations/20250909102627_change_type_of_default_email_captures.up.sql b/backend/migrations/20250909102627_change_type_of_default_email_captures.up.sql new file mode 100644 index 0000000000..6a163052c6 --- /dev/null +++ b/backend/migrations/20250909102627_change_type_of_default_email_captures.up.sql @@ -0,0 +1,3 @@ +-- Add up migration script here +UPDATE capture SET trigger_kind = 'default_email' WHERE trigger_kind = 'email'; +UPDATE capture_config SET trigger_kind = 'default_email' WHERE trigger_kind = 'email'; \ No newline at end of file diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 64005cb4a6..d2b8dd354b 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -2907,6 +2907,8 @@ paths: type: boolean sqs_used: type: boolean + email_used: + type: boolean required: - http_routes_used - websocket_used @@ -2916,6 +2918,7 @@ paths: - mqtt_used - gcp_used - sqs_used + - email_used /w/{workspace}/users/list: get: summary: list users @@ -10914,6 +10917,171 @@ paths: schema: type: string + /w/{workspace}/email_triggers/create: + post: + summary: create email trigger + operationId: createEmailTrigger + tags: + - email_trigger + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: new email trigger + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NewEmailTrigger" + responses: + "201": + description: email trigger created + content: + text/plain: + schema: + type: string + + /w/{workspace}/email_triggers/update/{path}: + post: + summary: update email trigger + operationId: updateEmailTrigger + tags: + - email_trigger + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + requestBody: + description: updated trigger + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EditEmailTrigger" + responses: + "200": + description: email trigger updated + content: + text/plain: + schema: + type: string + + /w/{workspace}/email_triggers/delete/{path}: + delete: + summary: delete email trigger + operationId: deleteEmailTrigger + tags: + - email_trigger + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: email trigger deleted + content: + text/plain: + schema: + type: string + + /w/{workspace}/email_triggers/get/{path}: + get: + summary: get email trigger + operationId: getEmailTrigger + tags: + - email_trigger + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: email trigger retrieved + content: + application/json: + schema: + $ref: "#/components/schemas/EmailTrigger" + + /w/{workspace}/email_triggers/list: + get: + summary: list email triggers + operationId: listEmailTriggers + tags: + - email_trigger + parameters: + - $ref: "#/components/parameters/WorkspaceId" + required: true + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" + - name: path + description: filter by path + in: query + schema: + type: string + - name: is_flow + in: query + schema: + type: boolean + - name: path_start + in: query + schema: + type: string + responses: + "200": + description: email trigger list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/EmailTrigger" + + /w/{workspace}/email_triggers/exists/{path}: + get: + summary: does email trigger exists + operationId: existsEmailTrigger + tags: + - email_trigger + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: email trigger exists + content: + application/json: + schema: + type: boolean + + /w/{workspace}/email_triggers/local_part_exists: + post: + summary: does email local part exists + operationId: existsEmailLocalPart + tags: + - email_trigger + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: email local part exists request + required: true + content: + application/json: + schema: + type: object + properties: + local_part: + type: string + workspaced_local_part: + type: boolean + trigger_path: + type: string + + required: + - local_part + responses: + "200": + description: email local part exists + content: + application/json: + schema: + type: boolean + /groups/list: get: summary: list instance groups @@ -11999,6 +12167,7 @@ paths: mqtt_trigger, gcp_trigger, sqs_trigger, + email_trigger, ] responses: "200": @@ -12043,6 +12212,7 @@ paths: mqtt_trigger, gcp_trigger, sqs_trigger, + email_trigger, ] requestBody: description: acl to add @@ -12098,6 +12268,7 @@ paths: mqtt_trigger, gcp_trigger, sqs_trigger, + email_trigger, ] requestBody: description: acl to add @@ -16131,6 +16302,8 @@ components: type: number email_count: type: number + default_email_count: + type: number websocket_count: type: number postgres_count: @@ -17086,6 +17259,75 @@ components: - subjects - is_flow + EmailTrigger: + allOf: + - $ref: "#/components/schemas/TriggerExtraProperty" + type: object + properties: + local_part: + type: string + workspaced_local_part: + type: boolean + error_handler_path: + type: string + error_handler_args: + $ref: "#/components/schemas/ScriptArgs" + retry: + $ref: "../../openflow.openapi.yaml#/components/schemas/Retry" + + required: + - local_part + + NewEmailTrigger: + type: object + properties: + path: + type: string + script_path: + type: string + local_part: + type: string + workspaced_local_part: + type: boolean + is_flow: + type: boolean + error_handler_path: + type: string + error_handler_args: + $ref: "#/components/schemas/ScriptArgs" + retry: + $ref: "../../openflow.openapi.yaml#/components/schemas/Retry" + + required: + - path + - script_path + - local_part + - is_flow + + EditEmailTrigger: + type: object + properties: + path: + type: string + script_path: + type: string + local_part: + type: string + workspaced_local_part: + type: boolean + is_flow: + type: boolean + error_handler_path: + type: string + error_handler_args: + $ref: "#/components/schemas/ScriptArgs" + retry: + $ref: "../../openflow.openapi.yaml#/components/schemas/Retry" + required: + - path + - script_path + - is_flow + Group: type: object properties: @@ -18141,7 +18383,7 @@ components: CaptureTriggerKind: type: string enum: - [webhook, http, websocket, kafka, email, nats, postgres, sqs, mqtt, gcp] + [webhook, http, websocket, kafka, default_email, nats, postgres, sqs, mqtt, gcp, email] Capture: type: object diff --git a/backend/windmill-api/src/capture.rs b/backend/windmill-api/src/capture.rs index 0b56bd143d..1231bcf042 100644 --- a/backend/windmill-api/src/capture.rs +++ b/backend/windmill-api/src/capture.rs @@ -146,6 +146,12 @@ struct HttpTriggerConfig { wrap_body: Option, } +#[cfg(all(feature = "enterprise", feature = "smtp", feature = "private"))] +#[derive(Serialize, Deserialize)] +struct EmailTriggerConfig { + local_part: String, +} + #[cfg(all(feature = "enterprise", feature = "kafka", feature = "private"))] #[derive(Serialize, Deserialize)] pub struct KafkaTriggerConfig { @@ -241,6 +247,8 @@ enum TriggerConfig { Mqtt(MqttTriggerConfig), #[cfg(all(feature = "enterprise", feature = "gcp_trigger", feature = "private"))] Gcp(GcpTriggerConfig), + #[cfg(all(feature = "enterprise", feature = "smtp", feature = "private"))] + Email(EmailTriggerConfig), } #[derive(Serialize, Deserialize)] diff --git a/backend/windmill-api/src/granular_acls.rs b/backend/windmill-api/src/granular_acls.rs index 0cdac8c3f0..a9612d4a7e 100644 --- a/backend/windmill-api/src/granular_acls.rs +++ b/backend/windmill-api/src/granular_acls.rs @@ -23,7 +23,7 @@ use windmill_common::{ utils::{not_found_if_none, StripPath}, }; -const KINDS: [&str; 14] = [ +const KINDS: [&str; 18] = [ "script", "group_", "resource", @@ -37,7 +37,11 @@ const KINDS: [&str; 14] = [ "websocket_trigger", "kafka_trigger", "nats_trigger", - "mqtt_trigger" + "postgres_trigger", + "mqtt_trigger", + "gcp_trigger", + "sqs_trigger", + "email_trigger", ]; pub fn workspaced_service() -> Router { @@ -131,7 +135,7 @@ async fn add_granular_acl( &w_id, DeployedObject::Folder { path: format!("f/{}", path) }, Some(format!("Folder '{}' changed permissions", path)), - true, + true, ) .await? } @@ -243,7 +247,7 @@ async fn remove_granular_acl( &w_id, DeployedObject::Folder { path: format!("f/{}", path) }, Some(format!("Folder '{}' changed permissions", path)), - true, + true, ) .await? } diff --git a/backend/windmill-api/src/scopes.rs b/backend/windmill-api/src/scopes.rs index d9ae3a3344..ef20131659 100644 --- a/backend/windmill-api/src/scopes.rs +++ b/backend/windmill-api/src/scopes.rs @@ -255,6 +255,7 @@ pub enum ScopeDomain { SqsTriggers, GcpTriggers, PostgresTriggers, + EmailTriggers, // System domains Audit, @@ -308,6 +309,7 @@ impl ScopeDomain { Self::SqsTriggers => "sqs_triggers", Self::GcpTriggers => "gcp_triggers", Self::PostgresTriggers => "postgres_triggers", + Self::EmailTriggers => "email_triggers", Self::Audit => "audit", Self::Settings => "settings", Self::Workers => "workers", @@ -354,6 +356,7 @@ impl ScopeDomain { "sqs_triggers" => Some(Self::SqsTriggers), "gcp_triggers" => Some(Self::GcpTriggers), "postgres_triggers" => Some(Self::PostgresTriggers), + "email_triggers" => Some(Self::EmailTriggers), "audit" => Some(Self::Audit), "settings" => Some(Self::Settings), "workers" => Some(Self::Workers), diff --git a/backend/windmill-api/src/token.rs b/backend/windmill-api/src/token.rs index 9ed17dfc6a..f38ce642e6 100644 --- a/backend/windmill-api/src/token.rs +++ b/backend/windmill-api/src/token.rs @@ -27,6 +27,7 @@ fn build_trigger_scope_domains() -> Vec { ("sqs_triggers", "AWS SQS"), ("gcp_triggers", "GCP Pub/Sub"), ("postgres_triggers", "PostgreSQL"), + ("email_triggers", "Email"), ]; TRIGGER_DOMAINS diff --git a/backend/windmill-api/src/triggers/email/handler_oss.rs b/backend/windmill-api/src/triggers/email/handler_oss.rs new file mode 100644 index 0000000000..2f3a318bdd --- /dev/null +++ b/backend/windmill-api/src/triggers/email/handler_oss.rs @@ -0,0 +1,68 @@ +#[cfg(not(feature = "private"))] +use crate::triggers::TriggerData; + +#[allow(unused)] +#[cfg(feature = "private")] +pub use super::handler_ee::*; + +#[cfg(not(feature = "private"))] +use { + super::EmailTrigger, + crate::{ + db::{ApiAuthed, DB}, + triggers::TriggerCrud, + }, + axum::async_trait, + sqlx::PgConnection, + windmill_common::error::{Error, Result}, + windmill_git_sync::DeployedObject, +}; + +#[cfg(not(feature = "private"))] +#[async_trait] +impl TriggerCrud for EmailTrigger { + type Trigger = (); + type TriggerConfig = (); + type TriggerConfigRequest = (); + type TestConnectionConfig = (); + + const TABLE_NAME: &'static str = ""; + const TRIGGER_TYPE: &'static str = ""; + const SUPPORTS_ENABLED: bool = false; + const SUPPORTS_SERVER_STATE: bool = false; + const SUPPORTS_TEST_CONNECTION: bool = false; + const ROUTE_PREFIX: &'static str = "/email_triggers"; + const DEPLOYMENT_NAME: &'static str = ""; + const IS_CLOUD_HOSTED: bool = false; + + fn get_deployed_object(path: String) -> DeployedObject { + DeployedObject::EmailTrigger { path } + } + + async fn create_trigger( + &self, + _db: &DB, + _tx: &mut PgConnection, + _authed: &ApiAuthed, + _w_id: &str, + _trigger: TriggerData, + ) -> Result<()> { + Err(Error::BadRequest( + "Email triggers are not available in open source version".to_string(), + )) + } + + async fn update_trigger( + &self, + _db: &DB, + _executor: &mut PgConnection, + _authed: &ApiAuthed, + _workspace_id: &str, + _path: &str, + _trigger: TriggerData, + ) -> Result<()> { + Err(Error::BadRequest( + "Email triggers are not available in open source version".to_string(), + )) + } +} diff --git a/backend/windmill-api/src/triggers/email/mod.rs b/backend/windmill-api/src/triggers/email/mod.rs new file mode 100644 index 0000000000..836af77fa0 --- /dev/null +++ b/backend/windmill-api/src/triggers/email/mod.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "private")] +mod handler_ee; +pub mod handler_oss; + +#[cfg(feature = "private")] +mod mod_ee; +#[cfg(feature = "private")] +pub use mod_ee::*; + +#[derive(Copy, Clone)] +pub struct EmailTrigger; diff --git a/backend/windmill-api/src/triggers/handler.rs b/backend/windmill-api/src/triggers/handler.rs index 7138b2661e..0e22ea2f34 100644 --- a/backend/windmill-api/src/triggers/handler.rs +++ b/backend/windmill-api/src/triggers/handler.rs @@ -231,8 +231,7 @@ pub trait TriggerCrud: Send + Sync + 'static { edited_by = $3, edited_at = now(), server_id = NULL, - error = NULL, - last_server_ping = NULL + error = NULL WHERE workspace_id = $4 AND path = $5 @@ -737,6 +736,16 @@ pub fn generate_trigger_routers() -> Router { ); } + #[cfg(all(feature = "enterprise", feature = "smtp", feature = "private"))] + { + use crate::triggers::email::EmailTrigger; + + router = router.nest( + EmailTrigger::ROUTE_PREFIX, + complete_trigger_routes(EmailTrigger), + ); + } + router } @@ -752,6 +761,7 @@ pub struct TriggersCount { http_routes_count: i64, webhook_count: i64, email_count: i64, + default_email_count: i64, websocket_count: i64, kafka_count: i64, nats_count: i64, @@ -872,6 +882,18 @@ pub async fn get_triggers_count_internal( }; #[cfg(not(all(feature = "gcp_trigger", feature = "enterprise", feature = "private")))] let gcp_count = 0; + + #[cfg(all(feature = "smtp", feature = "enterprise", feature = "private"))] + let email_count = { + use crate::triggers::email::EmailTrigger; + let count = EmailTrigger + .trigger_count(&mut tx, w_id, is_flow, path) + .await; + count + }; + #[cfg(not(all(feature = "smtp", feature = "enterprise", feature = "private")))] + let email_count = 0; + tx.commit().await?; let webhook_count = (if is_flow { @@ -890,7 +912,7 @@ pub async fn get_triggers_count_internal( .await? .unwrap_or(0); - let email_count = (if is_flow { + let default_email_count = (if is_flow { sqlx::query_scalar!( "SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]", w_id, @@ -911,6 +933,7 @@ pub async fn get_triggers_count_internal( schedule_count, http_routes_count, webhook_count, + default_email_count, email_count, websocket_count, kafka_count, diff --git a/backend/windmill-api/src/triggers/http/handler.rs b/backend/windmill-api/src/triggers/http/handler.rs index 78ea4029c5..e2e5546e09 100644 --- a/backend/windmill-api/src/triggers/http/handler.rs +++ b/backend/windmill-api/src/triggers/http/handler.rs @@ -458,8 +458,7 @@ impl TriggerCrud for HttpTrigger { } let route_path_key = - check_if_route_exist(db, &trigger.config, workspace_id, Some(path)) - .await?; + check_if_route_exist(db, &trigger.config, workspace_id, Some(path)).await?; sqlx::query!( r#" @@ -524,31 +523,29 @@ impl TriggerCrud for HttpTrigger { UPDATE http_trigger SET - workspaced_route = $1, - wrap_body = $2, - raw_string = $3, - authentication_resource_path = $4, - script_path = $5, - path = $6, - is_flow = $7, - http_method = $8, - static_asset_config = $9, - edited_by = $10, - email = $11, - is_async = $12, - authentication_method = $13, - summary = $14, - description = $15, + wrap_body = $1, + raw_string = $2, + authentication_resource_path = $3, + script_path = $4, + path = $5, + is_flow = $6, + http_method = $7, + static_asset_config = $8, + edited_by = $9, + email = $10, + is_async = $11, + authentication_method = $12, + summary = $13, + description = $14, edited_at = now(), - is_static_website = $16, - error_handler_path = $17, - error_handler_args = $18, - retry = $19 + is_static_website = $15, + error_handler_path = $16, + error_handler_args = $17, + retry = $18 WHERE - workspace_id = $20 AND - path = $21 + workspace_id = $19 AND + path = $20 "#, - trigger.config.workspaced_route, trigger.config.wrap_body, trigger.config.raw_string, trigger.config.authentication_resource_path, diff --git a/backend/windmill-api/src/triggers/mod.rs b/backend/windmill-api/src/triggers/mod.rs index 2bfe6b0225..a5e6dc61f2 100644 --- a/backend/windmill-api/src/triggers/mod.rs +++ b/backend/windmill-api/src/triggers/mod.rs @@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize}; use sqlx::{types::Json as SqlxJson, FromRow}; use std::{collections::HashMap, fmt::Debug}; +#[cfg(all(feature = "smtp", feature = "enterprise", feature = "private"))] +pub mod email; #[cfg(all(feature = "gcp_trigger", feature = "enterprise", feature = "private"))] pub mod gcp; #[cfg(feature = "http_trigger")] diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index c2b9e448c2..037f67c995 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -2012,6 +2012,7 @@ struct UsedTriggers { pub mqtt_used: bool, pub sqs_used: bool, pub gcp_used: bool, + pub email_used: bool, } async fn get_used_triggers( @@ -2031,7 +2032,8 @@ async fn get_used_triggers( EXISTS(SELECT 1 FROM postgres_trigger WHERE workspace_id = $1) AS "postgres_used!", EXISTS(SELECT 1 FROM mqtt_trigger WHERE workspace_id = $1) AS "mqtt_used!", EXISTS(SELECT 1 FROM sqs_trigger WHERE workspace_id = $1) AS "sqs_used!", - EXISTS(SELECT 1 FROM gcp_trigger WHERE workspace_id = $1) AS "gcp_used!" + EXISTS(SELECT 1 FROM gcp_trigger WHERE workspace_id = $1) AS "gcp_used!", + EXISTS(SELECT 1 FROM email_trigger WHERE workspace_id = $1) AS "email_used!" "#, w_id ) diff --git a/backend/windmill-common/src/triggers.rs b/backend/windmill-common/src/triggers.rs index 08356f24ad..43c4ae50fc 100644 --- a/backend/windmill-common/src/triggers.rs +++ b/backend/windmill-common/src/triggers.rs @@ -5,13 +5,14 @@ use std::fmt; use strum_macros::EnumIter; #[derive(sqlx::Type, Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, EnumIter)] -#[sqlx(type_name = "TRIGGER_KIND", rename_all = "lowercase")] -#[serde(rename_all = "lowercase")] +#[sqlx(type_name = "TRIGGER_KIND", rename_all = "snake_case")] +#[serde(rename_all = "snake_case")] pub enum TriggerKind { Webhook, Http, Websocket, Kafka, + DefaultEmail, Email, Nats, Mqtt, @@ -28,6 +29,7 @@ impl TriggerKind { TriggerKind::Websocket => "websocket".to_string(), TriggerKind::Kafka => "kafka".to_string(), TriggerKind::Email => "email".to_string(), + TriggerKind::DefaultEmail => "email".to_string(), // to the user we also show kind email for default email TriggerKind::Nats => "nats".to_string(), TriggerKind::Mqtt => "mqtt".to_string(), TriggerKind::Sqs => "sqs".to_string(), @@ -45,6 +47,7 @@ impl fmt::Display for TriggerKind { TriggerKind::Websocket => "websocket", TriggerKind::Kafka => "kafka", TriggerKind::Email => "email", + TriggerKind::DefaultEmail => "default_email", TriggerKind::Nats => "nats", TriggerKind::Mqtt => "mqtt", TriggerKind::Sqs => "sqs", diff --git a/backend/windmill-git-sync/src/lib.rs b/backend/windmill-git-sync/src/lib.rs index df63736acf..380cebb70b 100644 --- a/backend/windmill-git-sync/src/lib.rs +++ b/backend/windmill-git-sync/src/lib.rs @@ -34,6 +34,7 @@ pub enum DeployedObject { MqttTrigger { path: String }, SqsTrigger { path: String }, GcpTrigger { path: String }, + EmailTrigger { path: String }, Settings { setting_type: String }, Key { key_type: String }, } @@ -59,6 +60,7 @@ impl DeployedObject { DeployedObject::MqttTrigger { path } => path.to_owned(), DeployedObject::SqsTrigger { path } => path.to_owned(), DeployedObject::GcpTrigger { path } => path.to_owned(), + DeployedObject::EmailTrigger { path } => path.to_owned(), DeployedObject::Settings { .. } => "settings.yaml".to_string(), DeployedObject::Key { .. } => "encryption_key.yaml".to_string(), } @@ -95,6 +97,7 @@ impl DeployedObject { DeployedObject::MqttTrigger { .. } => None, DeployedObject::SqsTrigger { .. } => None, DeployedObject::GcpTrigger { .. } => None, + DeployedObject::EmailTrigger { .. } => None, DeployedObject::Settings { .. } => None, DeployedObject::Key { .. } => None, } diff --git a/backend/windmill-worker/src/ai_executor.rs b/backend/windmill-worker/src/ai_executor.rs index f96ebb8308..9778884553 100644 --- a/backend/windmill-worker/src/ai_executor.rs +++ b/backend/windmill-worker/src/ai_executor.rs @@ -1011,7 +1011,10 @@ async fn run_agent( let output_value = match content { Some(content_str) => match has_output_properties { true => serde_json::from_str::>(&content_str).map_err(|_e| { - Error::internal_err(format!("Failed to parse structured output: {}", content_str)) + Error::internal_err(format!( + "Failed to parse structured output: {}", + content_str + )) })?, false => to_raw_value(&content_str), }, diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 3d72311db1..ed3af82a69 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -615,7 +615,7 @@ new Triggers( [ { type: 'webhook', path: '', isDraft: false }, - { type: 'email', path: '', isDraft: false }, + { type: 'default_email', path: '', isDraft: false }, ...(draftTriggersFromUrl ?? savedFlow?.draft?.draft_triggers ?? []) ], selectedTriggerIndexFromUrl, diff --git a/frontend/src/lib/components/Path.svelte b/frontend/src/lib/components/Path.svelte index 2d2abd8c18..5fdbc08a20 100644 --- a/frontend/src/lib/components/Path.svelte +++ b/frontend/src/lib/components/Path.svelte @@ -21,7 +21,8 @@ NatsTriggerService, MqttTriggerService, SqsTriggerService, - GcpTriggerService + GcpTriggerService, + EmailTriggerService } from '$lib/gen' import { superadmin, userStore, workspaceStore } from '$lib/stores' import { createEventDispatcher, getContext, untrack } from 'svelte' @@ -52,6 +53,7 @@ | 'mqtt_trigger' | 'sqs_trigger' | 'gcp_trigger' + | 'email_trigger' let meta: Meta | undefined = $state(undefined) interface Props { fullNamePlaceholder?: string | undefined @@ -290,6 +292,11 @@ workspace: $workspaceStore!, path: path }) + } else if (kind === 'email_trigger') { + return await EmailTriggerService.existsEmailTrigger({ + workspace: $workspaceStore!, + path: path + }) } else { return false } diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index dc5746d1e7..e206b4c357 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -251,7 +251,7 @@ new Triggers( [ { type: 'webhook', path: '', isDraft: false }, - { type: 'email', path: '', isDraft: false }, + { type: 'default_email', path: '', isDraft: false }, ...(script.draft_triggers ?? []) ], undefined, diff --git a/frontend/src/lib/components/ShareModal.svelte b/frontend/src/lib/components/ShareModal.svelte index e4dc7df614..68ed40b83c 100644 --- a/frontend/src/lib/components/ShareModal.svelte +++ b/frontend/src/lib/components/ShareModal.svelte @@ -33,6 +33,7 @@ | 'sqs_trigger' | 'postgres_trigger' | 'gcp_trigger' + | 'email_trigger' let kind: Kind let path: string = $state('') diff --git a/frontend/src/lib/components/details/DetailPageTriggerPanel.svelte b/frontend/src/lib/components/details/DetailPageTriggerPanel.svelte deleted file mode 100644 index 9c1b6b2392..0000000000 --- a/frontend/src/lib/components/details/DetailPageTriggerPanel.svelte +++ /dev/null @@ -1,184 +0,0 @@ - - - - -{#if !simplfiedPoll} -
- - - - - Webhooks - - - - - - Schedules - - - - - - HTTP - - - - - - WebSockets - - - - - - Postgres - - - - - - Event streams - - - - - - Email - - - - - - CLI - - - - {#snippet content()} -
- {#if triggerSelected === 'webhooks'} - {@render webhooks?.()} - {:else if triggerSelected === 'routes'} - {@render routes?.()} - {:else if triggerSelected === 'emails'} - {@render emails?.()} - {:else if triggerSelected === 'schedules'} - {@render schedules?.()} - {:else if triggerSelected === 'websockets'} - {@render websockets?.()} - {:else if triggerSelected === 'postgres'} - {@render postgres?.()} - {:else if triggerSelected === 'kafka' || triggerSelected === 'nats' || triggerSelected === 'sqs' || triggerSelected === 'mqtt' || triggerSelected === 'gcp'} -
- - {#snippet children({ item })} - - - - - - {/snippet} - -
- {#if eventStreamType === 'kafka'} - {@render kafka?.()} - {:else if eventStreamType === 'nats'} - {@render nats?.()} - {:else if eventStreamType === 'sqs'} - {@render sqs?.()} - {:else if eventStreamType === 'mqtt'} - {@render mqtt?.()} - {:else if eventStreamType === 'gcp'} - {@render gcp?.()} - {/if} - {:else if triggerSelected === 'cli'} - {@render cli?.()} - {/if} -
- {/snippet} -
-
-{:else} - {@render schedules?.()} -{/if} diff --git a/frontend/src/lib/components/details/EmailTriggerPanel.svelte b/frontend/src/lib/components/details/EmailTriggerPanel.svelte deleted file mode 100644 index a38aa4c035..0000000000 --- a/frontend/src/lib/components/details/EmailTriggerPanel.svelte +++ /dev/null @@ -1,84 +0,0 @@ - - - - - { - token = e.detail - triggerTokens?.listTokens() - }} - newTokenWorkspace={$workspaceStore} - newTokenLabel={`email-${$userStore?.username ?? 'superadmin'}-${generateRandomString(4)}`} - {scopes} -/> - -
- - Email triggers execute scripts and flows when emails are sent to specific addresses. Each - trigger has its own unique email address that can be used to invoke the script or flow. - - {#if loading} - - {:else} - {#if emailDomain} - - {:else} -
- - Ask an instance superadmin to setup the instance for email triggering (docs) and to set the email domain in the instance settings. - -
- {/if} - - {#if !$enterpriseLicense} - - Email triggers on Windmill Community Edition are limited to 100 emails per day. - - {/if} - - - {/if} -
diff --git a/frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte b/frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte index c45ec03615..ad0ace5868 100644 --- a/frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte +++ b/frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte @@ -56,6 +56,7 @@ websocket: { icon: Unplug, countKey: 'websocket_count' }, postgres: { icon: Database, countKey: 'postgres_count' }, kafka: { icon: KafkaIcon, countKey: 'kafka_count', disabled: !$enterpriseLicense }, + default_email: { icon: Mail, countKey: 'default_email_count' }, email: { icon: Mail, countKey: 'email_count' }, nats: { icon: NatsIcon, countKey: 'nats_count', disabled: !$enterpriseLicense }, mqtt: { icon: MqttIcon, countKey: 'mqtt_count', disabled: !$enterpriseLicense }, @@ -72,11 +73,12 @@ 'websocket', 'postgres', 'kafka', - 'email', + 'default_email', 'nats', 'mqtt', 'sqs', 'gcp', + 'email', 'poll', 'cli' ] diff --git a/frontend/src/lib/components/search/GlobalSearchModal.svelte b/frontend/src/lib/components/search/GlobalSearchModal.svelte index 11d7ee625f..2540e69784 100644 --- a/frontend/src/lib/components/search/GlobalSearchModal.svelte +++ b/frontend/src/lib/components/search/GlobalSearchModal.svelte @@ -21,6 +21,7 @@ DollarSignIcon, HomeIcon, LayoutDashboardIcon, + MailIcon, PlayIcon, Route, Search, @@ -144,6 +145,13 @@ action: (newtab: boolean = false) => gotoPage('/mqtt_triggers', newtab), icon: MqttIcon, disabled: $userStore?.operator + }, + { + search_id: 'nav:email_triggers', + label: 'Go to Email triggers', + action: (newtab: boolean = false) => gotoPage('/email_triggers', newtab), + icon: MailIcon, + disabled: $userStore?.operator } ] diff --git a/frontend/src/lib/components/sidebar/OperatorMenu.svelte b/frontend/src/lib/components/sidebar/OperatorMenu.svelte index 9566a6e831..f82ff975be 100644 --- a/frontend/src/lib/components/sidebar/OperatorMenu.svelte +++ b/frontend/src/lib/components/sidebar/OperatorMenu.svelte @@ -114,6 +114,11 @@ id: 'triggers', href: `${base}/mqtt_triggers` }, + { + label: 'Email triggers', + id: 'triggers', + href: `${base}/email_triggers` + }, { label: 'Audit logs', id: 'audit_logs', diff --git a/frontend/src/lib/components/sidebar/SidebarContent.svelte b/frontend/src/lib/components/sidebar/SidebarContent.svelte index a9709ae5df..866d5bd84d 100644 --- a/frontend/src/lib/components/sidebar/SidebarContent.svelte +++ b/frontend/src/lib/components/sidebar/SidebarContent.svelte @@ -34,7 +34,8 @@ Unplug, AlertCircle, Database, - Pyramid + Pyramid, + MailIcon } from 'lucide-svelte' import UserMenu from './UserMenu.svelte' import DiscordIcon from '../icons/brands/Discord.svelte' @@ -261,6 +262,15 @@ kind: 'mqtt', aiId: 'sidebar-menu-link-mqtt', aiDescription: 'Button to navigate to MQTT triggers' + }, + { + label: 'Email', + href: '/email_triggers', + icon: MailIcon, + disabled: $userStore?.operator, + kind: 'email', + aiId: 'sidebar-menu-link-email', + aiDescription: 'Button to navigate to Email triggers' } ]) let triggerMenuLinks = $derived([ diff --git a/frontend/src/lib/components/triggers.ts b/frontend/src/lib/components/triggers.ts index a9a817fe0d..eee4f37e8b 100644 --- a/frontend/src/lib/components/triggers.ts +++ b/frontend/src/lib/components/triggers.ts @@ -44,6 +44,7 @@ export function setScheduledPollSchedule( export type TriggerKind = | 'webhooks' | 'emails' + | 'default_emails' | 'schedules' | 'cli' | 'routes' @@ -61,6 +62,8 @@ export function captureTriggerKindToTriggerKind(kind: CaptureTriggerKind): Trigg return 'webhooks' case 'email': return 'emails' + case 'default_email': + return 'default_emails' case 'http': return 'routes' case 'websocket': diff --git a/frontend/src/lib/components/triggers/AddTriggersButton.svelte b/frontend/src/lib/components/triggers/AddTriggersButton.svelte index bf5abfa6d2..f10a54a99f 100644 --- a/frontend/src/lib/components/triggers/AddTriggersButton.svelte +++ b/frontend/src/lib/components/triggers/AddTriggersButton.svelte @@ -85,6 +85,12 @@ icon: triggerIconMap.gcp, extra: cloudHosted ? extra : undefined }, + { + displayName: 'Email', + action: () => onAddDraftTrigger?.('email'), + icon: triggerIconMap.email, + extra: cloudHosted ? extra : undefined + }, { displayName: 'Scheduled Poll', action: (e) => { diff --git a/frontend/src/lib/components/triggers/CaptureWrapper.svelte b/frontend/src/lib/components/triggers/CaptureWrapper.svelte index 4177c3d146..a6908c6957 100644 --- a/frontend/src/lib/components/triggers/CaptureWrapper.svelte +++ b/frontend/src/lib/components/triggers/CaptureWrapper.svelte @@ -7,7 +7,7 @@ import type { ConnectionInfo } from '../common/alert/ConnectionIndicator.svelte' import type { CaptureInfo } from './CaptureSection.svelte' import WebhooksCapture from './webhook/WebhooksCapture.svelte' - import EmailTriggerCaptures from '../details/EmailTriggerCaptures.svelte' + import DefaultEmailCapture from './email/DefaultEmailCapture.svelte' import WebsocketCapture from './websocket/WebsocketCapture.svelte' import PostgresCapture from './postgres/PostgresCapture.svelte' import KafkaCapture from './kafka/KafkaCapture.svelte' @@ -15,6 +15,7 @@ import MqttCapture from './mqtt/MqttCapture.svelte' import SqsCapture from './sqs/SqsCapture.svelte' import GcpCapture from './gcp/GcpCapture.svelte' + import EmailCapture from './email/EmailCapture.svelte' interface Props { isFlow: boolean @@ -238,8 +239,8 @@ on:captureToggle={handleCapture} on:testWithArgs /> - {:else if captureType === 'email'} - + {:else if captureType === 'email'} + {/if} {/key} diff --git a/frontend/src/lib/components/triggers/TriggerLabel.svelte b/frontend/src/lib/components/triggers/TriggerLabel.svelte index 5010b3fa38..f906d66886 100644 --- a/frontend/src/lib/components/triggers/TriggerLabel.svelte +++ b/frontend/src/lib/components/triggers/TriggerLabel.svelte @@ -18,6 +18,14 @@ {label} +{#if trigger.type === 'default_email'} + + Default + +{/if} + {#if trigger.isPrimary} x.label && x.label.startsWith(labelPrefix + '-')) if (labelPrefix == 'email') { - $triggersCount = { ...($triggersCount ?? {}), email_count: tokens?.length } + $triggersCount = { ...($triggersCount ?? {}), default_email_count: tokens?.length } } else { $triggersCount = { ...($triggersCount ?? {}), webhook_count: tokens?.length } } diff --git a/frontend/src/lib/components/triggers/TriggersEditor.svelte b/frontend/src/lib/components/triggers/TriggersEditor.svelte index c054bf9adb..746bcd5de9 100644 --- a/frontend/src/lib/components/triggers/TriggersEditor.svelte +++ b/frontend/src/lib/components/triggers/TriggersEditor.svelte @@ -29,7 +29,8 @@ MqttTriggerService, HttpTriggerService, GcpTriggerService, - SqsTriggerService + SqsTriggerService, + EmailTriggerService } from '$lib/gen' import { sendUserToast } from '$lib/toast' @@ -105,7 +106,8 @@ gcp: () => GcpTriggerService.deleteGcpTrigger, sqs: () => SqsTriggerService.deleteSqsTrigger, mqtt: () => MqttTriggerService.deleteMqttTrigger, - http: () => HttpTriggerService.deleteHttpTrigger + http: () => HttpTriggerService.deleteHttpTrigger, + email: () => EmailTriggerService.deleteEmailTrigger } const deleteHandler = deleteHandlers[triggerType as keyof typeof deleteHandlers] @@ -225,6 +227,14 @@ isFlow, $userStore ) + } else if (triggerType === 'email') { + await triggersState.fetchEmailTriggers( + triggersCount, + $workspaceStore, + currentPath, + isFlow, + $userStore + ) } triggersState.selectedTriggerIndex = triggersState.triggers.findIndex( @@ -285,7 +295,7 @@ onDeleteDraft={deleteTrigger} onReset={handleResetDraft} webhookToken={$triggersCount?.webhook_count} - emailToken={$triggersCount?.email_count} + emailToken={$triggersCount?.default_email_count} /> {:else} @@ -359,8 +369,8 @@ onReset={() => { handleResetDraft(triggersState.selectedTriggerIndex) }} - on:email-domain={({ detail }) => { - emailDomain = detail + onEmailDomain={(domain) => { + emailDomain = domain }} /> diff --git a/frontend/src/lib/components/triggers/TriggersTable.svelte b/frontend/src/lib/components/triggers/TriggersTable.svelte index d972de0209..7f9dd914bf 100644 --- a/frontend/src/lib/components/triggers/TriggersTable.svelte +++ b/frontend/src/lib/components/triggers/TriggersTable.svelte @@ -92,7 +92,7 @@ > {`${webhookToken} token${webhookToken > 1 ? 's' : ''}`} - {:else if trigger.type === 'email' && emailToken} + {:else if trigger.type === 'default_email' && emailToken} @@ -101,7 +101,7 @@ {/if} - {#if !['email', 'webhook', 'cli'].includes(trigger.type)} + {#if !['default_email', 'webhook', 'cli'].includes(trigger.type)} {#if trigger.isDraft} onDeleteDraft?.(index)} small /> {:else if !!trigger.draftConfig && !trigger.isDraft} diff --git a/frontend/src/lib/components/triggers/TriggersWrapper.svelte b/frontend/src/lib/components/triggers/TriggersWrapper.svelte index 5932885494..586186c6ee 100644 --- a/frontend/src/lib/components/triggers/TriggersWrapper.svelte +++ b/frontend/src/lib/components/triggers/TriggersWrapper.svelte @@ -1,7 +1,8 @@ @@ -71,14 +74,14 @@ scopes={isFlow ? [`jobs:run:flows:${currentPath}`] : [`jobs:run:scripts:${currentPath}`]} {newItem} /> -{:else if selectedTrigger.type === 'email'} - {:else if selectedTrigger.type === 'schedule'} +{:else if selectedTrigger.type === 'email'} + {:else if selectedTrigger.type === 'poll'} {:else if selectedTrigger.type === 'cli'} diff --git a/frontend/src/lib/components/details/EmailTriggerCaptures.svelte b/frontend/src/lib/components/triggers/email/DefaultEmailCapture.svelte similarity index 90% rename from frontend/src/lib/components/details/EmailTriggerCaptures.svelte rename to frontend/src/lib/components/triggers/email/DefaultEmailCapture.svelte index 80fa612f30..068df9d707 100644 --- a/frontend/src/lib/components/details/EmailTriggerCaptures.svelte +++ b/frontend/src/lib/components/triggers/email/DefaultEmailCapture.svelte @@ -2,8 +2,8 @@ import Label from '$lib/components/Label.svelte' import { workspaceStore } from '$lib/stores' import { base32 } from 'rfc4648' - import ClipboardPanel from './ClipboardPanel.svelte' - import CaptureSection, { type CaptureInfo } from '../triggers/CaptureSection.svelte' + import ClipboardPanel from '../../details/ClipboardPanel.svelte' + import CaptureSection, { type CaptureInfo } from '../CaptureSection.svelte' import { fade } from 'svelte/transition' interface Props { @@ -40,7 +40,7 @@ {#if captureInfo} - import { run } from 'svelte/legacy' - import Button from '$lib/components/common/button/Button.svelte' import Label from '$lib/components/Label.svelte' import Tooltip from '$lib/components/Tooltip.svelte' @@ -9,20 +7,15 @@ import { SCRIPT_VIEW_SHOW_CREATE_TOKEN_BUTTON } from '$lib/consts' import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte' import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte' - import ClipboardPanel from './ClipboardPanel.svelte' + import ClipboardPanel from '../../details/ClipboardPanel.svelte' import Alert from '$lib/components/common/alert/Alert.svelte' import { base32 } from 'rfc4648' import { emptyString } from '$lib/utils' + import UserSettings from '$lib/components/UserSettings.svelte' let requestType: 'hash' | 'path' = $state('path') - function emailAddress( - requestType: 'hash' | 'path', - path: string, - hash: string | undefined, - isFlow: boolean, - token: string - ) { + function emailAddress() { const pathOrHash = requestType === 'hash' ? hash : path.replaceAll('/', '.') const plainPrefix = `${$workspaceStore}+${ (requestType === 'hash' ? 'hash.' : isFlow ? 'flow.' : '') + pathOrHash @@ -40,7 +33,7 @@ isFlow?: boolean hash?: string | undefined path: string - userSettings: any + userSettings: UserSettings emailDomain?: string | null email?: string } @@ -55,8 +48,8 @@ email = $bindable('') }: Props = $props() - run(() => { - email = emailAddress(requestType, path, hash, isFlow, token) + $effect(() => { + email = emailAddress() }) diff --git a/frontend/src/lib/components/triggers/email/DefaultEmailPanel.svelte b/frontend/src/lib/components/triggers/email/DefaultEmailPanel.svelte new file mode 100644 index 0000000000..3fb95b1477 --- /dev/null +++ b/frontend/src/lib/components/triggers/email/DefaultEmailPanel.svelte @@ -0,0 +1,93 @@ + + + + + { + token = e.detail + triggerTokens?.listTokens() + }} + newTokenWorkspace={$workspaceStore} + newTokenLabel={`email-${$userStore?.username ?? 'superadmin'}-${generateRandomString(4)}`} + {scopes} +/> + +
+ + Default email trigger is a partially fixed email address that can be used to trigger a script or + flow. The email address is composed of the encoded workspace and script or flow path as well as + the token. + + {#if loading} + + {:else} + {#if emailDomain} + + {:else} +
+ + Ask an instance superadmin to setup the instance for email triggering (docs) and to set the email domain in the instance settings. + +
+ {/if} + + {#if !$enterpriseLicense} + + Email triggers on Windmill Community Edition are limited to 100 emails per day. + + {/if} + + + {/if} +
diff --git a/frontend/src/lib/components/triggers/email/EmailCapture.svelte b/frontend/src/lib/components/triggers/email/EmailCapture.svelte new file mode 100644 index 0000000000..828a32b0d0 --- /dev/null +++ b/frontend/src/lib/components/triggers/email/EmailCapture.svelte @@ -0,0 +1,62 @@ + + +{#if captureInfo} + + {#snippet description()} + {#if captureInfo.active} +

+ Send an email to the test address below to simulate an email trigger. +

+ {:else} +

+ Start capturing to listen to email events on this test address. +

+ {/if} + {/snippet} + +
+{/if} diff --git a/frontend/src/lib/components/triggers/email/EmailTriggerEditor.svelte b/frontend/src/lib/components/triggers/email/EmailTriggerEditor.svelte new file mode 100644 index 0000000000..77014d0dac --- /dev/null +++ b/frontend/src/lib/components/triggers/email/EmailTriggerEditor.svelte @@ -0,0 +1,35 @@ + + +{#if open} + +{/if} diff --git a/frontend/src/lib/components/triggers/email/EmailTriggerEditorConfigSection.svelte b/frontend/src/lib/components/triggers/email/EmailTriggerEditorConfigSection.svelte new file mode 100644 index 0000000000..4a8de62444 --- /dev/null +++ b/frontend/src/lib/components/triggers/email/EmailTriggerEditorConfigSection.svelte @@ -0,0 +1,166 @@ + + +
+
+ {#snippet header()} + {#if showTestingBadge} + + {/if} + {/snippet} + {#if !userCanEditConfig && isDraftOnly} + + Email triggers can only be edited by workspace admins + +
+ {/if} +
+ + +
+ + +
{dirtyLocalPart ? addressError : ''}
+ {#if !isCloudHosted()} +
+ { + workspaced_local_part = !workspaced_local_part + dirtyLocalPart = true + }} + options={{ + right: 'Prefix with workspace', + rightTooltip: + 'Prefixes the email address with the workspace ID (e.g., ${workspace_id}-${local_part}@). Note: deploying the email trigger to another workspace updates the email address workspace prefix accordingly.', + rightDocumentationLink: + 'https://www.windmill.dev/docs/advanced/email_triggers#workspace-prefix' + }} + /> +
+ {/if} +
+
+
+
diff --git a/frontend/src/lib/components/triggers/email/EmailTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/email/EmailTriggerEditorInner.svelte new file mode 100644 index 0000000000..62e1eecb92 --- /dev/null +++ b/frontend/src/lib/components/triggers/email/EmailTriggerEditorInner.svelte @@ -0,0 +1,384 @@ + + +{#snippet config()} + {#if drawerLoading} + {#if showLoader} + + {/if} + {:else} +
+
+
+ +
+
+ + {#if !hideTarget} +
+

+ Pick a script or flow to be triggered +

+
+
+ + + {#if emptyString(script_path)} + + {/if} +
+
+
+ {/if} + + + +
+
+
+ + Error Handler + Retries + +
+ +
+
+
+
+
+ {/if} +{/snippet} + +{#snippet saveButton()} + {#if !drawerLoading} + + {/if} +{/snippet} + +{#if useDrawer} + + drawer?.closeDrawer()} + > + {#snippet actions()} + {@render saveButton()} + {/snippet} + {@render config()} + + +{:else} +
+ {#snippet header()} + {#if customLabel} + {@render customLabel()} + {/if} + {/snippet} + {#snippet action()} + {@render saveButton()} + {/snippet} + {#if description} + {@render description()} + {/if} + {@render config()} +
+{/if} diff --git a/frontend/src/lib/components/triggers/email/EmailTriggerPanel.svelte b/frontend/src/lib/components/triggers/email/EmailTriggerPanel.svelte new file mode 100644 index 0000000000..12ec403aeb --- /dev/null +++ b/frontend/src/lib/components/triggers/email/EmailTriggerPanel.svelte @@ -0,0 +1,87 @@ + + + + {#snippet description()} +
+ + Email triggers execute scripts and flows when emails are sent to specific addresses. Each + trigger can be configured with a specific local part. + + + {#if !$userStore?.is_admin && !$userStore?.is_super_admin && selectedTrigger.isDraft} + + {/if} + + {#if !$enterpriseLicense} + + Email triggers on Windmill Community Edition are limited to 100 emails per day. + + {/if} +
+ {/snippet} +
diff --git a/frontend/src/lib/components/triggers/email/utils.ts b/frontend/src/lib/components/triggers/email/utils.ts new file mode 100644 index 0000000000..82de2a0a0d --- /dev/null +++ b/frontend/src/lib/components/triggers/email/utils.ts @@ -0,0 +1,68 @@ +import { isCloudHosted } from '$lib/cloud' +import { type NewEmailTrigger, EmailTriggerService, SettingService } from '$lib/gen' +import { sendUserToast } from '$lib/toast' +import type { Writable } from 'svelte/store' +import { get } from 'svelte/store' + +export function getEmailAddress( + local_part: string | undefined, + workspaced_local_part: boolean, + workspace_id: string, + emailDomain: string +) { + return `${isCloudHosted() || workspaced_local_part ? workspace_id + '-' : ''}${local_part ?? ''}@${emailDomain}` +} + +export async function saveEmailTriggerFromCfg( + initialPath: string, + routeCfg: Record, + edit: boolean, + workspace: string, + isAdmin: boolean, + usedTriggerKinds: Writable +): Promise { + const requestBody: NewEmailTrigger = { + path: routeCfg.path, + script_path: routeCfg.script_path, + local_part: routeCfg.local_part, + is_flow: routeCfg.is_flow, + workspaced_local_part: routeCfg.workspaced_local_part, + error_handler_path: routeCfg.error_handler_path, + error_handler_args: routeCfg.error_handler_path ? routeCfg.error_handler_args : undefined, + retry: routeCfg.retry + } + try { + if (edit) { + await EmailTriggerService.updateEmailTrigger({ + workspace: workspace, + path: initialPath, + requestBody: { + ...requestBody, + local_part: isAdmin || !edit ? routeCfg.local_part : undefined + } + }) + sendUserToast(`Route ${routeCfg.path} updated`) + } else { + await EmailTriggerService.createEmailTrigger({ + workspace: workspace, + requestBody: requestBody + }) + sendUserToast(`Route ${routeCfg.path} created`) + } + if (!get(usedTriggerKinds).includes('email')) { + usedTriggerKinds.update((t) => [...t, 'email']) + } + return true + } catch (error) { + sendUserToast(error.body || error.message, true) + return false + } +} + +export async function getEmailDomain(): Promise { + return ( + ((await SettingService.getGlobal({ + key: 'email_domain' + })) as any) ?? 'mail.test.com' + ) +} diff --git a/frontend/src/lib/components/triggers/http/RouteEditorConfigSection.svelte b/frontend/src/lib/components/triggers/http/RouteEditorConfigSection.svelte index 039e893121..a60d4b9534 100644 --- a/frontend/src/lib/components/triggers/http/RouteEditorConfigSection.svelte +++ b/frontend/src/lib/components/triggers/http/RouteEditorConfigSection.svelte @@ -166,7 +166,7 @@ { workspaced_route = !workspaced_route dirtyRoutePath = true diff --git a/frontend/src/lib/components/triggers/http/utils.ts b/frontend/src/lib/components/triggers/http/utils.ts index a41bdf2c31..7591ccc4bd 100644 --- a/frontend/src/lib/components/triggers/http/utils.ts +++ b/frontend/src/lib/components/triggers/http/utils.ts @@ -60,7 +60,7 @@ export async function saveHttpRouteFromCfg( summary: routeCfg.summary, error_handler_path: routeCfg.error_handler_path, error_handler_args: routeCfg.error_handler_path ? routeCfg.error_handler_args : undefined, - retry: routeCfg.retry, + retry: routeCfg.retry } try { if (edit) { diff --git a/frontend/src/lib/components/triggers/triggers.svelte.ts b/frontend/src/lib/components/triggers/triggers.svelte.ts index 916d4202fe..9025237643 100644 --- a/frontend/src/lib/components/triggers/triggers.svelte.ts +++ b/frontend/src/lib/components/triggers/triggers.svelte.ts @@ -13,7 +13,9 @@ import { type TriggersCount, type HttpTrigger, HttpTriggerService, - GcpTriggerService + GcpTriggerService, + type EmailTrigger, + EmailTriggerService } from '$lib/gen' import { getLightConfig, sortTriggers, updateTriggersCount, type Trigger } from './utils' import type { Writable } from 'svelte/store' @@ -430,6 +432,32 @@ export class Triggers { } } + async fetchEmailTriggers( + triggersCountStore: Writable, + workspaceId: string | undefined, + path: string, + isFlow: boolean, + user: UserExt | undefined = undefined + ): Promise { + if (!workspaceId) return + try { + const emailTriggers: EmailTrigger[] = await EmailTriggerService.listEmailTriggers({ + workspace: workspaceId, + path, + isFlow + }) + const emailCount = this.updateTriggers(emailTriggers, 'email', user) + triggersCountStore.update((triggersCount) => { + return { + ...(triggersCount ?? {}), + email_count: emailCount + } + }) + } catch (error) { + console.error('Failed to fetch email triggers:', error) + } + } + async fetchTriggers( triggersCountStore: Writable, workspaceId: string | undefined, @@ -450,7 +478,8 @@ export class Triggers { this.fetchNatsTriggers(triggersCountStore, workspaceId, path, isFlow, user), this.fetchMqttTriggers(triggersCountStore, workspaceId, path, isFlow, user), this.fetchSqsTriggers(triggersCountStore, workspaceId, path, isFlow, user), - this.fetchGcpTriggers(triggersCountStore, workspaceId, path, isFlow, user) + this.fetchGcpTriggers(triggersCountStore, workspaceId, path, isFlow, user), + this.fetchEmailTriggers(triggersCountStore, workspaceId, path, isFlow, user) ]) } } diff --git a/frontend/src/lib/components/triggers/utils.ts b/frontend/src/lib/components/triggers/utils.ts index 3c6780921b..23d37413e1 100644 --- a/frontend/src/lib/components/triggers/utils.ts +++ b/frontend/src/lib/components/triggers/utils.ts @@ -25,6 +25,7 @@ import { saveMqttTriggerFromCfg } from './mqtt/utils' import { saveGcpTriggerFromCfg } from './gcp/utils' import type { Triggers } from './triggers.svelte' import { emptyString } from '$lib/utils' +import { saveEmailTriggerFromCfg } from './email/utils' export const CLOUD_DISABLED_TRIGGER_TYPES = [ 'nats', @@ -38,6 +39,7 @@ export const CLOUD_DISABLED_TRIGGER_TYPES = [ export type TriggerType = | 'webhook' + | 'default_email' | 'email' | 'schedule' | 'http' @@ -48,6 +50,7 @@ export type TriggerType = | 'mqtt' | 'sqs' | 'gcp' + | 'email' | 'poll' | 'cli' @@ -68,6 +71,7 @@ export type Trigger = { export const triggerIconMap = { webhook: Webhook, email: Mail, + default_email: Mail, schedule: Calendar, http: Route, websocket: Unplug, @@ -92,6 +96,7 @@ export function triggerTypeToCaptureKind(triggerType: TriggerType): CaptureTrigg const capturableTriggerTypes: TriggerType[] = [ 'webhook', 'email', + 'default_email', 'http', 'websocket', 'postgres', @@ -120,7 +125,7 @@ export function updateTriggersCount( // Map trigger types to their corresponding count property names const countPropertyMap: Record = { webhook: undefined, - email: undefined, + default_email: undefined, schedule: 'schedule_count', http: 'http_routes_count', websocket: 'websocket_count', @@ -130,6 +135,7 @@ export function updateTriggersCount( mqtt: 'mqtt_count', sqs: 'sqs_count', gcp: 'gcp_count', + email: 'email_count', poll: undefined, cli: undefined } @@ -178,6 +184,8 @@ export function triggerKindToTriggerType(kind: TriggerKind): TriggerType | undef return 'webhook' case 'emails': return 'email' + case 'default_emails': + return 'default_email' case 'schedules': return 'schedule' case 'routes': @@ -225,7 +233,7 @@ export async function deployTriggers( // Map of trigger types to their save functions const triggerSaveFunctions: Record = { webhook: undefined, - email: undefined, + default_email: undefined, schedule: (trigger: Trigger) => { if (trigger.isPrimary && initialPath) { trigger.draftConfig = { @@ -301,6 +309,15 @@ export async function deployTriggers( workspaceId, usedTriggerKinds ), + email: (trigger: Trigger) => + saveEmailTriggerFromCfg( + trigger.path ?? trigger.draftConfig?.path ?? '', + trigger.draftConfig ?? {}, + !trigger.isDraft, + workspaceId, + isAdmin, + usedTriggerKinds + ), poll: undefined, cli: undefined } @@ -398,6 +415,8 @@ export function getLightConfig( return { queue_url: trigger.queue_url } } else if (triggerType === 'gcp') { return { gcp_resource_path: trigger.gcp_resource_path, topic: trigger.topic } + } else if (triggerType === 'email') { + return { local_part: trigger.local_part } } else { return undefined } @@ -409,7 +428,7 @@ export function getTriggerLabel(trigger: Trigger): string { if (type === 'webhook') { return 'Webhook' - } else if (type === 'email') { + } else if (type === 'default_email') { return 'Email' } else if (type === 'cli') { return 'CLI' @@ -430,6 +449,8 @@ export function getTriggerLabel(trigger: Trigger): string { return `${config?.gcp_resource_path} - ${config?.topic}` } else if (type === 'websocket' && config?.url) { return `${config?.url}` + } else if (type === 'email' && config?.local_part) { + return `${config?.local_part}` } else if (isDraft && draftConfig?.path) { return `${draftConfig?.path}` } else if (isDraft) { @@ -443,7 +464,7 @@ export function sortTriggers(triggers: Trigger[]): Trigger[] { const triggerTypeOrder = [ 'webhook', 'cli', - 'email', + 'default_email', 'poll', 'schedule', 'http', @@ -453,7 +474,8 @@ export function sortTriggers(triggers: Trigger[]): Trigger[] { 'nats', 'mqtt', 'sqs', - 'gcp' + 'gcp', + 'email' ] return triggers.sort((a, b) => { diff --git a/frontend/src/routes/(root)/(logged)/+layout.svelte b/frontend/src/routes/(root)/(logged)/+layout.svelte index a4fa5abc59..a1b925fdb5 100644 --- a/frontend/src/routes/(root)/(logged)/+layout.svelte +++ b/frontend/src/routes/(root)/(logged)/+layout.svelte @@ -201,7 +201,8 @@ nats_used, sqs_used, mqtt_used, - gcp_used + gcp_used, + email_used } = await WorkspaceService.getUsedTriggers({ workspace: $workspaceStore ?? '' }) @@ -229,6 +230,9 @@ if (gcp_used) { usedKinds.push('gcp') } + if (email_used) { + usedKinds.push('email') + } $usedTriggerKinds = usedKinds } diff --git a/frontend/src/routes/(root)/(logged)/email_triggers/+page.js b/frontend/src/routes/(root)/(logged)/email_triggers/+page.js new file mode 100644 index 0000000000..4995d68d95 --- /dev/null +++ b/frontend/src/routes/(root)/(logged)/email_triggers/+page.js @@ -0,0 +1,5 @@ +export function load() { + return { + stuff: { title: 'Email triggers' } + } +} diff --git a/frontend/src/routes/(root)/(logged)/email_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/email_triggers/+page.svelte new file mode 100644 index 0000000000..0336a36fd0 --- /dev/null +++ b/frontend/src/routes/(root)/(logged)/email_triggers/+page.svelte @@ -0,0 +1,434 @@ + + + + + + (x.summary ?? '') + ' ' + x.path + ' (' + x.script_path + ')'} +/> + +{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.triggers} + +{:else} + + + {#if $userStore?.is_admin || $userStore?.is_super_admin} + + {/if} + +
+
+ +
+
Filter by path of
+ + {#snippet children({ item })} + + + {/snippet} + +
+ + +
+ {#if $userStore?.is_super_admin && $userStore.username.includes('@')} + + {:else if $userStore?.is_admin || $userStore?.is_super_admin} + + {/if} +
+
+ {#if loading} + {#each new Array(6) as _} + + {/each} + {:else if !triggers?.length} +
No email triggers
+ {:else if items?.length} +
+ {#each items.slice(0, nbDisplayed) as { workspace_id, workspaced_local_part, path, edited_by, edited_at, script_path, is_flow, extra_perms, canWrite, marked, local_part } (path)} + {@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`} + {@const emailAddress = getEmailAddress( + local_part, + workspaced_local_part ?? false, + workspace_id, + emailDomain ?? '' + )} + +
+
+ + + emailTriggerEditor?.openEdit(path, is_flow)} + class="min-w-0 grow hover:underline decoration-gray-400" + > +
+ {#if marked} + + {@html marked} + + {:else} + {emailAddress} + {/if} +
+
+ {path} +
+
+ runnable: {script_path} +
+
+ + + +
+ + + { + goto(href) + } + }, + { + displayName: 'Delete', + type: 'delete', + icon: Trash, + disabled: + !canWrite || !($userStore?.is_admin || $userStore?.is_super_admin), + action: async () => { + try { + await EmailTriggerService.deleteEmailTrigger({ + workspace: $workspaceStore ?? '', + path + }) + sendUserToast(`Successfully deleted Email trigger: ${path}`) + loadTriggers() + } catch (error) { + sendUserToast(error.body || error.message, true) + } + } + }, + { + displayName: canWrite ? 'Edit' : 'View', + icon: canWrite ? Pen : Eye, + action: () => { + emailTriggerEditor?.openEdit(path, is_flow) + } + }, + ...(isDeployable('trigger', path, deployUiSettings) + ? [ + { + displayName: 'Deploy to prod/staging', + icon: FileUp, + action: () => { + deploymentDrawer?.openDrawer(path, 'trigger', { + triggers: { + kind: 'routes' + } + }) + } + } + ] + : []), + { + displayName: 'Audit logs', + icon: Eye, + href: `${base}/audit_logs?resource=${path}` + }, + { + displayName: canWrite ? 'Share' : 'See Permissions', + icon: Share, + action: () => { + shareModal?.openDrawer(path, 'email_trigger') + } + } + ]} + /> +
+
+
+
+
edited by {edited_by}
+
at {displayDate(edited_at)}
+
+
+
+ {/each} +
+ {:else} + + {/if} +
+ {#if items && items?.length > 15 && nbDisplayed < items.length} + {nbDisplayed} items out of {items.length} + + {/if} +
+{/if} + + { + loadTriggers() + }} +/> diff --git a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte index 167770b964..786c26c58a 100644 --- a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte @@ -83,7 +83,7 @@ const triggersState = $state( new Triggers([ { type: 'webhook', path: '', isDraft: false }, - { type: 'email', path: '', isDraft: false }, + { type: 'default_email', path: '', isDraft: false }, { type: 'cli', path: '', isDraft: false } ]) ) @@ -504,7 +504,7 @@ {#if flow?.archived} This flow was archived {/if} - +
{#if !emptyString(flow?.description)} diff --git a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte index 6b58497faf..7a212a9f87 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte @@ -106,7 +106,7 @@ const triggersState = $state( new Triggers([ { type: 'webhook', path: '', isDraft: false }, - { type: 'email', path: '', isDraft: false }, + { type: 'default_email', path: '', isDraft: false }, { type: 'cli', path: '', isDraft: false } ]) )