diff --git a/.github/workflows/build-publish-rh-image.yml b/.github/workflows/build-publish-rh-image.yml index fd878a4e7acaa..26b4bd68969b7 100644 --- a/.github/workflows/build-publish-rh-image.yml +++ b/.github/workflows/build-publish-rh-image.yml @@ -64,7 +64,7 @@ jobs: platforms: linux/amd64 push: true build-args: | - features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,php,mysql,mssql,bigquery,oracledb,postgres_trigger,websocket,python,smtp,csharp,static_frontend,rust + features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,php,mysql,mssql,bigquery,oracledb,postgres_trigger,mqtt_trigger,websocket,python,smtp,csharp,static_frontend,rust secrets: | rh_username=${{ secrets.RH_USERNAME }} rh_password=${{ secrets.RH_PASSWORD }} @@ -81,7 +81,7 @@ jobs: platforms: linux/arm64 push: true build-args: | - features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,php,mysql,mssql,bigquery,oracledb,postgres_trigger,websocket,python,smtp,csharp,static_frontend,rust + features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,php,mysql,mssql,bigquery,oracledb,postgres_trigger,mqtt_trigger,websocket,python,smtp,csharp,static_frontend,rust secrets: | rh_username=${{ secrets.RH_USERNAME }} rh_password=${{ secrets.RH_PASSWORD }} diff --git a/.github/workflows/build_windows_worker_.yml b/.github/workflows/build_windows_worker_.yml index 8657bfca6bb5b..541dbea00979c 100644 --- a/.github/workflows/build_windows_worker_.yml +++ b/.github/workflows/build_windows_worker_.yml @@ -45,7 +45,7 @@ jobs: $env:OPENSSL_DIR="${Env:VCPKG_INSTALLATION_ROOT}\installed\x64-windows-static" mkdir frontend/build && cd backend New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force - cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,nats,sqs_trigger,php,mysql,mssql,bigquery,oracledb,postgres_trigger,websocket,python,smtp,csharp,static_frontend,rust + cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,nats,sqs_trigger,php,mysql,mssql,bigquery,oracledb,postgres_trigger,mqtt_trigger,websocket,python,smtp,csharp,static_frontend,rust - name: Rename binary with corresponding architecture run: | diff --git a/.github/workflows/docker-image-rpi4.yml b/.github/workflows/docker-image-rpi4.yml index c4507238354d7..c0fc7ad576c45 100644 --- a/.github/workflows/docker-image-rpi4.yml +++ b/.github/workflows/docker-image-rpi4.yml @@ -67,7 +67,7 @@ jobs: platforms: linux/amd64,linux/arm64 push: true build-args: | - features=embedding,parquet,openidconnect,deno_core,license,http_trigger,zip,oauth2,php,mysql,mssql,bigquery,oracledb,postgres_trigger,websocket,python,smtp,csharp,static_frontend,rust + features=embedding,parquet,openidconnect,deno_core,license,http_trigger,zip,oauth2,php,mysql,mssql,bigquery,oracledb,postgres_trigger,mqtt_trigger,websocket,python,smtp,csharp,static_frontend,rust tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev ${{ steps.meta-public.outputs.tags }} diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 32ea5d614f748..9d2c0584756b8 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -95,7 +95,7 @@ jobs: platforms: linux/amd64,linux/arm64 push: true build-args: | - features=embedding,parquet,openidconnect,jemalloc,deno_core,license,http_trigger,zip,oauth2,dind,php,mysql,mssql,bigquery,oracledb,postgres_trigger,websocket,python,smtp,csharp,static_frontend,rust + features=embedding,parquet,openidconnect,jemalloc,deno_core,license,http_trigger,zip,oauth2,dind,php,mysql,mssql,bigquery,oracledb,postgres_trigger,mqtt_trigger,websocket,python,smtp,csharp,static_frontend,rust tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEV_SHA }} ${{ steps.meta-public.outputs.tags }} @@ -158,7 +158,7 @@ jobs: platforms: linux/amd64,linux/arm64 push: true build-args: | - features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,otel,dind,php,mysql,mssql,bigquery,oracledb,postgres_trigger,websocket,python,smtp,csharp,static_frontend,rust + features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,otel,dind,php,mysql,mssql,bigquery,oracledb,postgres_trigger,mqtt_trigger,websocket,python,smtp,csharp,static_frontend,rust tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:${{ env.DEV_SHA }} ${{ steps.meta-ee-public.outputs.tags }} diff --git a/.github/workflows/publish_windows_worker.yml b/.github/workflows/publish_windows_worker.yml index b4b03a99e2203..5a5c46f5f9d69 100644 --- a/.github/workflows/publish_windows_worker.yml +++ b/.github/workflows/publish_windows_worker.yml @@ -47,7 +47,7 @@ jobs: $env:OPENSSL_DIR="${Env:VCPKG_INSTALLATION_ROOT}\installed\x64-windows-static" mkdir frontend/build && cd backend New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force - cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,php,mysql,mssql,bigquery,oracledb,postgres_trigger,websocket,python,smtp,csharp,static_frontend,rust + cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,php,mysql,mssql,bigquery,oracledb,postgres_trigger,mqtt_trigger,websocket,python,smtp,csharp,static_frontend,rust - name: Rename binary with corresponding architecture run: | diff --git a/backend/.sqlx/query-07da723ce5c9ee2d7c236e8eabe254c783fc34b617c8a9a95a0eb0cda535dab5.json b/backend/.sqlx/query-07da723ce5c9ee2d7c236e8eabe254c783fc34b617c8a9a95a0eb0cda535dab5.json index 6eacfceb627e8..b905255754700 100644 --- a/backend/.sqlx/query-07da723ce5c9ee2d7c236e8eabe254c783fc34b617c8a9a95a0eb0cda535dab5.json +++ b/backend/.sqlx/query-07da723ce5c9ee2d7c236e8eabe254c783fc34b617c8a9a95a0eb0cda535dab5.json @@ -20,7 +20,8 @@ "email", "nats", "postgres", - "sqs" + "sqs", + "mqtt" ] } } diff --git a/backend/.sqlx/query-115544a96173f9cb1d27757e7b931fb27912cfd05ba768a42cf9b3dfd7205e9a.json b/backend/.sqlx/query-115544a96173f9cb1d27757e7b931fb27912cfd05ba768a42cf9b3dfd7205e9a.json new file mode 100644 index 0000000000000..66a2152ae9254 --- /dev/null +++ b/backend/.sqlx/query-115544a96173f9cb1d27757e7b931fb27912cfd05ba768a42cf9b3dfd7205e9a.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE \n mqtt_trigger \n SET \n server_id = $1, \n last_server_ping = now(),\n error = 'Connecting...'\n WHERE \n enabled IS TRUE \n AND workspace_id = $2 \n AND path = $3 \n AND (last_server_ping IS NULL \n OR last_server_ping < now() - INTERVAL '15 seconds'\n ) \n RETURNING true\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "115544a96173f9cb1d27757e7b931fb27912cfd05ba768a42cf9b3dfd7205e9a" +} diff --git a/backend/.sqlx/query-186aef850c2eeb89c186ac6b2934dd3a703e2b9428096801e1d2d61fdbb99c9e.json b/backend/.sqlx/query-186aef850c2eeb89c186ac6b2934dd3a703e2b9428096801e1d2d61fdbb99c9e.json new file mode 100644 index 0000000000000..648c5dda19fa3 --- /dev/null +++ b/backend/.sqlx/query-186aef850c2eeb89c186ac6b2934dd3a703e2b9428096801e1d2d61fdbb99c9e.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE \n mqtt_trigger \n SET\n last_server_ping = NULL \n WHERE \n workspace_id = $1 \n AND path = $2 \n AND server_id IS NULL", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "186aef850c2eeb89c186ac6b2934dd3a703e2b9428096801e1d2d61fdbb99c9e" +} diff --git a/backend/.sqlx/query-31b6fccad46b22bcbba6bbce22209ccb1825116004ed72682854ce3352f454a5.json b/backend/.sqlx/query-31b6fccad46b22bcbba6bbce22209ccb1825116004ed72682854ce3352f454a5.json new file mode 100644 index 0000000000000..0871986a4e571 --- /dev/null +++ b/backend/.sqlx/query-31b6fccad46b22bcbba6bbce22209ccb1825116004ed72682854ce3352f454a5.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT \n \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 ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "websocket_used!", + "type_info": "Bool" + }, + { + "ordinal": 1, + "name": "http_routes_used!", + "type_info": "Bool" + }, + { + "ordinal": 2, + "name": "kafka_used!", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "nats_used!", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "postgres_used!", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "mqtt_used!", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "sqs_used!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null, + null, + null, + null, + null, + null, + null + ] + }, + "hash": "31b6fccad46b22bcbba6bbce22209ccb1825116004ed72682854ce3352f454a5" +} diff --git a/backend/.sqlx/query-3e33469f448fa86c7e0d0deb65ee2c13b5dcc4cbc4fcb0fe2bde1fdcb6d20e74.json b/backend/.sqlx/query-3e33469f448fa86c7e0d0deb65ee2c13b5dcc4cbc4fcb0fe2bde1fdcb6d20e74.json new file mode 100644 index 0000000000000..113f8885453ca --- /dev/null +++ b/backend/.sqlx/query-3e33469f448fa86c7e0d0deb65ee2c13b5dcc4cbc4fcb0fe2bde1fdcb6d20e74.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE \n capture_config \n SET \n last_server_ping = NULL \n WHERE \n workspace_id = $1 AND \n path = $2 AND \n is_flow = $3 AND \n trigger_kind = 'mqtt' AND \n server_id IS NULL\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "3e33469f448fa86c7e0d0deb65ee2c13b5dcc4cbc4fcb0fe2bde1fdcb6d20e74" +} diff --git a/backend/.sqlx/query-4844a797ddec207f1d2cbf41836e65d6435840f6d0740f300d7c2cf88f8fe46a.json b/backend/.sqlx/query-4844a797ddec207f1d2cbf41836e65d6435840f6d0740f300d7c2cf88f8fe46a.json new file mode 100644 index 0000000000000..3f66c4c9b4609 --- /dev/null +++ b/backend/.sqlx/query-4844a797ddec207f1d2cbf41836e65d6435840f6d0740f300d7c2cf88f8fe46a.json @@ -0,0 +1,25 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE \n mqtt_trigger\n SET \n last_server_ping = now(),\n error = $1\n WHERE\n workspace_id = $2\n AND path = $3\n AND server_id = $4 \n AND enabled IS TRUE\n RETURNING 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "4844a797ddec207f1d2cbf41836e65d6435840f6d0740f300d7c2cf88f8fe46a" +} diff --git a/backend/.sqlx/query-4ed74bbda2ad0ca5e4648787fe2d9c89e9b83571e30059861b6998935b35f78b.json b/backend/.sqlx/query-4ed74bbda2ad0ca5e4648787fe2d9c89e9b83571e30059861b6998935b35f78b.json new file mode 100644 index 0000000000000..e5b87450d3617 --- /dev/null +++ b/backend/.sqlx/query-4ed74bbda2ad0ca5e4648787fe2d9c89e9b83571e30059861b6998935b35f78b.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT EXISTS(\n SELECT \n 1 \n FROM \n mqtt_trigger \n WHERE \n path = $1 AND \n workspace_id = $2\n )", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "4ed74bbda2ad0ca5e4648787fe2d9c89e9b83571e30059861b6998935b35f78b" +} diff --git a/backend/.sqlx/query-548a1424a6b9ac9998b8fc5312bcff671ee7fe59154e9ef32800c117e64bce19.json b/backend/.sqlx/query-548a1424a6b9ac9998b8fc5312bcff671ee7fe59154e9ef32800c117e64bce19.json new file mode 100644 index 0000000000000..554b0fff0b904 --- /dev/null +++ b/backend/.sqlx/query-548a1424a6b9ac9998b8fc5312bcff671ee7fe59154e9ef32800c117e64bce19.json @@ -0,0 +1,50 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n path,\n is_flow,\n workspace_id,\n owner,\n email,\n trigger_config as \"trigger_config!: _\"\n FROM\n capture_config\n WHERE\n trigger_kind = 'mqtt' AND\n last_client_ping > NOW() - INTERVAL '10 seconds' AND\n trigger_config IS NOT NULL AND\n (last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds')\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "path", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "is_flow", + "type_info": "Bool" + }, + { + "ordinal": 2, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "owner", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "trigger_config!: _", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + true + ] + }, + "hash": "548a1424a6b9ac9998b8fc5312bcff671ee7fe59154e9ef32800c117e64bce19" +} diff --git a/backend/.sqlx/query-5c1de8473e0e96c1063a9a735a064c5a91e3ed8d9260c72b783fc12542b88fbd.json b/backend/.sqlx/query-5c1de8473e0e96c1063a9a735a064c5a91e3ed8d9260c72b783fc12542b88fbd.json index 0184cadf7428a..61c4d20b04a9d 100644 --- a/backend/.sqlx/query-5c1de8473e0e96c1063a9a735a064c5a91e3ed8d9260c72b783fc12542b88fbd.json +++ b/backend/.sqlx/query-5c1de8473e0e96c1063a9a735a064c5a91e3ed8d9260c72b783fc12542b88fbd.json @@ -28,7 +28,8 @@ "email", "nats", "postgres", - "sqs" + "sqs", + "mqtt" ] } } @@ -62,7 +63,8 @@ "email", "nats", "postgres", - "sqs" + "sqs", + "mqtt" ] } } diff --git a/backend/.sqlx/query-5d773db0a69c73e86ccd5fd978932e7ac64eb41a56d8ca9ff0a89c594432b531.json b/backend/.sqlx/query-5d773db0a69c73e86ccd5fd978932e7ac64eb41a56d8ca9ff0a89c594432b531.json new file mode 100644 index 0000000000000..7dee8c000358a --- /dev/null +++ b/backend/.sqlx/query-5d773db0a69c73e86ccd5fd978932e7ac64eb41a56d8ca9ff0a89c594432b531.json @@ -0,0 +1,36 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE \n mqtt_trigger \n SET\n mqtt_resource_path = $1,\n subscribe_topics = $2,\n client_version = $3,\n client_id = $4,\n v3_config = $5,\n v5_config = $6,\n is_flow = $7, \n edited_by = $8, \n email = $9,\n script_path = $10,\n path = $11,\n edited_at = now(), \n error = NULL,\n server_id = NULL\n WHERE \n workspace_id = $12 AND \n path = $13\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "JsonbArray", + { + "Custom": { + "name": "mqtt_client_version", + "kind": { + "Enum": [ + "v3", + "v5" + ] + } + } + }, + "Varchar", + "Jsonb", + "Jsonb", + "Bool", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "5d773db0a69c73e86ccd5fd978932e7ac64eb41a56d8ca9ff0a89c594432b531" +} diff --git a/backend/.sqlx/query-62475252dcf54f32433b97ae011daf5d4205d160d2aedf463c7dfe944e93257a.json b/backend/.sqlx/query-62475252dcf54f32433b97ae011daf5d4205d160d2aedf463c7dfe944e93257a.json index 9e86b3dbf4cbb..d2eab003f2018 100644 --- a/backend/.sqlx/query-62475252dcf54f32433b97ae011daf5d4205d160d2aedf463c7dfe944e93257a.json +++ b/backend/.sqlx/query-62475252dcf54f32433b97ae011daf5d4205d160d2aedf463c7dfe944e93257a.json @@ -20,7 +20,8 @@ "email", "nats", "postgres", - "sqs" + "sqs", + "mqtt" ] } } diff --git a/backend/.sqlx/query-65cd5a5a12eb79a2f21ed898616c5cd5c5e671ce1ed3d89639cd306c76a1d9db.json b/backend/.sqlx/query-65cd5a5a12eb79a2f21ed898616c5cd5c5e671ce1ed3d89639cd306c76a1d9db.json new file mode 100644 index 0000000000000..6d0ec8bd68e96 --- /dev/null +++ b/backend/.sqlx/query-65cd5a5a12eb79a2f21ed898616c5cd5c5e671ce1ed3d89639cd306c76a1d9db.json @@ -0,0 +1,135 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n mqtt_resource_path,\n subscribe_topics as \"subscribe_topics!: Vec>\",\n v3_config as \"v3_config!: Option>\",\n v5_config as \"v5_config!: Option>\",\n client_version AS \"client_version: _\",\n client_id,\n workspace_id,\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM \n mqtt_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mqtt_resource_path", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "subscribe_topics!: Vec>", + "type_info": "JsonbArray" + }, + { + "ordinal": 2, + "name": "v3_config!: Option>", + "type_info": "Jsonb" + }, + { + "ordinal": 3, + "name": "v5_config!: Option>", + "type_info": "Jsonb" + }, + { + "ordinal": 4, + "name": "client_version: _", + "type_info": { + "Custom": { + "name": "mqtt_client_version", + "kind": { + "Enum": [ + "v3", + "v5" + ] + } + } + } + }, + { + "ordinal": 5, + "name": "client_id", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "path", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "script_path", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "is_flow", + "type_info": "Bool" + }, + { + "ordinal": 10, + "name": "edited_by", + "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "edited_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 13, + "name": "server_id", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "last_server_ping", + "type_info": "Timestamptz" + }, + { + "ordinal": 15, + "name": "extra_perms", + "type_info": "Jsonb" + }, + { + "ordinal": 16, + "name": "error", + "type_info": "Text" + }, + { + "ordinal": 17, + "name": "enabled", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + true, + true, + false, + true, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + true, + false + ] + }, + "hash": "65cd5a5a12eb79a2f21ed898616c5cd5c5e671ce1ed3d89639cd306c76a1d9db" +} diff --git a/backend/.sqlx/query-71d51bbc35da7b9930e3ea3a634451217ccb9f1bc35b1ad6e10d16bc19c41447.json b/backend/.sqlx/query-71d51bbc35da7b9930e3ea3a634451217ccb9f1bc35b1ad6e10d16bc19c41447.json index 4b5e3a41fd52d..643e0854bb680 100644 --- a/backend/.sqlx/query-71d51bbc35da7b9930e3ea3a634451217ccb9f1bc35b1ad6e10d16bc19c41447.json +++ b/backend/.sqlx/query-71d51bbc35da7b9930e3ea3a634451217ccb9f1bc35b1ad6e10d16bc19c41447.json @@ -31,7 +31,8 @@ "email", "nats", "postgres", - "sqs" + "sqs", + "mqtt" ] } } diff --git a/backend/.sqlx/query-7b6e8dac5f83fcbae95056c4206bf4de1b12d6c8aab32f1f55d7f2c230c5c08e.json b/backend/.sqlx/query-7b6e8dac5f83fcbae95056c4206bf4de1b12d6c8aab32f1f55d7f2c230c5c08e.json new file mode 100644 index 0000000000000..a30054b9dc4f1 --- /dev/null +++ b/backend/.sqlx/query-7b6e8dac5f83fcbae95056c4206bf4de1b12d6c8aab32f1f55d7f2c230c5c08e.json @@ -0,0 +1,132 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n mqtt_resource_path,\n subscribe_topics as \"subscribe_topics!: Vec>\",\n v3_config as \"v3_config!: Option>\",\n v5_config as \"v5_config!: Option>\",\n client_version as \"client_version: _\",\n client_id,\n workspace_id,\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM\n mqtt_trigger\n WHERE\n enabled IS TRUE\n AND (last_server_ping IS NULL OR\n last_server_ping < now() - interval '15 seconds'\n )\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mqtt_resource_path", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "subscribe_topics!: Vec>", + "type_info": "JsonbArray" + }, + { + "ordinal": 2, + "name": "v3_config!: Option>", + "type_info": "Jsonb" + }, + { + "ordinal": 3, + "name": "v5_config!: Option>", + "type_info": "Jsonb" + }, + { + "ordinal": 4, + "name": "client_version: _", + "type_info": { + "Custom": { + "name": "mqtt_client_version", + "kind": { + "Enum": [ + "v3", + "v5" + ] + } + } + } + }, + { + "ordinal": 5, + "name": "client_id", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "path", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "script_path", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "is_flow", + "type_info": "Bool" + }, + { + "ordinal": 10, + "name": "edited_by", + "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "edited_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 13, + "name": "server_id", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "last_server_ping", + "type_info": "Timestamptz" + }, + { + "ordinal": 15, + "name": "extra_perms", + "type_info": "Jsonb" + }, + { + "ordinal": 16, + "name": "error", + "type_info": "Text" + }, + { + "ordinal": 17, + "name": "enabled", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + true, + true, + false, + true, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + true, + false + ] + }, + "hash": "7b6e8dac5f83fcbae95056c4206bf4de1b12d6c8aab32f1f55d7f2c230c5c08e" +} diff --git a/backend/.sqlx/query-8d31b4a531c59a2385210d1213c205100d6673a94e90000c8db4eb5809f17365.json b/backend/.sqlx/query-8d31b4a531c59a2385210d1213c205100d6673a94e90000c8db4eb5809f17365.json new file mode 100644 index 0000000000000..0d30c8ca0ac82 --- /dev/null +++ b/backend/.sqlx/query-8d31b4a531c59a2385210d1213c205100d6673a94e90000c8db4eb5809f17365.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE \n mqtt_trigger \n SET \n enabled = $1, \n email = $2, \n edited_by = $3, \n edited_at = now(), \n server_id = NULL, \n error = NULL\n WHERE \n path = $4 AND \n workspace_id = $5 \n RETURNING 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Bool", + "Varchar", + "Varchar", + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "8d31b4a531c59a2385210d1213c205100d6673a94e90000c8db4eb5809f17365" +} diff --git a/backend/.sqlx/query-9e88a43de7315052668619002321aadbc27ff5bd0c554bf5339060cc35797d3f.json b/backend/.sqlx/query-9e88a43de7315052668619002321aadbc27ff5bd0c554bf5339060cc35797d3f.json new file mode 100644 index 0000000000000..bb2a3c682b95f --- /dev/null +++ b/backend/.sqlx/query-9e88a43de7315052668619002321aadbc27ff5bd0c554bf5339060cc35797d3f.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE \n FROM \n mqtt_trigger \n WHERE \n workspace_id = $1 AND \n path = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "9e88a43de7315052668619002321aadbc27ff5bd0c554bf5339060cc35797d3f" +} diff --git a/backend/.sqlx/query-c223f8b7fa4ef1aa06e1ba2a56d677774aa237508d5610714efd2e9b8b93c7b8.json b/backend/.sqlx/query-c223f8b7fa4ef1aa06e1ba2a56d677774aa237508d5610714efd2e9b8b93c7b8.json index a2d04fed036c9..45ce8d3f95329 100644 --- a/backend/.sqlx/query-c223f8b7fa4ef1aa06e1ba2a56d677774aa237508d5610714efd2e9b8b93c7b8.json +++ b/backend/.sqlx/query-c223f8b7fa4ef1aa06e1ba2a56d677774aa237508d5610714efd2e9b8b93c7b8.json @@ -23,7 +23,8 @@ "email", "nats", "postgres", - "sqs" + "sqs", + "mqtt" ] } } diff --git a/backend/.sqlx/query-c5063e79aafa70b974276f5bea43ad71135d44b2e84c9efac43a6678f0cf9a18.json b/backend/.sqlx/query-c5063e79aafa70b974276f5bea43ad71135d44b2e84c9efac43a6678f0cf9a18.json new file mode 100644 index 0000000000000..9ab57a7e3a1e0 --- /dev/null +++ b/backend/.sqlx/query-c5063e79aafa70b974276f5bea43ad71135d44b2e84c9efac43a6678f0cf9a18.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE \n mqtt_trigger \n SET \n enabled = FALSE, \n error = $1, \n server_id = NULL, \n last_server_ping = NULL \n WHERE \n workspace_id = $2 AND \n path = $3\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "c5063e79aafa70b974276f5bea43ad71135d44b2e84c9efac43a6678f0cf9a18" +} diff --git a/backend/.sqlx/query-c5270ee815689e42b65df507b850da43239c9a5aaea41c9aed7ed33a6219a534.json b/backend/.sqlx/query-c5270ee815689e42b65df507b850da43239c9a5aaea41c9aed7ed33a6219a534.json index e29d94f8f50dc..8c2a88d485f3c 100644 --- a/backend/.sqlx/query-c5270ee815689e42b65df507b850da43239c9a5aaea41c9aed7ed33a6219a534.json +++ b/backend/.sqlx/query-c5270ee815689e42b65df507b850da43239c9a5aaea41c9aed7ed33a6219a534.json @@ -20,7 +20,8 @@ "email", "nats", "postgres", - "sqs" + "sqs", + "mqtt" ] } } diff --git a/backend/.sqlx/query-cff764601318bfecb8e0f15b64ff9f430d7b1efe8ba863cbcc4a49bab4951794.json b/backend/.sqlx/query-cff764601318bfecb8e0f15b64ff9f430d7b1efe8ba863cbcc4a49bab4951794.json new file mode 100644 index 0000000000000..a9e508d01def3 --- /dev/null +++ b/backend/.sqlx/query-cff764601318bfecb8e0f15b64ff9f430d7b1efe8ba863cbcc4a49bab4951794.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE \n capture_config \n SET \n last_server_ping = now(), \n error = $1 \n WHERE \n workspace_id = $2 AND \n path = $3 AND \n is_flow = $4 AND \n trigger_kind = 'mqtt' AND \n server_id = $5 AND \n last_client_ping > NOW() - INTERVAL '10 seconds' \n RETURNING 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Bool", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "cff764601318bfecb8e0f15b64ff9f430d7b1efe8ba863cbcc4a49bab4951794" +} diff --git a/backend/.sqlx/query-ddf2eccb78a310ed00c7d8b9c3f05d394a7cbcf0038c72a78add5c7b02ef5927.json b/backend/.sqlx/query-ddf2eccb78a310ed00c7d8b9c3f05d394a7cbcf0038c72a78add5c7b02ef5927.json index c2dfed73a2a83..5bfff47576491 100644 --- a/backend/.sqlx/query-ddf2eccb78a310ed00c7d8b9c3f05d394a7cbcf0038c72a78add5c7b02ef5927.json +++ b/backend/.sqlx/query-ddf2eccb78a310ed00c7d8b9c3f05d394a7cbcf0038c72a78add5c7b02ef5927.json @@ -15,7 +15,7 @@ ] }, "nullable": [ - null + true ] }, "hash": "ddf2eccb78a310ed00c7d8b9c3f05d394a7cbcf0038c72a78add5c7b02ef5927" diff --git a/backend/.sqlx/query-e17ec84003e2ec414622d100f5dfdda86bee33f31835317df512a20c805b35d7.json b/backend/.sqlx/query-e17ec84003e2ec414622d100f5dfdda86bee33f31835317df512a20c805b35d7.json index 66578654a3c1f..60332810b2915 100644 --- a/backend/.sqlx/query-e17ec84003e2ec414622d100f5dfdda86bee33f31835317df512a20c805b35d7.json +++ b/backend/.sqlx/query-e17ec84003e2ec414622d100f5dfdda86bee33f31835317df512a20c805b35d7.json @@ -28,7 +28,8 @@ "email", "nats", "postgres", - "sqs" + "sqs", + "mqtt" ] } } diff --git a/backend/.sqlx/query-e23e110e1f0438d21534fc4323e0e7bc1f0dbeca2e4f44ced05bae0ca5ca1039.json b/backend/.sqlx/query-e23e110e1f0438d21534fc4323e0e7bc1f0dbeca2e4f44ced05bae0ca5ca1039.json index 8d3bf8c9278bb..7b30e079f00fc 100644 --- a/backend/.sqlx/query-e23e110e1f0438d21534fc4323e0e7bc1f0dbeca2e4f44ced05bae0ca5ca1039.json +++ b/backend/.sqlx/query-e23e110e1f0438d21534fc4323e0e7bc1f0dbeca2e4f44ced05bae0ca5ca1039.json @@ -36,7 +36,8 @@ "email", "nats", "postgres", - "sqs" + "sqs", + "mqtt" ] } } diff --git a/backend/.sqlx/query-f050be8da0d99aa1af64f5d32a8df01c5a59b036b2669878cc557537efb492c8.json b/backend/.sqlx/query-f050be8da0d99aa1af64f5d32a8df01c5a59b036b2669878cc557537efb492c8.json new file mode 100644 index 0000000000000..7334c70fb4c3f --- /dev/null +++ b/backend/.sqlx/query-f050be8da0d99aa1af64f5d32a8df01c5a59b036b2669878cc557537efb492c8.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE \n capture_config \n SET \n error = $1, \n server_id = NULL, \n last_server_ping = NULL \n WHERE \n workspace_id = $2 AND \n path = $3 AND \n is_flow = $4 AND \n trigger_kind = 'mqtt'\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "f050be8da0d99aa1af64f5d32a8df01c5a59b036b2669878cc557537efb492c8" +} diff --git a/backend/.sqlx/query-f82974abc7da71fac397f754fe3b3c9c84b6b438b2d9f8cbba61192e999971f4.json b/backend/.sqlx/query-f82974abc7da71fac397f754fe3b3c9c84b6b438b2d9f8cbba61192e999971f4.json new file mode 100644 index 0000000000000..db7c69c456f16 --- /dev/null +++ b/backend/.sqlx/query-f82974abc7da71fac397f754fe3b3c9c84b6b438b2d9f8cbba61192e999971f4.json @@ -0,0 +1,36 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO mqtt_trigger (\n mqtt_resource_path,\n subscribe_topics,\n client_version,\n client_id,\n v3_config,\n v5_config,\n workspace_id,\n path, \n script_path, \n is_flow, \n email, \n enabled, \n edited_by\n ) \n VALUES (\n $1, \n $2, \n $3, \n $4, \n $5, \n $6, \n $7,\n $8,\n $9,\n $10,\n $11,\n $12,\n $13\n )", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "JsonbArray", + { + "Custom": { + "name": "mqtt_client_version", + "kind": { + "Enum": [ + "v3", + "v5" + ] + } + } + }, + "Varchar", + "Jsonb", + "Jsonb", + "Varchar", + "Varchar", + "Varchar", + "Bool", + "Varchar", + "Bool", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "f82974abc7da71fac397f754fe3b3c9c84b6b438b2d9f8cbba61192e999971f4" +} diff --git a/backend/.sqlx/query-fc932ecf697b921fe9143c36872e9cc4e9aca7f9129b0466aacae51334a3db96.json b/backend/.sqlx/query-fc932ecf697b921fe9143c36872e9cc4e9aca7f9129b0466aacae51334a3db96.json new file mode 100644 index 0000000000000..091d205b40ab3 --- /dev/null +++ b/backend/.sqlx/query-fc932ecf697b921fe9143c36872e9cc4e9aca7f9129b0466aacae51334a3db96.json @@ -0,0 +1,25 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE \n capture_config \n SET \n server_id = $1,\n last_server_ping = now(), \n error = 'Connecting...' \n WHERE \n last_client_ping > NOW() - INTERVAL '10 seconds' AND \n workspace_id = $2 AND \n path = $3 AND \n is_flow = $4 AND \n trigger_kind = 'mqtt' AND \n (last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds') \n RETURNING true\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Text", + "Text", + "Bool" + ] + }, + "nullable": [ + null + ] + }, + "hash": "fc932ecf697b921fe9143c36872e9cc4e9aca7f9129b0466aacae51334a3db96" +} diff --git a/backend/.vscode/settings.json b/backend/.vscode/settings.json index 6ebd36cf07c86..57c5e47f50bd9 100644 --- a/backend/.vscode/settings.json +++ b/backend/.vscode/settings.json @@ -11,5 +11,6 @@ "remote.autoForwardPorts": true, "conventionalCommits.scopes": [ "restructring triggers, decoding trigger message on work" - ] + ], + "rust-analyzer.cargo.features": ["mqtt_trigger"] } diff --git a/backend/Cargo.lock b/backend/Cargo.lock index b41f866244fb8..022b1418449fe 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -9710,6 +9710,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rumqttc" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1568e15fab2d546f940ed3a21f48bbbd1c494c90c99c4481339364a497f94a9" +dependencies = [ + "bytes", + "flume", + "futures-util", + "log", + "native-tls", + "rustls-native-certs 0.7.3", + "rustls-pemfile 2.2.0", + "rustls-webpki 0.102.8", + "thiserror 1.0.69", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.25.0", +] + [[package]] name = "rusqlite" version = "0.32.1" @@ -9869,6 +9889,20 @@ dependencies = [ "sct", ] +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring 0.17.10", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + [[package]] name = "rustls" version = "0.23.23" @@ -12354,6 +12388,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.1" @@ -13789,11 +13834,13 @@ dependencies = [ "regex", "reqwest 0.12.12", "rsa", + "rumqttc", "rust-embed", "rust_decimal", "samael", "serde", "serde_json", + "serde_repr", "serde_urlencoded", "sha2 0.10.8", "sql-builder", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 357972e39ac65..1f25da17cde9e 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -74,6 +74,7 @@ mssql = ["windmill-worker/mssql"] bigquery = ["windmill-worker/bigquery"] websocket = ["windmill-api/websocket"] postgres_trigger = ["windmill-api/postgres_trigger"] +mqtt_trigger = ["windmill-api/mqtt_trigger"] sqs_trigger = ["windmill-api/sqs_trigger"] python = ["windmill-worker/python"] smtp = ["windmill-api/smtp", "windmill-common/smtp"] @@ -170,6 +171,7 @@ tower-http = { version = "^0.6", features = ["trace", "cors"] } tower-cookies = "^0.10" serde = "^1" serde_json = { version = "^1", features = ["preserve_order", "raw_value"] } +serde_repr = "0.1.19" uuid = { version = "^1", features = ["serde", "v4"] } thiserror = "^2" anyhow = "^1" @@ -355,3 +357,4 @@ tokio-tungstenite = { version = "0.24.0", features = ["native-tls"] } tree-sitter = {version = "0.23.0", features = []} tree-sitter-c-sharp = "0.23.0" oracle = { version = "0.6.3", features = ["chrono"] } +rumqttc = { version = "0.24.0", features = ["use-native-tls"]} \ No newline at end of file diff --git a/backend/migrations/20250207172929_mqtt_trigger.down.sql b/backend/migrations/20250207172929_mqtt_trigger.down.sql new file mode 100644 index 0000000000000..5b2fba771a267 --- /dev/null +++ b/backend/migrations/20250207172929_mqtt_trigger.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here +DROP TABLE mqtt_trigger; +DROP TYPE MQTT_CLIENT_VERSION; \ No newline at end of file diff --git a/backend/migrations/20250207172929_mqtt_trigger.up.sql b/backend/migrations/20250207172929_mqtt_trigger.up.sql new file mode 100644 index 0000000000000..feebd3602c843 --- /dev/null +++ b/backend/migrations/20250207172929_mqtt_trigger.up.sql @@ -0,0 +1,72 @@ + -- Add up migration script here +CREATE TYPE MQTT_CLIENT_VERSION AS ENUM ('v3', 'v5'); + +CREATE TABLE mqtt_trigger ( + mqtt_resource_path VARCHAR(255) NOT NULL, + subscribe_topics JSONB[] NOT NULL, + client_version MQTT_CLIENT_VERSION DEFAULT 'v5' NOT NULL, + v5_config JSONB NULL, + v3_config JSONB NULL, + client_id VARCHAR(65535) DEFAULT NULL, + path VARCHAR(255) 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 '{}', + server_id VARCHAR(50) NULL, + last_server_ping TIMESTAMPTZ NULL, + error TEXT NULL, + enabled BOOLEAN NOT NULL, + PRIMARY KEY (path, workspace_id) +); + +GRANT ALL ON mqtt_trigger TO windmill_user; +GRANT ALL ON mqtt_trigger TO windmill_admin; + +ALTER TABLE mqtt_trigger ENABLE ROW LEVEL SECURITY; + +CREATE POLICY admin_policy ON mqtt_trigger FOR ALL TO windmill_admin USING (true); + +CREATE POLICY see_folder_extra_perms_user_select ON mqtt_trigger FOR SELECT TO windmill_user +USING (SPLIT_PART(mqtt_trigger.path, '/', 1) = 'f' AND SPLIT_PART(mqtt_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[])); +CREATE POLICY see_folder_extra_perms_user_insert ON mqtt_trigger FOR INSERT TO windmill_user +WITH CHECK (SPLIT_PART(mqtt_trigger.path, '/', 1) = 'f' AND SPLIT_PART(mqtt_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[])); +CREATE POLICY see_folder_extra_perms_user_update ON mqtt_trigger FOR UPDATE TO windmill_user +USING (SPLIT_PART(mqtt_trigger.path, '/', 1) = 'f' AND SPLIT_PART(mqtt_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[])); +CREATE POLICY see_folder_extra_perms_user_delete ON mqtt_trigger FOR DELETE TO windmill_user +USING (SPLIT_PART(mqtt_trigger.path, '/', 1) = 'f' AND SPLIT_PART(mqtt_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[])); + +CREATE POLICY see_own ON mqtt_trigger FOR ALL TO windmill_user +USING (SPLIT_PART(mqtt_trigger.path, '/', 1) = 'u' AND SPLIT_PART(mqtt_trigger.path, '/', 2) = current_setting('session.user')); +CREATE POLICY see_member ON mqtt_trigger FOR ALL TO windmill_user +USING (SPLIT_PART(mqtt_trigger.path, '/', 1) = 'g' AND SPLIT_PART(mqtt_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[])); + +CREATE POLICY see_extra_perms_user_select ON mqtt_trigger FOR SELECT TO windmill_user +USING (extra_perms ? CONCAT('u/', current_setting('session.user'))); +CREATE POLICY see_extra_perms_user_insert ON mqtt_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 mqtt_trigger FOR UPDATE TO windmill_user +USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean); +CREATE POLICY see_extra_perms_user_delete ON mqtt_trigger FOR DELETE TO windmill_user +USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean); + +CREATE POLICY see_extra_perms_groups_select ON mqtt_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 mqtt_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 mqtt_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 mqtt_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)); \ No newline at end of file diff --git a/backend/migrations/20250207173304_add_mqtt_type_value_to_trigger_kind_type.down.sql b/backend/migrations/20250207173304_add_mqtt_type_value_to_trigger_kind_type.down.sql new file mode 100644 index 0000000000000..0197d4e7b12ae --- /dev/null +++ b/backend/migrations/20250207173304_add_mqtt_type_value_to_trigger_kind_type.down.sql @@ -0,0 +1 @@ +-- Add down migration script here \ No newline at end of file diff --git a/backend/migrations/20250207173304_add_mqtt_type_value_to_trigger_kind_type.up.sql b/backend/migrations/20250207173304_add_mqtt_type_value_to_trigger_kind_type.up.sql new file mode 100644 index 0000000000000..62d397bc94f4d --- /dev/null +++ b/backend/migrations/20250207173304_add_mqtt_type_value_to_trigger_kind_type.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +ALTER TYPE TRIGGER_KIND ADD VALUE IF NOT EXISTS 'mqtt'; \ No newline at end of file diff --git a/backend/windmill-api/Cargo.toml b/backend/windmill-api/Cargo.toml index d5f8f6f420709..f86cb10bccb93 100644 --- a/backend/windmill-api/Cargo.toml +++ b/backend/windmill-api/Cargo.toml @@ -29,6 +29,7 @@ oauth2 = ["dep:async-oauth2"] http_trigger = ["dep:matchit"] static_frontend = ["dep:rust-embed"] postgres_trigger = ["dep:rust-postgres", "dep:pg_escape", "dep:byteorder", "dep:thiserror", "dep:rust_decimal", "dep:rust-postgres-native-tls"] +mqtt_trigger = ["dep:thiserror", "dep:rumqttc", "dep:serde_repr"] sqs_trigger = ["dep:aws-sdk-sqs", "dep:thiserror", "dep:aws-config"] [dependencies] @@ -120,5 +121,7 @@ byteorder = { workspace = true, optional = true } thiserror = { workspace = true, optional = true } rust_decimal = { workspace = true, optional = true } rust-postgres-native-tls = { workspace = true, optional = true} +rumqttc = { workspace = true, optional = true } +serde_repr = { workspace = true, optional = true } aws-sdk-sqs = { workspace = true, optional = true } -aws-config = { workspace = true, optional = true} \ No newline at end of file +aws-config = { workspace = true, optional = true} diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 5aaec60cde9fb..6c6597053e1db 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -2616,6 +2616,8 @@ paths: type: boolean postgres_used: type: boolean + mqtt_used: + type: boolean sqs_used: type: boolean required: @@ -2624,6 +2626,7 @@ paths: - kafka_used - nats_used - postgres_used + - mqtt_used - sqs_used /w/{workspace}/users/list: get: @@ -8782,6 +8785,195 @@ paths: schema: type: string + /w/{workspace}/mqtt_triggers/create: + post: + summary: create mqtt trigger + operationId: createMqttTrigger + tags: + - mqtt_trigger + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: new mqtt trigger + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NewMqttTrigger" + responses: + "201": + description: mqtt trigger created + content: + text/plain: + schema: + type: string + + /w/{workspace}/mqtt_triggers/update/{path}: + post: + summary: update mqtt trigger + operationId: updateMqttTrigger + tags: + - mqtt_trigger + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + requestBody: + description: updated trigger + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EditMqttTrigger" + responses: + "200": + description: mqtt trigger updated + content: + text/plain: + schema: + type: string + + /w/{workspace}/mqtt_triggers/delete/{path}: + delete: + summary: delete mqtt trigger + operationId: deleteMqttTrigger + tags: + - mqtt_trigger + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: mqtt trigger deleted + content: + text/plain: + schema: + type: string + + /w/{workspace}/mqtt_triggers/get/{path}: + get: + summary: get mqtt trigger + operationId: getMqttTrigger + tags: + - mqtt_trigger + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: mqtt trigger deleted + content: + application/json: + schema: + $ref: "#/components/schemas/MqttTrigger" + + /w/{workspace}/mqtt_triggers/list: + get: + summary: list mqtt triggers + operationId: listMqttTriggers + tags: + - mqtt_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: mqtt trigger list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/MqttTrigger" + + /w/{workspace}/mqtt_triggers/exists/{path}: + get: + summary: does mqtt trigger exists + operationId: existsMqttTrigger + tags: + - mqtt_trigger + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: mqtt trigger exists + content: + application/json: + schema: + type: boolean + + /w/{workspace}/mqtt_triggers/setenabled/{path}: + post: + summary: set enabled mqtt trigger + operationId: setMqttTriggerEnabled + tags: + - mqtt_trigger + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + requestBody: + description: updated mqtt trigger enable + required: true + content: + application/json: + schema: + type: object + properties: + enabled: + type: boolean + required: + - enabled + responses: + "200": + description: mqtt trigger enabled set + content: + text/plain: + schema: + type: string + + /w/{workspace}/mqtt_triggers/test: + post: + summary: test mqtt connection + operationId: testMqttConnection + tags: + - mqtt_trigger + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: test mqtt connection + required: true + content: + application/json: + schema: + type: object + properties: + connection: + type: object + required: + - connection + responses: + "200": + description: successfuly connected to mqtt + content: + text/plain: + schema: + type: string + /w/{workspace}/postgres_triggers/is_valid_postgres_configuration/{path}: get: @@ -10070,6 +10262,7 @@ paths: kafka_trigger, nats_trigger, postgres_trigger, + mqtt_trigger, sqs_trigger ] responses: @@ -10112,6 +10305,7 @@ paths: kafka_trigger, nats_trigger, postgres_trigger, + mqtt_trigger, sqs_trigger ] requestBody: @@ -10165,6 +10359,7 @@ paths: kafka_trigger, nats_trigger, postgres_trigger, + mqtt_trigger, sqs_trigger ] requestBody: @@ -13574,6 +13769,8 @@ components: type: number nats_count: type: number + mqtt_count: + type: number sqs_count: type: number @@ -13723,6 +13920,136 @@ components: - is_flow required: - runnable_result + + QoS: + type: number + enum: [0, 1, 2] + + MqttV3Config: + type: object + properties: + clean_session: + type: boolean + + MqttV5Config: + type: object + properties: + clean_start: + type: boolean + + SubscribeTopic: + type: object + properties: + qos: + $ref: "#/components/schemas/QoS" + topic: + type: string + required: + - qos + - topic + + MqttClientVersion: + type: string + enum: ["v3", "v5"] + + MqttTrigger: + allOf: + - $ref: "#/components/schemas/TriggerExtraProperty" + type: object + properties: + mqtt_resource_path: + type: string + subscribe_topics: + type: array + items: + $ref: "#/components/schemas/SubscribeTopic" + v3_config: + $ref: "#/components/schemas/MqttV3Config" + v5_config: + $ref: "#/components/schemas/MqttV5Config" + client_id: + type: string + client_version: + $ref: "#/components/schemas/MqttClientVersion" + server_id: + type: string + last_server_ping: + type: string + format: date-time + error: + type: string + enabled: + type: boolean + + required: + - enabled + - subscribe_topics + - mqtt_resource_path + + NewMqttTrigger: + type: object + properties: + mqtt_resource_path: + type: string + subscribe_topics: + type: array + items: + $ref: "#/components/schemas/SubscribeTopic" + client_id: + type: string + v3_config: + $ref: "#/components/schemas/MqttV3Config" + v5_config: + $ref: "#/components/schemas/MqttV5Config" + client_version: + $ref: "#/components/schemas/MqttClientVersion" + path: + type: string + script_path: + type: string + is_flow: + type: boolean + enabled: + type: boolean + required: + - path + - script_path + - is_flow + - subscribe_topics + - mqtt_resource_path + + EditMqttTrigger: + type: object + properties: + mqtt_resource_path: + type: string + subscribe_topics: + type: array + items: + $ref: "#/components/schemas/SubscribeTopic" + client_id: + type: string + v3_config: + $ref: "#/components/schemas/MqttV3Config" + v5_config: + $ref: "#/components/schemas/MqttV5Config" + client_version: + $ref: "#/components/schemas/MqttClientVersion" + path: + type: string + script_path: + type: string + is_flow: + type: boolean + enabled: + type: boolean + required: + - path + - script_path + - is_flow + - enabled + - subscribe_topics + - mqtt_resource_path SqsTrigger: allOf: @@ -15099,7 +15426,7 @@ components: CaptureTriggerKind: type: string - enum: [webhook, http, websocket, kafka, email, nats, postgres, sqs] + enum: [webhook, http, websocket, kafka, email, nats, postgres, sqs, mqtt] Capture: type: object @@ -15218,4 +15545,4 @@ components: service_url: type: string description: The service URL for the channel - example: "https://smba.trafficmanager.net/amer/12345678-1234-1234-1234-123456789012/" + example: "https://smba.trafficmanager.net/amer/12345678-1234-1234-1234-123456789012/" \ No newline at end of file diff --git a/backend/windmill-api/src/capture.rs b/backend/windmill-api/src/capture.rs index 7606822d656ac..4f70a9837f86a 100644 --- a/backend/windmill-api/src/capture.rs +++ b/backend/windmill-api/src/capture.rs @@ -6,36 +6,12 @@ * LICENSE-AGPL for a copy of the license. */ -use axum::{ - extract::{Extension, Path, Query}, - routing::{delete, get, head, post}, - Json, Router, -}; -#[cfg(feature = "http_trigger")] -use http::HeaderMap; -use hyper::StatusCode; -#[cfg(feature = "http_trigger")] -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use serde_json::value::RawValue; -use sqlx::types::Json as SqlxJson; -#[cfg(feature = "http_trigger")] -use std::collections::HashMap; -use std::fmt; -#[cfg(feature = "http_trigger")] -use windmill_common::error::Error; -use windmill_common::{ - db::UserDB, - error::{JsonResult, Result}, - utils::{not_found_if_none, paginate, Pagination, StripPath}, - worker::{to_raw_value, CLOUD_HOSTED}, -}; -use windmill_queue::{PushArgs, PushArgsOwned}; - #[cfg(feature = "http_trigger")] use crate::http_triggers::{build_http_trigger_extra, HttpMethod}; #[cfg(all(feature = "enterprise", feature = "kafka"))] use crate::kafka_triggers_ee::KafkaTriggerConfigConnection; +#[cfg(feature = "mqtt_trigger")] +use crate::mqtt_triggers::{MqttClientVersion, MqttV3Config, MqttV5Config, SubscribeTopic}; #[cfg(all(feature = "enterprise", feature = "nats"))] use crate::nats_triggers_ee::NatsTriggerConfigConnection; #[cfg(feature = "postgres_trigger")] @@ -43,16 +19,41 @@ use crate::postgres_triggers::{ create_logical_replication_slot_query, create_publication_query, drop_publication_query, generate_random_string, get_database_connection, PublicationData, }; +#[cfg(feature = "http_trigger")] +use http::HeaderMap; #[cfg(feature = "postgres_trigger")] use itertools::Itertools; #[cfg(feature = "postgres_trigger")] use pg_escape::quote_literal; +#[cfg(feature = "http_trigger")] +use serde::de::DeserializeOwned; +#[cfg(feature = "http_trigger")] +use std::collections::HashMap; +#[cfg(feature = "http_trigger")] +use windmill_common::error::Error; use crate::{ args::WebhookArgs, db::{ApiAuthed, DB}, users::fetch_api_authed, }; +use axum::{ + extract::{Extension, Path, Query}, + routing::{delete, get, head, post}, + Json, Router, +}; +use hyper::StatusCode; +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use sqlx::types::Json as SqlxJson; +use std::fmt; +use windmill_common::{ + db::UserDB, + error::{JsonResult, Result}, + utils::{not_found_if_none, paginate, Pagination, StripPath}, + worker::{to_raw_value, CLOUD_HOSTED}, +}; +use windmill_queue::{PushArgs, PushArgsOwned}; const KEEP_LAST: i64 = 20; @@ -98,6 +99,7 @@ pub enum TriggerKind { Kafka, Email, Nats, + Mqtt, Sqs, Postgres, } @@ -111,6 +113,7 @@ impl fmt::Display for TriggerKind { TriggerKind::Kafka => "kafka", TriggerKind::Email => "email", TriggerKind::Nats => "nats", + TriggerKind::Mqtt => "mqtt", TriggerKind::Sqs => "sqs", TriggerKind::Postgres => "postgres", }; @@ -155,6 +158,16 @@ pub struct NatsTriggerConfig { pub use_jetstream: bool, } +#[cfg(feature = "mqtt_trigger")] +#[derive(Debug, Serialize, Deserialize)] +pub struct MqttTriggerConfig { + pub mqtt_resource_path: String, + pub subscribe_topics: Vec, + pub v3_config: Option, + pub v5_config: Option, + pub client_version: Option, + pub client_id: Option, +} #[cfg(feature = "postgres_trigger")] #[derive(Serialize, Deserialize, Debug)] pub struct PostgresTriggerConfig { @@ -187,6 +200,8 @@ enum TriggerConfig { Kafka(KafkaTriggerConfig), #[cfg(all(feature = "enterprise", feature = "nats"))] Nats(NatsTriggerConfig), + #[cfg(feature = "mqtt_trigger")] + Mqtt(MqttTriggerConfig), } #[derive(Serialize, Deserialize)] @@ -300,8 +315,7 @@ async fn set_config( #[cfg(feature = "postgres_trigger")] let nc = if let TriggerKind::Postgres = nc.trigger_kind { set_postgres_trigger_config(&w_id, authed.clone(), &db, user_db.clone(), nc).await? - } - else { + } else { nc }; diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index 434fe208cc031..843a409906777 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -89,6 +89,8 @@ pub mod job_metrics; pub mod jobs; #[cfg(all(feature = "enterprise", feature = "kafka"))] mod kafka_triggers_ee; +#[cfg(feature = "mqtt_trigger")] +mod mqtt_triggers; #[cfg(all(feature = "enterprise", feature = "nats"))] mod nats_triggers_ee; #[cfg(feature = "oauth2")] @@ -321,6 +323,18 @@ pub async fn run_server( } }; + let mqtt_triggers_service = { + #[cfg(all(feature = "mqtt_trigger"))] + { + mqtt_triggers::workspaced_service() + } + + #[cfg(not(feature = "mqtt_trigger"))] + { + Router::new() + } + }; + let sqs_triggers_service = { #[cfg(all(feature = "enterprise", feature = "sqs_trigger"))] { @@ -387,6 +401,12 @@ pub async fn run_server( let db_killpill_rx = rx.resubscribe(); postgres_triggers::start_database(db.clone(), db_killpill_rx); } + + #[cfg(feature = "mqtt_trigger")] + { + let mqtt_killpill_rx = rx.resubscribe(); + mqtt_triggers::start_mqtt_consumer(db.clone(), mqtt_killpill_rx); + } #[cfg(all(feature = "enterprise", feature = "sqs_trigger"))] { @@ -447,6 +467,7 @@ pub async fn run_server( .nest("/websocket_triggers", websocket_triggers_service) .nest("/kafka_triggers", kafka_triggers_service) .nest("/nats_triggers", nats_triggers_service) + .nest("/mqtt_triggers", mqtt_triggers_service) .nest("/sqs_triggers", sqs_triggers_service) .nest("/postgres_triggers", postgres_triggers_service), ) @@ -579,7 +600,6 @@ pub async fn run_server( .on_failure(MyOnFailure {}), ) }; - let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); let port = listener.local_addr().map(|x| x.port()).unwrap_or(8000); let ip = listener diff --git a/backend/windmill-api/src/mqtt_triggers.rs b/backend/windmill-api/src/mqtt_triggers.rs new file mode 100644 index 0000000000000..36aa64dd5230b --- /dev/null +++ b/backend/windmill-api/src/mqtt_triggers.rs @@ -0,0 +1,1785 @@ +use crate::{ + capture::{insert_capture_payload, MqttTriggerConfig, TriggerKind}, + db::{ApiAuthed, DB}, + jobs::{run_flow_by_path_inner, run_script_by_path_inner, RunJobQuery}, + resources::try_get_resource_from_db_as, + users::fetch_api_authed, +}; +use axum::{ + async_trait, + extract::{Path, Query}, + Extension, Json, +}; +use axum::{ + routing::{delete, get, post}, + Router, +}; +use base64::prelude::*; +use bytes::Bytes; +use http::StatusCode; +use itertools::Itertools; +use rumqttc::{ + v5::{ + mqttbytes::{ + v5::{Filter, PublishProperties}, + QoS as V5QoS, + }, + AsyncClient as V5AsyncClient, Event as V5Event, EventLoop as V5EventLoop, + Incoming as V5Incoming, MqttOptions as V5MqttOptions, + }, + AsyncClient as V3AsyncClient, Event as V3Event, EventLoop as V3EventLoop, + Incoming as V3Incoming, MqttOptions as V3MqttOptions, QoS as V3QoS, SubscribeFilter, + TlsConfiguration, Transport, +}; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use sql_builder::{bind::Bind, SqlBuilder}; +use sqlx::{FromRow, Type}; +use std::collections::HashMap; +use std::time::Duration; +use windmill_audit::{audit_ee::audit_log, ActionKind}; +use windmill_common::{ + db::UserDB, + error::{self, JsonResult}, + utils::{not_found_if_none, paginate, report_critical_error, Pagination, StripPath}, + worker::{to_raw_value, CLOUD_HOSTED}, + INSTANCE_NAME, +}; + +use rand::seq::SliceRandom; +use serde_json::value::RawValue; +use sqlx::types::Json as SqlxJson; + +use windmill_queue::PushArgsOwned; + +pub fn workspaced_service() -> Router { + Router::new() + .route("/create", post(create_mqtt_trigger)) + .route("/list", get(list_mqtt_triggers)) + .route("/get/*path", get(get_mqtt_trigger)) + .route("/update/*path", post(update_mqtt_trigger)) + .route("/delete/*path", delete(delete_mqtt_trigger)) + .route("/exists/*path", get(exists_mqtt_trigger)) + .route("/setenabled/*path", post(set_enabled)) + .route("/test", post(test_mqtt_connection)) +} + +#[derive(Debug, thiserror::Error)] +enum Error { + #[error("{0}")] + Common(#[from] windmill_common::error::Error), + #[error("{0}")] + V5RumqttClient(#[from] rumqttc::v5::ClientError), + #[error("{0}")] + V5ConnectionError(#[from] rumqttc::v5::ConnectionError), + #[error("{0}")] + V3RumqttClient(#[from] rumqttc::ClientError), + #[error("{0}")] + V3ConnectionError(#[from] rumqttc::ConnectionError), + #[error("{0}")] + Base64Decode(#[from] base64::DecodeError), +} + +async fn run_job( + args: Option>>, + extra: Option>>, + db: &DB, + trigger: &MqttTrigger, +) -> anyhow::Result<()> { + let args = PushArgsOwned { args: args.unwrap_or_default(), extra }; + + let authed = fetch_api_authed( + trigger.edited_by.clone(), + trigger.email.clone(), + &trigger.workspace_id, + db, + Some(format!("mqtt-{}", trigger.path)), + ) + .await?; + + let user_db = UserDB::new(db.clone()); + + let run_query = RunJobQuery::default(); + + if trigger.is_flow { + run_flow_by_path_inner( + authed, + db.clone(), + user_db, + trigger.workspace_id.clone(), + StripPath(trigger.script_path.to_owned()), + run_query, + args, + None, + ) + .await?; + } else { + run_script_by_path_inner( + authed, + db.clone(), + user_db, + trigger.workspace_id.clone(), + StripPath(trigger.script_path.to_owned()), + run_query, + args, + None, + ) + .await?; + } + + Ok(()) +} + +#[derive(Clone, Debug, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum QualityOfService { + AtMostOnce = 0, + AtLeastOnce = 1, + ExactlyOnce = 2, +} + +impl Into for QualityOfService { + fn into(self) -> V3QoS { + match self { + QualityOfService::AtMostOnce => V3QoS::AtMostOnce, + QualityOfService::AtLeastOnce => V3QoS::AtLeastOnce, + QualityOfService::ExactlyOnce => V3QoS::ExactlyOnce, + } + } +} + +impl Into for QualityOfService { + fn into(self) -> V5QoS { + match self { + QualityOfService::AtMostOnce => V5QoS::AtMostOnce, + QualityOfService::AtLeastOnce => V5QoS::AtLeastOnce, + QualityOfService::ExactlyOnce => V5QoS::ExactlyOnce, + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct MqttV3Config { + clean_session: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct MqttV5Config { + clean_start: Option, +} + +#[derive(Debug, Deserialize, Serialize, Type)] +#[sqlx(type_name = "MQTT_CLIENT_VERSION")] +#[sqlx(rename_all = "lowercase")] +#[serde(rename_all = "lowercase")] +pub enum MqttClientVersion { + V3, + V5, +} + +#[derive(Debug, Deserialize)] +pub struct Tls { + ca_certificate: String, + //encoded in base64 + pkcs12_client_certificate: Option, + pkcs12_certificate_password: Option, +} + +#[derive(Debug, Deserialize)] +pub struct Credentials { + username: Option, + password: Option, +} + +#[derive(Debug, Deserialize)] +pub struct MqttResource { + broker: String, + port: u16, + credentials: Option, + tls: Option, +} +#[derive(Clone, Debug, FromRow, Serialize, Deserialize)] +pub struct SubscribeTopic { + qos: QualityOfService, + topic: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NewMqttTrigger { + mqtt_resource_path: String, + subscribe_topics: Vec, + v3_config: Option, + v5_config: Option, + #[serde(skip_serializing_if = "Option::is_none")] + client_version: Option, + client_id: Option, + path: String, + script_path: String, + is_flow: bool, + enabled: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct EditMqttTrigger { + mqtt_resource_path: String, + subscribe_topics: Vec, + v3_config: Option, + v5_config: Option, + client_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + client_version: Option, + path: String, + script_path: String, + is_flow: bool, +} + +#[derive(Debug, Serialize, Deserialize, FromRow)] +pub struct MqttTrigger { + mqtt_resource_path: String, + subscribe_topics: Vec>, + v3_config: Option>, + v5_config: Option>, + client_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + client_version: Option, + path: String, + script_path: String, + is_flow: bool, + workspace_id: String, + edited_by: String, + email: String, + edited_at: chrono::DateTime, + extra_perms: Option, + error: Option, + server_id: Option, + last_server_ping: Option>, + enabled: bool, +} + +#[derive(Deserialize, Serialize)] +pub struct ListMqttTriggerQuery { + page: Option, + per_page: Option, + path: Option, + is_flow: Option, + path_start: Option, +} + +#[derive(Deserialize)] +pub struct SetEnabled { + enabled: bool, +} + +struct MqttClientBuilder<'client> { + mqtt_resource: MqttResource, + client_id: &'client str, + subscribe_topics: Vec, + v3_config: Option<&'client MqttV3Config>, + v5_config: Option<&'client MqttV5Config>, + mqtt_client_version: Option<&'client MqttClientVersion>, +} + +impl<'client> MqttClientBuilder<'client> { + fn new( + mqtt_resource: MqttResource, + client_id: Option<&'client str>, + subscribe_topics: Vec, + v3_config: Option<&'client MqttV3Config>, + v5_config: Option<&'client MqttV5Config>, + mqtt_client_version: Option<&'client MqttClientVersion>, + ) -> Self { + Self { + mqtt_resource, + client_id: client_id.unwrap_or(""), + subscribe_topics, + v3_config, + v5_config, + mqtt_client_version, + } + } + + async fn build_client(&self) -> Result { + match self.mqtt_client_version { + Some(MqttClientVersion::V5) | None => self.build_v5_client().await, + Some(MqttClientVersion::V3) => self.build_v3_client().await, + } + } + + fn get_tls_configuration(&self) -> Result, Error> { + let transport = match self.mqtt_resource.tls { + Some(ref tls) if !tls.ca_certificate.is_empty() => { + let transport = rumqttc::Transport::Tls(TlsConfiguration::SimpleNative { + ca: tls.ca_certificate.as_bytes().to_vec(), + client_auth: { + match tls.pkcs12_client_certificate.as_ref() { + Some(client_certificate) => { + let client_certificate = + BASE64_STANDARD.decode(client_certificate)?; + let password = tls + .pkcs12_certificate_password + .clone() + .unwrap_or("".to_string()); + Some((client_certificate, password)) + } + _ => None, + } + }, + }); + + Some(transport) + } + _ => None, + }; + + Ok(transport) + } + + async fn build_v5_client(&self) -> Result { + let mut mqtt_options = V5MqttOptions::new( + self.client_id, + &self.mqtt_resource.broker, + self.mqtt_resource.port, + ); + + if let Some(credentials) = &self.mqtt_resource.credentials { + let username = credentials.username.as_deref().unwrap_or(""); + let password = credentials.password.as_deref().unwrap_or(""); + mqtt_options.set_credentials(username, password); + } + + if let Some(transport) = self.get_tls_configuration()? { + mqtt_options.set_transport(transport); + } + + mqtt_options.set_keep_alive(Duration::from_secs(KEEP_ALIVE)); + + if let Some(v5_config) = self.v5_config { + mqtt_options.set_clean_start(v5_config.clean_start.unwrap_or(true)); + } + + let (async_client, mut event_loop) = + V5AsyncClient::new(mqtt_options, self.subscribe_topics.len()); + event_loop.verify_connection().await?; + + if !self.subscribe_topics.is_empty() { + let subscribe_filters = self + .subscribe_topics + .iter() + .map(|topic| Filter::new(topic.topic.clone(), topic.qos.clone().into())) + .collect_vec(); + + async_client.subscribe_many(subscribe_filters).await?; + } + Ok(MqttClientResult::V5((V5MqttHandler, event_loop))) + } + + async fn build_v3_client(&self) -> Result { + let mut mqtt_options = V3MqttOptions::new( + self.client_id, + &self.mqtt_resource.broker, + self.mqtt_resource.port, + ); + + if let Some(credentials) = &self.mqtt_resource.credentials { + let username = credentials.username.as_deref().unwrap_or(""); + let password = credentials.password.as_deref().unwrap_or(""); + mqtt_options.set_credentials(username, password); + } + + if let Some(transport) = self.get_tls_configuration()? { + mqtt_options.set_transport(transport); + } + + mqtt_options.set_keep_alive(Duration::from_secs(KEEP_ALIVE)); + + if let Some(v3_config) = self.v3_config { + mqtt_options.set_clean_session(v3_config.clean_session.unwrap_or(true)); + } + + let (async_client, mut event_loop) = + V3AsyncClient::new(mqtt_options, self.subscribe_topics.len()); + event_loop.verify_connection().await?; + + if !self.subscribe_topics.is_empty() { + let subscribe_filters = self + .subscribe_topics + .iter() + .map(|topic| SubscribeFilter::new(topic.topic.clone(), topic.qos.clone().into())) + .collect_vec(); + + async_client.subscribe_many(subscribe_filters).await?; + } + Ok(MqttClientResult::V3((V3MqttHandler, event_loop))) + } +} + +fn convert_disconnect_packet_into_string( + disconnect: rumqttc::v5::mqttbytes::v5::Disconnect, +) -> String { + let err_message = disconnect + .properties + .map(|properties| properties.reason_string) + .flatten(); + let reason_code = disconnect.reason_code as u8; + format!( + "Disconnected by the broker, reason code: {}, {}", + reason_code, + err_message + .map(|err| format!("message: {}", err)) + .unwrap_or("".to_string()) + ) +} + +#[derive(Debug, Deserialize)] +pub struct TestMqttConnection { + mqtt_resource_path: String, + client_version: Option, + v3_config: Option, + v5_config: Option, +} + +pub async fn test_mqtt_connection( + authed: ApiAuthed, + Extension(db): Extension, + Extension(user_db): Extension, + Path(workspace_id): Path, + Json(test_postgres): Json, +) -> error::Result<()> { + let TestMqttConnection { mqtt_resource_path, client_version, v3_config, v5_config } = + test_postgres; + + let mqtt_resource = try_get_resource_from_db_as::( + authed, + Some(user_db), + &db, + &mqtt_resource_path, + &workspace_id, + ) + .await?; + + let connect_f = async { + let client_builder = MqttClientBuilder::new( + mqtt_resource, + Some(""), + vec![], + v3_config.as_ref(), + v5_config.as_ref(), + client_version.as_ref(), + ); + + client_builder.build_client().await.map_err(|err| { + error::Error::BadConfig(format!( + "Error connecting to mqtt broker: {}", + err.to_string() + )) + }) + }; + tokio::time::timeout(tokio::time::Duration::from_secs(30), connect_f) + .await + .map_err(|_| { + error::Error::BadConfig(format!( + "Timeout occured while trying connecting to mqtt broker after 30 seconds" + )) + })??; + + Ok(()) +} + +pub async fn create_mqtt_trigger( + authed: ApiAuthed, + Extension(user_db): Extension, + Path(w_id): Path, + Json(new_mqtt_trigger): Json, +) -> error::Result<(StatusCode, String)> { + if *CLOUD_HOSTED { + return Err(error::Error::BadRequest( + "Mqtt triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(), + )); + } + + let NewMqttTrigger { + mqtt_resource_path, + subscribe_topics, + path, + script_path, + enabled, + is_flow, + v3_config, + v5_config, + client_version, + client_id, + } = new_mqtt_trigger; + + let mut tx = user_db.begin(&authed).await?; + + let subscribe_topics = subscribe_topics.into_iter().map(SqlxJson).collect_vec(); + let v3_config = v3_config.map(SqlxJson); + let v5_config = v5_config.map(SqlxJson); + + sqlx::query!( + r#" + INSERT INTO mqtt_trigger ( + mqtt_resource_path, + subscribe_topics, + client_version, + client_id, + v3_config, + v5_config, + workspace_id, + path, + script_path, + is_flow, + email, + enabled, + edited_by + ) + VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13 + )"#, + mqtt_resource_path, + subscribe_topics.as_slice() as &[SqlxJson], + client_version as Option, + client_id, + v3_config as Option>, + v5_config as Option>, + &w_id, + &path, + script_path, + is_flow, + &authed.email, + enabled, + &authed.username + ) + .execute(&mut *tx) + .await?; + + audit_log( + &mut *tx, + &authed, + "mqtt_triggers.create", + ActionKind::Create, + &w_id, + Some(path.as_str()), + None, + ) + .await?; + + tx.commit().await?; + + Ok((StatusCode::CREATED, path.to_string())) +} + +pub async fn list_mqtt_triggers( + authed: ApiAuthed, + Extension(user_db): Extension, + Path(w_id): Path, + Query(lst): Query, +) -> error::JsonResult> { + let mut tx = user_db.begin(&authed).await?; + let (per_page, offset) = paginate(Pagination { per_page: lst.per_page, page: lst.page }); + let mut sqlb = SqlBuilder::select_from("mqtt_trigger") + .fields(&[ + "mqtt_resource_path", + "subscribe_topics", + "v3_config", + "v5_config", + "client_version", + "client_id", + "workspace_id", + "path", + "script_path", + "is_flow", + "edited_by", + "email", + "edited_at", + "server_id", + "last_server_ping", + "extra_perms", + "error", + "enabled", + ]) + .order_by("edited_at", true) + .and_where("workspace_id = ?".bind(&w_id)) + .offset(offset) + .limit(per_page) + .clone(); + if let Some(path) = lst.path { + sqlb.and_where_eq("script_path", "?".bind(&path)); + } + if let Some(is_flow) = lst.is_flow { + sqlb.and_where_eq("is_flow", "?".bind(&is_flow)); + } + if let Some(path_start) = &lst.path_start { + sqlb.and_where_like_left("path", path_start); + } + let sql = sqlb + .sql() + .map_err(|e| error::Error::InternalErr(e.to_string()))?; + let rows = sqlx::query_as::<_, MqttTrigger>(&sql) + .fetch_all(&mut *tx) + .await + .map_err(|e| { + tracing::debug!("Error fetching mqtt_trigger: {:#?}", e); + windmill_common::error::Error::InternalErr("server error".to_string()) + })?; + tx.commit().await.map_err(|e| { + tracing::debug!("Error commiting mqtt_trigger: {:#?}", e); + windmill_common::error::Error::InternalErr("server error".to_string()) + })?; + + Ok(Json(rows)) +} + +pub async fn get_mqtt_trigger( + authed: ApiAuthed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult { + let mut tx = user_db.begin(&authed).await?; + let path = path.to_path(); + let trigger = sqlx::query_as!( + MqttTrigger, + r#" + SELECT + mqtt_resource_path, + subscribe_topics as "subscribe_topics!: Vec>", + v3_config as "v3_config!: Option>", + v5_config as "v5_config!: Option>", + client_version AS "client_version: _", + client_id, + workspace_id, + path, + script_path, + is_flow, + edited_by, + email, + edited_at, + server_id, + last_server_ping, + extra_perms, + error, + enabled + FROM + mqtt_trigger + WHERE + workspace_id = $1 AND + path = $2 + "#, + w_id, + &path + ) + .fetch_optional(&mut *tx) + .await?; + tx.commit().await?; + + let trigger = not_found_if_none(trigger, "Mqtt Trigger", path)?; + + Ok(Json(trigger)) +} + +pub async fn update_mqtt_trigger( + authed: ApiAuthed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, + Json(mqtt_trigger): Json, +) -> error::Result { + let workspace_path = path.to_path(); + let EditMqttTrigger { + mqtt_resource_path, + subscribe_topics, + script_path, + path, + is_flow, + v3_config, + v5_config, + client_version, + client_id, + } = mqtt_trigger; + + let mut tx = user_db.begin(&authed).await?; + + let subscribe_topics = subscribe_topics.into_iter().map(SqlxJson).collect_vec(); + + let v3_config = v3_config.map(SqlxJson); + let v5_config = v5_config.map(SqlxJson); + + sqlx::query!( + r#" + UPDATE + mqtt_trigger + SET + mqtt_resource_path = $1, + subscribe_topics = $2, + client_version = $3, + client_id = $4, + v3_config = $5, + v5_config = $6, + is_flow = $7, + edited_by = $8, + email = $9, + script_path = $10, + path = $11, + edited_at = now(), + error = NULL, + server_id = NULL + WHERE + workspace_id = $12 AND + path = $13 + "#, + mqtt_resource_path, + subscribe_topics.as_slice() as &[SqlxJson], + client_version as Option, + client_id, + v3_config as Option>, + v5_config as Option>, + is_flow, + &authed.username, + &authed.email, + script_path, + path, + w_id, + workspace_path, + ) + .execute(&mut *tx) + .await?; + + audit_log( + &mut *tx, + &authed, + "mqtt_triggers.update", + ActionKind::Create, + &w_id, + Some(&path), + None, + ) + .await?; + + tx.commit().await?; + + Ok(workspace_path.to_string()) +} + +pub async fn delete_mqtt_trigger( + authed: ApiAuthed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> error::Result { + let path = path.to_path(); + let mut tx = user_db.begin(&authed).await?; + sqlx::query!( + r#" + DELETE + FROM + mqtt_trigger + WHERE + workspace_id = $1 AND + path = $2 + "#, + w_id, + path, + ) + .execute(&mut *tx) + .await?; + + audit_log( + &mut *tx, + &authed, + "mqtt_triggers.delete", + ActionKind::Delete, + &w_id, + Some(path), + None, + ) + .await?; + + tx.commit().await?; + + Ok(format!("Mqtt trigger {path} deleted")) +} + +pub async fn exists_mqtt_trigger( + Extension(db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult { + let path = path.to_path(); + let exists = sqlx::query_scalar!( + r#" + SELECT EXISTS( + SELECT + 1 + FROM + mqtt_trigger + WHERE + path = $1 AND + workspace_id = $2 + )"#, + path, + w_id, + ) + .fetch_one(&db) + .await? + .unwrap_or(false); + Ok(Json(exists)) +} + +pub async fn set_enabled( + authed: ApiAuthed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, + Json(payload): Json, +) -> error::Result { + let mut tx = user_db.begin(&authed).await?; + let path = path.to_path(); + + // important to set server_id, last_server_ping and error to NULL to stop current mqtt listener + let one_o = sqlx::query_scalar!( + r#" + UPDATE + mqtt_trigger + SET + enabled = $1, + email = $2, + edited_by = $3, + edited_at = now(), + server_id = NULL, + error = NULL + WHERE + path = $4 AND + workspace_id = $5 + RETURNING 1 + "#, + payload.enabled, + &authed.email, + &authed.username, + path, + w_id, + ) + .fetch_optional(&mut *tx) + .await? + .flatten(); + + not_found_if_none(one_o, "Mqtt trigger", path)?; + + audit_log( + &mut *tx, + &authed, + "mqtt_triggers.setenabled", + ActionKind::Update, + &w_id, + Some(path), + Some([("enabled", payload.enabled.to_string().as_ref())].into()), + ) + .await?; + + tx.commit().await?; + + Ok(format!( + "succesfully updated mqttq trigger at path {} to status {}", + path, payload.enabled + )) +} + +async fn loop_ping(db: &DB, mqtt: &MqttConfig, error: Option<&str>) { + loop { + if mqtt.update_ping(db, error).await.is_none() { + return; + } + + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + } +} + +enum MqttClientResult { + V3((V3MqttHandler, V3EventLoop)), + V5((V5MqttHandler, V5EventLoop)), +} + +const KEEP_ALIVE: u64 = 60; + +trait MqttEvent { + type IncomingPacket; + type PublishPacket; + type Event; + + fn handle_publish_packet(publish_packet: Self::PublishPacket) -> PublishData; + fn handle_event(&self, event: Self::Event) -> Result, String>; +} + +struct V5MqttHandler; + +impl MqttEvent for V5MqttHandler { + type IncomingPacket = V5Incoming; + type PublishPacket = rumqttc::v5::mqttbytes::v5::Publish; + type Event = V5Event; + + fn handle_publish_packet(publish_packet: Self::PublishPacket) -> PublishData { + PublishData::new( + String::from_utf8(publish_packet.topic.as_ref().to_vec()).unwrap_or("".to_string()), + publish_packet.retain, + publish_packet.pkid, + publish_packet.properties, + publish_packet.qos as u8, + ) + } + + fn handle_event(&self, event: Self::Event) -> Result, String> { + tracing::debug!("Inside V5 event"); + match event { + Self::Event::Incoming(packet) => match packet { + Self::IncomingPacket::Publish(publish_packet) => { + return Ok(Some(( + publish_packet.payload.clone(), + Self::handle_publish_packet(publish_packet), + ))) + } + Self::IncomingPacket::Disconnect(disconnect) => { + return Err(convert_disconnect_packet_into_string(disconnect)); + } + packet => { + tracing::debug!("Received = {:#?}", packet); + } + }, + Self::Event::Outgoing(packet) => { + tracing::debug!("Outgoing Received = {:#?}", packet); + } + } + + Ok(None) + } +} + +struct V3MqttHandler; + +impl MqttEvent for V3MqttHandler { + type IncomingPacket = V3Incoming; + type PublishPacket = rumqttc::mqttbytes::v4::Publish; + type Event = V3Event; + + fn handle_publish_packet(publish_packet: Self::PublishPacket) -> PublishData { + PublishData::new( + publish_packet.topic, + publish_packet.retain, + publish_packet.pkid, + None, + publish_packet.qos as u8, + ) + } + + fn handle_event(&self, event: Self::Event) -> Result, String> { + tracing::debug!("Inside V3 event"); + match event { + Self::Event::Incoming(packet) => match packet { + Self::IncomingPacket::Publish(publish_packet) => { + return Ok(Some(( + publish_packet.payload.clone(), + Self::handle_publish_packet(publish_packet), + ))) + } + packet => { + tracing::debug!("Received = {:?}", packet); + } + }, + Self::Event::Outgoing(packet) => { + tracing::debug!("Outgoing Received = {:?}", packet); + } + } + + Ok(None) + } +} + +const TIMEOUT_DURATION: u64 = 10; +const CONNECTION_TIMEOUT: Duration = Duration::from_secs(TIMEOUT_DURATION); + +#[async_trait] +trait EventLoop { + type Event; + type Error; + + async fn poll(&mut self) -> Result; + async fn verify_connection(&mut self) -> Result<(), Error>; +} + +#[async_trait] +impl EventLoop for V5EventLoop { + type Event = V5Event; + type Error = rumqttc::v5::ConnectionError; + async fn poll(&mut self) -> Result { + self.poll().await + } + + async fn verify_connection(&mut self) -> Result<(), Error> { + let start = std::time::Instant::now(); + + while start.elapsed() < CONNECTION_TIMEOUT { + match self.poll().await? { + Self::Event::Incoming(V5Incoming::ConnAck(_)) => return Ok(()), + Self::Event::Incoming(V5Incoming::Disconnect(disconnect)) => { + return Err(Error::Common(error::Error::BadConfig( + convert_disconnect_packet_into_string(disconnect), + ))); + } + _ => continue, + } + } + + Err(Error::Common(error::Error::BadConfig(format!( + "Timeout occured while trying connecting to mqtt broker after {} seconds", + TIMEOUT_DURATION + )))) + } +} + +#[async_trait] +impl EventLoop for V3EventLoop { + type Event = V3Event; + type Error = rumqttc::ConnectionError; + + async fn poll(&mut self) -> Result { + self.poll().await + } + + async fn verify_connection(&mut self) -> Result<(), Error> { + let start = std::time::Instant::now(); + + while start.elapsed() < CONNECTION_TIMEOUT { + match self.poll().await? { + Self::Event::Incoming(rumqttc::Packet::ConnAck(_)) => return Ok(()), + _ => continue, + } + } + + Err(Error::Common(error::Error::BadConfig(format!( + "Timeout occured while trying connecting to mqtt broker after {} seconds", + TIMEOUT_DURATION + )))) + } +} + +async fn handle_publish_packet(db: &DB, mqtt: &MqttConfig, payload: Bytes, publish: PublishData) { + let args = HashMap::from([("payload".to_string(), to_raw_value(&payload.as_ref()))]); + let extra = Some(HashMap::from([( + "wm_trigger".to_string(), + to_raw_value(&serde_json::json!({ + "kind": "mqtt", + "mqtt": { + "topic": publish.topic, + "retain": publish.retain, + "pkid": publish.pkid, + "qos": publish.qos, + "v5": publish.v5.map(|properties| { + serde_json::json!({ + "payload_format_indicator": properties.payload_format_indicator, + "topic_alias": properties.topic_alias, + "response_topic": properties.response_topic, + "correlation_data": properties.correlation_data.as_deref(), + "user_properties": properties.user_properties, + "subscription_identifiers": properties.subscription_identifiers, + "content_type": properties.content_type, + }) + }) + } + })), + )])); + mqtt.handle(&db, Some(args), extra).await; +} + +async fn handle_event(db: &DB, mqtt: &MqttConfig, handler: H, mut event_loop: E) -> () +where + H: MqttEvent, + E: EventLoop, + E::Error: ToString, +{ + loop { + let event = event_loop.poll().await; + + match event { + Ok(event) => { + let publish_data = handler.handle_event(event); + if let Ok(Some((payload, publish_data))) = publish_data { + handle_publish_packet(db, mqtt, payload, publish_data).await; + } + } + Err(err) => { + let err = err.to_string(); + tracing::debug!("Error: {}", &err); + mqtt.disable_with_error(&db, err).await; + return; + } + } + } +} + +#[derive(Debug)] +enum MqttConfig { + Trigger(MqttTrigger), + Capture(CaptureConfigForMqttTrigger), +} + +impl MqttConfig { + async fn update_ping(&self, db: &DB, error: Option<&str>) -> Option<()> { + match self { + MqttConfig::Trigger(trigger) => trigger.update_ping(db, error).await, + MqttConfig::Capture(capture) => capture.update_ping(db, error).await, + } + } + + async fn disable_with_error(&self, db: &DB, error: String) -> () { + match self { + MqttConfig::Trigger(trigger) => trigger.disable_with_error(&db, error).await, + MqttConfig::Capture(capture) => capture.disable_with_error(db, error).await, + } + } + + async fn start_consuming_messages( + &self, + db: &DB, + ) -> std::result::Result { + let mqtt_resource_path; + let subscribe_topics; + let workspace_id; + let authed; + let client_version; + let client_id; + let v3_config; + let v5_config; + match self { + MqttConfig::Capture(capture) => { + mqtt_resource_path = &capture.trigger_config.0.mqtt_resource_path; + subscribe_topics = capture.trigger_config.0.subscribe_topics.clone(); + workspace_id = &capture.trigger_config.0.mqtt_resource_path; + authed = capture.fetch_authed(&db).await?; + client_version = capture.trigger_config.0.client_version.as_ref(); + client_id = capture.trigger_config.0.client_id.as_deref(); + v3_config = capture.trigger_config.0.v3_config.as_ref(); + v5_config = capture.trigger_config.0.v5_config.as_ref(); + } + MqttConfig::Trigger(trigger) => { + mqtt_resource_path = &trigger.mqtt_resource_path; + subscribe_topics = trigger + .subscribe_topics + .iter() + .map(|topic| topic.0.clone()) + .collect_vec(); + workspace_id = &trigger.workspace_id; + client_version = trigger.client_version.as_ref(); + authed = trigger.fetch_authed(&db).await?; + client_id = trigger.client_id.as_deref(); + v3_config = trigger.v3_config.as_ref().map(|v3_config| &v3_config.0); + v5_config = trigger.v5_config.as_ref().map(|v5_config| &v5_config.0); + } + } + + let mqtt_resource = try_get_resource_from_db_as::( + authed, + Some(UserDB::new(db.clone())), + db, + mqtt_resource_path, + workspace_id, + ) + .await?; + + let client_builder = MqttClientBuilder::new( + mqtt_resource, + client_id, + subscribe_topics, + v3_config, + v5_config, + client_version, + ); + + client_builder.build_client().await + } + + async fn handle( + &self, + db: &DB, + args: Option>>, + extra: Option>>, + ) -> () { + match self { + MqttConfig::Trigger(trigger) => trigger.handle(&db, args, extra).await, + MqttConfig::Capture(capture) => capture.handle(&db, args, extra).await, + } + } +} + +impl MqttTrigger { + async fn try_to_listen_to_mqtt_messages( + self, + db: DB, + killpill_rx: tokio::sync::broadcast::Receiver<()>, + ) -> () { + let mqtt_trigger = sqlx::query_scalar!( + r#" + UPDATE + mqtt_trigger + SET + server_id = $1, + last_server_ping = now(), + error = 'Connecting...' + WHERE + enabled IS TRUE + AND workspace_id = $2 + AND path = $3 + AND (last_server_ping IS NULL + OR last_server_ping < now() - INTERVAL '15 seconds' + ) + RETURNING true + "#, + *INSTANCE_NAME, + self.workspace_id, + self.path, + ) + .fetch_optional(&db) + .await; + match mqtt_trigger { + Ok(has_lock) => { + if has_lock.flatten().unwrap_or(false) { + tracing::info!("Spawning new task to listen to mqtt notifications"); + tokio::spawn(async move { + listen_to_messages(MqttConfig::Trigger(self), db.clone(), killpill_rx) + .await; + }); + } else { + tracing::info!("Mqtt trigger {} already being listened to", self.path); + } + } + Err(err) => { + tracing::error!( + "Error acquiring lock for mqtt trigger {}: {:?}", + self.path, + err + ); + } + }; + } + + async fn update_ping(&self, db: &DB, error: Option<&str>) -> Option<()> { + let updated = sqlx::query_scalar!( + r#" + UPDATE + mqtt_trigger + SET + last_server_ping = now(), + error = $1 + WHERE + workspace_id = $2 + AND path = $3 + AND server_id = $4 + AND enabled IS TRUE + RETURNING 1 + "#, + error, + &self.workspace_id, + &self.path, + *INSTANCE_NAME + ) + .fetch_optional(db) + .await; + + match updated { + Ok(updated) => { + if updated.flatten().is_none() { + // allow faster restart of mqtt trigger + sqlx::query!( + r#" + UPDATE + mqtt_trigger + SET + last_server_ping = NULL + WHERE + workspace_id = $1 + AND path = $2 + AND server_id IS NULL"#, + &self.workspace_id, + &self.path, + ) + .execute(db) + .await + .ok(); + tracing::info!( + "Mqtt trigger {} changed, disabled, or deleted, stopping...", + self.path + ); + return None; + } + } + Err(err) => { + tracing::warn!( + "Error updating ping of mqtt trigger {}: {:?}", + self.path, + err + ); + } + }; + + Some(()) + } + + async fn disable_with_error(&self, db: &DB, error: String) -> () { + match sqlx::query!( + r#" + UPDATE + mqtt_trigger + SET + enabled = FALSE, + error = $1, + server_id = NULL, + last_server_ping = NULL + WHERE + workspace_id = $2 AND + path = $3 + "#, + error, + self.workspace_id, + self.path, + ) + .execute(db) + .await + { + Ok(_) => { + report_critical_error( + format!( + "Disabling mqtt trigger {} because of error: {}", + self.path, error + ), + db.clone(), + Some(&self.workspace_id), + None, + ) + .await; + } + Err(disable_err) => { + report_critical_error( + format!("Could not disable mqtt trigger {} with err {}, disabling because of error {}", self.path, disable_err, error), + db.clone(), + Some(&self.workspace_id), + None, + ).await; + } + } + } + + async fn fetch_authed(&self, db: &DB) -> error::Result { + fetch_api_authed( + self.edited_by.clone(), + self.email.clone(), + &self.workspace_id, + db, + Some(format!("mqtt-{}", self.path)), + ) + .await + } + + async fn handle( + &self, + db: &DB, + args: Option>>, + extra: Option>>, + ) -> () { + if let Err(err) = run_job(args, extra, db, self).await { + report_critical_error( + format!("Failed to trigger job from mqtt {}: {:?}", self.path, err), + db.clone(), + Some(&self.workspace_id), + None, + ) + .await; + }; + } +} + +struct PublishData { + topic: String, + retain: bool, + pkid: u16, + v5: Option, + qos: u8, +} + +impl PublishData { + fn new( + topic: String, + retain: bool, + pkid: u16, + v5: Option, + qos: u8, + ) -> PublishData { + PublishData { topic, retain, pkid, v5, qos } + } +} + +async fn listen_to_messages( + mqtt: MqttConfig, + db: DB, + mut killpill_rx: tokio::sync::broadcast::Receiver<()>, +) { + tokio::select! { + biased; + + _ = killpill_rx.recv() => { + return; + } + + _ = loop_ping(&db, &mqtt, Some("Connecting...")) => { + return; + } + + result = mqtt.start_consuming_messages(&db) => { + tokio::select! { + biased; + + _ = killpill_rx.recv() => { + return; + } + + _ = loop_ping(&db, &mqtt, None) => { + return; + } + + _ = async { + match result { + Ok(connection) => { + match connection { + MqttClientResult::V3((v3_handler, event_loop)) => handle_event(&db, &mqtt, v3_handler, event_loop).await, + MqttClientResult::V5((v5_handler, event_loop)) => handle_event(&db, &mqtt, v5_handler, event_loop).await, + } + } + Err(err) => { + tracing::error!( + "Mqtt trigger error while trying to start listening to notifications: {}", + &err + ); + mqtt.disable_with_error(&db, err.to_string()).await + } + } + } => {} + } + } + } +} + +#[derive(Debug, Deserialize)] +struct CaptureConfigForMqttTrigger { + trigger_config: SqlxJson, + path: String, + is_flow: bool, + workspace_id: String, + owner: String, + email: String, +} + +impl CaptureConfigForMqttTrigger { + async fn try_to_listen_to_mqtt_messages( + self, + db: DB, + killpill_rx: tokio::sync::broadcast::Receiver<()>, + ) -> () { + match sqlx::query_scalar!( + r#" + UPDATE + capture_config + SET + server_id = $1, + last_server_ping = now(), + error = 'Connecting...' + WHERE + last_client_ping > NOW() - INTERVAL '10 seconds' AND + workspace_id = $2 AND + path = $3 AND + is_flow = $4 AND + trigger_kind = 'mqtt' AND + (last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds') + RETURNING true + "#, + *INSTANCE_NAME, + self.workspace_id, + self.path, + self.is_flow, + ) + .fetch_optional(&db) + .await + { + Ok(has_lock) => { + if has_lock.flatten().unwrap_or(false) { + tokio::spawn(listen_to_messages( + MqttConfig::Capture(self), + db, + killpill_rx, + )); + } else { + tracing::info!("Mqtt {} already being listened to", self.path); + } + } + Err(err) => { + tracing::error!( + "Error acquiring lock for capture mqtt {}: {:?}", + self.path, + err + ); + } + }; + } + + async fn update_ping(&self, db: &DB, error: Option<&str>) -> Option<()> { + match sqlx::query_scalar!( + r#" + UPDATE + capture_config + SET + last_server_ping = now(), + error = $1 + WHERE + workspace_id = $2 AND + path = $3 AND + is_flow = $4 AND + trigger_kind = 'mqtt' AND + server_id = $5 AND + last_client_ping > NOW() - INTERVAL '10 seconds' + RETURNING 1 + "#, + error, + self.workspace_id, + self.path, + self.is_flow, + *INSTANCE_NAME + ) + .fetch_optional(db) + .await + { + Ok(updated) => { + if updated.flatten().is_none() { + // allow faster restart of mqtt capture + sqlx::query!( + r#"UPDATE + capture_config + SET + last_server_ping = NULL + WHERE + workspace_id = $1 AND + path = $2 AND + is_flow = $3 AND + trigger_kind = 'mqtt' AND + server_id IS NULL + "#, + self.workspace_id, + self.path, + self.is_flow, + ) + .execute(db) + .await + .ok(); + tracing::info!( + "Mqtt capture {} changed, disabled, or deleted, stopping...", + self.path + ); + return None; + } + } + Err(err) => { + tracing::warn!( + "Error updating ping of capture mqtt {}: {:?}", + self.path, + err + ); + } + }; + + Some(()) + } + + async fn fetch_authed(&self, db: &DB) -> error::Result { + fetch_api_authed( + self.owner.clone(), + self.email.clone(), + &self.workspace_id, + db, + Some(format!("mqtt-{}", self.get_trigger_path())), + ) + .await + } + + fn get_trigger_path(&self) -> String { + format!( + "{}-{}", + if self.is_flow { "flow" } else { "script" }, + self.path + ) + } + + async fn disable_with_error(&self, db: &DB, error: String) -> () { + if let Err(err) = sqlx::query!( + r#" + UPDATE + capture_config + SET + error = $1, + server_id = NULL, + last_server_ping = NULL + WHERE + workspace_id = $2 AND + path = $3 AND + is_flow = $4 AND + trigger_kind = 'mqtt' + "#, + error, + self.workspace_id, + self.path, + self.is_flow, + ) + .execute(db) + .await + { + tracing::error!( + "Could not disable mqtt capture {} ({}) with err {}, disabling because of error {}", + self.path, + self.workspace_id, + err, + error + ); + } + } + + async fn handle( + &self, + db: &DB, + args: Option>>, + extra: Option>>, + ) -> () { + let args = PushArgsOwned { args: args.unwrap_or_default(), extra }; + let extra = args.extra.as_ref().map(to_raw_value); + if let Err(err) = insert_capture_payload( + db, + &self.workspace_id, + &self.path, + self.is_flow, + &TriggerKind::Mqtt, + args, + extra, + &self.owner, + ) + .await + { + tracing::error!("Error inserting capture payload: {:?}", err); + } + } +} + +async fn listen_to_unlistened_mqtt_events( + db: &DB, + killpill_rx: &tokio::sync::broadcast::Receiver<()>, +) { + let mqtt_triggers = sqlx::query_as!( + MqttTrigger, + r#" + SELECT + mqtt_resource_path, + subscribe_topics as "subscribe_topics!: Vec>", + v3_config as "v3_config!: Option>", + v5_config as "v5_config!: Option>", + client_version as "client_version: _", + client_id, + workspace_id, + path, + script_path, + is_flow, + edited_by, + email, + edited_at, + server_id, + last_server_ping, + extra_perms, + error, + enabled + FROM + mqtt_trigger + WHERE + enabled IS TRUE + AND (last_server_ping IS NULL OR + last_server_ping < now() - interval '15 seconds' + ) + "# + ) + .fetch_all(db) + .await; + + match mqtt_triggers { + Ok(mut triggers) => { + triggers.shuffle(&mut rand::rng()); + for trigger in triggers { + trigger + .try_to_listen_to_mqtt_messages(db.clone(), killpill_rx.resubscribe()) + .await; + } + } + Err(err) => { + tracing::error!("Error fetching mqtt triggers: {:?}", err); + } + }; + + let mqtt_triggers_capture = sqlx::query_as!( + CaptureConfigForMqttTrigger, + r#" + SELECT + path, + is_flow, + workspace_id, + owner, + email, + trigger_config as "trigger_config!: _" + FROM + capture_config + WHERE + trigger_kind = 'mqtt' AND + last_client_ping > NOW() - INTERVAL '10 seconds' AND + trigger_config IS NOT NULL AND + (last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds') + "# + ) + .fetch_all(db) + .await; + + match mqtt_triggers_capture { + Ok(mut captures) => { + captures.shuffle(&mut rand::rng()); + for capture in captures { + capture + .try_to_listen_to_mqtt_messages(db.clone(), killpill_rx.resubscribe()) + .await; + } + } + Err(err) => { + tracing::error!("Error fetching captures mqtt triggers: {:?}", err); + } + }; +} + +pub fn start_mqtt_consumer(db: DB, mut killpill_rx: tokio::sync::broadcast::Receiver<()>) { + tokio::spawn(async move { + listen_to_unlistened_mqtt_events(&db, &killpill_rx).await; + loop { + tokio::select! { + biased; + _ = killpill_rx.recv() => { + return; + } + _ = tokio::time::sleep(tokio::time::Duration::from_secs(15)) => { + listen_to_unlistened_mqtt_events(&db, &killpill_rx).await + } + } + } + }); +} diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index f41a2b8e5d089..2a3331ef9a9c1 100644 --- a/backend/windmill-api/src/resources.rs +++ b/backend/windmill-api/src/resources.rs @@ -1208,10 +1208,7 @@ async fn update_resource_type( Ok(format!("resource_type {} updated", name)) } -#[cfg(any( - feature = "postgres_trigger", - all(feature = "sqs_trigger", feature = "enterprise") -))] +#[cfg(any(feature = "postgres_trigger", feature = "mqtt_trigger", all(feature = "sqs_trigger", feature = "enterprise")))] pub async fn try_get_resource_from_db_as( authed: ApiAuthed, user_db: Option, diff --git a/backend/windmill-api/src/variables.rs b/backend/windmill-api/src/variables.rs index 475fe7db98cdf..425c4d86fc36f 100644 --- a/backend/windmill-api/src/variables.rs +++ b/backend/windmill-api/src/variables.rs @@ -19,6 +19,7 @@ use axum::{ }; use hyper::StatusCode; use serde_json::Value; + use windmill_audit::audit_ee::{audit_log, AuditAuthorable}; use windmill_audit::ActionKind; use windmill_common::{ diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index ff464bbdef22b..bfe32b976d2c7 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -1363,7 +1363,8 @@ struct UsedTriggers { pub kafka_used: bool, pub nats_used: bool, pub postgres_used: bool, - pub sqs_used: bool, + pub mqtt_used: bool, + pub sqs_used: bool } async fn get_used_triggers( @@ -1378,11 +1379,11 @@ async fn get_used_triggers( SELECT EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) AS "websocket_used!", - EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) AS "http_routes_used!", EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as "kafka_used!", EXISTS(SELECT 1 FROM nats_trigger WHERE workspace_id = $1) as "nats_used!", 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!" "#, w_id diff --git a/cli/gen/core/OpenAPI.ts b/cli/gen/core/OpenAPI.ts index 7dbf2fb3d4624..fda891b01d7fd 100644 --- a/cli/gen/core/OpenAPI.ts +++ b/cli/gen/core/OpenAPI.ts @@ -54,7 +54,7 @@ export const OpenAPI: OpenAPIConfig = { PASSWORD: undefined, TOKEN: getEnv("WM_TOKEN"), USERNAME: undefined, - VERSION: '1.460.1', + VERSION: '1.465.0', WITH_CREDENTIALS: true, interceptors: { request: new Interceptors(), diff --git a/cli/gen/services.gen.ts b/cli/gen/services.gen.ts index 5148049bc94e2..0eefeb39a494f 100644 --- a/cli/gen/services.gen.ts +++ b/cli/gen/services.gen.ts @@ -3,7 +3,7 @@ import type { CancelablePromise } from './core/CancelablePromise.ts'; import { OpenAPI } from './core/OpenAPI.ts'; import { request as __request } from './core/request.ts'; -import type { BackendVersionResponse, BackendUptodateResponse, GetLicenseIdResponse, GetOpenApiYamlResponse, GetAuditLogData, GetAuditLogResponse, ListAuditLogsData, ListAuditLogsResponse, LoginData, LoginResponse, LogoutResponse, GetUserData, GetUserResponse, UpdateUserData, UpdateUserResponse, IsOwnerOfPathData, IsOwnerOfPathResponse, SetPasswordData, SetPasswordResponse, SetPasswordForUserData, SetPasswordForUserResponse, SetLoginTypeForUserData, SetLoginTypeForUserResponse, CreateUserGloballyData, CreateUserGloballyResponse, GlobalUserUpdateData, GlobalUserUpdateResponse, GlobalUsernameInfoData, GlobalUsernameInfoResponse, GlobalUserRenameData, GlobalUserRenameResponse, GlobalUserDeleteData, GlobalUserDeleteResponse, GlobalUsersOverwriteData, GlobalUsersOverwriteResponse, GlobalUsersExportResponse, DeleteUserData, DeleteUserResponse, ListWorkspacesResponse, IsDomainAllowedResponse, ListUserWorkspacesResponse, ListWorkspacesAsSuperAdminData, ListWorkspacesAsSuperAdminResponse, CreateWorkspaceData, CreateWorkspaceResponse, ExistsWorkspaceData, ExistsWorkspaceResponse, ExistsUsernameData, ExistsUsernameResponse, GetGlobalData, GetGlobalResponse, SetGlobalData, SetGlobalResponse, GetLocalResponse, TestSmtpData, TestSmtpResponse, TestCriticalChannelsData, TestCriticalChannelsResponse, GetCriticalAlertsData, GetCriticalAlertsResponse, AcknowledgeCriticalAlertData, AcknowledgeCriticalAlertResponse, AcknowledgeAllCriticalAlertsResponse, TestLicenseKeyData, TestLicenseKeyResponse, TestObjectStorageConfigData, TestObjectStorageConfigResponse, SendStatsResponse, GetLatestKeyRenewalAttemptResponse, RenewLicenseKeyData, RenewLicenseKeyResponse, CreateCustomerPortalSessionData, CreateCustomerPortalSessionResponse, TestMetadataData, TestMetadataResponse, ListGlobalSettingsResponse, GetCurrentEmailResponse, RefreshUserTokenData, RefreshUserTokenResponse, GetTutorialProgressResponse, UpdateTutorialProgressData, UpdateTutorialProgressResponse, LeaveInstanceResponse, GetUsageResponse, GetRunnableResponse, GlobalWhoamiResponse, ListWorkspaceInvitesResponse, WhoamiData, WhoamiResponse, AcceptInviteData, AcceptInviteResponse, DeclineInviteData, DeclineInviteResponse, InviteUserData, InviteUserResponse, AddUserData, AddUserResponse, DeleteInviteData, DeleteInviteResponse, ArchiveWorkspaceData, ArchiveWorkspaceResponse, UnarchiveWorkspaceData, UnarchiveWorkspaceResponse, DeleteWorkspaceData, DeleteWorkspaceResponse, LeaveWorkspaceData, LeaveWorkspaceResponse, GetWorkspaceNameData, GetWorkspaceNameResponse, ChangeWorkspaceNameData, ChangeWorkspaceNameResponse, ChangeWorkspaceIdData, ChangeWorkspaceIdResponse, ChangeWorkspaceColorData, ChangeWorkspaceColorResponse, WhoisData, WhoisResponse, UpdateOperatorSettingsData, UpdateOperatorSettingsResponse, ExistsEmailData, ExistsEmailResponse, ListUsersAsSuperAdminData, ListUsersAsSuperAdminResponse, ListPendingInvitesData, ListPendingInvitesResponse, GetSettingsData, GetSettingsResponse, GetDeployToData, GetDeployToResponse, GetIsPremiumData, GetIsPremiumResponse, GetPremiumInfoData, GetPremiumInfoResponse, SetAutomaticBillingData, SetAutomaticBillingResponse, GetThresholdAlertData, GetThresholdAlertResponse, SetThresholdAlertData, SetThresholdAlertResponse, EditSlackCommandData, EditSlackCommandResponse, RunSlackMessageTestJobData, RunSlackMessageTestJobResponse, EditDeployToData, EditDeployToResponse, EditAutoInviteData, EditAutoInviteResponse, EditWebhookData, EditWebhookResponse, EditCopilotConfigData, EditCopilotConfigResponse, GetCopilotInfoData, GetCopilotInfoResponse, EditErrorHandlerData, EditErrorHandlerResponse, EditLargeFileStorageConfigData, EditLargeFileStorageConfigResponse, EditWorkspaceGitSyncConfigData, EditWorkspaceGitSyncConfigResponse, EditWorkspaceDeployUiSettingsData, EditWorkspaceDeployUiSettingsResponse, EditWorkspaceDefaultAppData, EditWorkspaceDefaultAppResponse, EditDefaultScriptsData, EditDefaultScriptsResponse, GetDefaultScriptsData, GetDefaultScriptsResponse, SetEnvironmentVariableData, SetEnvironmentVariableResponse, GetWorkspaceEncryptionKeyData, GetWorkspaceEncryptionKeyResponse, SetWorkspaceEncryptionKeyData, SetWorkspaceEncryptionKeyResponse, GetWorkspaceDefaultAppData, GetWorkspaceDefaultAppResponse, GetLargeFileStorageConfigData, GetLargeFileStorageConfigResponse, GetWorkspaceUsageData, GetWorkspaceUsageResponse, GetUsedTriggersData, GetUsedTriggersResponse, ListUsersData, ListUsersResponse, ListUsersUsageData, ListUsersUsageResponse, ListUsernamesData, ListUsernamesResponse, UsernameToEmailData, UsernameToEmailResponse, CreateTokenData, CreateTokenResponse, CreateTokenImpersonateData, CreateTokenImpersonateResponse, DeleteTokenData, DeleteTokenResponse, ListTokensData, ListTokensResponse, GetOidcTokenData, GetOidcTokenResponse, CreateVariableData, CreateVariableResponse, EncryptValueData, EncryptValueResponse, DeleteVariableData, DeleteVariableResponse, UpdateVariableData, UpdateVariableResponse, GetVariableData, GetVariableResponse, GetVariableValueData, GetVariableValueResponse, ExistsVariableData, ExistsVariableResponse, ListVariableData, ListVariableResponse, ListContextualVariablesData, ListContextualVariablesResponse, WorkspaceGetCriticalAlertsData, WorkspaceGetCriticalAlertsResponse, WorkspaceAcknowledgeCriticalAlertData, WorkspaceAcknowledgeCriticalAlertResponse, WorkspaceAcknowledgeAllCriticalAlertsData, WorkspaceAcknowledgeAllCriticalAlertsResponse, WorkspaceMuteCriticalAlertsUiData, WorkspaceMuteCriticalAlertsUiResponse, LoginWithOauthData, LoginWithOauthResponse, ConnectSlackCallbackData, ConnectSlackCallbackResponse, ConnectSlackCallbackInstanceData, ConnectSlackCallbackInstanceResponse, ConnectCallbackData, ConnectCallbackResponse, CreateAccountData, CreateAccountResponse, RefreshTokenData, RefreshTokenResponse, DisconnectAccountData, DisconnectAccountResponse, DisconnectSlackData, DisconnectSlackResponse, ListOauthLoginsResponse, ListOauthConnectsResponse, GetOauthConnectData, GetOauthConnectResponse, SyncTeamsResponse, CreateResourceData, CreateResourceResponse, DeleteResourceData, DeleteResourceResponse, UpdateResourceData, UpdateResourceResponse, UpdateResourceValueData, UpdateResourceValueResponse, GetResourceData, GetResourceResponse, GetResourceValueInterpolatedData, GetResourceValueInterpolatedResponse, GetResourceValueData, GetResourceValueResponse, ExistsResourceData, ExistsResourceResponse, ListResourceData, ListResourceResponse, ListSearchResourceData, ListSearchResourceResponse, ListResourceNamesData, ListResourceNamesResponse, CreateResourceTypeData, CreateResourceTypeResponse, FileResourceTypeToFileExtMapData, FileResourceTypeToFileExtMapResponse, DeleteResourceTypeData, DeleteResourceTypeResponse, UpdateResourceTypeData, UpdateResourceTypeResponse, GetResourceTypeData, GetResourceTypeResponse, ExistsResourceTypeData, ExistsResourceTypeResponse, ListResourceTypeData, ListResourceTypeResponse, ListResourceTypeNamesData, ListResourceTypeNamesResponse, QueryResourceTypesData, QueryResourceTypesResponse, ListHubIntegrationsData, ListHubIntegrationsResponse, ListHubFlowsResponse, GetHubFlowByIdData, GetHubFlowByIdResponse, ListHubAppsResponse, GetHubAppByIdData, GetHubAppByIdResponse, GetPublicAppByCustomPathData, GetPublicAppByCustomPathResponse, GetHubScriptContentByPathData, GetHubScriptContentByPathResponse, GetHubScriptByPathData, GetHubScriptByPathResponse, GetTopHubScriptsData, GetTopHubScriptsResponse, QueryHubScriptsData, QueryHubScriptsResponse, ListSearchScriptData, ListSearchScriptResponse, ListScriptsData, ListScriptsResponse, ListScriptPathsData, ListScriptPathsResponse, CreateDraftData, CreateDraftResponse, DeleteDraftData, DeleteDraftResponse, CreateScriptData, CreateScriptResponse, ToggleWorkspaceErrorHandlerForScriptData, ToggleWorkspaceErrorHandlerForScriptResponse, GetCustomTagsData, GetCustomTagsResponse, GeDefaultTagsResponse, IsDefaultTagsPerWorkspaceResponse, ArchiveScriptByPathData, ArchiveScriptByPathResponse, ArchiveScriptByHashData, ArchiveScriptByHashResponse, DeleteScriptByHashData, DeleteScriptByHashResponse, DeleteScriptByPathData, DeleteScriptByPathResponse, GetScriptByPathData, GetScriptByPathResponse, GetTriggersCountOfScriptData, GetTriggersCountOfScriptResponse, ListTokensOfScriptData, ListTokensOfScriptResponse, GetScriptByPathWithDraftData, GetScriptByPathWithDraftResponse, GetScriptHistoryByPathData, GetScriptHistoryByPathResponse, GetScriptLatestVersionData, GetScriptLatestVersionResponse, UpdateScriptHistoryData, UpdateScriptHistoryResponse, RawScriptByPathData, RawScriptByPathResponse, RawScriptByPathTokenedData, RawScriptByPathTokenedResponse, ExistsScriptByPathData, ExistsScriptByPathResponse, GetScriptByHashData, GetScriptByHashResponse, RawScriptByHashData, RawScriptByHashResponse, GetScriptDeploymentStatusData, GetScriptDeploymentStatusResponse, RunScriptByPathData, RunScriptByPathResponse, OpenaiSyncScriptByPathData, OpenaiSyncScriptByPathResponse, RunWaitResultScriptByPathData, RunWaitResultScriptByPathResponse, RunWaitResultScriptByPathGetData, RunWaitResultScriptByPathGetResponse, OpenaiSyncFlowByPathData, OpenaiSyncFlowByPathResponse, RunWaitResultFlowByPathData, RunWaitResultFlowByPathResponse, ResultByIdData, ResultByIdResponse, ListFlowPathsData, ListFlowPathsResponse, ListSearchFlowData, ListSearchFlowResponse, ListFlowsData, ListFlowsResponse, GetFlowHistoryData, GetFlowHistoryResponse, GetFlowLatestVersionData, GetFlowLatestVersionResponse, GetFlowVersionData, GetFlowVersionResponse, UpdateFlowHistoryData, UpdateFlowHistoryResponse, GetFlowByPathData, GetFlowByPathResponse, GetTriggersCountOfFlowData, GetTriggersCountOfFlowResponse, ListTokensOfFlowData, ListTokensOfFlowResponse, ToggleWorkspaceErrorHandlerForFlowData, ToggleWorkspaceErrorHandlerForFlowResponse, GetFlowByPathWithDraftData, GetFlowByPathWithDraftResponse, ExistsFlowByPathData, ExistsFlowByPathResponse, CreateFlowData, CreateFlowResponse, UpdateFlowData, UpdateFlowResponse, ArchiveFlowByPathData, ArchiveFlowByPathResponse, DeleteFlowByPathData, DeleteFlowByPathResponse, ListRawAppsData, ListRawAppsResponse, ExistsRawAppData, ExistsRawAppResponse, GetRawAppDataData, GetRawAppDataResponse, ListSearchAppData, ListSearchAppResponse, ListAppsData, ListAppsResponse, CreateAppData, CreateAppResponse, ExistsAppData, ExistsAppResponse, GetAppByPathData, GetAppByPathResponse, GetAppLiteByPathData, GetAppLiteByPathResponse, GetAppByPathWithDraftData, GetAppByPathWithDraftResponse, GetAppHistoryByPathData, GetAppHistoryByPathResponse, GetAppLatestVersionData, GetAppLatestVersionResponse, UpdateAppHistoryData, UpdateAppHistoryResponse, GetPublicAppBySecretData, GetPublicAppBySecretResponse, GetPublicResourceData, GetPublicResourceResponse, GetPublicSecretOfAppData, GetPublicSecretOfAppResponse, GetAppByVersionData, GetAppByVersionResponse, CreateRawAppData, CreateRawAppResponse, UpdateRawAppData, UpdateRawAppResponse, DeleteRawAppData, DeleteRawAppResponse, DeleteAppData, DeleteAppResponse, UpdateAppData, UpdateAppResponse, CustomPathExistsData, CustomPathExistsResponse, ExecuteComponentData, ExecuteComponentResponse, RunFlowByPathData, RunFlowByPathResponse, RestartFlowAtStepData, RestartFlowAtStepResponse, RunScriptByHashData, RunScriptByHashResponse, RunScriptPreviewData, RunScriptPreviewResponse, RunCodeWorkflowTaskData, RunCodeWorkflowTaskResponse, RunRawScriptDependenciesData, RunRawScriptDependenciesResponse, RunFlowPreviewData, RunFlowPreviewResponse, ListQueueData, ListQueueResponse, GetQueueCountData, GetQueueCountResponse, GetCompletedCountData, GetCompletedCountResponse, CountCompletedJobsData, CountCompletedJobsResponse, ListFilteredUuidsData, ListFilteredUuidsResponse, CancelSelectionData, CancelSelectionResponse, ListCompletedJobsData, ListCompletedJobsResponse, ListJobsData, ListJobsResponse, GetDbClockResponse, CountJobsByTagData, CountJobsByTagResponse, GetJobData, GetJobResponse, GetRootJobIdData, GetRootJobIdResponse, GetJobLogsData, GetJobLogsResponse, GetJobArgsData, GetJobArgsResponse, GetJobUpdatesData, GetJobUpdatesResponse, GetLogFileFromStoreData, GetLogFileFromStoreResponse, GetFlowDebugInfoData, GetFlowDebugInfoResponse, GetCompletedJobData, GetCompletedJobResponse, GetCompletedJobResultData, GetCompletedJobResultResponse, GetCompletedJobResultMaybeData, GetCompletedJobResultMaybeResponse, DeleteCompletedJobData, DeleteCompletedJobResponse, CancelQueuedJobData, CancelQueuedJobResponse, CancelPersistentQueuedJobsData, CancelPersistentQueuedJobsResponse, ForceCancelQueuedJobData, ForceCancelQueuedJobResponse, CreateJobSignatureData, CreateJobSignatureResponse, GetResumeUrlsData, GetResumeUrlsResponse, GetSlackApprovalPayloadData, GetSlackApprovalPayloadResponse, ResumeSuspendedJobGetData, ResumeSuspendedJobGetResponse, ResumeSuspendedJobPostData, ResumeSuspendedJobPostResponse, SetFlowUserStateData, SetFlowUserStateResponse, GetFlowUserStateData, GetFlowUserStateResponse, ResumeSuspendedFlowAsOwnerData, ResumeSuspendedFlowAsOwnerResponse, CancelSuspendedJobGetData, CancelSuspendedJobGetResponse, CancelSuspendedJobPostData, CancelSuspendedJobPostResponse, GetSuspendedJobFlowData, GetSuspendedJobFlowResponse, PreviewScheduleData, PreviewScheduleResponse, CreateScheduleData, CreateScheduleResponse, UpdateScheduleData, UpdateScheduleResponse, SetScheduleEnabledData, SetScheduleEnabledResponse, DeleteScheduleData, DeleteScheduleResponse, GetScheduleData, GetScheduleResponse, ExistsScheduleData, ExistsScheduleResponse, ListSchedulesData, ListSchedulesResponse, ListSchedulesWithJobsData, ListSchedulesWithJobsResponse, SetDefaultErrorOrRecoveryHandlerData, SetDefaultErrorOrRecoveryHandlerResponse, CreateHttpTriggerData, CreateHttpTriggerResponse, UpdateHttpTriggerData, UpdateHttpTriggerResponse, DeleteHttpTriggerData, DeleteHttpTriggerResponse, GetHttpTriggerData, GetHttpTriggerResponse, ListHttpTriggersData, ListHttpTriggersResponse, ExistsHttpTriggerData, ExistsHttpTriggerResponse, ExistsRouteData, ExistsRouteResponse, CreateWebsocketTriggerData, CreateWebsocketTriggerResponse, UpdateWebsocketTriggerData, UpdateWebsocketTriggerResponse, DeleteWebsocketTriggerData, DeleteWebsocketTriggerResponse, GetWebsocketTriggerData, GetWebsocketTriggerResponse, ListWebsocketTriggersData, ListWebsocketTriggersResponse, ExistsWebsocketTriggerData, ExistsWebsocketTriggerResponse, SetWebsocketTriggerEnabledData, SetWebsocketTriggerEnabledResponse, TestWebsocketConnectionData, TestWebsocketConnectionResponse, CreateKafkaTriggerData, CreateKafkaTriggerResponse, UpdateKafkaTriggerData, UpdateKafkaTriggerResponse, DeleteKafkaTriggerData, DeleteKafkaTriggerResponse, GetKafkaTriggerData, GetKafkaTriggerResponse, ListKafkaTriggersData, ListKafkaTriggersResponse, ExistsKafkaTriggerData, ExistsKafkaTriggerResponse, SetKafkaTriggerEnabledData, SetKafkaTriggerEnabledResponse, TestKafkaConnectionData, TestKafkaConnectionResponse, CreateNatsTriggerData, CreateNatsTriggerResponse, UpdateNatsTriggerData, UpdateNatsTriggerResponse, DeleteNatsTriggerData, DeleteNatsTriggerResponse, GetNatsTriggerData, GetNatsTriggerResponse, ListNatsTriggersData, ListNatsTriggersResponse, ExistsNatsTriggerData, ExistsNatsTriggerResponse, SetNatsTriggerEnabledData, SetNatsTriggerEnabledResponse, TestNatsConnectionData, TestNatsConnectionResponse, CreateSqsTriggerData, CreateSqsTriggerResponse, UpdateSqsTriggerData, UpdateSqsTriggerResponse, DeleteSqsTriggerData, DeleteSqsTriggerResponse, GetSqsTriggerData, GetSqsTriggerResponse, ListSqsTriggersData, ListSqsTriggersResponse, ExistsSqsTriggerData, ExistsSqsTriggerResponse, SetSqsTriggerEnabledData, SetSqsTriggerEnabledResponse, TestSqsConnectionData, TestSqsConnectionResponse, IsValidPostgresConfigurationData, IsValidPostgresConfigurationResponse, CreateTemplateScriptData, CreateTemplateScriptResponse, GetTemplateScriptData, GetTemplateScriptResponse, ListPostgresReplicationSlotData, ListPostgresReplicationSlotResponse, CreatePostgresReplicationSlotData, CreatePostgresReplicationSlotResponse, DeletePostgresReplicationSlotData, DeletePostgresReplicationSlotResponse, ListPostgresPublicationData, ListPostgresPublicationResponse, GetPostgresPublicationData, GetPostgresPublicationResponse, CreatePostgresPublicationData, CreatePostgresPublicationResponse, UpdatePostgresPublicationData, UpdatePostgresPublicationResponse, DeletePostgresPublicationData, DeletePostgresPublicationResponse, CreatePostgresTriggerData, CreatePostgresTriggerResponse, UpdatePostgresTriggerData, UpdatePostgresTriggerResponse, DeletePostgresTriggerData, DeletePostgresTriggerResponse, GetPostgresTriggerData, GetPostgresTriggerResponse, ListPostgresTriggersData, ListPostgresTriggersResponse, ExistsPostgresTriggerData, ExistsPostgresTriggerResponse, SetPostgresTriggerEnabledData, SetPostgresTriggerEnabledResponse, TestPostgresConnectionData, TestPostgresConnectionResponse, ListInstanceGroupsResponse, GetInstanceGroupData, GetInstanceGroupResponse, CreateInstanceGroupData, CreateInstanceGroupResponse, UpdateInstanceGroupData, UpdateInstanceGroupResponse, DeleteInstanceGroupData, DeleteInstanceGroupResponse, AddUserToInstanceGroupData, AddUserToInstanceGroupResponse, RemoveUserFromInstanceGroupData, RemoveUserFromInstanceGroupResponse, ExportInstanceGroupsResponse, OverwriteInstanceGroupsData, OverwriteInstanceGroupsResponse, ListGroupsData, ListGroupsResponse, ListGroupNamesData, ListGroupNamesResponse, CreateGroupData, CreateGroupResponse, UpdateGroupData, UpdateGroupResponse, DeleteGroupData, DeleteGroupResponse, GetGroupData, GetGroupResponse, AddUserToGroupData, AddUserToGroupResponse, RemoveUserToGroupData, RemoveUserToGroupResponse, ListFoldersData, ListFoldersResponse, ListFolderNamesData, ListFolderNamesResponse, CreateFolderData, CreateFolderResponse, UpdateFolderData, UpdateFolderResponse, DeleteFolderData, DeleteFolderResponse, GetFolderData, GetFolderResponse, GetFolderUsageData, GetFolderUsageResponse, AddOwnerToFolderData, AddOwnerToFolderResponse, RemoveOwnerToFolderData, RemoveOwnerToFolderResponse, ListWorkersData, ListWorkersResponse, ExistsWorkerWithTagData, ExistsWorkerWithTagResponse, GetQueueMetricsResponse, GetCountsOfJobsWaitingPerTagResponse, ListWorkerGroupsResponse, GetConfigData, GetConfigResponse, UpdateConfigData, UpdateConfigResponse, DeleteConfigData, DeleteConfigResponse, ListConfigsResponse, ListAutoscalingEventsData, ListAutoscalingEventsResponse, GetGranularAclsData, GetGranularAclsResponse, AddGranularAclsData, AddGranularAclsResponse, RemoveGranularAclsData, RemoveGranularAclsResponse, SetCaptureConfigData, SetCaptureConfigResponse, PingCaptureConfigData, PingCaptureConfigResponse, GetCaptureConfigsData, GetCaptureConfigsResponse, ListCapturesData, ListCapturesResponse, GetCaptureData, GetCaptureResponse, DeleteCaptureData, DeleteCaptureResponse, StarData, StarResponse, UnstarData, UnstarResponse, GetInputHistoryData, GetInputHistoryResponse, GetArgsFromHistoryOrSavedInputData, GetArgsFromHistoryOrSavedInputResponse, ListInputsData, ListInputsResponse, CreateInputData, CreateInputResponse, UpdateInputData, UpdateInputResponse, DeleteInputData, DeleteInputResponse, DuckdbConnectionSettingsData, DuckdbConnectionSettingsResponse, DuckdbConnectionSettingsV2Data, DuckdbConnectionSettingsV2Response, PolarsConnectionSettingsData, PolarsConnectionSettingsResponse, PolarsConnectionSettingsV2Data, PolarsConnectionSettingsV2Response, S3ResourceInfoData, S3ResourceInfoResponse, DatasetStorageTestConnectionData, DatasetStorageTestConnectionResponse, ListStoredFilesData, ListStoredFilesResponse, LoadFileMetadataData, LoadFileMetadataResponse, LoadFilePreviewData, LoadFilePreviewResponse, LoadParquetPreviewData, LoadParquetPreviewResponse, LoadTableRowCountData, LoadTableRowCountResponse, LoadCsvPreviewData, LoadCsvPreviewResponse, DeleteS3FileData, DeleteS3FileResponse, MoveS3FileData, MoveS3FileResponse, FileUploadData, FileUploadResponse, FileDownloadData, FileDownloadResponse, FileDownloadParquetAsCsvData, FileDownloadParquetAsCsvResponse, GetJobMetricsData, GetJobMetricsResponse, SetJobProgressData, SetJobProgressResponse, GetJobProgressData, GetJobProgressResponse, ListLogFilesData, ListLogFilesResponse, GetLogFileData, GetLogFileResponse, ListConcurrencyGroupsResponse, DeleteConcurrencyGroupData, DeleteConcurrencyGroupResponse, GetConcurrencyKeyData, GetConcurrencyKeyResponse, ListExtendedJobsData, ListExtendedJobsResponse, SearchJobsIndexData, SearchJobsIndexResponse, SearchLogsIndexData, SearchLogsIndexResponse, CountSearchLogsIndexData, CountSearchLogsIndexResponse, ClearIndexData, ClearIndexResponse } from './types.gen.ts'; +import type { BackendVersionResponse, BackendUptodateResponse, GetLicenseIdResponse, GetOpenApiYamlResponse, GetAuditLogData, GetAuditLogResponse, ListAuditLogsData, ListAuditLogsResponse, LoginData, LoginResponse, LogoutResponse, GetUserData, GetUserResponse, UpdateUserData, UpdateUserResponse, IsOwnerOfPathData, IsOwnerOfPathResponse, SetPasswordData, SetPasswordResponse, SetPasswordForUserData, SetPasswordForUserResponse, SetLoginTypeForUserData, SetLoginTypeForUserResponse, CreateUserGloballyData, CreateUserGloballyResponse, GlobalUserUpdateData, GlobalUserUpdateResponse, GlobalUsernameInfoData, GlobalUsernameInfoResponse, GlobalUserRenameData, GlobalUserRenameResponse, GlobalUserDeleteData, GlobalUserDeleteResponse, GlobalUsersOverwriteData, GlobalUsersOverwriteResponse, GlobalUsersExportResponse, DeleteUserData, DeleteUserResponse, ListWorkspacesResponse, IsDomainAllowedResponse, ListUserWorkspacesResponse, ListWorkspacesAsSuperAdminData, ListWorkspacesAsSuperAdminResponse, CreateWorkspaceData, CreateWorkspaceResponse, ExistsWorkspaceData, ExistsWorkspaceResponse, ExistsUsernameData, ExistsUsernameResponse, GetGlobalData, GetGlobalResponse, SetGlobalData, SetGlobalResponse, GetLocalResponse, TestSmtpData, TestSmtpResponse, TestCriticalChannelsData, TestCriticalChannelsResponse, GetCriticalAlertsData, GetCriticalAlertsResponse, AcknowledgeCriticalAlertData, AcknowledgeCriticalAlertResponse, AcknowledgeAllCriticalAlertsResponse, TestLicenseKeyData, TestLicenseKeyResponse, TestObjectStorageConfigData, TestObjectStorageConfigResponse, SendStatsResponse, GetLatestKeyRenewalAttemptResponse, RenewLicenseKeyData, RenewLicenseKeyResponse, CreateCustomerPortalSessionData, CreateCustomerPortalSessionResponse, TestMetadataData, TestMetadataResponse, ListGlobalSettingsResponse, GetCurrentEmailResponse, RefreshUserTokenData, RefreshUserTokenResponse, GetTutorialProgressResponse, UpdateTutorialProgressData, UpdateTutorialProgressResponse, LeaveInstanceResponse, GetUsageResponse, GetRunnableResponse, GlobalWhoamiResponse, ListWorkspaceInvitesResponse, WhoamiData, WhoamiResponse, AcceptInviteData, AcceptInviteResponse, DeclineInviteData, DeclineInviteResponse, InviteUserData, InviteUserResponse, AddUserData, AddUserResponse, DeleteInviteData, DeleteInviteResponse, ArchiveWorkspaceData, ArchiveWorkspaceResponse, UnarchiveWorkspaceData, UnarchiveWorkspaceResponse, DeleteWorkspaceData, DeleteWorkspaceResponse, LeaveWorkspaceData, LeaveWorkspaceResponse, GetWorkspaceNameData, GetWorkspaceNameResponse, ChangeWorkspaceNameData, ChangeWorkspaceNameResponse, ChangeWorkspaceIdData, ChangeWorkspaceIdResponse, ChangeWorkspaceColorData, ChangeWorkspaceColorResponse, WhoisData, WhoisResponse, UpdateOperatorSettingsData, UpdateOperatorSettingsResponse, ExistsEmailData, ExistsEmailResponse, ListUsersAsSuperAdminData, ListUsersAsSuperAdminResponse, ListPendingInvitesData, ListPendingInvitesResponse, GetSettingsData, GetSettingsResponse, GetDeployToData, GetDeployToResponse, GetIsPremiumData, GetIsPremiumResponse, GetPremiumInfoData, GetPremiumInfoResponse, SetAutomaticBillingData, SetAutomaticBillingResponse, GetThresholdAlertData, GetThresholdAlertResponse, SetThresholdAlertData, SetThresholdAlertResponse, EditSlackCommandData, EditSlackCommandResponse, EditTeamsCommandData, EditTeamsCommandResponse, ListAvailableTeamsIdsData, ListAvailableTeamsIdsResponse, ListAvailableTeamsChannelsData, ListAvailableTeamsChannelsResponse, ConnectTeamsData, ConnectTeamsResponse, RunSlackMessageTestJobData, RunSlackMessageTestJobResponse, RunTeamsMessageTestJobData, RunTeamsMessageTestJobResponse, EditDeployToData, EditDeployToResponse, EditAutoInviteData, EditAutoInviteResponse, EditWebhookData, EditWebhookResponse, EditCopilotConfigData, EditCopilotConfigResponse, GetCopilotInfoData, GetCopilotInfoResponse, EditErrorHandlerData, EditErrorHandlerResponse, EditLargeFileStorageConfigData, EditLargeFileStorageConfigResponse, EditWorkspaceGitSyncConfigData, EditWorkspaceGitSyncConfigResponse, EditWorkspaceDeployUiSettingsData, EditWorkspaceDeployUiSettingsResponse, EditWorkspaceDefaultAppData, EditWorkspaceDefaultAppResponse, EditDefaultScriptsData, EditDefaultScriptsResponse, GetDefaultScriptsData, GetDefaultScriptsResponse, SetEnvironmentVariableData, SetEnvironmentVariableResponse, GetWorkspaceEncryptionKeyData, GetWorkspaceEncryptionKeyResponse, SetWorkspaceEncryptionKeyData, SetWorkspaceEncryptionKeyResponse, GetWorkspaceDefaultAppData, GetWorkspaceDefaultAppResponse, GetLargeFileStorageConfigData, GetLargeFileStorageConfigResponse, GetWorkspaceUsageData, GetWorkspaceUsageResponse, GetUsedTriggersData, GetUsedTriggersResponse, ListUsersData, ListUsersResponse, ListUsersUsageData, ListUsersUsageResponse, ListUsernamesData, ListUsernamesResponse, UsernameToEmailData, UsernameToEmailResponse, CreateTokenData, CreateTokenResponse, CreateTokenImpersonateData, CreateTokenImpersonateResponse, DeleteTokenData, DeleteTokenResponse, ListTokensData, ListTokensResponse, GetOidcTokenData, GetOidcTokenResponse, CreateVariableData, CreateVariableResponse, EncryptValueData, EncryptValueResponse, DeleteVariableData, DeleteVariableResponse, UpdateVariableData, UpdateVariableResponse, GetVariableData, GetVariableResponse, GetVariableValueData, GetVariableValueResponse, ExistsVariableData, ExistsVariableResponse, ListVariableData, ListVariableResponse, ListContextualVariablesData, ListContextualVariablesResponse, WorkspaceGetCriticalAlertsData, WorkspaceGetCriticalAlertsResponse, WorkspaceAcknowledgeCriticalAlertData, WorkspaceAcknowledgeCriticalAlertResponse, WorkspaceAcknowledgeAllCriticalAlertsData, WorkspaceAcknowledgeAllCriticalAlertsResponse, WorkspaceMuteCriticalAlertsUiData, WorkspaceMuteCriticalAlertsUiResponse, LoginWithOauthData, LoginWithOauthResponse, ConnectSlackCallbackData, ConnectSlackCallbackResponse, ConnectSlackCallbackInstanceData, ConnectSlackCallbackInstanceResponse, ConnectCallbackData, ConnectCallbackResponse, CreateAccountData, CreateAccountResponse, RefreshTokenData, RefreshTokenResponse, DisconnectAccountData, DisconnectAccountResponse, DisconnectSlackData, DisconnectSlackResponse, DisconnectTeamsData, DisconnectTeamsResponse, ListOauthLoginsResponse, ListOauthConnectsResponse, GetOauthConnectData, GetOauthConnectResponse, SyncTeamsResponse, SendMessageToConversationData, SendMessageToConversationResponse, CreateResourceData, CreateResourceResponse, DeleteResourceData, DeleteResourceResponse, UpdateResourceData, UpdateResourceResponse, UpdateResourceValueData, UpdateResourceValueResponse, GetResourceData, GetResourceResponse, GetResourceValueInterpolatedData, GetResourceValueInterpolatedResponse, GetResourceValueData, GetResourceValueResponse, ExistsResourceData, ExistsResourceResponse, ListResourceData, ListResourceResponse, ListSearchResourceData, ListSearchResourceResponse, ListResourceNamesData, ListResourceNamesResponse, CreateResourceTypeData, CreateResourceTypeResponse, FileResourceTypeToFileExtMapData, FileResourceTypeToFileExtMapResponse, DeleteResourceTypeData, DeleteResourceTypeResponse, UpdateResourceTypeData, UpdateResourceTypeResponse, GetResourceTypeData, GetResourceTypeResponse, ExistsResourceTypeData, ExistsResourceTypeResponse, ListResourceTypeData, ListResourceTypeResponse, ListResourceTypeNamesData, ListResourceTypeNamesResponse, QueryResourceTypesData, QueryResourceTypesResponse, ListHubIntegrationsData, ListHubIntegrationsResponse, ListHubFlowsResponse, GetHubFlowByIdData, GetHubFlowByIdResponse, ListHubAppsResponse, GetHubAppByIdData, GetHubAppByIdResponse, GetPublicAppByCustomPathData, GetPublicAppByCustomPathResponse, GetHubScriptContentByPathData, GetHubScriptContentByPathResponse, GetHubScriptByPathData, GetHubScriptByPathResponse, GetTopHubScriptsData, GetTopHubScriptsResponse, QueryHubScriptsData, QueryHubScriptsResponse, ListSearchScriptData, ListSearchScriptResponse, ListScriptsData, ListScriptsResponse, ListScriptPathsData, ListScriptPathsResponse, CreateDraftData, CreateDraftResponse, DeleteDraftData, DeleteDraftResponse, CreateScriptData, CreateScriptResponse, ToggleWorkspaceErrorHandlerForScriptData, ToggleWorkspaceErrorHandlerForScriptResponse, GetCustomTagsData, GetCustomTagsResponse, GeDefaultTagsResponse, IsDefaultTagsPerWorkspaceResponse, ArchiveScriptByPathData, ArchiveScriptByPathResponse, ArchiveScriptByHashData, ArchiveScriptByHashResponse, DeleteScriptByHashData, DeleteScriptByHashResponse, DeleteScriptByPathData, DeleteScriptByPathResponse, GetScriptByPathData, GetScriptByPathResponse, GetTriggersCountOfScriptData, GetTriggersCountOfScriptResponse, ListTokensOfScriptData, ListTokensOfScriptResponse, GetScriptByPathWithDraftData, GetScriptByPathWithDraftResponse, GetScriptHistoryByPathData, GetScriptHistoryByPathResponse, GetScriptLatestVersionData, GetScriptLatestVersionResponse, UpdateScriptHistoryData, UpdateScriptHistoryResponse, RawScriptByPathData, RawScriptByPathResponse, RawScriptByPathTokenedData, RawScriptByPathTokenedResponse, ExistsScriptByPathData, ExistsScriptByPathResponse, GetScriptByHashData, GetScriptByHashResponse, RawScriptByHashData, RawScriptByHashResponse, GetScriptDeploymentStatusData, GetScriptDeploymentStatusResponse, RunScriptByPathData, RunScriptByPathResponse, OpenaiSyncScriptByPathData, OpenaiSyncScriptByPathResponse, RunWaitResultScriptByPathData, RunWaitResultScriptByPathResponse, RunWaitResultScriptByPathGetData, RunWaitResultScriptByPathGetResponse, OpenaiSyncFlowByPathData, OpenaiSyncFlowByPathResponse, RunWaitResultFlowByPathData, RunWaitResultFlowByPathResponse, ResultByIdData, ResultByIdResponse, ListFlowPathsData, ListFlowPathsResponse, ListSearchFlowData, ListSearchFlowResponse, ListFlowsData, ListFlowsResponse, GetFlowHistoryData, GetFlowHistoryResponse, GetFlowLatestVersionData, GetFlowLatestVersionResponse, GetFlowVersionData, GetFlowVersionResponse, UpdateFlowHistoryData, UpdateFlowHistoryResponse, GetFlowByPathData, GetFlowByPathResponse, GetTriggersCountOfFlowData, GetTriggersCountOfFlowResponse, ListTokensOfFlowData, ListTokensOfFlowResponse, ToggleWorkspaceErrorHandlerForFlowData, ToggleWorkspaceErrorHandlerForFlowResponse, GetFlowByPathWithDraftData, GetFlowByPathWithDraftResponse, ExistsFlowByPathData, ExistsFlowByPathResponse, CreateFlowData, CreateFlowResponse, UpdateFlowData, UpdateFlowResponse, ArchiveFlowByPathData, ArchiveFlowByPathResponse, DeleteFlowByPathData, DeleteFlowByPathResponse, ListRawAppsData, ListRawAppsResponse, ExistsRawAppData, ExistsRawAppResponse, GetRawAppDataData, GetRawAppDataResponse, ListSearchAppData, ListSearchAppResponse, ListAppsData, ListAppsResponse, CreateAppData, CreateAppResponse, ExistsAppData, ExistsAppResponse, GetAppByPathData, GetAppByPathResponse, GetAppLiteByPathData, GetAppLiteByPathResponse, GetAppByPathWithDraftData, GetAppByPathWithDraftResponse, GetAppHistoryByPathData, GetAppHistoryByPathResponse, GetAppLatestVersionData, GetAppLatestVersionResponse, UpdateAppHistoryData, UpdateAppHistoryResponse, GetPublicAppBySecretData, GetPublicAppBySecretResponse, GetPublicResourceData, GetPublicResourceResponse, GetPublicSecretOfAppData, GetPublicSecretOfAppResponse, GetAppByVersionData, GetAppByVersionResponse, CreateRawAppData, CreateRawAppResponse, UpdateRawAppData, UpdateRawAppResponse, DeleteRawAppData, DeleteRawAppResponse, DeleteAppData, DeleteAppResponse, UpdateAppData, UpdateAppResponse, CustomPathExistsData, CustomPathExistsResponse, ExecuteComponentData, ExecuteComponentResponse, RunFlowByPathData, RunFlowByPathResponse, RestartFlowAtStepData, RestartFlowAtStepResponse, RunScriptByHashData, RunScriptByHashResponse, RunScriptPreviewData, RunScriptPreviewResponse, RunCodeWorkflowTaskData, RunCodeWorkflowTaskResponse, RunRawScriptDependenciesData, RunRawScriptDependenciesResponse, RunFlowPreviewData, RunFlowPreviewResponse, ListQueueData, ListQueueResponse, GetQueueCountData, GetQueueCountResponse, GetCompletedCountData, GetCompletedCountResponse, CountCompletedJobsData, CountCompletedJobsResponse, ListFilteredUuidsData, ListFilteredUuidsResponse, CancelSelectionData, CancelSelectionResponse, ListCompletedJobsData, ListCompletedJobsResponse, ListJobsData, ListJobsResponse, GetDbClockResponse, CountJobsByTagData, CountJobsByTagResponse, GetJobData, GetJobResponse, GetRootJobIdData, GetRootJobIdResponse, GetJobLogsData, GetJobLogsResponse, GetJobArgsData, GetJobArgsResponse, GetJobUpdatesData, GetJobUpdatesResponse, GetLogFileFromStoreData, GetLogFileFromStoreResponse, GetFlowDebugInfoData, GetFlowDebugInfoResponse, GetCompletedJobData, GetCompletedJobResponse, GetCompletedJobResultData, GetCompletedJobResultResponse, GetCompletedJobResultMaybeData, GetCompletedJobResultMaybeResponse, DeleteCompletedJobData, DeleteCompletedJobResponse, CancelQueuedJobData, CancelQueuedJobResponse, CancelPersistentQueuedJobsData, CancelPersistentQueuedJobsResponse, ForceCancelQueuedJobData, ForceCancelQueuedJobResponse, CreateJobSignatureData, CreateJobSignatureResponse, GetResumeUrlsData, GetResumeUrlsResponse, GetSlackApprovalPayloadData, GetSlackApprovalPayloadResponse, ResumeSuspendedJobGetData, ResumeSuspendedJobGetResponse, ResumeSuspendedJobPostData, ResumeSuspendedJobPostResponse, SetFlowUserStateData, SetFlowUserStateResponse, GetFlowUserStateData, GetFlowUserStateResponse, ResumeSuspendedFlowAsOwnerData, ResumeSuspendedFlowAsOwnerResponse, CancelSuspendedJobGetData, CancelSuspendedJobGetResponse, CancelSuspendedJobPostData, CancelSuspendedJobPostResponse, GetSuspendedJobFlowData, GetSuspendedJobFlowResponse, PreviewScheduleData, PreviewScheduleResponse, CreateScheduleData, CreateScheduleResponse, UpdateScheduleData, UpdateScheduleResponse, SetScheduleEnabledData, SetScheduleEnabledResponse, DeleteScheduleData, DeleteScheduleResponse, GetScheduleData, GetScheduleResponse, ExistsScheduleData, ExistsScheduleResponse, ListSchedulesData, ListSchedulesResponse, ListSchedulesWithJobsData, ListSchedulesWithJobsResponse, SetDefaultErrorOrRecoveryHandlerData, SetDefaultErrorOrRecoveryHandlerResponse, CreateHttpTriggerData, CreateHttpTriggerResponse, UpdateHttpTriggerData, UpdateHttpTriggerResponse, DeleteHttpTriggerData, DeleteHttpTriggerResponse, GetHttpTriggerData, GetHttpTriggerResponse, ListHttpTriggersData, ListHttpTriggersResponse, ExistsHttpTriggerData, ExistsHttpTriggerResponse, ExistsRouteData, ExistsRouteResponse, CreateWebsocketTriggerData, CreateWebsocketTriggerResponse, UpdateWebsocketTriggerData, UpdateWebsocketTriggerResponse, DeleteWebsocketTriggerData, DeleteWebsocketTriggerResponse, GetWebsocketTriggerData, GetWebsocketTriggerResponse, ListWebsocketTriggersData, ListWebsocketTriggersResponse, ExistsWebsocketTriggerData, ExistsWebsocketTriggerResponse, SetWebsocketTriggerEnabledData, SetWebsocketTriggerEnabledResponse, TestWebsocketConnectionData, TestWebsocketConnectionResponse, CreateKafkaTriggerData, CreateKafkaTriggerResponse, UpdateKafkaTriggerData, UpdateKafkaTriggerResponse, DeleteKafkaTriggerData, DeleteKafkaTriggerResponse, GetKafkaTriggerData, GetKafkaTriggerResponse, ListKafkaTriggersData, ListKafkaTriggersResponse, ExistsKafkaTriggerData, ExistsKafkaTriggerResponse, SetKafkaTriggerEnabledData, SetKafkaTriggerEnabledResponse, TestKafkaConnectionData, TestKafkaConnectionResponse, CreateNatsTriggerData, CreateNatsTriggerResponse, UpdateNatsTriggerData, UpdateNatsTriggerResponse, DeleteNatsTriggerData, DeleteNatsTriggerResponse, GetNatsTriggerData, GetNatsTriggerResponse, ListNatsTriggersData, ListNatsTriggersResponse, ExistsNatsTriggerData, ExistsNatsTriggerResponse, SetNatsTriggerEnabledData, SetNatsTriggerEnabledResponse, TestNatsConnectionData, TestNatsConnectionResponse, CreateSqsTriggerData, CreateSqsTriggerResponse, UpdateSqsTriggerData, UpdateSqsTriggerResponse, DeleteSqsTriggerData, DeleteSqsTriggerResponse, GetSqsTriggerData, GetSqsTriggerResponse, ListSqsTriggersData, ListSqsTriggersResponse, ExistsSqsTriggerData, ExistsSqsTriggerResponse, SetSqsTriggerEnabledData, SetSqsTriggerEnabledResponse, TestSqsConnectionData, TestSqsConnectionResponse, CreateMqttTriggerData, CreateMqttTriggerResponse, UpdateMqttTriggerData, UpdateMqttTriggerResponse, DeleteMqttTriggerData, DeleteMqttTriggerResponse, GetMqttTriggerData, GetMqttTriggerResponse, ListMqttTriggersData, ListMqttTriggersResponse, ExistsMqttTriggerData, ExistsMqttTriggerResponse, SetMqttTriggerEnabledData, SetMqttTriggerEnabledResponse, TestMqttConnectionData, TestMqttConnectionResponse, IsValidPostgresConfigurationData, IsValidPostgresConfigurationResponse, CreateTemplateScriptData, CreateTemplateScriptResponse, GetTemplateScriptData, GetTemplateScriptResponse, ListPostgresReplicationSlotData, ListPostgresReplicationSlotResponse, CreatePostgresReplicationSlotData, CreatePostgresReplicationSlotResponse, DeletePostgresReplicationSlotData, DeletePostgresReplicationSlotResponse, ListPostgresPublicationData, ListPostgresPublicationResponse, GetPostgresPublicationData, GetPostgresPublicationResponse, CreatePostgresPublicationData, CreatePostgresPublicationResponse, UpdatePostgresPublicationData, UpdatePostgresPublicationResponse, DeletePostgresPublicationData, DeletePostgresPublicationResponse, CreatePostgresTriggerData, CreatePostgresTriggerResponse, UpdatePostgresTriggerData, UpdatePostgresTriggerResponse, DeletePostgresTriggerData, DeletePostgresTriggerResponse, GetPostgresTriggerData, GetPostgresTriggerResponse, ListPostgresTriggersData, ListPostgresTriggersResponse, ExistsPostgresTriggerData, ExistsPostgresTriggerResponse, SetPostgresTriggerEnabledData, SetPostgresTriggerEnabledResponse, TestPostgresConnectionData, TestPostgresConnectionResponse, ListInstanceGroupsResponse, GetInstanceGroupData, GetInstanceGroupResponse, CreateInstanceGroupData, CreateInstanceGroupResponse, UpdateInstanceGroupData, UpdateInstanceGroupResponse, DeleteInstanceGroupData, DeleteInstanceGroupResponse, AddUserToInstanceGroupData, AddUserToInstanceGroupResponse, RemoveUserFromInstanceGroupData, RemoveUserFromInstanceGroupResponse, ExportInstanceGroupsResponse, OverwriteInstanceGroupsData, OverwriteInstanceGroupsResponse, ListGroupsData, ListGroupsResponse, ListGroupNamesData, ListGroupNamesResponse, CreateGroupData, CreateGroupResponse, UpdateGroupData, UpdateGroupResponse, DeleteGroupData, DeleteGroupResponse, GetGroupData, GetGroupResponse, AddUserToGroupData, AddUserToGroupResponse, RemoveUserToGroupData, RemoveUserToGroupResponse, ListFoldersData, ListFoldersResponse, ListFolderNamesData, ListFolderNamesResponse, CreateFolderData, CreateFolderResponse, UpdateFolderData, UpdateFolderResponse, DeleteFolderData, DeleteFolderResponse, GetFolderData, GetFolderResponse, GetFolderUsageData, GetFolderUsageResponse, AddOwnerToFolderData, AddOwnerToFolderResponse, RemoveOwnerToFolderData, RemoveOwnerToFolderResponse, ListWorkersData, ListWorkersResponse, ExistsWorkerWithTagData, ExistsWorkerWithTagResponse, GetQueueMetricsResponse, GetCountsOfJobsWaitingPerTagResponse, ListWorkerGroupsResponse, GetConfigData, GetConfigResponse, UpdateConfigData, UpdateConfigResponse, DeleteConfigData, DeleteConfigResponse, ListConfigsResponse, ListAutoscalingEventsData, ListAutoscalingEventsResponse, GetGranularAclsData, GetGranularAclsResponse, AddGranularAclsData, AddGranularAclsResponse, RemoveGranularAclsData, RemoveGranularAclsResponse, SetCaptureConfigData, SetCaptureConfigResponse, PingCaptureConfigData, PingCaptureConfigResponse, GetCaptureConfigsData, GetCaptureConfigsResponse, ListCapturesData, ListCapturesResponse, GetCaptureData, GetCaptureResponse, DeleteCaptureData, DeleteCaptureResponse, StarData, StarResponse, UnstarData, UnstarResponse, GetInputHistoryData, GetInputHistoryResponse, GetArgsFromHistoryOrSavedInputData, GetArgsFromHistoryOrSavedInputResponse, ListInputsData, ListInputsResponse, CreateInputData, CreateInputResponse, UpdateInputData, UpdateInputResponse, DeleteInputData, DeleteInputResponse, DuckdbConnectionSettingsData, DuckdbConnectionSettingsResponse, DuckdbConnectionSettingsV2Data, DuckdbConnectionSettingsV2Response, PolarsConnectionSettingsData, PolarsConnectionSettingsResponse, PolarsConnectionSettingsV2Data, PolarsConnectionSettingsV2Response, S3ResourceInfoData, S3ResourceInfoResponse, DatasetStorageTestConnectionData, DatasetStorageTestConnectionResponse, ListStoredFilesData, ListStoredFilesResponse, LoadFileMetadataData, LoadFileMetadataResponse, LoadFilePreviewData, LoadFilePreviewResponse, LoadParquetPreviewData, LoadParquetPreviewResponse, LoadTableRowCountData, LoadTableRowCountResponse, LoadCsvPreviewData, LoadCsvPreviewResponse, DeleteS3FileData, DeleteS3FileResponse, MoveS3FileData, MoveS3FileResponse, FileUploadData, FileUploadResponse, FileDownloadData, FileDownloadResponse, FileDownloadParquetAsCsvData, FileDownloadParquetAsCsvResponse, GetJobMetricsData, GetJobMetricsResponse, SetJobProgressData, SetJobProgressResponse, GetJobProgressData, GetJobProgressResponse, ListLogFilesData, ListLogFilesResponse, GetLogFileData, GetLogFileResponse, ListConcurrencyGroupsResponse, DeleteConcurrencyGroupData, DeleteConcurrencyGroupResponse, GetConcurrencyKeyData, GetConcurrencyKeyResponse, ListExtendedJobsData, ListExtendedJobsResponse, SearchJobsIndexData, SearchJobsIndexResponse, SearchLogsIndexData, SearchLogsIndexResponse, CountSearchLogsIndexData, CountSearchLogsIndexResponse, ClearIndexData, ClearIndexResponse } from './types.gen.ts'; /** * get backend version @@ -1194,6 +1194,72 @@ export const editSlackCommand = (data: EditSlackCommandData): CancelablePromise< mediaType: 'application/json' }); }; +/** + * edit teams command + * @param data The data for the request. + * @param data.workspace + * @param data.requestBody WorkspaceInvite + * @returns string status + * @throws ApiError + */ +export const editTeamsCommand = (data: EditTeamsCommandData): CancelablePromise => { return __request(OpenAPI, { + method: 'POST', + url: '/w/{workspace}/workspaces/edit_teams_command', + path: { + workspace: data.workspace + }, + body: data.requestBody, + mediaType: 'application/json' +}); }; + +/** + * list available teams ids + * @param data The data for the request. + * @param data.workspace + * @returns unknown status + * @throws ApiError + */ +export const listAvailableTeamsIds = (data: ListAvailableTeamsIdsData): CancelablePromise => { return __request(OpenAPI, { + method: 'GET', + url: '/w/{workspace}/workspaces/available_teams_ids', + path: { + workspace: data.workspace + } +}); }; + +/** + * list available teams channels + * @param data The data for the request. + * @param data.workspace + * @returns unknown status + * @throws ApiError + */ +export const listAvailableTeamsChannels = (data: ListAvailableTeamsChannelsData): CancelablePromise => { return __request(OpenAPI, { + method: 'GET', + url: '/w/{workspace}/workspaces/available_teams_channels', + path: { + workspace: data.workspace + } +}); }; + +/** + * connect teams + * @param data The data for the request. + * @param data.workspace + * @param data.requestBody connect teams + * @returns string status + * @throws ApiError + */ +export const connectTeams = (data: ConnectTeamsData): CancelablePromise => { return __request(OpenAPI, { + method: 'POST', + url: '/w/{workspace}/workspaces/connect_teams', + path: { + workspace: data.workspace + }, + body: data.requestBody, + mediaType: 'application/json' +}); }; + /** * run a job that sends a message to Slack * @param data The data for the request. @@ -1212,6 +1278,24 @@ export const runSlackMessageTestJob = (data: RunSlackMessageTestJobData): Cancel mediaType: 'application/json' }); }; +/** + * run a job that sends a message to Teams + * @param data The data for the request. + * @param data.workspace + * @param data.requestBody path to hub script to run and its corresponding args + * @returns unknown status + * @throws ApiError + */ +export const runTeamsMessageTestJob = (data: RunTeamsMessageTestJobData): CancelablePromise => { return __request(OpenAPI, { + method: 'POST', + url: '/w/{workspace}/workspaces/run_teams_message_test_job', + path: { + workspace: data.workspace + }, + body: data.requestBody, + mediaType: 'application/json' +}); }; + /** * edit deploy to * @param data The data for the request. @@ -2065,6 +2149,21 @@ export const disconnectSlack = (data: DisconnectSlackData): CancelablePromise => { return __request(OpenAPI, { + method: 'POST', + url: '/w/{workspace}/oauth/disconnect_teams', + path: { + workspace: data.workspace + } +}); }; + /** * list oauth logins * @returns unknown list of oauth and saml login clients @@ -2110,6 +2209,21 @@ export const syncTeams = (): CancelablePromise => { return __ url: '/teams/sync' }); }; +/** + * send update to Microsoft Teams activity + * Respond to a Microsoft Teams activity after a workspace command is run + * @param data The data for the request. + * @param data.requestBody + * @returns unknown Activity processed successfully + * @throws ApiError + */ +export const sendMessageToConversation = (data: SendMessageToConversationData): CancelablePromise => { return __request(OpenAPI, { + method: 'POST', + url: '/teams/activities', + body: data.requestBody, + mediaType: 'application/json' +}); }; + /** * create resource * @param data The data for the request. @@ -6245,6 +6359,160 @@ export const testSqsConnection = (data: TestSqsConnectionData): CancelablePromis mediaType: 'application/json' }); }; +/** + * create mqtt trigger + * @param data The data for the request. + * @param data.workspace + * @param data.requestBody new mqtt trigger + * @returns string mqtt trigger created + * @throws ApiError + */ +export const createMqttTrigger = (data: CreateMqttTriggerData): CancelablePromise => { return __request(OpenAPI, { + method: 'POST', + url: '/w/{workspace}/mqtt_triggers/create', + path: { + workspace: data.workspace + }, + body: data.requestBody, + mediaType: 'application/json' +}); }; + +/** + * update mqtt trigger + * @param data The data for the request. + * @param data.workspace + * @param data.path + * @param data.requestBody updated trigger + * @returns string mqtt trigger updated + * @throws ApiError + */ +export const updateMqttTrigger = (data: UpdateMqttTriggerData): CancelablePromise => { return __request(OpenAPI, { + method: 'POST', + url: '/w/{workspace}/mqtt_triggers/update/{path}', + path: { + workspace: data.workspace, + path: data.path + }, + body: data.requestBody, + mediaType: 'application/json' +}); }; + +/** + * delete mqtt trigger + * @param data The data for the request. + * @param data.workspace + * @param data.path + * @returns string mqtt trigger deleted + * @throws ApiError + */ +export const deleteMqttTrigger = (data: DeleteMqttTriggerData): CancelablePromise => { return __request(OpenAPI, { + method: 'DELETE', + url: '/w/{workspace}/mqtt_triggers/delete/{path}', + path: { + workspace: data.workspace, + path: data.path + } +}); }; + +/** + * get mqtt trigger + * @param data The data for the request. + * @param data.workspace + * @param data.path + * @returns MqttTrigger mqtt trigger deleted + * @throws ApiError + */ +export const getMqttTrigger = (data: GetMqttTriggerData): CancelablePromise => { return __request(OpenAPI, { + method: 'GET', + url: '/w/{workspace}/mqtt_triggers/get/{path}', + path: { + workspace: data.workspace, + path: data.path + } +}); }; + +/** + * list mqtt triggers + * @param data The data for the request. + * @param data.workspace + * @param data.page which page to return (start at 1, default 1) + * @param data.perPage number of items to return for a given page (default 30, max 100) + * @param data.path filter by path + * @param data.isFlow + * @param data.pathStart + * @returns MqttTrigger mqtt trigger list + * @throws ApiError + */ +export const listMqttTriggers = (data: ListMqttTriggersData): CancelablePromise => { return __request(OpenAPI, { + method: 'GET', + url: '/w/{workspace}/mqtt_triggers/list', + path: { + workspace: data.workspace + }, + query: { + page: data.page, + per_page: data.perPage, + path: data.path, + is_flow: data.isFlow, + path_start: data.pathStart + } +}); }; + +/** + * does mqtt trigger exists + * @param data The data for the request. + * @param data.workspace + * @param data.path + * @returns boolean mqtt trigger exists + * @throws ApiError + */ +export const existsMqttTrigger = (data: ExistsMqttTriggerData): CancelablePromise => { return __request(OpenAPI, { + method: 'GET', + url: '/w/{workspace}/mqtt_triggers/exists/{path}', + path: { + workspace: data.workspace, + path: data.path + } +}); }; + +/** + * set enabled mqtt trigger + * @param data The data for the request. + * @param data.workspace + * @param data.path + * @param data.requestBody updated mqtt trigger enable + * @returns string mqtt trigger enabled set + * @throws ApiError + */ +export const setMqttTriggerEnabled = (data: SetMqttTriggerEnabledData): CancelablePromise => { return __request(OpenAPI, { + method: 'POST', + url: '/w/{workspace}/mqtt_triggers/setenabled/{path}', + path: { + workspace: data.workspace, + path: data.path + }, + body: data.requestBody, + mediaType: 'application/json' +}); }; + +/** + * test mqtt connection + * @param data The data for the request. + * @param data.workspace + * @param data.requestBody test mqtt connection + * @returns string successfuly connected to mqtt + * @throws ApiError + */ +export const testMqttConnection = (data: TestMqttConnectionData): CancelablePromise => { return __request(OpenAPI, { + method: 'POST', + url: '/w/{workspace}/mqtt_triggers/test', + path: { + workspace: data.workspace + }, + body: data.requestBody, + mediaType: 'application/json' +}); }; + /** * check if postgres configuration is set to logical * @param data The data for the request. diff --git a/cli/gen/types.gen.ts b/cli/gen/types.gen.ts index 8fdbed7a57efb..f0de8f56f78d4 100644 --- a/cli/gen/types.gen.ts +++ b/cli/gen/types.gen.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -export type AIProvider = 'openai' | 'anthropic' | 'mistral' | 'deepseek' | 'groq' | 'openrouter' | 'customai'; +export type AIProvider = 'openai' | 'anthropic' | 'mistral' | 'deepseek' | 'googleai' | 'groq' | 'openrouter' | 'customai'; export type AIResource = { path: string; @@ -626,6 +626,7 @@ export type TriggersCount = { postgres_count?: number; kafka_count?: number; nats_count?: number; + mqtt_count?: number; sqs_count?: number; }; @@ -683,6 +684,75 @@ export type WebsocketTriggerInitialMessage = { }; }; +export type QoS = 0 | 1 | 2; + +export type CommonMqttConfig = { + will?: { + topic?: string; + message?: Array<(number)>; + qos?: QoS; + retain?: boolean; + }; +}; + +export type MqttV3Config = CommonMqttConfig & { + clean_session?: boolean; +}; + +export type MqttV5Config = CommonMqttConfig & { + clean_start?: boolean; + keep_alive?: number; + session_expiration?: number; + receive_maximum?: number; + maximum_packet_size?: number; +}; + +export type SubscribeTopic = { + qos: QoS; + topic: string; +}; + +export type MqttClientVersion = 'v3' | 'v5'; + +export type MqttTrigger = TriggerExtraProperty & { + mqtt_resource_path: string; + subscribe_topics: Array; + v3_config?: MqttV3Config; + v5_config?: MqttV5Config; + client_id?: string; + client_version?: MqttClientVersion; + server_id?: string; + last_server_ping?: string; + error?: string; + enabled: boolean; +}; + +export type NewMqttTrigger = { + mqtt_resource_path: string; + subscribe_topics: Array; + client_id?: string; + v3_config?: MqttV3Config; + v5_config?: MqttV5Config; + client_version?: MqttClientVersion; + path: string; + script_path: string; + is_flow: boolean; + enabled?: boolean; +}; + +export type EditMqttTrigger = { + mqtt_resource_path: string; + subscribe_topics: Array; + client_id?: string; + v3_config?: MqttV3Config; + v5_config?: MqttV5Config; + client_version?: MqttClientVersion; + path: string; + script_path: string; + is_flow: boolean; + enabled: boolean; +}; + export type SqsTrigger = TriggerExtraProperty & { queue_url: string; aws_resource_path: string; @@ -1287,7 +1357,7 @@ export type CriticalAlert = { workspace_id?: (string) | null; }; -export type CaptureTriggerKind = 'webhook' | 'http' | 'websocket' | 'kafka' | 'email' | 'nats' | 'postgres' | 'sqs'; +export type CaptureTriggerKind = 'webhook' | 'http' | 'websocket' | 'kafka' | 'email' | 'nats' | 'postgres' | 'sqs' | 'mqtt'; export type Capture = { trigger_kind: CaptureTriggerKind; @@ -2442,6 +2512,9 @@ export type GetSettingsResponse = ({ slack_name?: string; slack_team_id?: string; slack_command_script?: string; + teams_team_id?: string; + teams_command_script?: string; + teams_team_name?: string; auto_invite_domain?: string; auto_invite_operator?: boolean; auto_add?: boolean; @@ -2538,6 +2611,51 @@ export type EditSlackCommandData = { export type EditSlackCommandResponse = (string); +export type EditTeamsCommandData = { + /** + * WorkspaceInvite + */ + requestBody: { + slack_command_script?: string; + }; + workspace: string; +}; + +export type EditTeamsCommandResponse = (string); + +export type ListAvailableTeamsIdsData = { + workspace: string; +}; + +export type ListAvailableTeamsIdsResponse = (Array<{ + team_name?: string; + team_id?: string; +}>); + +export type ListAvailableTeamsChannelsData = { + workspace: string; +}; + +export type ListAvailableTeamsChannelsResponse = (Array<{ + channel_name?: string; + channel_id?: string; + service_url?: string; + tenant_id?: string; +}>); + +export type ConnectTeamsData = { + /** + * connect teams + */ + requestBody: { + team_id?: string; + team_name?: string; + }; + workspace: string; +}; + +export type ConnectTeamsResponse = (string); + export type RunSlackMessageTestJobData = { /** * path to hub script to run and its corresponding args @@ -2554,6 +2672,22 @@ export type RunSlackMessageTestJobResponse = ({ job_uuid?: string; }); +export type RunTeamsMessageTestJobData = { + /** + * path to hub script to run and its corresponding args + */ + requestBody: { + hub_script_path?: string; + channel?: string; + test_msg?: string; + }; + workspace: string; +}; + +export type RunTeamsMessageTestJobResponse = ({ + job_uuid?: string; +}); + export type EditDeployToData = { requestBody: { deploy_to?: string; @@ -2756,6 +2890,7 @@ export type GetUsedTriggersResponse = ({ kafka_used: boolean; nats_used: boolean; postgres_used: boolean; + mqtt_used: boolean; sqs_used: boolean; }); @@ -3063,6 +3198,12 @@ export type DisconnectSlackData = { export type DisconnectSlackResponse = (string); +export type DisconnectTeamsData = { + workspace: string; +}; + +export type DisconnectTeamsResponse = (string); + export type ListOauthLoginsResponse = ({ oauth: Array<{ type: string; @@ -3089,6 +3230,31 @@ export type GetOauthConnectResponse = ({ export type SyncTeamsResponse = (Array); +export type SendMessageToConversationData = { + requestBody: { + /** + * The ID of the Teams conversation/activity + */ + conversation_id: string; + /** + * Used for styling the card conditionally + */ + success?: boolean; + /** + * The message text to be sent in the Teams card + */ + text: string; + /** + * The card block to be sent in the Teams card + */ + card_block?: { + [key: string]: unknown; + }; + }; +}; + +export type SendMessageToConversationResponse = (unknown); + export type CreateResourceData = { /** * new resource @@ -5945,6 +6111,95 @@ export type TestSqsConnectionData = { export type TestSqsConnectionResponse = (string); +export type CreateMqttTriggerData = { + /** + * new mqtt trigger + */ + requestBody: NewMqttTrigger; + workspace: string; +}; + +export type CreateMqttTriggerResponse = (string); + +export type UpdateMqttTriggerData = { + path: string; + /** + * updated trigger + */ + requestBody: EditMqttTrigger; + workspace: string; +}; + +export type UpdateMqttTriggerResponse = (string); + +export type DeleteMqttTriggerData = { + path: string; + workspace: string; +}; + +export type DeleteMqttTriggerResponse = (string); + +export type GetMqttTriggerData = { + path: string; + workspace: string; +}; + +export type GetMqttTriggerResponse = (MqttTrigger); + +export type ListMqttTriggersData = { + isFlow?: boolean; + /** + * which page to return (start at 1, default 1) + */ + page?: number; + /** + * filter by path + */ + path?: string; + pathStart?: string; + /** + * number of items to return for a given page (default 30, max 100) + */ + perPage?: number; + workspace: string; +}; + +export type ListMqttTriggersResponse = (Array); + +export type ExistsMqttTriggerData = { + path: string; + workspace: string; +}; + +export type ExistsMqttTriggerResponse = (boolean); + +export type SetMqttTriggerEnabledData = { + path: string; + /** + * updated mqtt trigger enable + */ + requestBody: { + enabled: boolean; + }; + workspace: string; +}; + +export type SetMqttTriggerEnabledResponse = (string); + +export type TestMqttConnectionData = { + /** + * test mqtt connection + */ + requestBody: { + connection: { + [key: string]: unknown; + }; + }; + workspace: string; +}; + +export type TestMqttConnectionResponse = (string); + export type IsValidPostgresConfigurationData = { path: string; workspace: string; @@ -6479,7 +6734,7 @@ export type ListAutoscalingEventsData = { export type ListAutoscalingEventsResponse = (Array); export type GetGranularAclsData = { - kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger' | 'postgres_trigger' | 'sqs_trigger'; + kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger' | 'postgres_trigger' | 'mqtt_trigger' | 'sqs_trigger'; path: string; workspace: string; }; @@ -6489,7 +6744,7 @@ export type GetGranularAclsResponse = ({ }); export type AddGranularAclsData = { - kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger' | 'postgres_trigger' | 'sqs_trigger'; + kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger' | 'postgres_trigger' | 'mqtt_trigger' | 'sqs_trigger'; path: string; /** * acl to add @@ -6504,7 +6759,7 @@ export type AddGranularAclsData = { export type AddGranularAclsResponse = (string); export type RemoveGranularAclsData = { - kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger' | 'postgres_trigger' | 'sqs_trigger'; + kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger' | 'postgres_trigger' | 'mqtt_trigger' | 'sqs_trigger'; path: string; /** * acl to add diff --git a/cli/sync.ts b/cli/sync.ts index 89f7b8dae4a8f..72d12d9480b1f 100644 --- a/cli/sync.ts +++ b/cli/sync.ts @@ -641,6 +641,7 @@ export async function elementsToMap( path.endsWith(".kafka_trigger" + ext) || path.endsWith(".nats_trigger" + ext) || path.endsWith(".postgres_trigger" + ext) || + path.endsWith(".mqtt_trigger" + ext) || path.endsWith(".sqs_trigger" + ext)) ) continue; @@ -887,6 +888,7 @@ function getOrderFromPath(p: string) { typ == "kafka_trigger" || typ == "nats_trigger" || typ == "postgres_trigger" || + typ == "mqtt_trigger" || typ == "sqs_trigger" ) { return 8; @@ -1720,6 +1722,12 @@ export async function push(opts: GlobalOptions & SyncOptions) { path: removeSuffix(target, ".postgres_trigger.json"), }); break; + case "mqtt_trigger": + await wmill.deleteMqttTrigger({ + workspace: workspaceId, + path: removeSuffix(target, ".mqtt_trigger.json"), + }); + break; case "sqs_trigger": await wmill.deleteSqsTrigger({ workspace: workspaceId, diff --git a/cli/trigger.ts b/cli/trigger.ts index 39a95890b9c1b..15e165e1c8e45 100644 --- a/cli/trigger.ts +++ b/cli/trigger.ts @@ -2,6 +2,7 @@ import * as wmill from "./gen/services.gen.ts"; import { HttpTrigger, KafkaTrigger, + MqttTrigger, NatsTrigger, PostgresTrigger, SqsTrigger, @@ -24,6 +25,7 @@ type Trigger = { kafka: KafkaTrigger; nats: NatsTrigger; postgres: PostgresTrigger; + mqtt: MqttTrigger; sqs: SqsTrigger; }; @@ -56,6 +58,7 @@ async function getTrigger( kafka: wmill.getKafkaTrigger, nats: wmill.getNatsTrigger, postgres: wmill.getPostgresTrigger, + mqtt: wmill.getMqttTrigger, sqs: wmill.getSqsTrigger, }; const triggerFunction = triggerFunctions[triggerType]; @@ -82,6 +85,7 @@ async function updateTrigger( kafka: wmill.updateKafkaTrigger, nats: wmill.updateNatsTrigger, postgres: wmill.updatePostgresTrigger, + mqtt: wmill.updateMqttTrigger, sqs: wmill.updateSqsTrigger, }; const triggerFunction = triggerFunctions[triggerType]; @@ -106,6 +110,7 @@ async function createTrigger( kafka: wmill.createKafkaTrigger, nats: wmill.createNatsTrigger, postgres: wmill.createPostgresTrigger, + mqtt: wmill.createMqttTrigger, sqs: wmill.createSqsTrigger, }; const triggerFunction = triggerFunctions[triggerType]; @@ -180,6 +185,9 @@ async function list(opts: GlobalOptions) { const postgresTriggers = await wmill.listPostgresTriggers({ workspace: workspace.workspaceId, }); + const mqttTriggers = await wmill.listMqttTriggers({ + workspace: workspace.workspaceId, + }); const sqsTriggers = await wmill.listSqsTriggers({ workspace: workspace.workspaceId, }); @@ -190,6 +198,7 @@ async function list(opts: GlobalOptions) { ...kafkaTriggers.map((x) => ({ path: x.path, kind: "kafka" })), ...natsTriggers.map((x) => ({ path: x.path, kind: "nats" })), ...postgresTriggers.map((x) => ({ path: x.path, kind: "postgres" })), + ...mqttTriggers.map((x) => ({ path: x.path, kind: "mqtt" })), ...sqsTriggers.map((x) => ({ path: x.path, kind: "sqs" })), ]; @@ -204,7 +213,7 @@ async function list(opts: GlobalOptions) { function checkIfValidTrigger(kind: string | undefined): kind is TriggerType { if ( kind && - ["http", "websocket", "kafka", "nats", "postgres", "sqs"].includes(kind) + ["http", "websocket", "kafka", "nats", "postgres", "mqtt", "sqs"].includes(kind) ) { return true; } else { diff --git a/cli/types.ts b/cli/types.ts index 18479edbcb675..62f4f9bf53024 100644 --- a/cli/types.ts +++ b/cli/types.ts @@ -150,6 +150,8 @@ export async function pushObj( await pushTrigger("nats", workspace, p, befObj, newObj); } else if (typeEnding === "postgres_trigger") { await pushTrigger("postgres", workspace, p, befObj, newObj); + } else if (typeEnding === "mqtt_trigger") { + await pushTrigger("mqtt", workspace, p, befObj, newObj); } else if (typeEnding === "sqs_trigger") { await pushTrigger("sqs", workspace, p, befObj, newObj); } else if (typeEnding === "user") { @@ -199,6 +201,7 @@ export function getTypeStrFromPath( | "kafka_trigger" | "nats_trigger" | "postgres_trigger" + | "mqtt_trigger" | "sqs_trigger" | "user" | "group" @@ -245,6 +248,7 @@ export function getTypeStrFromPath( typeEnding === "kafka_trigger" || typeEnding === "nats_trigger" || typeEnding === "postgres_trigger" || + typeEnding === "mqtt_trigger" || typeEnding === "sqs_trigger" || typeEnding === "user" || typeEnding === "group" || diff --git a/frontend/src/lib/components/Path.svelte b/frontend/src/lib/components/Path.svelte index 8a83a2f87275d..4909cd29b924f 100644 --- a/frontend/src/lib/components/Path.svelte +++ b/frontend/src/lib/components/Path.svelte @@ -18,6 +18,7 @@ KafkaTriggerService, PostgresTriggerService, NatsTriggerService, + MqttTriggerService, SqsTriggerService } from '$lib/gen' import { superadmin, userStore, workspaceStore } from '$lib/stores' @@ -44,6 +45,7 @@ | 'kafka_trigger' | 'postgres_trigger' | 'nats_trigger' + | 'mqtt_trigger' | 'sqs_trigger' let meta: Meta | undefined = undefined export let fullNamePlaceholder: string | undefined = undefined @@ -248,6 +250,11 @@ workspace: $workspaceStore!, path: path }) + } else if (kind === 'mqtt_trigger') { + return await MqttTriggerService.existsMqttTrigger({ + workspace: $workspaceStore!, + path: path + }) } else if (kind == 'sqs_trigger') { return await SqsTriggerService.existsSqsTrigger({ workspace: $workspaceStore!, diff --git a/frontend/src/lib/components/ShareModal.svelte b/frontend/src/lib/components/ShareModal.svelte index 611653f413063..be1e82388689f 100644 --- a/frontend/src/lib/components/ShareModal.svelte +++ b/frontend/src/lib/components/ShareModal.svelte @@ -28,6 +28,7 @@ | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger' + | 'mqtt_trigger' | 'sqs_trigger' | 'postgres_trigger' let kind: Kind diff --git a/frontend/src/lib/components/details/DetailPageDetailPanel.svelte b/frontend/src/lib/components/details/DetailPageDetailPanel.svelte index 562b60e08a193..91760991157b0 100644 --- a/frontend/src/lib/components/details/DetailPageDetailPanel.svelte +++ b/frontend/src/lib/components/details/DetailPageDetailPanel.svelte @@ -15,6 +15,7 @@ | 'postgres' | 'scheduledPoll' | 'kafka' + | 'mqtt' | 'sqs' | 'nats' = 'webhooks' export let flow_json: any | undefined = undefined @@ -58,6 +59,7 @@ + diff --git a/frontend/src/lib/components/details/DetailPageLayout.svelte b/frontend/src/lib/components/details/DetailPageLayout.svelte index 808632f83e0ad..b662b1fbe97cd 100644 --- a/frontend/src/lib/components/details/DetailPageLayout.svelte +++ b/frontend/src/lib/components/details/DetailPageLayout.svelte @@ -30,6 +30,7 @@ | 'scheduledPoll' | 'kafka' | 'nats' + | 'mqtt' | 'sqs' >('webhooks') @@ -68,6 +69,7 @@ + @@ -116,6 +118,7 @@ + diff --git a/frontend/src/lib/components/details/DetailPageTriggerPanel.svelte b/frontend/src/lib/components/details/DetailPageTriggerPanel.svelte index 563972556b356..53a07cedf7747 100644 --- a/frontend/src/lib/components/details/DetailPageTriggerPanel.svelte +++ b/frontend/src/lib/components/details/DetailPageTriggerPanel.svelte @@ -12,11 +12,9 @@ } from 'lucide-svelte' import HighlightTheme from '../HighlightTheme.svelte' - import KafkaIcon from '../icons/KafkaIcon.svelte' - import NatsIcon from '../icons/NatsIcon.svelte' import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte' import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte' - import { AwsIcon } from '../icons' + import { MqttIcon, NatsIcon, KafkaIcon, AwsIcon } from '../icons' export let triggerSelected: | 'webhooks' @@ -29,13 +27,14 @@ | 'postgres' | 'nats' | 'sqs' + | 'mqtt' | 'scheduledPoll' = 'webhooks' export let simplfiedPoll: boolean = false - export let eventStreamType: 'kafka' | 'nats' | 'sqs' = 'kafka' + export let eventStreamType: 'kafka' | 'nats' | 'sqs' | 'mqtt' = 'kafka' $: { - if (triggerSelected === 'kafka' || triggerSelected === 'nats' || triggerSelected === 'sqs') { + if (triggerSelected === 'kafka' || triggerSelected === 'nats' || triggerSelected === 'sqs' || triggerSelected === 'mqtt') { eventStreamType = triggerSelected } } @@ -76,7 +75,7 @@ Postgres - + Event streams @@ -109,11 +108,12 @@ {:else if triggerSelected === 'postgres'} - {:else if triggerSelected === 'kafka' || triggerSelected === 'nats' || triggerSelected === 'sqs'} + {:else if triggerSelected === 'kafka' || triggerSelected === 'nats' || triggerSelected === 'sqs' || triggerSelected === 'mqtt'}
+
@@ -123,6 +123,8 @@ {:else if eventStreamType === 'sqs'} + {:else if eventStreamType === 'mqtt'} + {/if} {:else if triggerSelected === 'cli'} diff --git a/frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte b/frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte index 2982be5ab6102..de921ab40d494 100644 --- a/frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte +++ b/frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte @@ -10,9 +10,7 @@ import { type TriggerContext } from '$lib/components/triggers' import { FlowService, ScriptService } from '$lib/gen' import { enterpriseLicense, workspaceStore } from '$lib/stores' - import KafkaIcon from '$lib/components/icons/KafkaIcon.svelte' - import NatsIcon from '$lib/components/icons/NatsIcon.svelte' - import AwsIcon from '$lib/components/icons/AwsIcon.svelte' + import { MqttIcon, NatsIcon, KafkaIcon, AwsIcon } from '$lib/components/icons' const { selectedTrigger, triggersCount } = getContext('TriggerContext') @@ -28,6 +26,7 @@ | 'websockets' | 'kafka' | 'nats' + | 'mqtt' | 'emails' | 'eventStreams' | 'postgres' @@ -68,6 +67,7 @@ kafka: { icon: KafkaIcon, countKey: 'kafka_count' }, emails: { icon: Mail, countKey: 'email_count' }, nats: { icon: NatsIcon, countKey: 'nats_count' }, + mqtt: { icon: MqttIcon, countKey: 'mqtt_count' }, sqs: { icon: AwsIcon, countKey: 'sqs_count' }, eventStreams: { icon: PlugZap } } diff --git a/frontend/src/lib/components/icons/MqttIcon.svelte b/frontend/src/lib/components/icons/MqttIcon.svelte new file mode 100644 index 0000000000000..05b7fa8a7d66e --- /dev/null +++ b/frontend/src/lib/components/icons/MqttIcon.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/frontend/src/lib/components/icons/index.ts b/frontend/src/lib/components/icons/index.ts index 20c541a14cd60..7b863e94b42b5 100644 --- a/frontend/src/lib/components/icons/index.ts +++ b/frontend/src/lib/components/icons/index.ts @@ -98,6 +98,7 @@ import SpotifyIcon from './SpotifyIcon.svelte' import XeroIcon from './XeroIcon.svelte' import KafkaIcon from './KafkaIcon.svelte' import NatsIcon from './NatsIcon.svelte' +import MqttIcon from './MqttIcon.svelte' export const APP_TO_ICON_COMPONENT = { postgresql: PostgresIcon, mysql: Mysql, @@ -297,5 +298,6 @@ export { ZitadelIcon, XeroIcon, KafkaIcon, - NatsIcon + NatsIcon, + MqttIcon } diff --git a/frontend/src/lib/components/sidebar/OperatorMenu.svelte b/frontend/src/lib/components/sidebar/OperatorMenu.svelte index 70b4e55e30f23..4eecd8590d108 100644 --- a/frontend/src/lib/components/sidebar/OperatorMenu.svelte +++ b/frontend/src/lib/components/sidebar/OperatorMenu.svelte @@ -89,6 +89,11 @@ id: 'triggers', href: `${base}/sqs_triggers` }, + { + label: 'MQTT triggers', + id: 'triggers', + href: `${base}/mqtt_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 c59b4636b6970..54c8e9752e8ca 100644 --- a/frontend/src/lib/components/sidebar/SidebarContent.svelte +++ b/frontend/src/lib/components/sidebar/SidebarContent.svelte @@ -50,6 +50,7 @@ import SideBarNotification from './SideBarNotification.svelte' import KafkaIcon from '../icons/KafkaIcon.svelte' import NatsIcon from '../icons/NatsIcon.svelte' + import MqttIcon from '../icons/MqttIcon.svelte' import AwsIcon from '../icons/AwsIcon.svelte' import { Menubar, @@ -134,7 +135,14 @@ icon: AwsIcon, disabled: $userStore?.operator || !$enterpriseLicense, kind: 'sqs' - } + }, + { + label: 'MQTT', + href: '/mqtt_triggers', + icon: MqttIcon, + disabled: $userStore?.operator, + kind: 'mqtt' + }, ] $: extraTriggerLinks = defaultExtraTriggerLinks.filter((link) => { @@ -165,7 +173,7 @@ icon: FolderCog, faIcon: undefined } - ] + ] : []), ...($superadmin ? [ @@ -175,7 +183,7 @@ icon: ServerCog, faIcon: undefined } - ] + ] : []), ...(!$superadmin && !$userStore?.is_admin ? [ @@ -188,7 +196,7 @@ icon: LogOut, faIcon: undefined } - ] + ] : []) ], disabled: $userStore?.operator @@ -232,7 +240,7 @@ href: `${base}/service_logs`, icon: Logs } - ] + ] : []), ...($enterpriseLicense ? [ @@ -244,16 +252,16 @@ icon: AlertCircle, notificationCount: numUnacknowledgedCriticalAlerts } - ] + ] : []) ] - } + } : { label: 'Audit logs', href: `${base}/audit_logs`, icon: Eye, disabled: $userStore?.operator - } + } ] let hasNewChangelogs = false diff --git a/frontend/src/lib/components/triggers.ts b/frontend/src/lib/components/triggers.ts index eec8edbd4a2b7..419f11ecb494d 100644 --- a/frontend/src/lib/components/triggers.ts +++ b/frontend/src/lib/components/triggers.ts @@ -51,6 +51,7 @@ export type TriggerKind = | 'kafka' | 'nats' | 'postgres' + | 'mqtt' | 'sqs' export function captureTriggerKindToTriggerKind(kind: CaptureTriggerKind): TriggerKind { switch (kind) { @@ -66,6 +67,8 @@ export function captureTriggerKindToTriggerKind(kind: CaptureTriggerKind): Trigg return 'kafka' case 'nats': return 'nats' + case 'mqtt': + return 'mqtt' case 'sqs': return 'sqs' case 'postgres': diff --git a/frontend/src/lib/components/triggers/CaptureButton.svelte b/frontend/src/lib/components/triggers/CaptureButton.svelte index 44fd2326b8e55..ba61aea130fe9 100644 --- a/frontend/src/lib/components/triggers/CaptureButton.svelte +++ b/frontend/src/lib/components/triggers/CaptureButton.svelte @@ -9,7 +9,7 @@ import { captureTriggerKindToTriggerKind } from '../triggers' import CaptureIcon from './CaptureIcon.svelte' import NatsIcon from '../icons/NatsIcon.svelte' - import AwsIcon from '../icons/AwsIcon.svelte' + import { MqttIcon, AwsIcon } from '../icons' export let small = false @@ -130,6 +130,15 @@

Nats

+ diff --git a/frontend/src/lib/components/triggers/CaptureWrapper.svelte b/frontend/src/lib/components/triggers/CaptureWrapper.svelte index 4cbefe6fb6945..d3c5a995733d8 100644 --- a/frontend/src/lib/components/triggers/CaptureWrapper.svelte +++ b/frontend/src/lib/components/triggers/CaptureWrapper.svelte @@ -14,9 +14,11 @@ import type { CaptureInfo } from './CaptureSection.svelte' import CaptureTable from './CaptureTable.svelte' import NatsTriggersConfigSection from './nats/NatsTriggersConfigSection.svelte' + import MqttEditorConfigSection from './mqtt/MqttEditorConfigSection.svelte' import SqsTriggerEditorConfigSection from './sqs/SqsTriggerEditorConfigSection.svelte' import PostgresEditorConfigSection from './postgres/PostgresEditorConfigSection.svelte' import { invalidRelations } from './postgres/utils' + import { DEFAULT_V3_CONFIG, DEFAULT_V5_CONFIG } from './mqtt/constant' export let isFlow: boolean export let path: string @@ -78,11 +80,8 @@ acc[c.trigger_kind] = c return acc }, {}) - - if ( - (captureType === 'postgres' || captureType === 'websocket' || captureType === 'kafka' || captureType === 'sqs') && - captureActive - ) { + const streamingCapture = ['postgres', 'websocket', 'kafka', 'sqs', 'mqtt'] + if (streamingCapture.includes(captureType) && captureActive) { const config = captureConfigs[captureType] if (config && config.error) { const serverEnabled = getServerEnabled(config) @@ -121,7 +120,19 @@ const triggerConfig = captureConfigs[captureType].trigger_config args = isObject(triggerConfig) ? triggerConfig : {} } else { - args = {} + switch (captureType) { + case 'mqtt': + //define these field so any reactive statement that may use them will not crash trying to access their property + args = { + v3_config: DEFAULT_V3_CONFIG, + v5_config: DEFAULT_V5_CONFIG, + client_version: 'v5', + subscribe_topics: [] + } + break + default: + args = {} + } } ready = true } @@ -151,7 +162,7 @@ let config: CaptureConfig | undefined $: config = captureConfigs[captureType] - const streamingCaptures = ['sqs', 'websocket', 'postgres', 'kafka', 'nats'] + const streamingCaptures = ['mqtt', 'sqs', 'websocket', 'postgres', 'kafka', 'nats'] let cloudDisabled = streamingCaptures.includes(captureType) && isCloudHosted() function updateConnectionInfo(config: CaptureConfig | undefined, captureActive: boolean) { @@ -299,6 +310,24 @@ on:addPreprocessor on:captureToggle={handleCapture} /> + {:else if captureType === 'mqtt'} + {:else if captureType === 'sqs'} export let noButton = false export let testLoading: boolean = false @@ -21,7 +22,8 @@ nats: 'NATS server(s)', kafka: 'Kafka broker(s)', sqs: 'SQS', - postgres: 'Postgres' + postgres: 'Postgres', + mqtt: 'MQTT broker' } let promise: CancelablePromise | null = null @@ -48,6 +50,11 @@ workspace: $workspaceStore!, requestBody: args as any }) + } else if (kind === 'mqtt') { + promise = MqttTriggerService.testMqttConnection({ + workspace: $workspaceStore!, + requestBody: args as any + }) } else if (kind === 'sqs') { promise = SqsTriggerService.testSqsConnection({ workspace: $workspaceStore!, diff --git a/frontend/src/lib/components/triggers/TriggersEditor.svelte b/frontend/src/lib/components/triggers/TriggersEditor.svelte index 3a6035b1ec381..cce1ea8ce671a 100644 --- a/frontend/src/lib/components/triggers/TriggersEditor.svelte +++ b/frontend/src/lib/components/triggers/TriggersEditor.svelte @@ -15,9 +15,10 @@ import PostgresTriggersPanel from './postgres/PostgresTriggersPanel.svelte' import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte' import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte' - import { AwsIcon, KafkaIcon, NatsIcon } from '../icons' + import { KafkaIcon, MqttIcon, NatsIcon, AwsIcon } from '../icons' import KafkaTriggersPanel from './kafka/KafkaTriggersPanel.svelte' import NatsTriggersPanel from './nats/NatsTriggersPanel.svelte' + import MqttTriggersPanel from './mqtt/MqttTriggersPanel.svelte' import SqsTriggerPanel from './sqs/SqsTriggerPanel.svelte' export let noEditor: boolean @@ -30,10 +31,15 @@ export let canHavePreprocessor: boolean = false export let hasPreprocessor: boolean = false export let args: Record = {} - let eventStreamType: 'kafka' | 'nats' | 'sqs' = 'kafka' + let eventStreamType: 'kafka' | 'nats' | 'sqs' | 'mqtt' = 'kafka' $: { - if ($selectedTrigger === 'kafka' || $selectedTrigger === 'nats' || $selectedTrigger === 'sqs') { + if ( + $selectedTrigger === 'kafka' || + $selectedTrigger === 'nats' || + $selectedTrigger === 'sqs' || + $selectedTrigger === 'mqtt' + ) { eventStreamType = $selectedTrigger } } @@ -57,7 +63,7 @@ Postgres Event streams @@ -153,11 +159,12 @@ isEditor={true} /> - {:else if $selectedTrigger === 'kafka' || $selectedTrigger === 'nats' || $selectedTrigger === 'sqs'} + {:else if $selectedTrigger === 'kafka' || $selectedTrigger === 'nats' || $selectedTrigger === 'sqs' || $selectedTrigger === 'mqtt'}
+ {#if eventStreamType === 'kafka'} @@ -197,6 +204,19 @@ {canHavePreprocessor} {hasPreprocessor} /> + {:else if eventStreamType === 'mqtt'} + {/if}
{:else if $selectedTrigger === 'schedules'} diff --git a/frontend/src/lib/components/triggers/TriggersEditorSection.svelte b/frontend/src/lib/components/triggers/TriggersEditorSection.svelte index 6ea557c9b43c8..33365688934fb 100644 --- a/frontend/src/lib/components/triggers/TriggersEditorSection.svelte +++ b/frontend/src/lib/components/triggers/TriggersEditorSection.svelte @@ -32,6 +32,7 @@ kafka: '+ New Kafka trigger', email: 'Email trigger', nats: '+ New NATS trigger', + mqtt: '+ New MQTT trigger', sqs: '+ New SQS trigger', postgres: '+ New Postgres trigger' } diff --git a/frontend/src/lib/components/triggers/TriggersWrapper.svelte b/frontend/src/lib/components/triggers/TriggersWrapper.svelte index b64ed9d959dd3..b3ffe6620ef9b 100644 --- a/frontend/src/lib/components/triggers/TriggersWrapper.svelte +++ b/frontend/src/lib/components/triggers/TriggersWrapper.svelte @@ -8,6 +8,7 @@ import EmailTriggerConfigSection from '../details/EmailTriggerConfigSection.svelte' import KafkaTriggersConfigSection from './kafka/KafkaTriggersConfigSection.svelte' import NatsTriggersConfigSection from './nats/NatsTriggersConfigSection.svelte' + import MqttEditorConfigSection from './mqtt/MqttEditorConfigSection.svelte' import SqsTriggerEditorConfigSection from './sqs/SqsTriggerEditorConfigSection.svelte' import PostgresEditorConfigSection from './postgres/PostgresEditorConfigSection.svelte' @@ -71,6 +72,18 @@ {:else if triggerType === 'nats'} + {:else if triggerType === 'mqtt'} + {:else if triggerType === 'sqs'} + import Section from '$lib/components/Section.svelte' + import CaptureSection, { type CaptureInfo } from '../CaptureSection.svelte' + import CaptureTable from '../CaptureTable.svelte' + import Required from '$lib/components/Required.svelte' + import { Plus, X } from 'lucide-svelte' + import Subsection from '$lib/components/Subsection.svelte' + import ResourcePicker from '$lib/components/ResourcePicker.svelte' + import type { MqttClientVersion, MqttV3Config, MqttV5Config, SubscribeTopic } from '$lib/gen' + import Button from '$lib/components/common/button/Button.svelte' + import { fade } from 'svelte/transition' + import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte' + import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte' + import Toggle from '$lib/components/Toggle.svelte' + import { emptyStringTrimmed } from '$lib/utils' + import { DEFAULT_V3_CONFIG, DEFAULT_V5_CONFIG } from './constant' + import TestTriggerConnection from '../TestTriggerConnection.svelte' + + export let can_write: boolean = false + export let headless: boolean = false + export let showCapture: boolean = false + export let mqtt_resource_path: string = '' + export let subscribe_topics: SubscribeTopic[] = [] + export let captureTable: CaptureTable | undefined = undefined + export let captureInfo: CaptureInfo | undefined = undefined + export let v3_config: MqttV3Config = DEFAULT_V3_CONFIG + export let v5_config: MqttV5Config = DEFAULT_V5_CONFIG + export let client_version: MqttClientVersion = 'v5' + export let isValid: boolean = false + export let client_id: string + $: isValid = subscribe_topics.length > 0 && !emptyStringTrimmed(mqtt_resource_path) + + +
+ {#if showCapture && captureInfo} + + {/if} +
+
+ + + {#if !emptyStringTrimmed(mqtt_resource_path)} + + {/if} + + + +

Choose which topics you want to subscribe to +

+
+ {#each subscribe_topics as v, i} +
+
+
+ + +
+ + +
+ +
+ {/each} + +
+ +
+
+
+ + +
+ + + + + + + + {#if client_version === 'v5'} + { + v5_config.clean_start = !v5_config.clean_start + }} + options={{ + right: 'Clean start', + rightTooltip: '', + rightDocumentationLink: '' + }} + class="py-1" + /> + {:else if client_version === 'v3'} + { + v3_config.clean_session = !v3_config.clean_session + }} + options={{ + right: 'Clean session', + rightTooltip: '', + rightDocumentationLink: '' + }} + class="py-1" + /> + {/if} +
+
+
+
+
diff --git a/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditor.svelte b/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditor.svelte new file mode 100644 index 0000000000000..2bf129ae4dec1 --- /dev/null +++ b/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditor.svelte @@ -0,0 +1,27 @@ + + +{#if open} + +{/if} diff --git a/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditorInner.svelte new file mode 100644 index 0000000000000..c66612e5e722f --- /dev/null +++ b/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditorInner.svelte @@ -0,0 +1,263 @@ + + + + + + {#if !drawerLoading && can_write} + {#if edit} +
+ { + sendUserToast(`${e.detail ? 'enabled' : 'disabled'} mqtt trigger ${initialPath}`) + }} + /> +
+ {/if} + + {/if} +
+ {#if drawerLoading} +
+ +

Loading...

+
+ {:else} +
+ + {#if edit} + Changes can take up to 30 seconds to take effect. + {:else} + New mqtt triggers can take up to 30 seconds to start listening. + {/if} + +
+
+
+ +
+ +
+

+ Pick a script or flow to be triggered +

+
+ + {#if emptyString(script_path)} + + {/if} +
+
+ + +
+ {/if} +
+
diff --git a/frontend/src/lib/components/triggers/mqtt/MqttTriggersPanel.svelte b/frontend/src/lib/components/triggers/mqtt/MqttTriggersPanel.svelte new file mode 100644 index 0000000000000..834b6c16bb56c --- /dev/null +++ b/frontend/src/lib/components/triggers/mqtt/MqttTriggersPanel.svelte @@ -0,0 +1,126 @@ + + + { + loadTriggers() + }} + bind:this={mqttTriggerEditor} +/> + +{#if isCloudHosted()} + + MQTT triggers are disabled in the multi-tenant cloud. + +{:else} +
+ + Windmill can connect to an MQTT broker and subscribes to specific topics thus allowing the + execution of script/flows based on the event triggered by those subscribed topics + + + {#if !newItem && mqttTriggers && mqttTriggers.length > 0} +
+
+
+ {#each mqttTriggers as mqttTriggers (mqttTriggers.path)} +
+
{mqttTriggers.path}
+ +
+ +
+
+ {/each} +
+
+
+ {/if} + + { + mqttTriggerEditor?.openNew(isFlow, path, e.detail.config) + }} + on:addPreprocessor + on:updateSchema + on:testWithArgs + cloudDisabled={false} + triggerType="mqtt" + {isFlow} + {path} + {isEditor} + {canHavePreprocessor} + {hasPreprocessor} + {newItem} + {openForm} + bind:showCapture={dontCloseOnLoad} + /> +
+{/if} diff --git a/frontend/src/lib/components/triggers/mqtt/constant.ts b/frontend/src/lib/components/triggers/mqtt/constant.ts new file mode 100644 index 0000000000000..7470a7c3612bf --- /dev/null +++ b/frontend/src/lib/components/triggers/mqtt/constant.ts @@ -0,0 +1,9 @@ +import type { MqttV3Config, MqttV5Config } from "$lib/gen" + +export const DEFAULT_V5_CONFIG: MqttV5Config = { + clean_start: true, +} + +export const DEFAULT_V3_CONFIG: MqttV3Config = { + clean_session: true +} \ No newline at end of file diff --git a/frontend/src/lib/script_helpers.ts b/frontend/src/lib/script_helpers.ts index 1ce65316dacc1..67a125805023c 100644 --- a/frontend/src/lib/script_helpers.ts +++ b/frontend/src/lib/script_helpers.ts @@ -580,7 +580,7 @@ export async function main(approver?: string) { export const BUN_PREPROCESSOR_MODULE_CODE = ` export async function preprocessor( wm_trigger: { - kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs', + kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs' | 'mqtt', http?: { route: string // The route path, e.g. "/users/:id" path: string // The actual path called, e.g. "/users/123" @@ -614,6 +614,21 @@ export async function preprocessor( string_value?: string, data_type: string }> + }, + mqtt?: { + topic: string, + retain: boolean, + pkid: number, + qos: number, + v5?: { + payload_format_indicator?: number, + topic_alias?: number, + response_topic?: string, + correlation_data?: Array, + user_properties?: Array<[string, string]>, + subscription_identifiers?: Array, + content_type?: string + } } }, /* your other args */ @@ -627,7 +642,7 @@ export async function preprocessor( const DENO_PREPROCESSOR_MODULE_CODE = ` export async function preprocessor( wm_trigger: { - kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs', + kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs' | 'mqtt', http?: { route: string // The route path, e.g. "/users/:id" path: string // The actual path called, e.g. "/users/123" @@ -661,6 +676,21 @@ export async function preprocessor( string_value?: string, data_type: string }> + }, + mqtt?: { + topic: string, + retain: boolean, + pkid: number, + qos: number, + v5?: { + payload_format_indicator?: number, + topic_alias?: number, + response_topic?: string, + correlation_data?: Array, + user_properties?: Array<[string, string]>, + subscription_identifiers?: Array, + content_type?: string + } } }, /* your other args */ @@ -734,13 +764,31 @@ class Sqs(TypedDict): attributes: dict[str, str] message_attributes: dict[str, MessageAttribute] | None +class V5Properties: + payload_format_indicator: int | None + topic_alias: int | None + response_topic: str | None + correlation_data: list[int] | None + user_properties: list[tuple[str, str]] | None + subscription_identifiers: list[int] | None + content_type: str | None + +class Mqtt(TypeDict): + topic: str + retain: bool + pkid: int + qos: int + v5: V5Properties | None + + class WmTrigger(TypedDict): - kind: Literal["http", "email", "webhook", "websocket", "kafka", "nats", "postgres", "sqs"] + kind: Literal["http", "email", "webhook", "websocket", "kafka", "nats", "postgres", "sqs", "mqtt"] http: Http | None websocket: Websocket | None kafka: Kafka | None nats: Nats | None sqs: Sqs | None + mqtt: Mqtt | None def preprocessor( wm_trigger: WmTrigger, diff --git a/frontend/src/routes/(root)/(logged)/+layout.svelte b/frontend/src/routes/(root)/(logged)/+layout.svelte index 84de14dd4e60b..b5ba08dd15e0c 100644 --- a/frontend/src/routes/(root)/(logged)/+layout.svelte +++ b/frontend/src/routes/(root)/(logged)/+layout.svelte @@ -192,12 +192,17 @@ async function loadUsedTriggerKinds() { let usedKinds: string[] = [] - const { http_routes_used, websocket_used, kafka_used, postgres_used, nats_used , sqs_used} = - await WorkspaceService.getUsedTriggers( - { - workspace: $workspaceStore ?? '' - } - ) + const { + http_routes_used, + websocket_used, + kafka_used, + postgres_used, + nats_used, + sqs_used, + mqtt_used + } = await WorkspaceService.getUsedTriggers({ + workspace: $workspaceStore ?? '' + }) if (http_routes_used) { usedKinds.push('http') } @@ -213,6 +218,9 @@ if (nats_used) { usedKinds.push('nats') } + if (mqtt_used) { + usedKinds.push('mqtt') + } if (sqs_used) { usedKinds.push('sqs') } 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 5497a234b2d97..50f6c120a0e25 100644 --- a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte @@ -65,6 +65,7 @@ import NatsTriggersPanel from '$lib/components/triggers/nats/NatsTriggersPanel.svelte' import PostgresTriggersPanel from '$lib/components/triggers/postgres/PostgresTriggersPanel.svelte' import Toggle from '$lib/components/Toggle.svelte' + import MqttTriggersPanel from '$lib/components/triggers/mqtt/MqttTriggersPanel.svelte' import SqsTriggerPanel from '$lib/components/triggers/sqs/SqsTriggerPanel.svelte' import { onDestroy } from 'svelte' import LogViewer from '$lib/components/LogViewer.svelte' @@ -626,6 +627,11 @@ + +
+ +
+
diff --git a/frontend/src/routes/(root)/(logged)/mqtt_triggers/+page.js b/frontend/src/routes/(root)/(logged)/mqtt_triggers/+page.js new file mode 100644 index 0000000000000..f013642df6113 --- /dev/null +++ b/frontend/src/routes/(root)/(logged)/mqtt_triggers/+page.js @@ -0,0 +1,5 @@ +export function load() { + return { + stuff: { title: 'MQTT triggers' } + } +} diff --git a/frontend/src/routes/(root)/(logged)/mqtt_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/mqtt_triggers/+page.svelte new file mode 100644 index 0000000000000..3b9dc21dae822 --- /dev/null +++ b/frontend/src/routes/(root)/(logged)/mqtt_triggers/+page.svelte @@ -0,0 +1,415 @@ + + + + + (x.summary ?? '') + ' ' + x.path + ' (' + x.script_path + ')'} +/> + + + + + + + {#if isCloudHosted()} + + MQTT triggers are disabled in the multi-tenant cloud. + +
+ {/if} +
+
+ +
+
Filter by path of
+ + + + +
+ + +
+ {#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 mqtt triggers
+ {:else if items?.length} +
+ {#each items.slice(0, nbDisplayed) as { path, edited_by, edited_at, script_path, is_flow, extra_perms, canWrite, error, last_server_ping, server_id, enabled } (path)} + {@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`} + {@const ping = last_server_ping ? new Date(last_server_ping) : undefined} + {@const pinging = ping && ping.getTime() > new Date().getTime() - 15 * 1000} + +
+
+ + + mqttTriggerEditor?.openEdit(path, is_flow)} + class="min-w-0 grow hover:underline decoration-gray-400" + > +
+ {path} +
+
+ runnable: {script_path} +
+
+ + + +
+ {#if (enabled && (!pinging || error)) || (!enabled && error) || (enabled && !server_id)} + + + + + +
+ {#if enabled} + {#if !server_id} + MQTT is starting... + {:else} + MQTT is not connected{error ? ': ' + error : ''} + {/if} + {:else} + MQTT was disabled because of an error: {error} + {/if} +
+
+ {:else if enabled} + + + + +
+ MQTT is connected{!server_id ? ' (shutting down...)' : ''} +
+
+ {/if} +
+ + { + setTriggerEnabled(path, e.detail) + }} + /> + +
+ + { + goto(href) + } + }, + { + displayName: 'Delete', + type: 'delete', + icon: Trash, + disabled: !canWrite, + action: async () => { + await MqttTriggerService.deleteMqttTrigger({ + workspace: $workspaceStore ?? '', + path + }) + loadTriggers() + } + }, + { + displayName: canWrite ? 'Edit' : 'View', + icon: canWrite ? Pen : Eye, + action: () => { + mqttTriggerEditor?.openEdit(path, is_flow) + } + }, + { + displayName: 'Audit logs', + icon: Eye, + href: `${base}/audit_logs?resource=${path}` + }, + { + displayName: canWrite ? 'Share' : 'See Permissions', + icon: Share, + action: () => { + shareModal.openDrawer(path, 'mqtt_trigger') + } + } + ]} + /> +
+
+
+
edited by {edited_by}
the {displayDate(edited_at)}
+
+ {/each} +
+ {:else} + + {/if} +
+ {#if items && items?.length > 15 && nbDisplayed < items.length} + {nbDisplayed} items out of {items.length} + + {/if} + + + { + loadTriggers() + }} +/> 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 ad9ed12186fea..6e75ea6a2c282 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte @@ -89,6 +89,7 @@ import PostgresTriggersPanel from '$lib/components/triggers/postgres/PostgresTriggersPanel.svelte' import Toggle from '$lib/components/Toggle.svelte' import InputSelectedBadge from '$lib/components/schema/InputSelectedBadge.svelte' + import MqttTriggersPanel from '$lib/components/triggers/mqtt/MqttTriggersPanel.svelte' import SqsTriggerPanel from '$lib/components/triggers/sqs/SqsTriggerPanel.svelte' let script: Script | undefined @@ -773,6 +774,11 @@
+ +
+ +
+