From f3fd1e90b09769214f8ade86f62bc30da3cb74a7 Mon Sep 17 00:00:00 2001 From: dieriba Date: Tue, 2 Sep 2025 13:28:06 +0200 Subject: [PATCH] feat: refactor trigger crud (#6472) * base * base crate * websocket updated * crud refactored * fix and nits * fix compiler warning, nits and update repo ref * fix oss * fix compilation * update ref * fix * update feature * listener base * add listener * refactor logic done and implemented for postgres * fix capture * websocket * implem for all triggers * update sqlx,repo ref and fix * fix oss * unify struct fix websocket * nits and fix oss runtime axum error * perf cache query * update .sqlx * update .sqlx * fix * fix unused * fix * fix * nits and fix http handler update endpoint * update .sqlx * update repo ref * nits * fix * update ref * fix * update .sqlx --- ...af91de289f52462e74343770a8b0d47d0577d.json | 26 + ...c30afc081eeed1151a0b5e043d785baebfc9d.json | 39 - ...8771a8f42c28c87b78220eec1bddeae1f648f.json | 26 + ...60f5c3561c37e9c6f2834d234a862a475a6eb.json | 26 - ...348ab17dee7b8358b6719fa399511962bc3e9.json | 28 - ...52354641c2f735d071540f858bc064f2432cd.json | 28 + ...2d54f97bc2d0ef52f5812244a97c289523592.json | 39 + ...ac4dded0536faf52c370d704ed9a451041caf.json | 24 - ...9418f85d4d3d599f4177403fad5ad99380715.json | 26 - ...b20f15e4a7839c87c853f280623bafb30b80c.json | 28 - ...2d76f7e174e39c45ede29dcd6aa9c65850fef.json | 153 -- ...c5c32f582a3911efcf88e437aa90d7d5a49b5.json | 24 - ...935a0d3b2ca6d057f446ed9b02d4386e2ddd9.json | 23 - ...96b0a7dbdd27f85c7d886eded79b3af83731a.json | 28 + ...16cf95ad56f790f0eddb6e2fbe3d83d3accaa.json | 26 - ...32d8a250bd0d65ce33d7eb19eaea62466a803.json | 140 -- ...9f733cff812e62dea1c1bce8cefd3a3979b14.json | 24 - ...232f173446d29c08fc29ddb8d55294f2c070e.json | 24 - ...2cf0e4086030f3b7994e4892392765cbe1517.json | 23 - ...81d870b847a8ba07e6197212fdf85ec901b09.json | 15 - ...c810221c5fc8fc29ca4f1984c819e64e29437.json | 125 -- ...1a3e01be0726d02d3f863e0ea5c476a8e785e.json | 26 + ...a660bfd5a8c0a451a0deac24632f0ccf66165.json | 26 - ...d9323e6c66107a1b7b37dadbbb7b91339e9d5.json | 124 -- ...ccbbff020a2042409fc318e045852323cef5e.json | 125 -- ...8c591605211c070e52b2919cf9d7dede0ea16.json | 26 - ...c3449b02925c8caf85d9203529471479fdde8.json | 124 -- ...764231013b32e7dc26fb3b775e872d2222005.json | 26 - ...d9c89e9b83571e30059861b6998935b35f78b.json | 23 - ...f4968ecebec1614580e55de3d5895f0595e52.json | 15 - ...2fbe1f9e0172b38492a99be19147d04a961fe.json | 26 - ...153c43903f929ae5d62fbba12610f89c36d55.json | 2 +- ...8f97b5ece60bfe24fc3783a83de47e3aa583.json} | 4 +- ...dea0bfdb683de9555e374ce5731e0b3c379f9.json | 24 - ...aef744a1b10b4f47bc7355a97011bc366aae.json} | 6 +- ...957c87f2307a218115ca749ea0e71eca2002e.json | 23 - ...00edb7e0ebad5de40ba5645c1ce3d4a6b84be.json | 136 -- ...a37fcc985bf61978cc07167ed35d7dbf92d11.json | 23 - ...340a155562fd6cde328298a66496c90d8cd59.json | 26 - ...6ddb29140dd72ce5115657418eec503b205d.json} | 4 +- ...7832c5a674a5553674fd9762c981e0d03bb57.json | 15 - ...a213e360ebe8827d5b4680d3c227fb8ddec82.json | 187 --- ...205100d6673a94e90000c8db4eb5809f17365.json | 26 - ...052d2064fcc8f373fecfebd40914768b2b1cf.json | 24 - ...d56c53acd02eb350b46f6b815a5ddbb69d163.json | 137 -- ...50ff78df6c5dcf92b08d611afe23c5b865521.json | 37 + ...1aadbc27ff5bd0c554bf5339060cc35797d3f.json | 15 - ...ea74ffbb4c0ed7e694f8f03d92f2a7c118af9.json | 15 - ...6c23f8bbcefc288a56d61122bfd6fe5ca7e8d.json | 24 - ...f4a3600c1ccfe6847bd526a751bc7baf825c5.json | 23 - ...a4bf51211f2f73d78c546c894c8e147677ac7.json | 24 - ...f6104a430a11d7c5cb9447025d11506585708.json | 23 - ...23b4376a93342702856da2ac70f6bbfc7933e.json | 23 - ...f14572ac204cfe377166aa1d91cf58f731f50.json | 15 - ...93c551c30ed85eabe8ec11cafa6358f354c8b.json | 37 - ...87e794b461a7a965912bcb598619451eced31.json | 136 -- ...0ce006b1ceed26e29d812d524c711d178167e.json | 37 - ...041ac728553e20adf722eb795d01d6650ff0f.json | 28 - ...f5243ec9aa66ea1a9da9662fc877f4becb04e.json | 168 -- ...72e8da54885cdd135e0877dec9cf407064158.json | 141 -- ...05cddac517bdc06dca0a8be071330338e948a.json | 15 - ...33ba30fa85efcc83fafe4dd7b4e535b96a8d8.json | 15 - ...03cb0bf241d959a6c8f7bb896160001a6672b.json | 26 + ...0078a25f117a3bfec78d5270a4c4970f2465e.json | 37 + ...fd2d5749015c66d7de277081598ba3bb6dd8f.json | 39 - ...b29ba44c4ff4d2c5b38369f0ede61d4708a21.json | 188 --- ...a630be29176bd5d1246c974642d40201782fc.json | 26 - ...0f7fa1ca50b45cc82e2fd9d4e61768b258735.json | 150 -- ...1f351d6df8282415e54f0ea4f905417657ba8.json | 28 + ...c5f7c93fbdb1e7d090b11612aa284342e86a1.json | 169 -- ...05a50d1ed4f69629c43de7d874769c340d909.json | 26 - ...3d746de874ca6600178f5db9797eda44f1733.json | 39 + backend/Cargo.lock | 1 + backend/Cargo.toml | 1 + backend/ee-repo-ref.txt | 2 +- backend/src/main.rs | 2 +- backend/windmill-api/Cargo.toml | 1 + backend/windmill-api/src/args.rs | 2 +- backend/windmill-api/src/auth.rs | 75 +- backend/windmill-api/src/capture.rs | 62 +- backend/windmill-api/src/db.rs | 7 +- backend/windmill-api/src/ee_oss.rs | 23 + backend/windmill-api/src/flows.rs | 5 +- backend/windmill-api/src/gcp_triggers_oss.rs | 77 +- backend/windmill-api/src/http_triggers.rs | 1469 ----------------- backend/windmill-api/src/jobs.rs | 2 +- .../windmill-api/src/kafka_triggers_oss.rs | 11 +- backend/windmill-api/src/lib.rs | 128 +- backend/windmill-api/src/mqtt_triggers.rs | 812 +-------- backend/windmill-api/src/nats_triggers_oss.rs | 7 - backend/windmill-api/src/openapi.rs | 7 +- .../windmill-api/src/postgres_triggers/mod.rs | 300 +--- .../src/postgres_triggers/trigger.rs | 52 +- backend/windmill-api/src/resources.rs | 7 +- backend/windmill-api/src/scripts.rs | 5 +- backend/windmill-api/src/sqs_triggers_oss.rs | 11 +- backend/windmill-api/src/trigger_helpers.rs | 2 + backend/windmill-api/src/triggers.rs | 249 --- .../src/triggers/gcp/handler_oss.rs | 64 + backend/windmill-api/src/triggers/gcp/mod.rs | 11 + backend/windmill-api/src/triggers/handler.rs | 923 +++++++++++ .../windmill-api/src/triggers/http/handler.rs | 1015 ++++++++++++ .../{ => triggers/http}/http_trigger_args.rs | 0 .../{ => triggers/http}/http_trigger_auth.rs | 0 backend/windmill-api/src/triggers/http/mod.rs | 279 ++++ .../src/triggers/kafka/handler_oss.rs | 68 + .../windmill-api/src/triggers/kafka/mod.rs | 11 + backend/windmill-api/src/triggers/mod.rs | 140 ++ .../windmill-api/src/triggers/mqtt/handler.rs | 238 +++ backend/windmill-api/src/triggers/mqtt/mod.rs | 346 ++++ .../src/triggers/nats/handler_oss.rs | 65 + backend/windmill-api/src/triggers/nats/mod.rs | 11 + .../postgres}/handler.rs | 1144 ++++--------- .../postgres}/mapper.rs | 2 +- .../windmill-api/src/triggers/postgres/mod.rs | 547 ++++++ .../src/triggers/sqs/handler_oss.rs | 66 + backend/windmill-api/src/triggers/sqs/mod.rs | 11 + .../src/triggers/trigger_helpers.rs | 811 +++++++++ .../src/triggers/websocket/handler.rs | 258 +++ .../src/triggers/websocket/mod.rs | 121 ++ backend/windmill-api/src/utils.rs | 7 +- .../windmill-api/src/websocket_triggers.rs | 539 +----- backend/windmill-api/src/workspaces_export.rs | 342 +--- backend/windmill-common/src/cache.rs | 2 +- backend/windmill-common/src/flows.rs | 2 +- backend/windmill-common/src/indexer.rs | 2 +- backend/windmill-common/src/jobs.rs | 41 +- backend/windmill-common/src/lib.rs | 7 +- backend/windmill-common/src/triggers.rs | 52 +- backend/windmill-git-sync/src/lib.rs | 11 +- backend/windmill-queue/src/jobs.rs | 38 +- backend/windmill-worker/src/ai_executor.rs | 3 +- backend/windmill-worker/src/worker_flow.rs | 4 +- 133 files changed, 6183 insertions(+), 7895 deletions(-) create mode 100644 backend/.sqlx/query-0382065b3dfd78b384e26f81317af91de289f52462e74343770a8b0d47d0577d.json delete mode 100644 backend/.sqlx/query-04a8482180a3f8e07761742f13ac30afc081eeed1151a0b5e043d785baebfc9d.json create mode 100644 backend/.sqlx/query-06af027f6ed10200de2006a2fc48771a8f42c28c87b78220eec1bddeae1f648f.json delete mode 100644 backend/.sqlx/query-070b8ad0b59f485fa5bf68082b060f5c3561c37e9c6f2834d234a862a475a6eb.json delete mode 100644 backend/.sqlx/query-0d8415a79d8312db4d46da46fdf348ab17dee7b8358b6719fa399511962bc3e9.json create mode 100644 backend/.sqlx/query-0ef1e5bbbefc117a4cdaf414b3652354641c2f735d071540f858bc064f2432cd.json create mode 100644 backend/.sqlx/query-0f697b1ab3105e2ea036f8ecace2d54f97bc2d0ef52f5812244a97c289523592.json delete mode 100644 backend/.sqlx/query-13444bbd5547e101c41206c5f97ac4dded0536faf52c370d704ed9a451041caf.json delete mode 100644 backend/.sqlx/query-1625a84fbcf8c5f77eb0519f60d9418f85d4d3d599f4177403fad5ad99380715.json delete mode 100644 backend/.sqlx/query-1e333ccb7657bc721d18ea2230cb20f15e4a7839c87c853f280623bafb30b80c.json delete mode 100644 backend/.sqlx/query-208f30f8a570db41ee52f01a4402d76f7e174e39c45ede29dcd6aa9c65850fef.json delete mode 100644 backend/.sqlx/query-2139f1fb1877294bbf55d786000c5c32f582a3911efcf88e437aa90d7d5a49b5.json delete mode 100644 backend/.sqlx/query-2238aaed46031f71b17c1cabcef935a0d3b2ca6d057f446ed9b02d4386e2ddd9.json create mode 100644 backend/.sqlx/query-23eb4d45bf2df21e22fc6c9590b96b0a7dbdd27f85c7d886eded79b3af83731a.json delete mode 100644 backend/.sqlx/query-25975935d59f88df117bae0fa5016cf95ad56f790f0eddb6e2fbe3d83d3accaa.json delete mode 100644 backend/.sqlx/query-264bc51ec372dfc6c1c4eb3608b32d8a250bd0d65ce33d7eb19eaea62466a803.json delete mode 100644 backend/.sqlx/query-2e6165543e34216dfaedf6e10729f733cff812e62dea1c1bce8cefd3a3979b14.json delete mode 100644 backend/.sqlx/query-31bc3dcea29be9cc0242771d25a232f173446d29c08fc29ddb8d55294f2c070e.json delete mode 100644 backend/.sqlx/query-33ee913ce263600a3f94f90e4a42cf0e4086030f3b7994e4892392765cbe1517.json delete mode 100644 backend/.sqlx/query-3997dcf2c11817e59bf50fd896381d870b847a8ba07e6197212fdf85ec901b09.json delete mode 100644 backend/.sqlx/query-3a6c93a91d9a21aaf9e9709932ac810221c5fc8fc29ca4f1984c819e64e29437.json create mode 100644 backend/.sqlx/query-3b6bd7b41f130ce6df62fdecb351a3e01be0726d02d3f863e0ea5c476a8e785e.json delete mode 100644 backend/.sqlx/query-3d8fdf7adf42b27808a5b223df3a660bfd5a8c0a451a0deac24632f0ccf66165.json delete mode 100644 backend/.sqlx/query-3ebf788b86d6aed59b8c61c08f1d9323e6c66107a1b7b37dadbbb7b91339e9d5.json delete mode 100644 backend/.sqlx/query-494d49e92dce7a1cd7074156704ccbbff020a2042409fc318e045852323cef5e.json delete mode 100644 backend/.sqlx/query-4bfa42153df81184e02ffc7ab6c8c591605211c070e52b2919cf9d7dede0ea16.json delete mode 100644 backend/.sqlx/query-4c6fbed80ba44defe3d125c258bc3449b02925c8caf85d9203529471479fdde8.json delete mode 100644 backend/.sqlx/query-4eab48b7e86788835c3ba739cca764231013b32e7dc26fb3b775e872d2222005.json delete mode 100644 backend/.sqlx/query-4ed74bbda2ad0ca5e4648787fe2d9c89e9b83571e30059861b6998935b35f78b.json delete mode 100644 backend/.sqlx/query-508d267b0d77fd12446654a502bf4968ecebec1614580e55de3d5895f0595e52.json delete mode 100644 backend/.sqlx/query-5140cfccd4a8fb7d64087de34bc2fbe1f9e0172b38492a99be19147d04a961fe.json rename backend/.sqlx/{query-aaa8dde748e5bcd462122d3235e54c784399b2cac7b27f0de33e78bd77661565.json => query-5cc0d9e3dcd9c20e6e6ec1acf38c8f97b5ece60bfe24fc3783a83de47e3aa583.json} (51%) delete mode 100644 backend/.sqlx/query-6a19c440a7a8064f3969cf6f48adea0bfdb683de9555e374ce5731e0b3c379f9.json rename backend/.sqlx/{query-d580bac9f4be4329da29781efa00cd0b5379955a42b7e070c7e11f8479441482.json => query-6ca3ae85ddeb2334c9ae4bd7682daef744a1b10b4f47bc7355a97011bc366aae.json} (77%) delete mode 100644 backend/.sqlx/query-6d418b5cd7a4df54cfe3ec06e2a957c87f2307a218115ca749ea0e71eca2002e.json delete mode 100644 backend/.sqlx/query-711e665344873454428d84e35f300edb7e0ebad5de40ba5645c1ce3d4a6b84be.json delete mode 100644 backend/.sqlx/query-746ee16a04267cd251d4cefbc44a37fcc985bf61978cc07167ed35d7dbf92d11.json delete mode 100644 backend/.sqlx/query-7f039cc86874128ee8c9f577897340a155562fd6cde328298a66496c90d8cd59.json rename backend/.sqlx/{query-f2a78401a363442f165d018d61a294b4575e2cdc31e8da3d453addd0edc3edf6.json => query-85d945cf5ade707291a161078ff96ddb29140dd72ce5115657418eec503b205d.json} (56%) delete mode 100644 backend/.sqlx/query-870b12a46f26c9e29889dd28a5c7832c5a674a5553674fd9762c981e0d03bb57.json delete mode 100644 backend/.sqlx/query-89be9b595a892c82f2a9f3cfa4da213e360ebe8827d5b4680d3c227fb8ddec82.json delete mode 100644 backend/.sqlx/query-8d31b4a531c59a2385210d1213c205100d6673a94e90000c8db4eb5809f17365.json delete mode 100644 backend/.sqlx/query-8e1afb488096330890b1675d2b3052d2064fcc8f373fecfebd40914768b2b1cf.json delete mode 100644 backend/.sqlx/query-8ffd68b82cfce337fc32a6e80c8d56c53acd02eb350b46f6b815a5ddbb69d163.json create mode 100644 backend/.sqlx/query-9abbb75c238372cc3dbaebf1dbc50ff78df6c5dcf92b08d611afe23c5b865521.json delete mode 100644 backend/.sqlx/query-9e88a43de7315052668619002321aadbc27ff5bd0c554bf5339060cc35797d3f.json delete mode 100644 backend/.sqlx/query-a269c388056eabe4b045948f451ea74ffbb4c0ed7e694f8f03d92f2a7c118af9.json delete mode 100644 backend/.sqlx/query-a8b470b463ca4b7c00c7ef6e9f36c23f8bbcefc288a56d61122bfd6fe5ca7e8d.json delete mode 100644 backend/.sqlx/query-aae8699bbaa4d6111eabee715a6f4a3600c1ccfe6847bd526a751bc7baf825c5.json delete mode 100644 backend/.sqlx/query-ad07b57ad928a4e5260833f4fe2a4bf51211f2f73d78c546c894c8e147677ac7.json delete mode 100644 backend/.sqlx/query-adb0090afd3ce918d8b80ff51d9f6104a430a11d7c5cb9447025d11506585708.json delete mode 100644 backend/.sqlx/query-b386d320f9fe1d569a16e6626b723b4376a93342702856da2ac70f6bbfc7933e.json delete mode 100644 backend/.sqlx/query-c19a60a9dc3f95af218baf40c62f14572ac204cfe377166aa1d91cf58f731f50.json delete mode 100644 backend/.sqlx/query-c22438f9bff27ccd3228a186a8093c551c30ed85eabe8ec11cafa6358f354c8b.json delete mode 100644 backend/.sqlx/query-c287f025c77c34e4d521204e7a787e794b461a7a965912bcb598619451eced31.json delete mode 100644 backend/.sqlx/query-c3730ae025a4dcc25b2713e3e670ce006b1ceed26e29d812d524c711d178167e.json delete mode 100644 backend/.sqlx/query-c4cd9668733b2b439ac7a911d15041ac728553e20adf722eb795d01d6650ff0f.json delete mode 100644 backend/.sqlx/query-c61df65f2e4f8bc146b0f70e605f5243ec9aa66ea1a9da9662fc877f4becb04e.json delete mode 100644 backend/.sqlx/query-d3bc837633eb92cd9a8d37ad09a72e8da54885cdd135e0877dec9cf407064158.json delete mode 100644 backend/.sqlx/query-d54840373df5da9662ed11ed0a605cddac517bdc06dca0a8be071330338e948a.json delete mode 100644 backend/.sqlx/query-dea056c89313f4facd62cbbc9fa33ba30fa85efcc83fafe4dd7b4e535b96a8d8.json create mode 100644 backend/.sqlx/query-e56c71de41ba0750ae9ae8540c903cb0bf241d959a6c8f7bb896160001a6672b.json create mode 100644 backend/.sqlx/query-e9941e580a21848fa71b38c58030078a25f117a3bfec78d5270a4c4970f2465e.json delete mode 100644 backend/.sqlx/query-e9b4938b8c5fbd57845cf560fc2fd2d5749015c66d7de277081598ba3bb6dd8f.json delete mode 100644 backend/.sqlx/query-ea59131c1fba8fb14a95b4a486db29ba44c4ff4d2c5b38369f0ede61d4708a21.json delete mode 100644 backend/.sqlx/query-eda1e5d1109a13feb14be254eaca630be29176bd5d1246c974642d40201782fc.json delete mode 100644 backend/.sqlx/query-eebc9b609d9b595524223ada91e0f7fa1ca50b45cc82e2fd9d4e61768b258735.json create mode 100644 backend/.sqlx/query-f28b71a304721643dd90fa468f61f351d6df8282415e54f0ea4f905417657ba8.json delete mode 100644 backend/.sqlx/query-f434cac3a8eded700b09a8c99dcc5f7c93fbdb1e7d090b11612aa284342e86a1.json delete mode 100644 backend/.sqlx/query-fd1db7530acf3c84b2ab696504905a50d1ed4f69629c43de7d874769c340d909.json create mode 100644 backend/.sqlx/query-fe78796c5ea2384bdb6191b66943d746de874ca6600178f5db9797eda44f1733.json delete mode 100644 backend/windmill-api/src/http_triggers.rs delete mode 100644 backend/windmill-api/src/triggers.rs create mode 100644 backend/windmill-api/src/triggers/gcp/handler_oss.rs create mode 100644 backend/windmill-api/src/triggers/gcp/mod.rs create mode 100644 backend/windmill-api/src/triggers/handler.rs create mode 100644 backend/windmill-api/src/triggers/http/handler.rs rename backend/windmill-api/src/{ => triggers/http}/http_trigger_args.rs (100%) rename backend/windmill-api/src/{ => triggers/http}/http_trigger_auth.rs (100%) create mode 100644 backend/windmill-api/src/triggers/http/mod.rs create mode 100644 backend/windmill-api/src/triggers/kafka/handler_oss.rs create mode 100644 backend/windmill-api/src/triggers/kafka/mod.rs create mode 100644 backend/windmill-api/src/triggers/mod.rs create mode 100644 backend/windmill-api/src/triggers/mqtt/handler.rs create mode 100644 backend/windmill-api/src/triggers/mqtt/mod.rs create mode 100644 backend/windmill-api/src/triggers/nats/handler_oss.rs create mode 100644 backend/windmill-api/src/triggers/nats/mod.rs rename backend/windmill-api/src/{postgres_triggers => triggers/postgres}/handler.rs (50%) rename backend/windmill-api/src/{postgres_triggers => triggers/postgres}/mapper.rs (99%) create mode 100644 backend/windmill-api/src/triggers/postgres/mod.rs create mode 100644 backend/windmill-api/src/triggers/sqs/handler_oss.rs create mode 100644 backend/windmill-api/src/triggers/sqs/mod.rs create mode 100644 backend/windmill-api/src/triggers/trigger_helpers.rs create mode 100644 backend/windmill-api/src/triggers/websocket/handler.rs create mode 100644 backend/windmill-api/src/triggers/websocket/mod.rs diff --git a/backend/.sqlx/query-0382065b3dfd78b384e26f81317af91de289f52462e74343770a8b0d47d0577d.json b/backend/.sqlx/query-0382065b3dfd78b384e26f81317af91de289f52462e74343770a8b0d47d0577d.json new file mode 100644 index 0000000000..7e2371e9d8 --- /dev/null +++ b/backend/.sqlx/query-0382065b3dfd78b384e26f81317af91de289f52462e74343770a8b0d47d0577d.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO kafka_trigger (\n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n script_path,\n is_flow,\n enabled,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, now(), $11, $12, $13\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "VarcharArray", + "Varchar", + "Bool", + "Bool", + "Varchar", + "Varchar", + "Varchar", + "Jsonb", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "0382065b3dfd78b384e26f81317af91de289f52462e74343770a8b0d47d0577d" +} diff --git a/backend/.sqlx/query-04a8482180a3f8e07761742f13ac30afc081eeed1151a0b5e043d785baebfc9d.json b/backend/.sqlx/query-04a8482180a3f8e07761742f13ac30afc081eeed1151a0b5e043d785baebfc9d.json deleted file mode 100644 index 2fe60e6570..0000000000 --- a/backend/.sqlx/query-04a8482180a3f8e07761742f13ac30afc081eeed1151a0b5e043d785baebfc9d.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "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 error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\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", - "Varchar", - "Jsonb", - "Jsonb" - ] - }, - "nullable": [] - }, - "hash": "04a8482180a3f8e07761742f13ac30afc081eeed1151a0b5e043d785baebfc9d" -} diff --git a/backend/.sqlx/query-06af027f6ed10200de2006a2fc48771a8f42c28c87b78220eec1bddeae1f648f.json b/backend/.sqlx/query-06af027f6ed10200de2006a2fc48771a8f42c28c87b78220eec1bddeae1f648f.json new file mode 100644 index 0000000000..8566af0709 --- /dev/null +++ b/backend/.sqlx/query-06af027f6ed10200de2006a2fc48771a8f42c28c87b78220eec1bddeae1f648f.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO postgres_trigger (\n workspace_id,\n path,\n postgres_resource_path,\n replication_slot_name,\n publication_name,\n script_path,\n is_flow,\n enabled,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, now(), $11, $12, $13\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Bool", + "Bool", + "Varchar", + "Varchar", + "Varchar", + "Jsonb", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "06af027f6ed10200de2006a2fc48771a8f42c28c87b78220eec1bddeae1f648f" +} diff --git a/backend/.sqlx/query-070b8ad0b59f485fa5bf68082b060f5c3561c37e9c6f2834d234a862a475a6eb.json b/backend/.sqlx/query-070b8ad0b59f485fa5bf68082b060f5c3561c37e9c6f2834d234a862a475a6eb.json deleted file mode 100644 index 280db517dd..0000000000 --- a/backend/.sqlx/query-070b8ad0b59f485fa5bf68082b060f5c3561c37e9c6f2834d234a862a475a6eb.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE \n gcp_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": "070b8ad0b59f485fa5bf68082b060f5c3561c37e9c6f2834d234a862a475a6eb" -} diff --git a/backend/.sqlx/query-0d8415a79d8312db4d46da46fdf348ab17dee7b8358b6719fa399511962bc3e9.json b/backend/.sqlx/query-0d8415a79d8312db4d46da46fdf348ab17dee7b8358b6719fa399511962bc3e9.json deleted file mode 100644 index 2040b0ed88..0000000000 --- a/backend/.sqlx/query-0d8415a79d8312db4d46da46fdf348ab17dee7b8358b6719fa399511962bc3e9.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE \n nats_trigger\n SET\n nats_resource_path = $1,\n subjects = $2,\n stream_name = $3,\n consumer_name = $4,\n use_jetstream = $5,\n script_path = $6,\n path = $7,\n is_flow = $8,\n edited_by = $9,\n email = $10,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $13,\n error_handler_args = $14,\n retry = $15\n WHERE\n workspace_id = $11 AND path = $12\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "VarcharArray", - "Varchar", - "Varchar", - "Bool", - "Varchar", - "Varchar", - "Bool", - "Varchar", - "Varchar", - "Text", - "Text", - "Varchar", - "Jsonb", - "Jsonb" - ] - }, - "nullable": [] - }, - "hash": "0d8415a79d8312db4d46da46fdf348ab17dee7b8358b6719fa399511962bc3e9" -} diff --git a/backend/.sqlx/query-0ef1e5bbbefc117a4cdaf414b3652354641c2f735d071540f858bc064f2432cd.json b/backend/.sqlx/query-0ef1e5bbbefc117a4cdaf414b3652354641c2f735d071540f858bc064f2432cd.json new file mode 100644 index 0000000000..a867ace2cb --- /dev/null +++ b/backend/.sqlx/query-0ef1e5bbbefc117a4cdaf414b3652354641c2f735d071540f858bc064f2432cd.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE nats_trigger \n SET \n nats_resource_path = $1,\n subjects = $2,\n stream_name = $3,\n consumer_name = $4,\n use_jetstream = $5,\n script_path = $6,\n path = $7,\n is_flow = $8,\n edited_by = $9,\n email = $10,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $13,\n error_handler_args = $14,\n retry = $15\n WHERE \n workspace_id = $11 AND path = $12\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "VarcharArray", + "Varchar", + "Varchar", + "Bool", + "Varchar", + "Varchar", + "Bool", + "Varchar", + "Varchar", + "Text", + "Text", + "Varchar", + "Jsonb", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "0ef1e5bbbefc117a4cdaf414b3652354641c2f735d071540f858bc064f2432cd" +} diff --git a/backend/.sqlx/query-0f697b1ab3105e2ea036f8ecace2d54f97bc2d0ef52f5812244a97c289523592.json b/backend/.sqlx/query-0f697b1ab3105e2ea036f8ecace2d54f97bc2d0ef52f5812244a97c289523592.json new file mode 100644 index 0000000000..ec215e0f29 --- /dev/null +++ b/backend/.sqlx/query-0f697b1ab3105e2ea036f8ecace2d54f97bc2d0ef52f5812244a97c289523592.json @@ -0,0 +1,39 @@ +{ + "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 error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\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", + "Varchar", + "Jsonb", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "0f697b1ab3105e2ea036f8ecace2d54f97bc2d0ef52f5812244a97c289523592" +} diff --git a/backend/.sqlx/query-13444bbd5547e101c41206c5f97ac4dded0536faf52c370d704ed9a451041caf.json b/backend/.sqlx/query-13444bbd5547e101c41206c5f97ac4dded0536faf52c370d704ed9a451041caf.json deleted file mode 100644 index 6b8e275167..0000000000 --- a/backend/.sqlx/query-13444bbd5547e101c41206c5f97ac4dded0536faf52c370d704ed9a451041caf.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) FROM sqs_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Bool", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "13444bbd5547e101c41206c5f97ac4dded0536faf52c370d704ed9a451041caf" -} diff --git a/backend/.sqlx/query-1625a84fbcf8c5f77eb0519f60d9418f85d4d3d599f4177403fad5ad99380715.json b/backend/.sqlx/query-1625a84fbcf8c5f77eb0519f60d9418f85d4d3d599f4177403fad5ad99380715.json deleted file mode 100644 index f0ee89dbc9..0000000000 --- a/backend/.sqlx/query-1625a84fbcf8c5f77eb0519f60d9418f85d4d3d599f4177403fad5ad99380715.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE \n sqs_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": "1625a84fbcf8c5f77eb0519f60d9418f85d4d3d599f4177403fad5ad99380715" -} diff --git a/backend/.sqlx/query-1e333ccb7657bc721d18ea2230cb20f15e4a7839c87c853f280623bafb30b80c.json b/backend/.sqlx/query-1e333ccb7657bc721d18ea2230cb20f15e4a7839c87c853f280623bafb30b80c.json deleted file mode 100644 index 19ae194f04..0000000000 --- a/backend/.sqlx/query-1e333ccb7657bc721d18ea2230cb20f15e4a7839c87c853f280623bafb30b80c.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO nats_trigger (\n workspace_id,\n path,\n nats_resource_path,\n subjects,\n stream_name,\n consumer_name,\n use_jetstream,\n script_path,\n is_flow,\n enabled,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13, $14, $15\n )\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Varchar", - "VarcharArray", - "Varchar", - "Varchar", - "Bool", - "Varchar", - "Bool", - "Bool", - "Varchar", - "Varchar", - "Varchar", - "Jsonb", - "Jsonb" - ] - }, - "nullable": [] - }, - "hash": "1e333ccb7657bc721d18ea2230cb20f15e4a7839c87c853f280623bafb30b80c" -} diff --git a/backend/.sqlx/query-208f30f8a570db41ee52f01a4402d76f7e174e39c45ede29dcd6aa9c65850fef.json b/backend/.sqlx/query-208f30f8a570db41ee52f01a4402d76f7e174e39c45ede29dcd6aa9c65850fef.json deleted file mode 100644 index 24744c96fb..0000000000 --- a/backend/.sqlx/query-208f30f8a570db41ee52f01a4402d76f7e174e39c45ede29dcd6aa9c65850fef.json +++ /dev/null @@ -1,153 +0,0 @@ -{ - "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 error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\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" - }, - { - "ordinal": 18, - "name": "error_handler_path", - "type_info": "Varchar" - }, - { - "ordinal": 19, - "name": "error_handler_args: _", - "type_info": "Jsonb" - }, - { - "ordinal": 20, - "name": "retry: _", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false, - false, - true, - true, - false, - true, - false, - false, - false, - false, - false, - false, - false, - true, - true, - false, - true, - false, - true, - true, - true - ] - }, - "hash": "208f30f8a570db41ee52f01a4402d76f7e174e39c45ede29dcd6aa9c65850fef" -} diff --git a/backend/.sqlx/query-2139f1fb1877294bbf55d786000c5c32f582a3911efcf88e437aa90d7d5a49b5.json b/backend/.sqlx/query-2139f1fb1877294bbf55d786000c5c32f582a3911efcf88e437aa90d7d5a49b5.json deleted file mode 100644 index fef0a60ca7..0000000000 --- a/backend/.sqlx/query-2139f1fb1877294bbf55d786000c5c32f582a3911efcf88e437aa90d7d5a49b5.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) FROM kafka_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Bool", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "2139f1fb1877294bbf55d786000c5c32f582a3911efcf88e437aa90d7d5a49b5" -} diff --git a/backend/.sqlx/query-2238aaed46031f71b17c1cabcef935a0d3b2ca6d057f446ed9b02d4386e2ddd9.json b/backend/.sqlx/query-2238aaed46031f71b17c1cabcef935a0d3b2ca6d057f446ed9b02d4386e2ddd9.json deleted file mode 100644 index aa32886ba8..0000000000 --- a/backend/.sqlx/query-2238aaed46031f71b17c1cabcef935a0d3b2ca6d057f446ed9b02d4386e2ddd9.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT EXISTS(SELECT 1 FROM websocket_trigger WHERE path = $1 AND workspace_id = $2)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "exists", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "2238aaed46031f71b17c1cabcef935a0d3b2ca6d057f446ed9b02d4386e2ddd9" -} diff --git a/backend/.sqlx/query-23eb4d45bf2df21e22fc6c9590b96b0a7dbdd27f85c7d886eded79b3af83731a.json b/backend/.sqlx/query-23eb4d45bf2df21e22fc6c9590b96b0a7dbdd27f85c7d886eded79b3af83731a.json new file mode 100644 index 0000000000..ecaefdeb62 --- /dev/null +++ b/backend/.sqlx/query-23eb4d45bf2df21e22fc6c9590b96b0a7dbdd27f85c7d886eded79b3af83731a.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO websocket_trigger (\n workspace_id,\n path,\n url,\n script_path,\n is_flow,\n enabled,\n filters,\n initial_messages,\n url_runnable_args,\n edited_by,\n can_return_message,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13, $14, $15\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Bool", + "Bool", + "JsonbArray", + "JsonbArray", + "Jsonb", + "Varchar", + "Bool", + "Varchar", + "Varchar", + "Jsonb", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "23eb4d45bf2df21e22fc6c9590b96b0a7dbdd27f85c7d886eded79b3af83731a" +} diff --git a/backend/.sqlx/query-25975935d59f88df117bae0fa5016cf95ad56f790f0eddb6e2fbe3d83d3accaa.json b/backend/.sqlx/query-25975935d59f88df117bae0fa5016cf95ad56f790f0eddb6e2fbe3d83d3accaa.json deleted file mode 100644 index 4ae1f07aa8..0000000000 --- a/backend/.sqlx/query-25975935d59f88df117bae0fa5016cf95ad56f790f0eddb6e2fbe3d83d3accaa.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE websocket_trigger SET enabled = $1, email = $2, edited_by = $3, edited_at = now(), server_id = NULL, error = NULL\n WHERE path = $4 AND workspace_id = $5 RETURNING 1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "?column?", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Bool", - "Varchar", - "Varchar", - "Text", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "25975935d59f88df117bae0fa5016cf95ad56f790f0eddb6e2fbe3d83d3accaa" -} diff --git a/backend/.sqlx/query-264bc51ec372dfc6c1c4eb3608b32d8a250bd0d65ce33d7eb19eaea62466a803.json b/backend/.sqlx/query-264bc51ec372dfc6c1c4eb3608b32d8a250bd0d65ce33d7eb19eaea62466a803.json deleted file mode 100644 index 3e9ba6053e..0000000000 --- a/backend/.sqlx/query-264bc51ec372dfc6c1c4eb3608b32d8a250bd0d65ce33d7eb19eaea62466a803.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n aws_auth_resource_type AS \"aws_auth_resource_type: _\",\n aws_resource_path,\n message_attributes,\n queue_url,\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 error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n sqs_trigger\n WHERE \n workspace_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "aws_auth_resource_type: _", - "type_info": { - "Custom": { - "name": "aws_auth_resource_type", - "kind": { - "Enum": [ - "oidc", - "credentials" - ] - } - } - } - }, - { - "ordinal": 1, - "name": "aws_resource_path", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "message_attributes", - "type_info": "TextArray" - }, - { - "ordinal": 3, - "name": "queue_url", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "path", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "script_path", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "is_flow", - "type_info": "Bool" - }, - { - "ordinal": 8, - "name": "edited_by", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 10, - "name": "edited_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 11, - "name": "server_id", - "type_info": "Varchar" - }, - { - "ordinal": 12, - "name": "last_server_ping", - "type_info": "Timestamptz" - }, - { - "ordinal": 13, - "name": "extra_perms", - "type_info": "Jsonb" - }, - { - "ordinal": 14, - "name": "error", - "type_info": "Text" - }, - { - "ordinal": 15, - "name": "enabled", - "type_info": "Bool" - }, - { - "ordinal": 16, - "name": "error_handler_path", - "type_info": "Varchar" - }, - { - "ordinal": 17, - "name": "error_handler_args: _", - "type_info": "Jsonb" - }, - { - "ordinal": 18, - "name": "retry: _", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - true, - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - true, - true, - false, - true, - true, - true - ] - }, - "hash": "264bc51ec372dfc6c1c4eb3608b32d8a250bd0d65ce33d7eb19eaea62466a803" -} diff --git a/backend/.sqlx/query-2e6165543e34216dfaedf6e10729f733cff812e62dea1c1bce8cefd3a3979b14.json b/backend/.sqlx/query-2e6165543e34216dfaedf6e10729f733cff812e62dea1c1bce8cefd3a3979b14.json deleted file mode 100644 index 4a15e2d4fa..0000000000 --- a/backend/.sqlx/query-2e6165543e34216dfaedf6e10729f733cff812e62dea1c1bce8cefd3a3979b14.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) FROM websocket_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Bool", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "2e6165543e34216dfaedf6e10729f733cff812e62dea1c1bce8cefd3a3979b14" -} diff --git a/backend/.sqlx/query-31bc3dcea29be9cc0242771d25a232f173446d29c08fc29ddb8d55294f2c070e.json b/backend/.sqlx/query-31bc3dcea29be9cc0242771d25a232f173446d29c08fc29ddb8d55294f2c070e.json deleted file mode 100644 index 49fc84c349..0000000000 --- a/backend/.sqlx/query-31bc3dcea29be9cc0242771d25a232f173446d29c08fc29ddb8d55294f2c070e.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) FROM http_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Bool", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "31bc3dcea29be9cc0242771d25a232f173446d29c08fc29ddb8d55294f2c070e" -} diff --git a/backend/.sqlx/query-33ee913ce263600a3f94f90e4a42cf0e4086030f3b7994e4892392765cbe1517.json b/backend/.sqlx/query-33ee913ce263600a3f94f90e4a42cf0e4086030f3b7994e4892392765cbe1517.json deleted file mode 100644 index 9b7581526e..0000000000 --- a/backend/.sqlx/query-33ee913ce263600a3f94f90e4a42cf0e4086030f3b7994e4892392765cbe1517.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT EXISTS(\n SELECT \n 1 \n FROM \n gcp_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": "33ee913ce263600a3f94f90e4a42cf0e4086030f3b7994e4892392765cbe1517" -} diff --git a/backend/.sqlx/query-3997dcf2c11817e59bf50fd896381d870b847a8ba07e6197212fdf85ec901b09.json b/backend/.sqlx/query-3997dcf2c11817e59bf50fd896381d870b847a8ba07e6197212fdf85ec901b09.json deleted file mode 100644 index 4c90c24dd6..0000000000 --- a/backend/.sqlx/query-3997dcf2c11817e59bf50fd896381d870b847a8ba07e6197212fdf85ec901b09.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM kafka_trigger WHERE workspace_id = $1 AND path = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "3997dcf2c11817e59bf50fd896381d870b847a8ba07e6197212fdf85ec901b09" -} diff --git a/backend/.sqlx/query-3a6c93a91d9a21aaf9e9709932ac810221c5fc8fc29ca4f1984c819e64e29437.json b/backend/.sqlx/query-3a6c93a91d9a21aaf9e9709932ac810221c5fc8fc29ca4f1984c819e64e29437.json deleted file mode 100644 index c820c649f4..0000000000 --- a/backend/.sqlx/query-3a6c93a91d9a21aaf9e9709932ac810221c5fc8fc29ca4f1984c819e64e29437.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\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 replication_slot_name,\n publication_name,\n postgres_resource_path,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n postgres_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "path", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "script_path", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "is_flow", - "type_info": "Bool" - }, - { - "ordinal": 4, - "name": "edited_by", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "edited_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "server_id", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "last_server_ping", - "type_info": "Timestamptz" - }, - { - "ordinal": 9, - "name": "extra_perms", - "type_info": "Jsonb" - }, - { - "ordinal": 10, - "name": "error", - "type_info": "Text" - }, - { - "ordinal": 11, - "name": "enabled", - "type_info": "Bool" - }, - { - "ordinal": 12, - "name": "replication_slot_name", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "publication_name", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "postgres_resource_path", - "type_info": "Varchar" - }, - { - "ordinal": 15, - "name": "error_handler_path", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "error_handler_args: _", - "type_info": "Jsonb" - }, - { - "ordinal": 17, - "name": "retry: _", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - true, - true, - true, - true, - false, - false, - false, - false, - true, - true, - true - ] - }, - "hash": "3a6c93a91d9a21aaf9e9709932ac810221c5fc8fc29ca4f1984c819e64e29437" -} diff --git a/backend/.sqlx/query-3b6bd7b41f130ce6df62fdecb351a3e01be0726d02d3f863e0ea5c476a8e785e.json b/backend/.sqlx/query-3b6bd7b41f130ce6df62fdecb351a3e01be0726d02d3f863e0ea5c476a8e785e.json new file mode 100644 index 0000000000..fa18a36f44 --- /dev/null +++ b/backend/.sqlx/query-3b6bd7b41f130ce6df62fdecb351a3e01be0726d02d3f863e0ea5c476a8e785e.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE kafka_trigger \n SET \n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n edited_by = $7,\n email = $8,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $11,\n error_handler_args = $12,\n retry = $13\n WHERE \n workspace_id = $9 AND path = $10\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "VarcharArray", + "Varchar", + "Varchar", + "Bool", + "Varchar", + "Varchar", + "Text", + "Text", + "Varchar", + "Jsonb", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "3b6bd7b41f130ce6df62fdecb351a3e01be0726d02d3f863e0ea5c476a8e785e" +} diff --git a/backend/.sqlx/query-3d8fdf7adf42b27808a5b223df3a660bfd5a8c0a451a0deac24632f0ccf66165.json b/backend/.sqlx/query-3d8fdf7adf42b27808a5b223df3a660bfd5a8c0a451a0deac24632f0ccf66165.json deleted file mode 100644 index c48927fdf2..0000000000 --- a/backend/.sqlx/query-3d8fdf7adf42b27808a5b223df3a660bfd5a8c0a451a0deac24632f0ccf66165.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE nats_trigger SET enabled = $1, email = $2, edited_by = $3, edited_at = now(), server_id = NULL, error = NULL\n WHERE path = $4 AND workspace_id = $5 RETURNING 1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "?column?", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Bool", - "Varchar", - "Varchar", - "Text", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "3d8fdf7adf42b27808a5b223df3a660bfd5a8c0a451a0deac24632f0ccf66165" -} diff --git a/backend/.sqlx/query-3ebf788b86d6aed59b8c61c08f1d9323e6c66107a1b7b37dadbbb7b91339e9d5.json b/backend/.sqlx/query-3ebf788b86d6aed59b8c61c08f1d9323e6c66107a1b7b37dadbbb7b91339e9d5.json deleted file mode 100644 index 493a2ba2ec..0000000000 --- a/backend/.sqlx/query-3ebf788b86d6aed59b8c61c08f1d9323e6c66107a1b7b37dadbbb7b91339e9d5.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT \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 replication_slot_name,\n publication_name,\n postgres_resource_path,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n postgres_trigger\n WHERE \n workspace_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "path", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "script_path", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "is_flow", - "type_info": "Bool" - }, - { - "ordinal": 4, - "name": "edited_by", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "edited_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "server_id", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "last_server_ping", - "type_info": "Timestamptz" - }, - { - "ordinal": 9, - "name": "extra_perms", - "type_info": "Jsonb" - }, - { - "ordinal": 10, - "name": "error", - "type_info": "Text" - }, - { - "ordinal": 11, - "name": "enabled", - "type_info": "Bool" - }, - { - "ordinal": 12, - "name": "replication_slot_name", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "publication_name", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "postgres_resource_path", - "type_info": "Varchar" - }, - { - "ordinal": 15, - "name": "error_handler_path", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "error_handler_args: _", - "type_info": "Jsonb" - }, - { - "ordinal": 17, - "name": "retry: _", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - true, - true, - true, - true, - false, - false, - false, - false, - true, - true, - true - ] - }, - "hash": "3ebf788b86d6aed59b8c61c08f1d9323e6c66107a1b7b37dadbbb7b91339e9d5" -} diff --git a/backend/.sqlx/query-494d49e92dce7a1cd7074156704ccbbff020a2042409fc318e045852323cef5e.json b/backend/.sqlx/query-494d49e92dce7a1cd7074156704ccbbff020a2042409fc318e045852323cef5e.json deleted file mode 100644 index 25d6e0e6d3..0000000000 --- a/backend/.sqlx/query-494d49e92dce7a1cd7074156704ccbbff020a2042409fc318e045852323cef5e.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT \n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\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 error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n kafka_trigger \n WHERE \n workspace_id = $1 AND \n path = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "path", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "kafka_resource_path", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "group_id", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "topics", - "type_info": "VarcharArray" - }, - { - "ordinal": 5, - "name": "script_path", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "is_flow", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "edited_by", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "edited_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "server_id", - "type_info": "Varchar" - }, - { - "ordinal": 11, - "name": "last_server_ping", - "type_info": "Timestamptz" - }, - { - "ordinal": 12, - "name": "extra_perms", - "type_info": "Jsonb" - }, - { - "ordinal": 13, - "name": "error", - "type_info": "Text" - }, - { - "ordinal": 14, - "name": "enabled", - "type_info": "Bool" - }, - { - "ordinal": 15, - "name": "error_handler_path", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "error_handler_args: _", - "type_info": "Jsonb" - }, - { - "ordinal": 17, - "name": "retry: _", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - false, - true, - false, - true, - true, - true - ] - }, - "hash": "494d49e92dce7a1cd7074156704ccbbff020a2042409fc318e045852323cef5e" -} diff --git a/backend/.sqlx/query-4bfa42153df81184e02ffc7ab6c8c591605211c070e52b2919cf9d7dede0ea16.json b/backend/.sqlx/query-4bfa42153df81184e02ffc7ab6c8c591605211c070e52b2919cf9d7dede0ea16.json deleted file mode 100644 index 94ea76acc2..0000000000 --- a/backend/.sqlx/query-4bfa42153df81184e02ffc7ab6c8c591605211c070e52b2919cf9d7dede0ea16.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE kafka_trigger\n SET\n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n edited_by = $7,\n email = $8,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $11,\n error_handler_args = $12,\n retry = $13\n WHERE\n workspace_id = $9 AND path = $10\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "VarcharArray", - "Varchar", - "Varchar", - "Bool", - "Varchar", - "Varchar", - "Text", - "Text", - "Varchar", - "Jsonb", - "Jsonb" - ] - }, - "nullable": [] - }, - "hash": "4bfa42153df81184e02ffc7ab6c8c591605211c070e52b2919cf9d7dede0ea16" -} diff --git a/backend/.sqlx/query-4c6fbed80ba44defe3d125c258bc3449b02925c8caf85d9203529471479fdde8.json b/backend/.sqlx/query-4c6fbed80ba44defe3d125c258bc3449b02925c8caf85d9203529471479fdde8.json deleted file mode 100644 index bb5c6096f8..0000000000 --- a/backend/.sqlx/query-4c6fbed80ba44defe3d125c258bc3449b02925c8caf85d9203529471479fdde8.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT \n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\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 error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM kafka_trigger\n WHERE workspace_id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "path", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "kafka_resource_path", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "group_id", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "topics", - "type_info": "VarcharArray" - }, - { - "ordinal": 5, - "name": "script_path", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "is_flow", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "edited_by", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "edited_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "server_id", - "type_info": "Varchar" - }, - { - "ordinal": 11, - "name": "last_server_ping", - "type_info": "Timestamptz" - }, - { - "ordinal": 12, - "name": "extra_perms", - "type_info": "Jsonb" - }, - { - "ordinal": 13, - "name": "error", - "type_info": "Text" - }, - { - "ordinal": 14, - "name": "enabled", - "type_info": "Bool" - }, - { - "ordinal": 15, - "name": "error_handler_path", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "error_handler_args: _", - "type_info": "Jsonb" - }, - { - "ordinal": 17, - "name": "retry: _", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - false, - true, - false, - true, - true, - true - ] - }, - "hash": "4c6fbed80ba44defe3d125c258bc3449b02925c8caf85d9203529471479fdde8" -} diff --git a/backend/.sqlx/query-4eab48b7e86788835c3ba739cca764231013b32e7dc26fb3b775e872d2222005.json b/backend/.sqlx/query-4eab48b7e86788835c3ba739cca764231013b32e7dc26fb3b775e872d2222005.json deleted file mode 100644 index af68ad8234..0000000000 --- a/backend/.sqlx/query-4eab48b7e86788835c3ba739cca764231013b32e7dc26fb3b775e872d2222005.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE postgres_trigger \n SET \n script_path = $1, \n path = $2, \n is_flow = $3, \n edited_by = $4, \n email = $5, \n postgres_resource_path = $6, \n replication_slot_name = $7,\n publication_name = $8,\n edited_at = now(), \n error = NULL,\n server_id = NULL,\n error_handler_path = $11,\n error_handler_args = $12,\n retry = $13\n WHERE \n workspace_id = $9 AND \n path = $10\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Bool", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Text", - "Text", - "Varchar", - "Jsonb", - "Jsonb" - ] - }, - "nullable": [] - }, - "hash": "4eab48b7e86788835c3ba739cca764231013b32e7dc26fb3b775e872d2222005" -} diff --git a/backend/.sqlx/query-4ed74bbda2ad0ca5e4648787fe2d9c89e9b83571e30059861b6998935b35f78b.json b/backend/.sqlx/query-4ed74bbda2ad0ca5e4648787fe2d9c89e9b83571e30059861b6998935b35f78b.json deleted file mode 100644 index e5b87450d3..0000000000 --- a/backend/.sqlx/query-4ed74bbda2ad0ca5e4648787fe2d9c89e9b83571e30059861b6998935b35f78b.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "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-508d267b0d77fd12446654a502bf4968ecebec1614580e55de3d5895f0595e52.json b/backend/.sqlx/query-508d267b0d77fd12446654a502bf4968ecebec1614580e55de3d5895f0595e52.json deleted file mode 100644 index 4a95ebaf74..0000000000 --- a/backend/.sqlx/query-508d267b0d77fd12446654a502bf4968ecebec1614580e55de3d5895f0595e52.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n DELETE FROM postgres_trigger \n WHERE \n workspace_id = $1 AND \n path = $2\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "508d267b0d77fd12446654a502bf4968ecebec1614580e55de3d5895f0595e52" -} diff --git a/backend/.sqlx/query-5140cfccd4a8fb7d64087de34bc2fbe1f9e0172b38492a99be19147d04a961fe.json b/backend/.sqlx/query-5140cfccd4a8fb7d64087de34bc2fbe1f9e0172b38492a99be19147d04a961fe.json deleted file mode 100644 index c80c36b18d..0000000000 --- a/backend/.sqlx/query-5140cfccd4a8fb7d64087de34bc2fbe1f9e0172b38492a99be19147d04a961fe.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO kafka_trigger (\n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n script_path,\n is_flow,\n enabled,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, now(), $11, $12, $13\n )\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "VarcharArray", - "Varchar", - "Bool", - "Bool", - "Varchar", - "Varchar", - "Varchar", - "Jsonb", - "Jsonb" - ] - }, - "nullable": [] - }, - "hash": "5140cfccd4a8fb7d64087de34bc2fbe1f9e0172b38492a99be19147d04a961fe" -} diff --git a/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json b/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json index 36ddb8ab9f..713ccb9dd3 100644 --- a/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json +++ b/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json @@ -15,7 +15,7 @@ ] }, "nullable": [ - true + null ] }, "hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55" diff --git a/backend/.sqlx/query-aaa8dde748e5bcd462122d3235e54c784399b2cac7b27f0de33e78bd77661565.json b/backend/.sqlx/query-5cc0d9e3dcd9c20e6e6ec1acf38c8f97b5ece60bfe24fc3783a83de47e3aa583.json similarity index 51% rename from backend/.sqlx/query-aaa8dde748e5bcd462122d3235e54c784399b2cac7b27f0de33e78bd77661565.json rename to backend/.sqlx/query-5cc0d9e3dcd9c20e6e6ec1acf38c8f97b5ece60bfe24fc3783a83de47e3aa583.json index db7e60bbf5..fc2540e85d 100644 --- a/backend/.sqlx/query-aaa8dde748e5bcd462122d3235e54c784399b2cac7b27f0de33e78bd77661565.json +++ b/backend/.sqlx/query-5cc0d9e3dcd9c20e6e6ec1acf38c8f97b5ece60bfe24fc3783a83de47e3aa583.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO http_trigger (\n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n authentication_resource_path,\n wrap_body,\n raw_string,\n script_path, \n summary,\n description,\n is_flow, \n is_async, \n authentication_method, \n http_method, \n static_asset_config, \n edited_by, \n email, \n edited_at, \n is_static_website,\n error_handler_path,\n error_handler_args,\n retry\n ) \n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), $19, $20, $21, $22\n )\n ", + "query": "\n INSERT INTO http_trigger (\n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n authentication_resource_path,\n wrap_body,\n raw_string,\n script_path, \n summary,\n description,\n is_flow, \n is_async, \n authentication_method, \n http_method, \n static_asset_config, \n edited_by, \n email, \n edited_at, \n is_static_website,\n error_handler_path,\n error_handler_args,\n retry\n ) \n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), $19, $20, $21, $22\n )\n ", "describe": { "columns": [], "parameters": { @@ -58,5 +58,5 @@ }, "nullable": [] }, - "hash": "aaa8dde748e5bcd462122d3235e54c784399b2cac7b27f0de33e78bd77661565" + "hash": "5cc0d9e3dcd9c20e6e6ec1acf38c8f97b5ece60bfe24fc3783a83de47e3aa583" } diff --git a/backend/.sqlx/query-6a19c440a7a8064f3969cf6f48adea0bfdb683de9555e374ce5731e0b3c379f9.json b/backend/.sqlx/query-6a19c440a7a8064f3969cf6f48adea0bfdb683de9555e374ce5731e0b3c379f9.json deleted file mode 100644 index 4ea7dabd04..0000000000 --- a/backend/.sqlx/query-6a19c440a7a8064f3969cf6f48adea0bfdb683de9555e374ce5731e0b3c379f9.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) FROM gcp_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Bool", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "6a19c440a7a8064f3969cf6f48adea0bfdb683de9555e374ce5731e0b3c379f9" -} diff --git a/backend/.sqlx/query-d580bac9f4be4329da29781efa00cd0b5379955a42b7e070c7e11f8479441482.json b/backend/.sqlx/query-6ca3ae85ddeb2334c9ae4bd7682daef744a1b10b4f47bc7355a97011bc366aae.json similarity index 77% rename from backend/.sqlx/query-d580bac9f4be4329da29781efa00cd0b5379955a42b7e070c7e11f8479441482.json rename to backend/.sqlx/query-6ca3ae85ddeb2334c9ae4bd7682daef744a1b10b4f47bc7355a97011bc366aae.json index 2e063cf7d3..658b99f032 100644 --- a/backend/.sqlx/query-d580bac9f4be4329da29781efa00cd0b5379955a42b7e070c7e11f8479441482.json +++ b/backend/.sqlx/query-6ca3ae85ddeb2334c9ae4bd7682daef744a1b10b4f47bc7355a97011bc366aae.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n UPDATE \n http_trigger \n SET \n workspaced_route = $1,\n wrap_body = $2,\n raw_string = $3,\n authentication_resource_path = $4,\n script_path = $5, \n path = $6, \n is_flow = $7, \n http_method = $8, \n static_asset_config = $9, \n edited_by = $10, \n email = $11, \n is_async = $12, \n authentication_method = $13, \n edited_at = now(), \n is_static_website = $14,\n error_handler_path = $15,\n error_handler_args = $16,\n retry = $17\n WHERE \n workspace_id = $18 AND \n path = $19\n ", + "query": "\n UPDATE \n http_trigger \n SET \n workspaced_route = $1,\n wrap_body = $2,\n raw_string = $3,\n authentication_resource_path = $4,\n script_path = $5, \n path = $6, \n is_flow = $7, \n http_method = $8, \n static_asset_config = $9, \n edited_by = $10, \n email = $11, \n is_async = $12, \n authentication_method = $13, \n summary = $14,\n description = $15,\n edited_at = now(), \n is_static_website = $16,\n error_handler_path = $17,\n error_handler_args = $18,\n retry = $19\n WHERE \n workspace_id = $20 AND \n path = $21\n ", "describe": { "columns": [], "parameters": { @@ -45,6 +45,8 @@ } } }, + "Varchar", + "Text", "Bool", "Varchar", "Jsonb", @@ -55,5 +57,5 @@ }, "nullable": [] }, - "hash": "d580bac9f4be4329da29781efa00cd0b5379955a42b7e070c7e11f8479441482" + "hash": "6ca3ae85ddeb2334c9ae4bd7682daef744a1b10b4f47bc7355a97011bc366aae" } diff --git a/backend/.sqlx/query-6d418b5cd7a4df54cfe3ec06e2a957c87f2307a218115ca749ea0e71eca2002e.json b/backend/.sqlx/query-6d418b5cd7a4df54cfe3ec06e2a957c87f2307a218115ca749ea0e71eca2002e.json deleted file mode 100644 index c20295fe97..0000000000 --- a/backend/.sqlx/query-6d418b5cd7a4df54cfe3ec06e2a957c87f2307a218115ca749ea0e71eca2002e.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT EXISTS(SELECT 1 FROM kafka_trigger WHERE path = $1 AND workspace_id = $2)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "exists", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "6d418b5cd7a4df54cfe3ec06e2a957c87f2307a218115ca749ea0e71eca2002e" -} diff --git a/backend/.sqlx/query-711e665344873454428d84e35f300edb7e0ebad5de40ba5645c1ce3d4a6b84be.json b/backend/.sqlx/query-711e665344873454428d84e35f300edb7e0ebad5de40ba5645c1ce3d4a6b84be.json deleted file mode 100644 index 3abff2fa36..0000000000 --- a/backend/.sqlx/query-711e665344873454428d84e35f300edb7e0ebad5de40ba5645c1ce3d4a6b84be.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT \n workspace_id,\n path,\n nats_resource_path,\n subjects,\n stream_name,\n consumer_name,\n use_jetstream,\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 error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n nats_trigger\n WHERE \n workspace_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "path", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "nats_resource_path", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "subjects", - "type_info": "VarcharArray" - }, - { - "ordinal": 4, - "name": "stream_name", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "consumer_name", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "use_jetstream", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "script_path", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "is_flow", - "type_info": "Bool" - }, - { - "ordinal": 9, - "name": "edited_by", - "type_info": "Varchar" - }, - { - "ordinal": 10, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 11, - "name": "edited_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 12, - "name": "server_id", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "last_server_ping", - "type_info": "Timestamptz" - }, - { - "ordinal": 14, - "name": "extra_perms", - "type_info": "Jsonb" - }, - { - "ordinal": 15, - "name": "error", - "type_info": "Text" - }, - { - "ordinal": 16, - "name": "enabled", - "type_info": "Bool" - }, - { - "ordinal": 17, - "name": "error_handler_path", - "type_info": "Varchar" - }, - { - "ordinal": 18, - "name": "error_handler_args: _", - "type_info": "Jsonb" - }, - { - "ordinal": 19, - "name": "retry: _", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - true, - true, - false, - false, - false, - false, - false, - false, - true, - true, - false, - true, - false, - true, - true, - true - ] - }, - "hash": "711e665344873454428d84e35f300edb7e0ebad5de40ba5645c1ce3d4a6b84be" -} diff --git a/backend/.sqlx/query-746ee16a04267cd251d4cefbc44a37fcc985bf61978cc07167ed35d7dbf92d11.json b/backend/.sqlx/query-746ee16a04267cd251d4cefbc44a37fcc985bf61978cc07167ed35d7dbf92d11.json deleted file mode 100644 index 4a7e81455e..0000000000 --- a/backend/.sqlx/query-746ee16a04267cd251d4cefbc44a37fcc985bf61978cc07167ed35d7dbf92d11.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT EXISTS(SELECT 1 FROM nats_trigger WHERE path = $1 AND workspace_id = $2)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "exists", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "746ee16a04267cd251d4cefbc44a37fcc985bf61978cc07167ed35d7dbf92d11" -} diff --git a/backend/.sqlx/query-7f039cc86874128ee8c9f577897340a155562fd6cde328298a66496c90d8cd59.json b/backend/.sqlx/query-7f039cc86874128ee8c9f577897340a155562fd6cde328298a66496c90d8cd59.json deleted file mode 100644 index 51928cf94c..0000000000 --- a/backend/.sqlx/query-7f039cc86874128ee8c9f577897340a155562fd6cde328298a66496c90d8cd59.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO postgres_trigger (\n publication_name,\n replication_slot_name,\n workspace_id, \n path, \n script_path, \n is_flow, \n email, \n enabled, \n postgres_resource_path, \n edited_by,\n error_handler_path,\n error_handler_args,\n retry\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", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Bool", - "Varchar", - "Bool", - "Varchar", - "Varchar", - "Varchar", - "Jsonb", - "Jsonb" - ] - }, - "nullable": [] - }, - "hash": "7f039cc86874128ee8c9f577897340a155562fd6cde328298a66496c90d8cd59" -} diff --git a/backend/.sqlx/query-f2a78401a363442f165d018d61a294b4575e2cdc31e8da3d453addd0edc3edf6.json b/backend/.sqlx/query-85d945cf5ade707291a161078ff96ddb29140dd72ce5115657418eec503b205d.json similarity index 56% rename from backend/.sqlx/query-f2a78401a363442f165d018d61a294b4575e2cdc31e8da3d453addd0edc3edf6.json rename to backend/.sqlx/query-85d945cf5ade707291a161078ff96ddb29140dd72ce5115657418eec503b205d.json index c9c1ac057e..fae73f363c 100644 --- a/backend/.sqlx/query-f2a78401a363442f165d018d61a294b4575e2cdc31e8da3d453addd0edc3edf6.json +++ b/backend/.sqlx/query-85d945cf5ade707291a161078ff96ddb29140dd72ce5115657418eec503b205d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n UPDATE \n http_trigger \n SET \n route_path = $1, \n route_path_key = $2, \n workspaced_route = $3,\n wrap_body = $4,\n raw_string = $5,\n authentication_resource_path = $6,\n script_path = $7, \n path = $8, \n is_flow = $9, \n http_method = $10, \n static_asset_config = $11, \n edited_by = $12, \n email = $13, \n is_async = $14, \n authentication_method = $15, \n summary = $16,\n description = $17,\n edited_at = now(), \n is_static_website = $18,\n error_handler_path = $19,\n error_handler_args = $20,\n retry = $21\n WHERE \n workspace_id = $22 AND \n path = $23\n ", + "query": "\n UPDATE \n http_trigger \n SET \n route_path = $1, \n route_path_key = $2,\n workspaced_route = $3,\n wrap_body = $4,\n raw_string = $5,\n authentication_resource_path = $6,\n script_path = $7, \n path = $8, \n is_flow = $9, \n http_method = $10, \n static_asset_config = $11, \n edited_by = $12, \n email = $13, \n is_async = $14, \n authentication_method = $15, \n summary = $16,\n description = $17,\n edited_at = now(), \n is_static_website = $18,\n error_handler_path = $19,\n error_handler_args = $20,\n retry = $21\n WHERE \n workspace_id = $22 AND \n path = $23\n ", "describe": { "columns": [], "parameters": { @@ -59,5 +59,5 @@ }, "nullable": [] }, - "hash": "f2a78401a363442f165d018d61a294b4575e2cdc31e8da3d453addd0edc3edf6" + "hash": "85d945cf5ade707291a161078ff96ddb29140dd72ce5115657418eec503b205d" } diff --git a/backend/.sqlx/query-870b12a46f26c9e29889dd28a5c7832c5a674a5553674fd9762c981e0d03bb57.json b/backend/.sqlx/query-870b12a46f26c9e29889dd28a5c7832c5a674a5553674fd9762c981e0d03bb57.json deleted file mode 100644 index f5d544c5aa..0000000000 --- a/backend/.sqlx/query-870b12a46f26c9e29889dd28a5c7832c5a674a5553674fd9762c981e0d03bb57.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM nats_trigger WHERE workspace_id = $1 AND path = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "870b12a46f26c9e29889dd28a5c7832c5a674a5553674fd9762c981e0d03bb57" -} diff --git a/backend/.sqlx/query-89be9b595a892c82f2a9f3cfa4da213e360ebe8827d5b4680d3c227fb8ddec82.json b/backend/.sqlx/query-89be9b595a892c82f2a9f3cfa4da213e360ebe8827d5b4680d3c227fb8ddec82.json deleted file mode 100644 index 9b33a91ae2..0000000000 --- a/backend/.sqlx/query-89be9b595a892c82f2a9f3cfa4da213e360ebe8827d5b4680d3c227fb8ddec82.json +++ /dev/null @@ -1,187 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n authentication_resource_path,\n script_path, \n is_flow, \n summary,\n description,\n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n authentication_method AS \"authentication_method: _\", \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n http_trigger\n WHERE \n workspace_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "workspaced_route", - "type_info": "Bool" - }, - { - "ordinal": 2, - "name": "path", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "route_path", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "route_path_key", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "authentication_resource_path", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "script_path", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "is_flow", - "type_info": "Bool" - }, - { - "ordinal": 8, - "name": "summary", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "description", - "type_info": "Text" - }, - { - "ordinal": 10, - "name": "edited_by", - "type_info": "Varchar" - }, - { - "ordinal": 11, - "name": "edited_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 12, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "extra_perms", - "type_info": "Jsonb" - }, - { - "ordinal": 14, - "name": "is_async", - "type_info": "Bool" - }, - { - "ordinal": 15, - "name": "authentication_method: _", - "type_info": { - "Custom": { - "name": "authentication_method", - "kind": { - "Enum": [ - "none", - "windmill", - "api_key", - "basic_http", - "custom_script", - "signature" - ] - } - } - } - }, - { - "ordinal": 16, - "name": "http_method: _", - "type_info": { - "Custom": { - "name": "http_method", - "kind": { - "Enum": [ - "get", - "post", - "put", - "delete", - "patch" - ] - } - } - } - }, - { - "ordinal": 17, - "name": "static_asset_config: _", - "type_info": "Jsonb" - }, - { - "ordinal": 18, - "name": "is_static_website", - "type_info": "Bool" - }, - { - "ordinal": 19, - "name": "wrap_body", - "type_info": "Bool" - }, - { - "ordinal": 20, - "name": "raw_string", - "type_info": "Bool" - }, - { - "ordinal": 21, - "name": "error_handler_path", - "type_info": "Varchar" - }, - { - "ordinal": 22, - "name": "error_handler_args: _", - "type_info": "Jsonb" - }, - { - "ordinal": 23, - "name": "retry: _", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - false, - false, - true, - true, - false, - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - true, - true, - true - ] - }, - "hash": "89be9b595a892c82f2a9f3cfa4da213e360ebe8827d5b4680d3c227fb8ddec82" -} diff --git a/backend/.sqlx/query-8d31b4a531c59a2385210d1213c205100d6673a94e90000c8db4eb5809f17365.json b/backend/.sqlx/query-8d31b4a531c59a2385210d1213c205100d6673a94e90000c8db4eb5809f17365.json deleted file mode 100644 index 0d30c8ca0a..0000000000 --- a/backend/.sqlx/query-8d31b4a531c59a2385210d1213c205100d6673a94e90000c8db4eb5809f17365.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "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-8e1afb488096330890b1675d2b3052d2064fcc8f373fecfebd40914768b2b1cf.json b/backend/.sqlx/query-8e1afb488096330890b1675d2b3052d2064fcc8f373fecfebd40914768b2b1cf.json deleted file mode 100644 index a6780ad262..0000000000 --- a/backend/.sqlx/query-8e1afb488096330890b1675d2b3052d2064fcc8f373fecfebd40914768b2b1cf.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) FROM postgres_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Bool", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "8e1afb488096330890b1675d2b3052d2064fcc8f373fecfebd40914768b2b1cf" -} diff --git a/backend/.sqlx/query-8ffd68b82cfce337fc32a6e80c8d56c53acd02eb350b46f6b815a5ddbb69d163.json b/backend/.sqlx/query-8ffd68b82cfce337fc32a6e80c8d56c53acd02eb350b46f6b815a5ddbb69d163.json deleted file mode 100644 index 40ef813403..0000000000 --- a/backend/.sqlx/query-8ffd68b82cfce337fc32a6e80c8d56c53acd02eb350b46f6b815a5ddbb69d163.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT\n workspace_id,\n path,\n nats_resource_path,\n subjects,\n stream_name,\n consumer_name,\n use_jetstream,\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 error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM nats_trigger\n WHERE workspace_id = $1 AND path = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "path", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "nats_resource_path", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "subjects", - "type_info": "VarcharArray" - }, - { - "ordinal": 4, - "name": "stream_name", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "consumer_name", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "use_jetstream", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "script_path", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "is_flow", - "type_info": "Bool" - }, - { - "ordinal": 9, - "name": "edited_by", - "type_info": "Varchar" - }, - { - "ordinal": 10, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 11, - "name": "edited_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 12, - "name": "server_id", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "last_server_ping", - "type_info": "Timestamptz" - }, - { - "ordinal": 14, - "name": "extra_perms", - "type_info": "Jsonb" - }, - { - "ordinal": 15, - "name": "error", - "type_info": "Text" - }, - { - "ordinal": 16, - "name": "enabled", - "type_info": "Bool" - }, - { - "ordinal": 17, - "name": "error_handler_path", - "type_info": "Varchar" - }, - { - "ordinal": 18, - "name": "error_handler_args: _", - "type_info": "Jsonb" - }, - { - "ordinal": 19, - "name": "retry: _", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - true, - true, - false, - false, - false, - false, - false, - false, - true, - true, - false, - true, - false, - true, - true, - true - ] - }, - "hash": "8ffd68b82cfce337fc32a6e80c8d56c53acd02eb350b46f6b815a5ddbb69d163" -} diff --git a/backend/.sqlx/query-9abbb75c238372cc3dbaebf1dbc50ff78df6c5dcf92b08d611afe23c5b865521.json b/backend/.sqlx/query-9abbb75c238372cc3dbaebf1dbc50ff78df6c5dcf92b08d611afe23c5b865521.json new file mode 100644 index 0000000000..65dc60db73 --- /dev/null +++ b/backend/.sqlx/query-9abbb75c238372cc3dbaebf1dbc50ff78df6c5dcf92b08d611afe23c5b865521.json @@ -0,0 +1,37 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO sqs_trigger (\n workspace_id,\n path,\n queue_url,\n aws_resource_path,\n message_attributes,\n aws_auth_resource_type,\n script_path,\n is_flow,\n enabled,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now(), $12, $13, $14\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "TextArray", + { + "Custom": { + "name": "aws_auth_resource_type", + "kind": { + "Enum": [ + "oidc", + "credentials" + ] + } + } + }, + "Varchar", + "Bool", + "Bool", + "Varchar", + "Varchar", + "Varchar", + "Jsonb", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "9abbb75c238372cc3dbaebf1dbc50ff78df6c5dcf92b08d611afe23c5b865521" +} diff --git a/backend/.sqlx/query-9e88a43de7315052668619002321aadbc27ff5bd0c554bf5339060cc35797d3f.json b/backend/.sqlx/query-9e88a43de7315052668619002321aadbc27ff5bd0c554bf5339060cc35797d3f.json deleted file mode 100644 index bb2a3c682b..0000000000 --- a/backend/.sqlx/query-9e88a43de7315052668619002321aadbc27ff5bd0c554bf5339060cc35797d3f.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "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-a269c388056eabe4b045948f451ea74ffbb4c0ed7e694f8f03d92f2a7c118af9.json b/backend/.sqlx/query-a269c388056eabe4b045948f451ea74ffbb4c0ed7e694f8f03d92f2a7c118af9.json deleted file mode 100644 index e81acc3584..0000000000 --- a/backend/.sqlx/query-a269c388056eabe4b045948f451ea74ffbb4c0ed7e694f8f03d92f2a7c118af9.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM http_trigger \n WHERE workspace_id = $1 \n AND path = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "a269c388056eabe4b045948f451ea74ffbb4c0ed7e694f8f03d92f2a7c118af9" -} diff --git a/backend/.sqlx/query-a8b470b463ca4b7c00c7ef6e9f36c23f8bbcefc288a56d61122bfd6fe5ca7e8d.json b/backend/.sqlx/query-a8b470b463ca4b7c00c7ef6e9f36c23f8bbcefc288a56d61122bfd6fe5ca7e8d.json deleted file mode 100644 index 6ac4ee6755..0000000000 --- a/backend/.sqlx/query-a8b470b463ca4b7c00c7ef6e9f36c23f8bbcefc288a56d61122bfd6fe5ca7e8d.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) FROM mqtt_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Bool", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "a8b470b463ca4b7c00c7ef6e9f36c23f8bbcefc288a56d61122bfd6fe5ca7e8d" -} diff --git a/backend/.sqlx/query-aae8699bbaa4d6111eabee715a6f4a3600c1ccfe6847bd526a751bc7baf825c5.json b/backend/.sqlx/query-aae8699bbaa4d6111eabee715a6f4a3600c1ccfe6847bd526a751bc7baf825c5.json deleted file mode 100644 index 9b94f6bab7..0000000000 --- a/backend/.sqlx/query-aae8699bbaa4d6111eabee715a6f4a3600c1ccfe6847bd526a751bc7baf825c5.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT EXISTS(\n SELECT 1 \n FROM postgres_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": "aae8699bbaa4d6111eabee715a6f4a3600c1ccfe6847bd526a751bc7baf825c5" -} diff --git a/backend/.sqlx/query-ad07b57ad928a4e5260833f4fe2a4bf51211f2f73d78c546c894c8e147677ac7.json b/backend/.sqlx/query-ad07b57ad928a4e5260833f4fe2a4bf51211f2f73d78c546c894c8e147677ac7.json deleted file mode 100644 index 5fc32269c2..0000000000 --- a/backend/.sqlx/query-ad07b57ad928a4e5260833f4fe2a4bf51211f2f73d78c546c894c8e147677ac7.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) FROM nats_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Bool", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "ad07b57ad928a4e5260833f4fe2a4bf51211f2f73d78c546c894c8e147677ac7" -} diff --git a/backend/.sqlx/query-adb0090afd3ce918d8b80ff51d9f6104a430a11d7c5cb9447025d11506585708.json b/backend/.sqlx/query-adb0090afd3ce918d8b80ff51d9f6104a430a11d7c5cb9447025d11506585708.json deleted file mode 100644 index bbe9b8bb86..0000000000 --- a/backend/.sqlx/query-adb0090afd3ce918d8b80ff51d9f6104a430a11d7c5cb9447025d11506585708.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT EXISTS(\n SELECT 1 FROM http_trigger \n WHERE path = $1 AND workspace_id = $2\n )", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "exists", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "adb0090afd3ce918d8b80ff51d9f6104a430a11d7c5cb9447025d11506585708" -} diff --git a/backend/.sqlx/query-b386d320f9fe1d569a16e6626b723b4376a93342702856da2ac70f6bbfc7933e.json b/backend/.sqlx/query-b386d320f9fe1d569a16e6626b723b4376a93342702856da2ac70f6bbfc7933e.json deleted file mode 100644 index 13ac3c15ea..0000000000 --- a/backend/.sqlx/query-b386d320f9fe1d569a16e6626b723b4376a93342702856da2ac70f6bbfc7933e.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT EXISTS(\n SELECT \n 1 \n FROM \n sqs_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": "b386d320f9fe1d569a16e6626b723b4376a93342702856da2ac70f6bbfc7933e" -} diff --git a/backend/.sqlx/query-c19a60a9dc3f95af218baf40c62f14572ac204cfe377166aa1d91cf58f731f50.json b/backend/.sqlx/query-c19a60a9dc3f95af218baf40c62f14572ac204cfe377166aa1d91cf58f731f50.json deleted file mode 100644 index 65cc991931..0000000000 --- a/backend/.sqlx/query-c19a60a9dc3f95af218baf40c62f14572ac204cfe377166aa1d91cf58f731f50.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n DELETE \n FROM \n gcp_trigger \n WHERE \n workspace_id = $1 AND \n path = $2\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "c19a60a9dc3f95af218baf40c62f14572ac204cfe377166aa1d91cf58f731f50" -} diff --git a/backend/.sqlx/query-c22438f9bff27ccd3228a186a8093c551c30ed85eabe8ec11cafa6358f354c8b.json b/backend/.sqlx/query-c22438f9bff27ccd3228a186a8093c551c30ed85eabe8ec11cafa6358f354c8b.json deleted file mode 100644 index 836709359e..0000000000 --- a/backend/.sqlx/query-c22438f9bff27ccd3228a186a8093c551c30ed85eabe8ec11cafa6358f354c8b.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO sqs_trigger (\n aws_auth_resource_type,\n aws_resource_path,\n queue_url,\n message_attributes,\n workspace_id, \n path, \n script_path, \n is_flow, \n email, \n enabled, \n edited_by,\n error_handler_path,\n error_handler_args,\n retry\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 $14\n )", - "describe": { - "columns": [], - "parameters": { - "Left": [ - { - "Custom": { - "name": "aws_auth_resource_type", - "kind": { - "Enum": [ - "oidc", - "credentials" - ] - } - } - }, - "Varchar", - "Varchar", - "TextArray", - "Varchar", - "Varchar", - "Varchar", - "Bool", - "Varchar", - "Bool", - "Varchar", - "Varchar", - "Jsonb", - "Jsonb" - ] - }, - "nullable": [] - }, - "hash": "c22438f9bff27ccd3228a186a8093c551c30ed85eabe8ec11cafa6358f354c8b" -} diff --git a/backend/.sqlx/query-c287f025c77c34e4d521204e7a787e794b461a7a965912bcb598619451eced31.json b/backend/.sqlx/query-c287f025c77c34e4d521204e7a787e794b461a7a965912bcb598619451eced31.json deleted file mode 100644 index e6facf2719..0000000000 --- a/backend/.sqlx/query-c287f025c77c34e4d521204e7a787e794b461a7a965912bcb598619451eced31.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT \n workspace_id,\n path,\n url,\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 filters AS \"filters: _\",\n initial_messages AS \"initial_messages: _\",\n url_runnable_args AS \"url_runnable_args: _\",\n can_return_message,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n websocket_trigger\n WHERE \n workspace_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "path", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "url", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "script_path", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "is_flow", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "edited_by", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "edited_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "server_id", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "last_server_ping", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "extra_perms", - "type_info": "Jsonb" - }, - { - "ordinal": 11, - "name": "error", - "type_info": "Text" - }, - { - "ordinal": 12, - "name": "enabled", - "type_info": "Bool" - }, - { - "ordinal": 13, - "name": "filters: _", - "type_info": "JsonbArray" - }, - { - "ordinal": 14, - "name": "initial_messages: _", - "type_info": "JsonbArray" - }, - { - "ordinal": 15, - "name": "url_runnable_args: _", - "type_info": "Jsonb" - }, - { - "ordinal": 16, - "name": "can_return_message", - "type_info": "Bool" - }, - { - "ordinal": 17, - "name": "error_handler_path", - "type_info": "Varchar" - }, - { - "ordinal": 18, - "name": "error_handler_args: _", - "type_info": "Jsonb" - }, - { - "ordinal": 19, - "name": "retry: _", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - false, - true, - false, - false, - true, - true, - false, - true, - true, - true - ] - }, - "hash": "c287f025c77c34e4d521204e7a787e794b461a7a965912bcb598619451eced31" -} diff --git a/backend/.sqlx/query-c3730ae025a4dcc25b2713e3e670ce006b1ceed26e29d812d524c711d178167e.json b/backend/.sqlx/query-c3730ae025a4dcc25b2713e3e670ce006b1ceed26e29d812d524c711d178167e.json deleted file mode 100644 index daa7b50c85..0000000000 --- a/backend/.sqlx/query-c3730ae025a4dcc25b2713e3e670ce006b1ceed26e29d812d524c711d178167e.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE \n sqs_trigger \n SET \n aws_auth_resource_type = $1,\n aws_resource_path = $2,\n queue_url = $3,\n message_attributes = $4, \n is_flow = $5, \n edited_by = $6, \n email = $7,\n script_path = $8,\n path = $9,\n edited_at = now(), \n error = NULL,\n server_id = NULL,\n error_handler_path = $12,\n error_handler_args = $13,\n retry = $14\n WHERE \n workspace_id = $10 AND \n path = $11\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - { - "Custom": { - "name": "aws_auth_resource_type", - "kind": { - "Enum": [ - "oidc", - "credentials" - ] - } - } - }, - "Varchar", - "Varchar", - "TextArray", - "Bool", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Text", - "Text", - "Varchar", - "Jsonb", - "Jsonb" - ] - }, - "nullable": [] - }, - "hash": "c3730ae025a4dcc25b2713e3e670ce006b1ceed26e29d812d524c711d178167e" -} diff --git a/backend/.sqlx/query-c4cd9668733b2b439ac7a911d15041ac728553e20adf722eb795d01d6650ff0f.json b/backend/.sqlx/query-c4cd9668733b2b439ac7a911d15041ac728553e20adf722eb795d01d6650ff0f.json deleted file mode 100644 index dc8629a042..0000000000 --- a/backend/.sqlx/query-c4cd9668733b2b439ac7a911d15041ac728553e20adf722eb795d01d6650ff0f.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO websocket_trigger (\n workspace_id,\n path,\n url,\n script_path,\n is_flow,\n enabled,\n filters,\n initial_messages,\n url_runnable_args,\n edited_by,\n can_return_message,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13, $14, $15\n )\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Bool", - "Bool", - "JsonbArray", - "JsonbArray", - "Jsonb", - "Varchar", - "Bool", - "Varchar", - "Varchar", - "Jsonb", - "Jsonb" - ] - }, - "nullable": [] - }, - "hash": "c4cd9668733b2b439ac7a911d15041ac728553e20adf722eb795d01d6650ff0f" -} diff --git a/backend/.sqlx/query-c61df65f2e4f8bc146b0f70e605f5243ec9aa66ea1a9da9662fc877f4becb04e.json b/backend/.sqlx/query-c61df65f2e4f8bc146b0f70e605f5243ec9aa66ea1a9da9662fc877f4becb04e.json deleted file mode 100644 index 528ed0ce7d..0000000000 --- a/backend/.sqlx/query-c61df65f2e4f8bc146b0f70e605f5243ec9aa66ea1a9da9662fc877f4becb04e.json +++ /dev/null @@ -1,168 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\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 error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\",\n auto_acknowledge_msg\n FROM \n gcp_trigger\n WHERE \n workspace_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "gcp_resource_path", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "subscription_id", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "topic_id", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "delivery_type: _", - "type_info": { - "Custom": { - "name": "delivery_mode", - "kind": { - "Enum": [ - "push", - "pull" - ] - } - } - } - }, - { - "ordinal": 5, - "name": "delivery_config: _", - "type_info": "Jsonb" - }, - { - "ordinal": 6, - "name": "subscription_mode: _", - "type_info": { - "Custom": { - "name": "gcp_subscription_mode", - "kind": { - "Enum": [ - "create_update", - "existing" - ] - } - } - } - }, - { - "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" - }, - { - "ordinal": 18, - "name": "error_handler_path", - "type_info": "Varchar" - }, - { - "ordinal": 19, - "name": "error_handler_args: _", - "type_info": "Jsonb" - }, - { - "ordinal": 20, - "name": "retry: _", - "type_info": "Jsonb" - }, - { - "ordinal": 21, - "name": "auto_acknowledge_msg", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - false, - false, - false, - false, - false, - false, - false, - true, - true, - false, - true, - false, - true, - true, - true, - true - ] - }, - "hash": "c61df65f2e4f8bc146b0f70e605f5243ec9aa66ea1a9da9662fc877f4becb04e" -} diff --git a/backend/.sqlx/query-d3bc837633eb92cd9a8d37ad09a72e8da54885cdd135e0877dec9cf407064158.json b/backend/.sqlx/query-d3bc837633eb92cd9a8d37ad09a72e8da54885cdd135e0877dec9cf407064158.json deleted file mode 100644 index 5c129f5088..0000000000 --- a/backend/.sqlx/query-d3bc837633eb92cd9a8d37ad09a72e8da54885cdd135e0877dec9cf407064158.json +++ /dev/null @@ -1,141 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n aws_auth_resource_type AS \"aws_auth_resource_type: _\",\n aws_resource_path,\n message_attributes,\n queue_url,\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 error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n sqs_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "aws_auth_resource_type: _", - "type_info": { - "Custom": { - "name": "aws_auth_resource_type", - "kind": { - "Enum": [ - "oidc", - "credentials" - ] - } - } - } - }, - { - "ordinal": 1, - "name": "aws_resource_path", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "message_attributes", - "type_info": "TextArray" - }, - { - "ordinal": 3, - "name": "queue_url", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "path", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "script_path", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "is_flow", - "type_info": "Bool" - }, - { - "ordinal": 8, - "name": "edited_by", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 10, - "name": "edited_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 11, - "name": "server_id", - "type_info": "Varchar" - }, - { - "ordinal": 12, - "name": "last_server_ping", - "type_info": "Timestamptz" - }, - { - "ordinal": 13, - "name": "extra_perms", - "type_info": "Jsonb" - }, - { - "ordinal": 14, - "name": "error", - "type_info": "Text" - }, - { - "ordinal": 15, - "name": "enabled", - "type_info": "Bool" - }, - { - "ordinal": 16, - "name": "error_handler_path", - "type_info": "Varchar" - }, - { - "ordinal": 17, - "name": "error_handler_args: _", - "type_info": "Jsonb" - }, - { - "ordinal": 18, - "name": "retry: _", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false, - false, - true, - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - true, - true, - false, - true, - true, - true - ] - }, - "hash": "d3bc837633eb92cd9a8d37ad09a72e8da54885cdd135e0877dec9cf407064158" -} diff --git a/backend/.sqlx/query-d54840373df5da9662ed11ed0a605cddac517bdc06dca0a8be071330338e948a.json b/backend/.sqlx/query-d54840373df5da9662ed11ed0a605cddac517bdc06dca0a8be071330338e948a.json deleted file mode 100644 index 5b298f8db3..0000000000 --- a/backend/.sqlx/query-d54840373df5da9662ed11ed0a605cddac517bdc06dca0a8be071330338e948a.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM websocket_trigger WHERE workspace_id = $1 AND path = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "d54840373df5da9662ed11ed0a605cddac517bdc06dca0a8be071330338e948a" -} diff --git a/backend/.sqlx/query-dea056c89313f4facd62cbbc9fa33ba30fa85efcc83fafe4dd7b4e535b96a8d8.json b/backend/.sqlx/query-dea056c89313f4facd62cbbc9fa33ba30fa85efcc83fafe4dd7b4e535b96a8d8.json deleted file mode 100644 index 12e4b6d81b..0000000000 --- a/backend/.sqlx/query-dea056c89313f4facd62cbbc9fa33ba30fa85efcc83fafe4dd7b4e535b96a8d8.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n DELETE \n FROM \n sqs_trigger \n WHERE \n workspace_id = $1 AND \n path = $2\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "dea056c89313f4facd62cbbc9fa33ba30fa85efcc83fafe4dd7b4e535b96a8d8" -} diff --git a/backend/.sqlx/query-e56c71de41ba0750ae9ae8540c903cb0bf241d959a6c8f7bb896160001a6672b.json b/backend/.sqlx/query-e56c71de41ba0750ae9ae8540c903cb0bf241d959a6c8f7bb896160001a6672b.json new file mode 100644 index 0000000000..f19d888a67 --- /dev/null +++ b/backend/.sqlx/query-e56c71de41ba0750ae9ae8540c903cb0bf241d959a6c8f7bb896160001a6672b.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE postgres_trigger \n SET \n postgres_resource_path = $1,\n replication_slot_name = $2,\n publication_name = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n edited_by = $7,\n email = $8,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $11,\n error_handler_args = $12,\n retry = $13\n WHERE \n workspace_id = $9 AND path = $10\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Bool", + "Varchar", + "Varchar", + "Text", + "Text", + "Varchar", + "Jsonb", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "e56c71de41ba0750ae9ae8540c903cb0bf241d959a6c8f7bb896160001a6672b" +} diff --git a/backend/.sqlx/query-e9941e580a21848fa71b38c58030078a25f117a3bfec78d5270a4c4970f2465e.json b/backend/.sqlx/query-e9941e580a21848fa71b38c58030078a25f117a3bfec78d5270a4c4970f2465e.json new file mode 100644 index 0000000000..1e67891f48 --- /dev/null +++ b/backend/.sqlx/query-e9941e580a21848fa71b38c58030078a25f117a3bfec78d5270a4c4970f2465e.json @@ -0,0 +1,37 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE sqs_trigger \n SET \n queue_url = $1,\n aws_resource_path = $2,\n message_attributes = $3,\n aws_auth_resource_type = $4,\n script_path = $5,\n path = $6,\n is_flow = $7,\n edited_by = $8,\n email = $9,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $12,\n error_handler_args = $13,\n retry = $14\n WHERE \n workspace_id = $10 AND path = $11\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "TextArray", + { + "Custom": { + "name": "aws_auth_resource_type", + "kind": { + "Enum": [ + "oidc", + "credentials" + ] + } + } + }, + "Varchar", + "Varchar", + "Bool", + "Varchar", + "Varchar", + "Text", + "Text", + "Varchar", + "Jsonb", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "e9941e580a21848fa71b38c58030078a25f117a3bfec78d5270a4c4970f2465e" +} diff --git a/backend/.sqlx/query-e9b4938b8c5fbd57845cf560fc2fd2d5749015c66d7de277081598ba3bb6dd8f.json b/backend/.sqlx/query-e9b4938b8c5fbd57845cf560fc2fd2d5749015c66d7de277081598ba3bb6dd8f.json deleted file mode 100644 index 5b8934eec6..0000000000 --- a/backend/.sqlx/query-e9b4938b8c5fbd57845cf560fc2fd2d5749015c66d7de277081598ba3bb6dd8f.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "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 error_handler_path,\n error_handler_args,\n retry\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 $14,\n $15,\n $16\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", - "Varchar", - "Jsonb", - "Jsonb" - ] - }, - "nullable": [] - }, - "hash": "e9b4938b8c5fbd57845cf560fc2fd2d5749015c66d7de277081598ba3bb6dd8f" -} diff --git a/backend/.sqlx/query-ea59131c1fba8fb14a95b4a486db29ba44c4ff4d2c5b38369f0ede61d4708a21.json b/backend/.sqlx/query-ea59131c1fba8fb14a95b4a486db29ba44c4ff4d2c5b38369f0ede61d4708a21.json deleted file mode 100644 index f9c079f0f6..0000000000 --- a/backend/.sqlx/query-ea59131c1fba8fb14a95b4a486db29ba44c4ff4d2c5b38369f0ede61d4708a21.json +++ /dev/null @@ -1,188 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT \n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n script_path, \n summary,\n description,\n is_flow, \n http_method as \"http_method: _\", \n edited_by, \n email, \n edited_at, \n extra_perms, \n is_async, \n authentication_method as \"authentication_method: _\", \n static_asset_config as \"static_asset_config: _\", \n is_static_website,\n authentication_resource_path,\n wrap_body,\n raw_string,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n http_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "path", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "route_path", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "route_path_key", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "workspaced_route", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "script_path", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "summary", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "description", - "type_info": "Text" - }, - { - "ordinal": 8, - "name": "is_flow", - "type_info": "Bool" - }, - { - "ordinal": 9, - "name": "http_method: _", - "type_info": { - "Custom": { - "name": "http_method", - "kind": { - "Enum": [ - "get", - "post", - "put", - "delete", - "patch" - ] - } - } - } - }, - { - "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": "extra_perms", - "type_info": "Jsonb" - }, - { - "ordinal": 14, - "name": "is_async", - "type_info": "Bool" - }, - { - "ordinal": 15, - "name": "authentication_method: _", - "type_info": { - "Custom": { - "name": "authentication_method", - "kind": { - "Enum": [ - "none", - "windmill", - "api_key", - "basic_http", - "custom_script", - "signature" - ] - } - } - } - }, - { - "ordinal": 16, - "name": "static_asset_config: _", - "type_info": "Jsonb" - }, - { - "ordinal": 17, - "name": "is_static_website", - "type_info": "Bool" - }, - { - "ordinal": 18, - "name": "authentication_resource_path", - "type_info": "Varchar" - }, - { - "ordinal": 19, - "name": "wrap_body", - "type_info": "Bool" - }, - { - "ordinal": 20, - "name": "raw_string", - "type_info": "Bool" - }, - { - "ordinal": 21, - "name": "error_handler_path", - "type_info": "Varchar" - }, - { - "ordinal": 22, - "name": "error_handler_args: _", - "type_info": "Jsonb" - }, - { - "ordinal": 23, - "name": "retry: _", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - true, - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - true, - true, - true - ] - }, - "hash": "ea59131c1fba8fb14a95b4a486db29ba44c4ff4d2c5b38369f0ede61d4708a21" -} diff --git a/backend/.sqlx/query-eda1e5d1109a13feb14be254eaca630be29176bd5d1246c974642d40201782fc.json b/backend/.sqlx/query-eda1e5d1109a13feb14be254eaca630be29176bd5d1246c974642d40201782fc.json deleted file mode 100644 index fdc34ee1c7..0000000000 --- a/backend/.sqlx/query-eda1e5d1109a13feb14be254eaca630be29176bd5d1246c974642d40201782fc.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE kafka_trigger SET enabled = $1, email = $2, edited_by = $3, edited_at = now(), server_id = NULL, error = NULL\n WHERE path = $4 AND workspace_id = $5 RETURNING 1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "?column?", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Bool", - "Varchar", - "Varchar", - "Text", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "eda1e5d1109a13feb14be254eaca630be29176bd5d1246c974642d40201782fc" -} diff --git a/backend/.sqlx/query-eebc9b609d9b595524223ada91e0f7fa1ca50b45cc82e2fd9d4e61768b258735.json b/backend/.sqlx/query-eebc9b609d9b595524223ada91e0f7fa1ca50b45cc82e2fd9d4e61768b258735.json deleted file mode 100644 index 1a9deeb950..0000000000 --- a/backend/.sqlx/query-eebc9b609d9b595524223ada91e0f7fa1ca50b45cc82e2fd9d4e61768b258735.json +++ /dev/null @@ -1,150 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n mqtt_resource_path,\n subscribe_topics as \"subscribe_topics: _\",\n v3_config as \"v3_config: _\",\n v5_config as \"v5_config: _\",\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 error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n mqtt_trigger\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "mqtt_resource_path", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "subscribe_topics: _", - "type_info": "JsonbArray" - }, - { - "ordinal": 2, - "name": "v3_config: _", - "type_info": "Jsonb" - }, - { - "ordinal": 3, - "name": "v5_config: _", - "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" - }, - { - "ordinal": 18, - "name": "error_handler_path", - "type_info": "Varchar" - }, - { - "ordinal": 19, - "name": "error_handler_args: _", - "type_info": "Jsonb" - }, - { - "ordinal": 20, - "name": "retry: _", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - true, - true, - false, - true, - false, - false, - false, - false, - false, - false, - false, - true, - true, - false, - true, - false, - true, - true, - true - ] - }, - "hash": "eebc9b609d9b595524223ada91e0f7fa1ca50b45cc82e2fd9d4e61768b258735" -} diff --git a/backend/.sqlx/query-f28b71a304721643dd90fa468f61f351d6df8282415e54f0ea4f905417657ba8.json b/backend/.sqlx/query-f28b71a304721643dd90fa468f61f351d6df8282415e54f0ea4f905417657ba8.json new file mode 100644 index 0000000000..b0095248ef --- /dev/null +++ b/backend/.sqlx/query-f28b71a304721643dd90fa468f61f351d6df8282415e54f0ea4f905417657ba8.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO nats_trigger (\n workspace_id,\n path,\n nats_resource_path,\n subjects,\n stream_name,\n consumer_name,\n use_jetstream,\n script_path,\n is_flow,\n enabled,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13, $14, $15\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "VarcharArray", + "Varchar", + "Varchar", + "Bool", + "Varchar", + "Bool", + "Bool", + "Varchar", + "Varchar", + "Varchar", + "Jsonb", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "f28b71a304721643dd90fa468f61f351d6df8282415e54f0ea4f905417657ba8" +} diff --git a/backend/.sqlx/query-f434cac3a8eded700b09a8c99dcc5f7c93fbdb1e7d090b11612aa284342e86a1.json b/backend/.sqlx/query-f434cac3a8eded700b09a8c99dcc5f7c93fbdb1e7d090b11612aa284342e86a1.json deleted file mode 100644 index 21d2ddb41e..0000000000 --- a/backend/.sqlx/query-f434cac3a8eded700b09a8c99dcc5f7c93fbdb1e7d090b11612aa284342e86a1.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\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 error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\",\n auto_acknowledge_msg\n FROM \n gcp_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "gcp_resource_path", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "subscription_id", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "topic_id", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "delivery_type: _", - "type_info": { - "Custom": { - "name": "delivery_mode", - "kind": { - "Enum": [ - "push", - "pull" - ] - } - } - } - }, - { - "ordinal": 5, - "name": "delivery_config: _", - "type_info": "Jsonb" - }, - { - "ordinal": 6, - "name": "subscription_mode: _", - "type_info": { - "Custom": { - "name": "gcp_subscription_mode", - "kind": { - "Enum": [ - "create_update", - "existing" - ] - } - } - } - }, - { - "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" - }, - { - "ordinal": 18, - "name": "error_handler_path", - "type_info": "Varchar" - }, - { - "ordinal": 19, - "name": "error_handler_args: _", - "type_info": "Jsonb" - }, - { - "ordinal": 20, - "name": "retry: _", - "type_info": "Jsonb" - }, - { - "ordinal": 21, - "name": "auto_acknowledge_msg", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - false, - false, - false, - false, - false, - false, - false, - true, - true, - false, - true, - false, - true, - true, - true, - true - ] - }, - "hash": "f434cac3a8eded700b09a8c99dcc5f7c93fbdb1e7d090b11612aa284342e86a1" -} diff --git a/backend/.sqlx/query-fd1db7530acf3c84b2ab696504905a50d1ed4f69629c43de7d874769c340d909.json b/backend/.sqlx/query-fd1db7530acf3c84b2ab696504905a50d1ed4f69629c43de7d874769c340d909.json deleted file mode 100644 index 06cf05a25d..0000000000 --- a/backend/.sqlx/query-fd1db7530acf3c84b2ab696504905a50d1ed4f69629c43de7d874769c340d909.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE postgres_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": "fd1db7530acf3c84b2ab696504905a50d1ed4f69629c43de7d874769c340d909" -} diff --git a/backend/.sqlx/query-fe78796c5ea2384bdb6191b66943d746de874ca6600178f5db9797eda44f1733.json b/backend/.sqlx/query-fe78796c5ea2384bdb6191b66943d746de874ca6600178f5db9797eda44f1733.json new file mode 100644 index 0000000000..6881d0aef0 --- /dev/null +++ b/backend/.sqlx/query-fe78796c5ea2384bdb6191b66943d746de874ca6600178f5db9797eda44f1733.json @@ -0,0 +1,39 @@ +{ + "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 error_handler_path,\n error_handler_args,\n retry\n ) \n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16\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", + "Varchar", + "Jsonb", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "fe78796c5ea2384bdb6191b66943d746de874ca6600178f5db9797eda44f1733" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index f0fe796f51..3a8ff6af01 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -15166,6 +15166,7 @@ dependencies = [ "async-oauth2", "async-recursion", "async-stream", + "async-trait", "async_zip", "aws-config", "aws-sdk-sqs", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index f1335287db..a64e0d3ec9 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -248,6 +248,7 @@ itertools = "^0" regex = "^1" semver = "^1" duckdb = { version = "1.3.2", features = ["bundled"] } +async-trait = "0.1.88" v8 = "=130.0.7" # Exact version NOTE: Do not forget to update version and hash in flake.nix deno_fetch = "0.214.0" diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 76cf87e58c..48a07783b5 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -a5911739330d09cdc084bb277e7e3db8f8d86801 \ No newline at end of file +66381121ea7ba806a54cd44416870c354e461098 \ No newline at end of file diff --git a/backend/src/main.rs b/backend/src/main.rs index 9d560a1933..18452f6c22 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -919,7 +919,7 @@ Windmill Community Edition {GIT_VERSION} #[cfg(feature = "http_trigger")] "notify_http_trigger_change" => { tracing::info!("HTTP trigger change detected: {}", n.payload()); - match windmill_api::http_triggers::refresh_routers(&db).await { + match windmill_api::triggers::http::refresh_routers(&db).await { Ok((true, _)) => { tracing::info!("Refreshed HTTP routers (trigger change)"); }, diff --git a/backend/windmill-api/Cargo.toml b/backend/windmill-api/Cargo.toml index 3f4a4528f9..39b3a4ed5f 100644 --- a/backend/windmill-api/Cargo.toml +++ b/backend/windmill-api/Cargo.toml @@ -146,6 +146,7 @@ aws-sdk-sts = { workspace = true, optional = true } rustls = { workspace = true } aws-config = { workspace = true, optional = true } +async-trait.workspace = true google-cloud-pubsub = { workspace = true, optional = true } google-cloud-googleapis = { workspace = true , optional = true } tonic = { workspace = true, optional = true } diff --git a/backend/windmill-api/src/args.rs b/backend/windmill-api/src/args.rs index 0b8f4d5210..a541f8c738 100644 --- a/backend/windmill-api/src/args.rs +++ b/backend/windmill-api/src/args.rs @@ -20,7 +20,7 @@ use windmill_queue::PushArgsOwned; use crate::{ db::ApiAuthed, - trigger_helpers::{get_runnable_format, RunnableId}, + triggers::trigger_helpers::{get_runnable_format, RunnableId}, }; #[derive(Debug)] diff --git a/backend/windmill-api/src/auth.rs b/backend/windmill-api/src/auth.rs index a1ccc69769..01147c0ed8 100644 --- a/backend/windmill-api/src/auth.rs +++ b/backend/windmill-api/src/auth.rs @@ -3,12 +3,13 @@ use crate::ee_oss::ExternalJwks; use axum::{ async_trait, extract::{FromRequestParts, OriginalUri, Query}, - Extension, + Extension, Json, }; use chrono::TimeZone; use http::{request::Parts, StatusCode}; use quick_cache::sync::Cache; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; use tower_cookies::Cookies; use tracing::Span; @@ -22,7 +23,7 @@ use tokio::sync::RwLock; use windmill_common::{ auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims, TOKEN_PREFIX_LEN}, - error::Error, + error::{Error, JsonResult}, jwt, users::{COOKIE_NAME, SUPERADMIN_SECRET_EMAIL}, }; @@ -641,3 +642,71 @@ fn username_override_from_label(label: Option) -> Option { _ => None, } } + +#[derive(FromRow, Serialize)] +pub struct TruncatedTokenWithEmail { + pub label: Option, + pub token_prefix: Option, + pub expiration: Option>, + pub created_at: chrono::DateTime, + pub last_used_at: chrono::DateTime, + pub scopes: Option>, + pub email: Option, +} + +pub async fn list_tokens_internal( + db: &DB, + w_id: &str, + path: &str, + is_flow: bool, +) -> JsonResult> { + let tokens = if is_flow { + sqlx::query_as!( + TruncatedTokenWithEmail, + r#" + SELECT label, + concat(substring(token for 10)) AS token_prefix, + expiration, + created_at, + last_used_at, + scopes, + email + FROM token + WHERE workspace_id = $1 + AND ( + scopes @> ARRAY['jobs:run:flows:' || $2]::text[] + OR scopes @> ARRAY['run:flow/' || $2]::text[] + ) + "#, + w_id, + path + ) + .fetch_all(db) + .await? + } else { + sqlx::query_as!( + TruncatedTokenWithEmail, + r#" + SELECT label, + concat(substring(token for 10)) AS token_prefix, + expiration, + created_at, + last_used_at, + scopes, + email + FROM token + WHERE workspace_id = $1 + AND ( + scopes @> ARRAY['jobs:run:scripts:' || $2]::text[] + OR scopes @> ARRAY['run:script/' || $2]::text[] + ) + "#, + w_id, + path + ) + .fetch_all(db) + .await? + }; + + Ok(Json(tokens)) +} diff --git a/backend/windmill-api/src/capture.rs b/backend/windmill-api/src/capture.rs index 4bd2161248..0b56bd143d 100644 --- a/backend/windmill-api/src/capture.rs +++ b/backend/windmill-api/src/capture.rs @@ -8,54 +8,54 @@ #[cfg(feature = "http_trigger")] use { - crate::http_trigger_args::{HttpMethod, RawHttpTriggerArgs}, + crate::triggers::http::{http_trigger_args::RawHttpTriggerArgs, HttpMethod}, axum::response::{IntoResponse, Response}, std::collections::HashMap, }; -#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))] +#[cfg(all(feature = "enterprise", feature = "gcp_trigger", feature = "private"))] use { - crate::gcp_triggers_oss::{ + crate::triggers::gcp::{ manage_google_subscription, process_google_push_request, validate_jwt_token, - CreateUpdateConfig, SubscriptionMode, + CreateUpdateConfig, GcpSubscriptionMode, }, axum::extract::Request, http::HeaderMap, }; #[cfg(any( - all(feature = "enterprise", feature = "gcp_trigger"), + all(feature = "enterprise", feature = "gcp_trigger", feature = "private"), feature = "postgres_trigger" ))] use windmill_common::utils::empty_as_none; -#[cfg(all(feature = "enterprise", feature = "sqs_trigger"))] +#[cfg(all(feature = "enterprise", feature = "sqs_trigger", feature = "private"))] use windmill_common::auth::aws::AwsAuthResourceType; #[cfg(any( feature = "http_trigger", - all(feature = "enterprise", feature = "gcp_trigger") + all(feature = "enterprise", feature = "gcp_trigger", feature = "private") ))] use serde::de::DeserializeOwned; #[cfg(any( feature = "http_trigger", feature = "postgres_trigger", - all(feature = "enterprise", feature = "gcp_trigger") + all(feature = "enterprise", feature = "gcp_trigger", feature = "private") ))] use windmill_common::error::Error; -#[cfg(all(feature = "enterprise", feature = "kafka"))] -use crate::kafka_triggers_oss::KafkaTriggerConfigConnection; +#[cfg(all(feature = "enterprise", feature = "kafka", feature = "private"))] +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_oss::NatsTriggerConfigConnection; +#[cfg(all(feature = "enterprise", feature = "nats", feature = "private"))] +use crate::triggers::nats::NatsTriggerConfigConnection; #[cfg(feature = "postgres_trigger")] -use crate::postgres_triggers::{ +use crate::triggers::postgres::{ create_logical_replication_slot, create_pg_publication, generate_random_string, get_default_pg_connection, PublicationData, }; @@ -122,7 +122,7 @@ pub fn workspaced_unauthed_service() -> Router { head(|| async {}).fallback(http_payload) }); - #[cfg(all(feature = "enterprise", feature = "gcp_trigger"))] + #[cfg(all(feature = "enterprise", feature = "gcp_trigger", feature = "private"))] let router = router.route("/gcp/:runnable_kind/*path", post(gcp_payload)); router @@ -146,7 +146,7 @@ struct HttpTriggerConfig { wrap_body: Option, } -#[cfg(all(feature = "enterprise", feature = "kafka"))] +#[cfg(all(feature = "enterprise", feature = "kafka", feature = "private"))] #[derive(Serialize, Deserialize)] pub struct KafkaTriggerConfig { #[serde(flatten)] @@ -155,7 +155,7 @@ pub struct KafkaTriggerConfig { pub group_id: String, } -#[cfg(all(feature = "enterprise", feature = "sqs_trigger"))] +#[cfg(all(feature = "enterprise", feature = "sqs_trigger", feature = "private"))] #[derive(Debug, Serialize, Deserialize)] pub struct SqsTriggerConfig { pub queue_url: String, @@ -164,11 +164,11 @@ pub struct SqsTriggerConfig { pub aws_auth_resource_type: AwsAuthResourceType, } -#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))] +#[cfg(all(feature = "enterprise", feature = "gcp_trigger", feature = "private"))] #[derive(Debug, Serialize, Deserialize)] pub struct GcpTriggerConfig { pub gcp_resource_path: String, - pub subscription_mode: SubscriptionMode, + pub subscription_mode: GcpSubscriptionMode, #[serde(default, deserialize_with = "empty_as_none")] pub subscription_id: Option, #[serde(default, deserialize_with = "empty_as_none")] @@ -179,7 +179,7 @@ pub struct GcpTriggerConfig { pub auto_acknowledge_msg: Option, } -#[cfg(all(feature = "enterprise", feature = "nats"))] +#[cfg(all(feature = "enterprise", feature = "nats", feature = "private"))] #[derive(Serialize, Deserialize)] pub struct NatsTriggerConfig { #[serde(flatten)] @@ -231,15 +231,15 @@ enum TriggerConfig { Postgres(PostgresTriggerConfig), #[cfg(feature = "websocket")] Websocket(WebsocketTriggerConfig), - #[cfg(all(feature = "enterprise", feature = "sqs_trigger"))] + #[cfg(all(feature = "enterprise", feature = "sqs_trigger", feature = "private"))] Sqs(SqsTriggerConfig), - #[cfg(all(feature = "enterprise", feature = "kafka"))] + #[cfg(all(feature = "enterprise", feature = "kafka", feature = "private"))] Kafka(KafkaTriggerConfig), - #[cfg(all(feature = "enterprise", feature = "nats"))] + #[cfg(all(feature = "enterprise", feature = "nats", feature = "private"))] Nats(NatsTriggerConfig), #[cfg(feature = "mqtt_trigger")] Mqtt(MqttTriggerConfig), - #[cfg(all(feature = "enterprise", feature = "gcp_trigger"))] + #[cfg(all(feature = "enterprise", feature = "gcp_trigger", feature = "private"))] Gcp(GcpTriggerConfig), } @@ -365,7 +365,7 @@ async fn set_postgres_trigger_config( Ok(capture_config) } -#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))] +#[cfg(all(feature = "enterprise", feature = "gcp_trigger", feature = "private"))] async fn set_gcp_trigger_config( w_id: &str, authed: ApiAuthed, @@ -392,14 +392,14 @@ async fn set_gcp_trigger_config( ) .await?; gcp_config.create_update = Some(config); - gcp_config.subscription_mode = SubscriptionMode::CreateUpdate; + gcp_config.subscription_mode = GcpSubscriptionMode::CreateUpdate; capture_config.trigger_config = Some(TriggerConfig::Gcp(gcp_config)); Ok(capture_config) } #[inline] -#[cfg(not(all(feature = "enterprise", feature = "gcp_trigger")))] +#[cfg(not(all(feature = "enterprise", feature = "gcp_trigger", feature = "private")))] async fn set_gcp_trigger_config( _w_id: &str, _authed: ApiAuthed, @@ -721,7 +721,7 @@ pub async fn get_active_capture_owner_and_email( #[cfg(any( feature = "http_trigger", - all(feature = "enterprise", feature = "gcp_trigger") + all(feature = "enterprise", feature = "gcp_trigger", feature = "private") ))] async fn get_capture_trigger_config_and_owner( db: &DB, @@ -900,7 +900,7 @@ async fn webhook_payload( Ok(StatusCode::NO_CONTENT) } -#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))] +#[cfg(all(feature = "enterprise", feature = "gcp_trigger", feature = "private"))] async fn gcp_payload( Extension(db): Extension, Extension(user_db): Extension, @@ -908,7 +908,7 @@ async fn gcp_payload( headers: HeaderMap, request: Request, ) -> Result { - use crate::{gcp_triggers_oss::GcpTrigger, trigger_helpers::TriggerJobArgs}; + use crate::triggers::{gcp::GcpTrigger, trigger_helpers::TriggerJobArgs}; let is_flow = matches!(runnable_kind, RunnableKind::Flow); let (gcp_trigger_config, owner, email): (GcpTriggerConfig, _, _) = @@ -931,9 +931,9 @@ async fn gcp_payload( ) .await?; - let (payload, gcp) = process_google_push_request(headers, request).await?; + let (payload, trigger_info) = process_google_push_request(headers, request).await?; - let (main_args, preprocessor_args) = GcpTrigger::build_capture_payloads(payload, gcp); + let (main_args, preprocessor_args) = GcpTrigger::build_capture_payloads(&payload, trigger_info); let _ = insert_capture_payload( &db, diff --git a/backend/windmill-api/src/db.rs b/backend/windmill-api/src/db.rs index 6df8633772..1f351ca99c 100644 --- a/backend/windmill-api/src/db.rs +++ b/backend/windmill-api/src/db.rs @@ -10,19 +10,18 @@ use futures::FutureExt; use sqlx::{ migrate::{Migrate, MigrateError}, pool::PoolConnection, - Executor, PgConnection, Pool, Postgres, + Executor, PgConnection, Postgres, }; use tokio::task::JoinHandle; use windmill_audit::audit_oss::{AuditAuthor, AuditAuthorable}; -use windmill_common::utils::generate_lock_id; +pub use windmill_common::db::DB; use windmill_common::{ db::{Authable, Authed}, error::Error, + utils::generate_lock_id, }; -pub type DB = Pool; - async fn current_database(conn: &mut PgConnection) -> Result { // language=SQL Ok(sqlx::query_scalar("SELECT current_database()") diff --git a/backend/windmill-api/src/ee_oss.rs b/backend/windmill-api/src/ee_oss.rs index 3fb1271a73..cec4e4db24 100644 --- a/backend/windmill-api/src/ee_oss.rs +++ b/backend/windmill-api/src/ee_oss.rs @@ -1,6 +1,12 @@ #[cfg(feature = "private")] #[allow(unused)] pub use crate::ee::*; +#[cfg(all( + feature = "enterprise", + any(feature = "nats", feature = "kafka", feature = "sqs_trigger"), + not(feature = "private") +))] +use {crate::db::ApiAuthed, windmill_common::DB}; #[cfg(not(feature = "private"))] use anyhow::anyhow; @@ -36,3 +42,20 @@ impl ExternalJwks { None } } + +#[cfg(all( + feature = "enterprise", + any(feature = "nats", feature = "kafka", feature = "sqs_trigger"), + not(feature = "private") +))] +pub async fn interpolate( + _authed: &ApiAuthed, + _db: &DB, + _w_id: &str, + _s: String, +) -> Result { + // Implementation is not open source + Err(anyhow!( + "Interpolation is not available in open source version" + )) +} diff --git a/backend/windmill-api/src/flows.rs b/backend/windmill-api/src/flows.rs index 4e9b3e4a18..69bb707ad7 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -10,11 +10,10 @@ use std::collections::HashMap; use crate::utils::WithStarredInfoQuery; use crate::{ + auth::{list_tokens_internal, TruncatedTokenWithEmail}, db::{ApiAuthed, DB}, schedule::clear_schedule, - triggers::{ - get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail, - }, + triggers::{get_triggers_count_internal, TriggersCount}, users::{maybe_refresh_folders, require_owner_of_path}, utils::check_scopes, webhook_util::{WebhookMessage, WebhookShared}, diff --git a/backend/windmill-api/src/gcp_triggers_oss.rs b/backend/windmill-api/src/gcp_triggers_oss.rs index b18e61db95..ce2f81f3c9 100644 --- a/backend/windmill-api/src/gcp_triggers_oss.rs +++ b/backend/windmill-api/src/gcp_triggers_oss.rs @@ -2,27 +2,19 @@ #[allow(unused)] pub use crate::gcp_triggers_ee::*; -use serde_json::value::RawValue; -use sqlx::prelude::FromRow; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use sqlx::types::Json as SqlxJson; -use windmill_common::worker::to_raw_value; -use windmill_common::triggers::TriggerKind; -use crate::trigger_helpers::TriggerJobArgs; - #[cfg(not(feature = "private"))] use { - crate::db::{ApiAuthed, DB}, - axum::{extract::Request, Router}, - http::HeaderMap, - windmill_common::db::UserDB, - windmill_common::{ - error::{Error as WindmillError, Result as WindmillResult}, - utils::empty_as_none, - }, + crate::{db::DB, trigger_helpers::TriggerJobArgs}, + serde::{Deserialize, Serialize}, + serde_json::value::RawValue, + sqlx::FromRow, + std::collections::HashMap, + windmill_common::{triggers::TriggerKind, utils::empty_as_none, worker::to_raw_value}, }; +#[cfg(not(feature = "private"))] +type SqlxJson = sqlx::types::Json; + #[derive(sqlx::Type, Debug, Deserialize, Serialize)] #[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))] #[sqlx(type_name = "DELIVERY_MODE", rename_all = "lowercase")] @@ -77,11 +69,6 @@ pub enum SubscriptionMode { CreateUpdate, } -#[cfg(not(feature = "private"))] -pub fn workspaced_service() -> Router { - Router::new() -} - #[cfg(not(feature = "private"))] pub fn start_consuming_gcp_pubsub_event( _db: DB, @@ -91,49 +78,6 @@ pub fn start_consuming_gcp_pubsub_event( } #[cfg(not(feature = "private"))] -pub async fn manage_google_subscription( - _authed: ApiAuthed, - _db: &DB, - _workspace_id: &str, - _gcp_resource_path: &str, - _path: &str, - _topic_id: &str, - _subscription_id: &mut Option, - _base_endpoint: &mut Option, - _subscription_mode: SubscriptionMode, - _create_update_config: Option, - _trigger_mode: bool, - _is_flow: bool, -) -> WindmillResult { - Ok(CreateUpdateConfig::default()) -} - -#[cfg(not(feature = "private"))] -pub async fn process_google_push_request( - _headers: HeaderMap, - _request: Request, -) -> Result<(String, HashMap>), WindmillError> { - Ok((String::new(), HashMap::new())) -} - -#[cfg(not(feature = "private"))] -pub async fn validate_jwt_token( - _db: &DB, - _user_db: UserDB, - _authed: ApiAuthed, - _headers: &HeaderMap, - _gcp_resource_path: &str, - _workspace_id: &str, - _delivery_config: &PushConfig, -) -> Result<(), windmill_common::error::Error> { - Ok(()) -} - -#[cfg(not(feature = "private"))] -pub fn gcp_push_route_handler() -> Router { - Router::new() -} - #[derive(FromRow, Deserialize, Serialize, Debug)] pub struct GcpTrigger { pub gcp_resource_path: String, @@ -164,6 +108,7 @@ pub struct GcpTrigger { pub auto_acknowledge_msg: Option, } +#[cfg(not(feature = "private"))] impl TriggerJobArgs for GcpTrigger { fn v1_payload_fn(payload: String) -> HashMap> { HashMap::from([("payload".to_string(), to_raw_value(&payload))]) @@ -172,4 +117,4 @@ impl TriggerJobArgs for GcpTrigger { fn trigger_kind() -> TriggerKind { TriggerKind::Gcp } -} \ No newline at end of file +} diff --git a/backend/windmill-api/src/http_triggers.rs b/backend/windmill-api/src/http_triggers.rs deleted file mode 100644 index b8a52a58ba..0000000000 --- a/backend/windmill-api/src/http_triggers.rs +++ /dev/null @@ -1,1469 +0,0 @@ -#[cfg(feature = "http_trigger")] -use crate::http_trigger_args::{HttpMethod, RawHttpTriggerArgs}; -#[cfg(feature = "parquet")] -use crate::job_helpers_oss::get_workspace_s3_resource; -use crate::{ - auth::{AuthCache, OptTokened}, - db::{ApiAuthed, DB}, - resources::try_get_resource_from_db_as, - trigger_helpers::{ - get_runnable_format, trigger_runnable, trigger_runnable_and_wait_for_result, RunnableId, - }, - users::fetch_api_authed, - utils::{check_scopes, non_empty_str, ExpiringCacheEntry}, -}; -use anyhow::anyhow; -use axum::response::Response; -use axum::{ - extract::{Path, Query}, - response::IntoResponse, - routing::{delete, get, post}, - Extension, Json, Router, -}; -#[cfg(feature = "parquet")] -use http::header::IF_NONE_MATCH; -use http::{HeaderMap, StatusCode}; -use quick_cache::sync::Cache; -use serde::{Deserialize, Serialize}; -use serde_json::value::RawValue; -use sql_builder::{bind::Bind, SqlBuilder}; -use sqlx::prelude::FromRow; -use sqlx::PgConnection; -use std::borrow::Cow; -use std::collections::HashSet; -use std::{collections::HashMap, sync::Arc}; -use tokio::sync::{RwLock, RwLockReadGuard}; -use tower_http::cors::CorsLayer; -use windmill_audit::{audit_oss::audit_log, ActionKind}; -use windmill_common::error::{Error, Result as WindmillResult}; -use windmill_common::flows::Retry; -#[cfg(feature = "parquet")] -use windmill_common::s3_helpers::build_object_store_client; -use windmill_common::{ - db::UserDB, - error::{self, JsonResult}, - s3_helpers::S3Object, - triggers::TriggerKind, - utils::{empty_as_none, not_found_if_none, paginate, require_admin, Pagination, StripPath}, - worker::CLOUD_HOSTED, -}; -use windmill_git_sync::handle_deployment_metadata; - -lazy_static::lazy_static! { - static ref ROUTE_PATH_KEY_RE: regex::Regex = regex::Regex::new(r"/?:[-\w]+").unwrap(); - static ref VALID_ROUTE_PATH_RE: regex::Regex = regex::Regex::new(r"^:?[-\w]+(/:?[-\w]+)*$").unwrap(); -} - -pub fn routes_global_service() -> Router { - let cors = CorsLayer::new() - .allow_methods([ - http::Method::GET, - http::Method::POST, - http::Method::DELETE, - http::Method::PUT, - http::Method::PATCH, - ]) - .allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION]) - .allow_origin(tower_http::cors::Any); - Router::new() - .route( - "/*path", - get(route_job) - .post(route_job) - .delete(route_job) - .put(route_job) - .patch(route_job) - .head(|| async { "" }), - ) - .layer(cors) -} - -pub fn workspaced_service() -> Router { - Router::new() - .route("/create", post(create_trigger)) - .route("/create_many", post(create_many_http_trigger)) - .route("/list", get(list_triggers)) - .route("/get/*path", get(get_trigger)) - .route("/update/*path", post(update_trigger)) - .route("/delete/*path", delete(delete_trigger)) - .route("/exists/*path", get(exists_trigger)) - .route("/route_exists", post(exists_route)) -} - -#[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Clone, Copy)] -#[sqlx(type_name = "AUTHENTICATION_METHOD", rename_all = "snake_case")] -#[serde(rename_all(serialize = "snake_case", deserialize = "snake_case"))] -pub enum AuthenticationMethod { - None, - Windmill, - ApiKey, - BasicHttp, - CustomScript, - Signature, -} - -#[derive(Debug, Deserialize)] -struct NewTrigger { - path: String, - route_path: String, - script_path: String, - is_flow: bool, - is_async: bool, - authentication_resource_path: Option, - authentication_method: AuthenticationMethod, - static_asset_config: Option>, - http_method: HttpMethod, - workspaced_route: Option, - summary: Option, - description: Option, - is_static_website: bool, - wrap_body: Option, - raw_string: Option, - error_handler_path: Option, - error_handler_args: Option>>>, - retry: Option>, -} - -#[derive(FromRow, Serialize)] -pub struct HttpTrigger { - pub workspace_id: String, - pub path: String, - pub route_path: String, - pub route_path_key: String, - pub script_path: String, - pub is_flow: bool, - pub edited_by: String, - pub email: String, - pub edited_at: chrono::DateTime, - pub extra_perms: serde_json::Value, - pub is_async: bool, - pub authentication_method: AuthenticationMethod, - pub http_method: HttpMethod, - pub summary: Option, - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub static_asset_config: Option>, - pub is_static_website: bool, - pub authentication_resource_path: Option, - pub workspaced_route: bool, - pub wrap_body: bool, - pub raw_string: bool, - pub error_handler_path: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub error_handler_args: Option>>, - #[serde(skip_serializing_if = "Option::is_none")] - pub retry: Option>>, -} - -#[derive(Deserialize)] -struct EditTrigger { - path: String, - route_path: Option, - script_path: String, - is_flow: bool, - is_async: bool, - authentication_method: AuthenticationMethod, - #[serde(deserialize_with = "non_empty_str")] - authentication_resource_path: Option, - summary: Option, - description: Option, - http_method: HttpMethod, - static_asset_config: Option>, - workspaced_route: Option, - is_static_website: bool, - wrap_body: Option, - raw_string: Option, - error_handler_path: Option, - error_handler_args: Option>>>, - retry: Option>, -} - -#[derive(Deserialize)] -pub struct ListTriggerQuery { - pub page: Option, - pub per_page: Option, - pub path: Option, - pub is_flow: Option, - #[serde(default, deserialize_with = "empty_as_none")] - pub path_start: Option, -} - -async fn list_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("http_trigger"); - sqlb.fields(&[ - "workspace_id", - "path", - "route_path", - "route_path_key", - "workspaced_route", - "wrap_body", - "raw_string", - "script_path", - "summary", - "description", - "is_flow", - "http_method", - "edited_by", - "email", - "edited_at", - "extra_perms", - "is_async", - "authentication_method", - "static_asset_config", - "is_static_website", - "authentication_resource_path", - "error_handler_path", - "error_handler_args", - "retry", - ]) - .order_by("edited_at", true) - .and_where("workspace_id = ?".bind(&w_id)) - .offset(offset) - .limit(per_page); - 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::internal_err(e.to_string()))?; - let rows = sqlx::query_as::<_, HttpTrigger>(&sql) - .fetch_all(&mut *tx) - .await?; - tx.commit().await?; - - Ok(Json(rows)) -} - -async fn get_trigger( - authed: ApiAuthed, - Extension(user_db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, -) -> error::JsonResult { - let path = path.to_path(); - check_scopes(&authed, || format!("http_triggers:read:{}", path))?; - let mut tx = user_db.begin(&authed).await?; - let trigger = sqlx::query_as!( - HttpTrigger, - r#" - SELECT - workspace_id, - path, - route_path, - route_path_key, - workspaced_route, - script_path, - summary, - description, - is_flow, - http_method as "http_method: _", - edited_by, - email, - edited_at, - extra_perms, - is_async, - authentication_method as "authentication_method: _", - static_asset_config as "static_asset_config: _", - is_static_website, - authentication_resource_path, - wrap_body, - raw_string, - error_handler_path, - error_handler_args as "error_handler_args: _", - retry as "retry: _" - FROM - http_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, "Trigger", path)?; - - Ok(Json(trigger)) -} - -fn validate_authentication_method( - authentication_method: AuthenticationMethod, - raw_string: Option, -) -> WindmillResult<()> { - match (authentication_method, raw_string) { - (AuthenticationMethod::CustomScript, raw) if !raw.unwrap_or(false) == true => { - return Err(Error::BadRequest( - "To use custom script authentication, please enable the raw body option." - .to_string(), - )); - } - _ => {} - } - - Ok(()) -} - -async fn increase_trigger_version(tx: &mut PgConnection) -> WindmillResult<()> { - sqlx::query!("SELECT nextval('http_trigger_version_seq')",) - .fetch_one(tx) - .await?; - - Ok(()) -} - -async fn create_trigger_inner( - tx: &mut PgConnection, - w_id: &str, - authed: &ApiAuthed, - new_http_trigger: &NewTrigger, - route_path_key: &str, -) -> WindmillResult<()> { - check_scopes(&authed, || { - format!("http_triggers:write:{}", &new_http_trigger.path) - })?; - - sqlx::query!( - r#" - INSERT INTO http_trigger ( - workspace_id, - path, - route_path, - route_path_key, - workspaced_route, - authentication_resource_path, - wrap_body, - raw_string, - script_path, - summary, - description, - is_flow, - is_async, - authentication_method, - http_method, - static_asset_config, - edited_by, - email, - edited_at, - is_static_website, - error_handler_path, - error_handler_args, - retry - ) - VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), $19, $20, $21, $22 - ) - "#, - w_id, - new_http_trigger.path, - new_http_trigger.route_path, - &route_path_key, - new_http_trigger.workspaced_route, - new_http_trigger.authentication_resource_path, - new_http_trigger.wrap_body.unwrap_or(false), - new_http_trigger.raw_string.unwrap_or(false), - new_http_trigger.script_path, - new_http_trigger.summary, - new_http_trigger.description, - new_http_trigger.is_flow, - new_http_trigger.is_async, - new_http_trigger.authentication_method as _, - new_http_trigger.http_method as _, - new_http_trigger.static_asset_config as _, - &authed.username, - &authed.email, - new_http_trigger.is_static_website, - new_http_trigger.error_handler_path, - new_http_trigger.error_handler_args as _, - new_http_trigger.retry as _ - ) - .execute(&mut *tx) - .await?; - - audit_log( - &mut *tx, - authed, - "http_triggers.create", - ActionKind::Create, - &w_id, - Some(new_http_trigger.path.as_str()), - None, - ) - .await?; - - increase_trigger_version(tx).await?; - - Ok(()) -} - -fn check_no_duplicates<'trigger>( - new_http_triggers: &[NewTrigger], - route_path_key: &[Cow<'trigger, str>], -) -> Result<(), Error> { - let mut seen = HashSet::with_capacity(new_http_triggers.len()); - - for (i, trigger) in new_http_triggers.iter().enumerate() { - if !seen.insert(( - &route_path_key[i], - trigger.http_method, - trigger.workspaced_route, - )) { - return Err(Error::BadRequest(format!( - "Duplicate HTTP route detected: '{}'. Each HTTP route must have a unique 'route_path'.", - &trigger.route_path - ))); - } - } - - Ok(()) -} - -async fn create_many_http_trigger( - authed: ApiAuthed, - Extension(db): Extension, - Extension(user_db): Extension, - Path(w_id): Path, - Json(new_http_triggers): Json>, -) -> WindmillResult<(StatusCode, String)> { - require_admin(authed.is_admin, &authed.username)?; - - let error_wrapper = |path: &str, error: Error| -> Error { - anyhow!( - "Error occurred for HTTP route at route path: {}, error: {}", - path, - error - ) - .into() - }; - - let mut route_path_keys = Vec::with_capacity(new_http_triggers.len()); - - for new_http_trigger in new_http_triggers.iter() { - let route_path_key = validate_http_trigger(&db, &w_id, new_http_trigger) - .await - .map_err(|err| error_wrapper(&new_http_trigger.route_path, err))?; - - route_path_keys.push(route_path_key); - } - - check_no_duplicates(&new_http_triggers, &route_path_keys)?; - - let mut tx = user_db.begin(&authed).await?; - - for (i, new_http_trigger) in new_http_triggers.iter().enumerate() { - create_trigger_inner( - &mut tx, - &w_id, - &authed, - new_http_trigger, - &route_path_keys[i], - ) - .await - .map_err(|err| error_wrapper(&new_http_trigger.route_path, err))?; - } - - tx.commit().await?; - - for http_trigger in new_http_triggers.into_iter() { - handle_deployment_metadata( - &authed.email, - &authed.username, - &db, - &w_id, - windmill_git_sync::DeployedObject::HttpTrigger { path: http_trigger.path.clone() }, - Some(format!("HTTP route '{}' created", http_trigger.path)), - true, - ) - .await?; - } - - Ok((StatusCode::CREATED, format!("Created all HTTP routes"))) -} - -async fn validate_http_trigger<'trigger>( - db: &DB, - w_id: &str, - new_http_trigger: &'trigger NewTrigger, -) -> WindmillResult> { - if !VALID_ROUTE_PATH_RE.is_match(&new_http_trigger.route_path) { - return Err(error::Error::BadRequest("Invalid route path".to_string())); - } - - validate_authentication_method( - new_http_trigger.authentication_method, - new_http_trigger.raw_string, - )?; - - // route path key is extracted from the route path to check for uniqueness - // it replaces /?:{key} with :key - // it will also remove the leading / if present, not an issue as we only allow : after slashes - let route_path_key = ROUTE_PATH_KEY_RE.replace_all(&new_http_trigger.route_path, ":key"); - - let exists = route_path_key_exists( - &route_path_key, - &new_http_trigger.http_method, - &w_id, - None, - new_http_trigger.workspaced_route, - db, - ) - .await?; - - if exists { - return Err(error::Error::BadRequest( - "A route already exists with this path".to_string(), - )); - } - - if *CLOUD_HOSTED - && (new_http_trigger.is_static_website || new_http_trigger.static_asset_config.is_some()) - { - return Err(error::Error::BadRequest( - "Static website and static asset are not supported on cloud".to_string(), - )); - } - - Ok(route_path_key) -} - -async fn create_trigger( - authed: ApiAuthed, - Extension(db): Extension, - Extension(user_db): Extension, - Path(w_id): Path, - Json(new_http_trigger): Json, -) -> WindmillResult<(StatusCode, String)> { - require_admin(authed.is_admin, &authed.username)?; - - let route_path_key = validate_http_trigger(&db, &w_id, &new_http_trigger).await?; - - let mut tx = user_db.begin(&authed).await?; - - let http_trigger_path = new_http_trigger.path.clone(); - - create_trigger_inner(&mut tx, &w_id, &authed, &new_http_trigger, &route_path_key).await?; - - tx.commit().await?; - - handle_deployment_metadata( - &authed.email, - &authed.username, - &db, - &w_id, - windmill_git_sync::DeployedObject::HttpTrigger { path: new_http_trigger.path.clone() }, - Some(format!("HTTP route '{}' created", new_http_trigger.path)), - true, - ) - .await?; - - Ok((StatusCode::CREATED, format!("{}", http_trigger_path))) -} - -async fn update_trigger( - authed: ApiAuthed, - Extension(user_db): Extension, - Extension(db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, - Json(ct): Json, -) -> WindmillResult { - let path = path.to_path(); - check_scopes(&authed, || format!("http_triggers:write:{}", path))?; - - if *CLOUD_HOSTED && (ct.is_static_website || ct.static_asset_config.is_some()) { - return Err(error::Error::BadRequest( - "Static website and static asset are not supported on cloud".to_string(), - )); - } - - validate_authentication_method(ct.authentication_method, ct.raw_string)?; - - let mut tx; - if authed.is_admin { - let Some(route_path) = ct.route_path else { - return Err(error::Error::BadRequest( - "route_path is required".to_string(), - )); - }; - - if !VALID_ROUTE_PATH_RE.is_match(&route_path) { - return Err(error::Error::BadRequest("Invalid route path".to_string())); - } - - let route_path_key = ROUTE_PATH_KEY_RE.replace_all(&route_path, ":key"); - - let exists = route_path_key_exists( - &route_path_key, - &ct.http_method, - &w_id, - Some(&path), - ct.workspaced_route, - &db, - ) - .await?; - if exists { - return Err(error::Error::BadRequest( - "A route already exists with this path".to_string(), - )); - } - - tx = user_db.begin(&authed).await?; - sqlx::query!( - r#" - UPDATE - http_trigger - SET - route_path = $1, - route_path_key = $2, - workspaced_route = $3, - wrap_body = $4, - raw_string = $5, - authentication_resource_path = $6, - script_path = $7, - path = $8, - is_flow = $9, - http_method = $10, - static_asset_config = $11, - edited_by = $12, - email = $13, - is_async = $14, - authentication_method = $15, - summary = $16, - description = $17, - edited_at = now(), - is_static_website = $18, - error_handler_path = $19, - error_handler_args = $20, - retry = $21 - WHERE - workspace_id = $22 AND - path = $23 - "#, - route_path, - &route_path_key, - ct.workspaced_route, - ct.wrap_body, - ct.raw_string, - ct.authentication_resource_path, - ct.script_path, - ct.path, - ct.is_flow, - ct.http_method as _, - ct.static_asset_config as _, - &authed.username, - &authed.email, - ct.is_async, - ct.authentication_method as _, - ct.summary, - ct.description, - ct.is_static_website, - ct.error_handler_path, - ct.error_handler_args as _, - ct.retry as _, - w_id, - path, - ) - .execute(&mut *tx) - .await?; - } else { - tx = user_db.begin(&authed).await?; - sqlx::query!( - r#" - UPDATE - http_trigger - SET - workspaced_route = $1, - wrap_body = $2, - raw_string = $3, - authentication_resource_path = $4, - script_path = $5, - path = $6, - is_flow = $7, - http_method = $8, - static_asset_config = $9, - edited_by = $10, - email = $11, - is_async = $12, - authentication_method = $13, - edited_at = now(), - is_static_website = $14, - error_handler_path = $15, - error_handler_args = $16, - retry = $17 - WHERE - workspace_id = $18 AND - path = $19 - "#, - ct.workspaced_route, - ct.wrap_body, - ct.raw_string, - ct.authentication_resource_path, - ct.script_path, - ct.path, - ct.is_flow, - ct.http_method as _, - ct.static_asset_config as _, - &authed.username, - &authed.email, - ct.is_async, - ct.authentication_method as _, - ct.is_static_website, - ct.error_handler_path, - ct.error_handler_args as _, - ct.retry as _, - w_id, - path, - ) - .execute(&mut *tx) - .await?; - } - - audit_log( - &mut *tx, - &authed, - "http_triggers.update", - ActionKind::Update, - &w_id, - Some(&ct.path), - None, - ) - .await?; - - increase_trigger_version(&mut tx).await?; - - tx.commit().await?; - - handle_deployment_metadata( - &authed.email, - &authed.username, - &db, - &w_id, - windmill_git_sync::DeployedObject::HttpTrigger { path: ct.path.clone() }, - Some(format!("HTTP route '{}' updated", ct.path)), - true, - ) - .await?; - - Ok(ct.path.to_string()) -} - -async fn delete_trigger( - authed: ApiAuthed, - Extension(db): Extension, - Extension(user_db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, -) -> WindmillResult { - let path = path.to_path(); - check_scopes(&authed, || format!("http_triggers:write:{}", path))?; - require_admin(authed.is_admin, &authed.username)?; - let mut tx = user_db.begin(&authed).await?; - sqlx::query!( - "DELETE FROM http_trigger - WHERE workspace_id = $1 - AND path = $2", - w_id, - path - ) - .execute(&mut *tx) - .await?; - - audit_log( - &mut *tx, - &authed, - "http_triggers.delete", - ActionKind::Delete, - &w_id, - Some(path), - None, - ) - .await?; - - increase_trigger_version(&mut tx).await?; - - tx.commit().await?; - - handle_deployment_metadata( - &authed.email, - &authed.username, - &db, - &w_id, - windmill_git_sync::DeployedObject::HttpTrigger { path: path.to_string() }, - Some(format!("HTTP route '{}' deleted", path)), - true, - ) - .await?; - - Ok(format!("HTTP route {path} deleted")) -} - -async fn exists_trigger( - Extension(db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, -) -> JsonResult { - let path = path.to_path(); - let exists = sqlx::query_scalar!( - "SELECT EXISTS( - SELECT 1 FROM http_trigger - WHERE path = $1 AND workspace_id = $2 - )", - path, - w_id - ) - .fetch_one(&db) - .await? - .unwrap_or(false); - - Ok(Json(exists)) -} - -#[derive(Deserialize)] -struct RouteExists { - route_path: String, - http_method: HttpMethod, - trigger_path: Option, - workspaced_route: Option, -} - -async fn route_path_key_exists( - route_path_key: &str, - http_method: &HttpMethod, - w_id: &str, - trigger_path: Option<&str>, - workspaced_route: Option, - db: &DB, -) -> WindmillResult { - let exists = if *CLOUD_HOSTED { - sqlx::query_scalar!( - r#" - SELECT EXISTS( - SELECT 1 - FROM http_trigger - WHERE - route_path_key = $1 - AND workspace_id = $2 - AND http_method = $3 - AND ($4::TEXT IS NULL OR path != $4) - ) - "#, - &route_path_key, - w_id, - http_method as &HttpMethod, - trigger_path - ) - .fetch_one(db) - .await? - .unwrap_or(false) - } else { - let route_path_key = match workspaced_route { - Some(true) => Cow::Owned(format!("{}/{}", w_id, route_path_key.trim_matches('/'))), - _ => Cow::Borrowed(route_path_key), - }; - sqlx::query_scalar!( - r#" - SELECT EXISTS( - SELECT 1 - FROM http_trigger - WHERE - ((workspaced_route IS TRUE AND workspace_id || '/' || route_path_key = $1) - OR (workspaced_route IS FALSE AND route_path_key = $1)) - AND http_method = $2 - AND ($3::TEXT IS NULL OR path != $3) - ) - "#, - &route_path_key, - http_method as &HttpMethod, - trigger_path - ) - .fetch_one(db) - .await? - .unwrap_or(false) - }; - - Ok(exists) -} - -async fn exists_route( - Extension(db): Extension, - Path(w_id): Path, - Json(RouteExists { route_path, http_method, trigger_path, workspaced_route }): Json< - RouteExists, - >, -) -> JsonResult { - let route_path_key = ROUTE_PATH_KEY_RE.replace_all(route_path.as_str(), ":key"); - - let exists = route_path_key_exists( - &route_path_key, - &http_method, - &w_id, - trigger_path.as_deref(), - workspaced_route, - &db, - ) - .await?; - - Ok(Json(exists)) -} - -#[derive(Debug, Deserialize, Clone)] -pub struct TriggerRoute { - path: String, - script_path: String, - is_flow: bool, - route_path: String, - workspace_id: String, - is_async: bool, - authentication_method: AuthenticationMethod, - edited_by: String, - email: String, - static_asset_config: Option>, - is_static_website: bool, - authentication_resource_path: Option, - workspaced_route: bool, - wrap_body: bool, - raw_string: bool, - error_handler_path: Option, - error_handler_args: Option>>>, - retry: Option>, -} - -pub struct RoutersCache { - routers: HashMap>, - version: i64, -} - -lazy_static::lazy_static! { - static ref HTTP_ACCESS_CACHE: Cache<(String, String, ApiAuthed), ExpiringCacheEntry<()>> = Cache::new(100); - static ref HTTP_AUTH_CACHE: Cache<(String, String, ApiAuthed), ExpiringCacheEntry> = Cache::new(100); - - static ref HTTP_ROUTERS_CACHE: RwLock = RwLock::new(RoutersCache { - routers: HashMap::new(), - version: 0, - }); -} - -pub async fn refresh_routers_loop( - db: &DB, - mut killpill_rx: tokio::sync::broadcast::Receiver<()>, -) -> () { - match refresh_routers(db).await { - Ok(_) => { - tracing::info!("Loaded HTTP routers"); - } - Err(err) => { - tracing::error!("Error loading HTTP routers: {err:#}"); - } - }; - let db = db.clone(); - tokio::spawn(async move { - loop { - tokio::select! { - _ = killpill_rx.recv() => { - break; - } - _ = tokio::time::sleep(std::time::Duration::from_secs(60)) => { - match refresh_routers(&db).await { - Ok((true, _)) => { - tracing::info!("Refreshed HTTP routers"); - } - Err(err) => { - tracing::error!("Error refreshing HTTP routers: {err:#}"); - } - _ => {} - } - } - } - } - }); -} - -pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, RoutersCache>), Error> { - let version = sqlx::query_scalar!("SELECT last_value FROM http_trigger_version_seq",) - .fetch_one(db) - .await?; - let routers_cache = HTTP_ROUTERS_CACHE.read().await; - if routers_cache.version == 0 || version > routers_cache.version { - drop(routers_cache); - let mut routers = HashMap::new(); - - for http_method in [ - HttpMethod::Get, - HttpMethod::Post, - HttpMethod::Put, - HttpMethod::Patch, - HttpMethod::Delete, - ] { - let triggers = sqlx::query_as!( - TriggerRoute, - r#" - SELECT - path, - script_path, - is_flow, - route_path, - authentication_resource_path, - workspace_id, - is_async, - authentication_method AS "authentication_method: _", - edited_by, - email, - static_asset_config AS "static_asset_config: _", - wrap_body, - raw_string, - workspaced_route, - is_static_website, - error_handler_path, - error_handler_args as "error_handler_args: _", - retry as "retry: _" - FROM - http_trigger - WHERE - http_method = $1 - "#, - &http_method as &HttpMethod - ) - .fetch_all(db) - .await?; - - let mut router = matchit::Router::new(); - - for trigger in triggers { - let full_path = if trigger.workspaced_route || *CLOUD_HOSTED { - format!("/{}/{}", trigger.workspace_id, trigger.route_path) - } else { - format!("/{}", trigger.route_path) - }; - - if trigger.is_static_website { - router - .insert(format!("{}/*wm_subpath", full_path), trigger.clone()) - .unwrap_or_else(|e| { - tracing::warn!( - "Failed to consider HTTP route {}/*wm_subpath: {:?}", - full_path, - e, - ); - }); - } - router - .insert(full_path.clone(), trigger.clone()) - .unwrap_or_else(|e| { - tracing::warn!("Failed to consider HTTP route {}: {:?}", full_path, e,); - }); - } - - routers.insert(http_method, router); - } - - let mut routers_cache = HTTP_ROUTERS_CACHE.write().await; - *routers_cache = RoutersCache { routers, version }; - - Ok((true, routers_cache.downgrade())) - } else { - tracing::debug!("No HTTP routers refresh needed"); - Ok((false, routers_cache)) - } -} - -async fn get_http_route_trigger( - route_path: &str, - auth_cache: &Arc, - token: Option<&String>, - db: &DB, - user_db: UserDB, - method: &http::Method, -) -> WindmillResult<(TriggerRoute, String, HashMap, ApiAuthed)> { - let http_method: HttpMethod = method.try_into()?; - - let requested_path = format!("/{}", route_path); - - let routers_cache = HTTP_ROUTERS_CACHE.read().await; - - let routers_cache = if routers_cache.routers.is_empty() { - tracing::warn!("HTTP routers are not loaded, loading from db"); - let (_, routers_cache) = refresh_routers(db).await?; - routers_cache - } else { - routers_cache - }; - - let router = routers_cache - .routers - .get(&http_method) - .ok_or(error::Error::internal_err( - "HTTP routers could not be loaded".to_string(), - ))?; - - let trigger_match = router.at(requested_path.as_str()).ok(); - - let matchit::Match { value: trigger, params } = - not_found_if_none(trigger_match, "Trigger", requested_path.as_str())?; - - let params: HashMap = params - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - - let username_override = if let AuthenticationMethod::Windmill = trigger.authentication_method { - let opt_authed = if let Some(token) = token { - auth_cache - .get_authed(Some(trigger.workspace_id.clone()), token) - .await - } else { - None - }; - if let Some(authed) = opt_authed { - // check that the user has access to the trigger - let cache_key = ( - trigger.workspace_id.clone(), - trigger.path.clone(), - authed.clone(), - ); - let exists = match HTTP_ACCESS_CACHE.get(&cache_key) { - Some(cache_entry) if cache_entry.expiry > std::time::Instant::now() => { - tracing::debug!("HTTP access cache hit for route {}", trigger.path); - true - } - _ => { - tracing::debug!("HTTP access cache miss for route {}", trigger.path); - let mut tx = user_db.begin(&authed).await?; - let exists = sqlx::query_scalar!( - r#" - SELECT EXISTS( - SELECT 1 - FROM - http_trigger - WHERE - workspace_id = $1 AND - path = $2 - ) - "#, - trigger.workspace_id, - trigger.path - ) - .fetch_one(&mut *tx) - .await? - .unwrap_or(false); - if exists { - HTTP_ACCESS_CACHE.insert( - cache_key, - ExpiringCacheEntry { - value: (), - expiry: std::time::Instant::now() - + std::time::Duration::from_secs(10), - }, - ); - } - exists - } - }; - if exists { - Some(authed.display_username().to_owned()) - } else { - return Err(error::Error::NotAuthorized("Unauthorized".to_string())); - } - } else { - return Err(error::Error::NotAuthorized( - "Requires authentication".to_string(), - )); - } - } else { - None - }; - - let authed = fetch_api_authed( - trigger.edited_by.clone(), - trigger.email.clone(), - &trigger.workspace_id, - &db, - Some(username_override.unwrap_or(format!("HTTP-{}", trigger.path))), - ) - .await?; - - Ok((trigger.clone(), route_path.to_string(), params, authed)) -} - -async fn route_job( - Extension(db): Extension, - Extension(user_db): Extension, - Extension(auth_cache): Extension>, - OptTokened { token }: OptTokened, - Path(route_path): Path, - headers: HeaderMap, - args: RawHttpTriggerArgs, -) -> Result { - let route_path = route_path.to_path().trim_end_matches("/"); - let (trigger, called_path, params, authed) = get_http_route_trigger( - route_path, - &auth_cache, - token.as_ref(), - &db, - user_db.clone(), - &args.0.metadata.method, - ) - .await - .map_err(|e| e.into_response())?; - - if trigger.script_path.is_empty() && trigger.static_asset_config.is_none() { - return Err(Error::NotFound(format!( - "Runnable path of HTTP route at path: {}", - trigger.path - )) - .into_response()); - } - - let args = args - .process_args( - &authed, - &db, - &trigger.workspace_id, - match trigger.authentication_method { - AuthenticationMethod::CustomScript | AuthenticationMethod::Signature => true, - _ => trigger.raw_string, - }, - ) - .await - .map_err(|e| e.into_response())?; - - match trigger.authentication_method { - AuthenticationMethod::None - | AuthenticationMethod::Windmill - | AuthenticationMethod::CustomScript => {} - _ => { - let resource_path = match trigger.authentication_resource_path { - Some(resource_path) => resource_path, - None => { - return Err(Error::BadRequest( - "Missing authentication resource path".to_string(), - ) - .into_response()) - } - }; - - let cache_key = ( - trigger.workspace_id.clone(), - resource_path.clone(), - authed.clone(), - ); - - let authentication_method = match HTTP_AUTH_CACHE.get(&cache_key) { - Some(cache_entry) if cache_entry.expiry > std::time::Instant::now() => { - tracing::debug!("HTTP auth method cache hit for route {}", trigger.path); - cache_entry.value - } - _ => { - tracing::debug!("HTTP auth method cache miss for route {}", trigger.path); - let auth_method = try_get_resource_from_db_as::< - crate::http_trigger_auth::AuthenticationMethod, - >( - &authed, - Some(user_db.clone()), - &db, - &resource_path, - &trigger.workspace_id, - ) - .await - .map_err(|e| e.into_response())?; - HTTP_AUTH_CACHE.insert( - cache_key, - ExpiringCacheEntry { - value: auth_method.clone(), - expiry: std::time::Instant::now() + std::time::Duration::from_secs(60), - }, - ); - auth_method - } - }; - - let raw_payload = args.0.metadata.raw_string.as_ref(); - - let response = authentication_method - .authenticate_http_request(&headers, raw_payload) - .map_err(|e| e.into_response())?; - - if let Some(response) = response { - return Ok(response); - } - } - } - - #[cfg(not(feature = "parquet"))] - if trigger.static_asset_config.is_some() { - return Err(error::Error::internal_err( - "Static asset configuration is not supported in this build".to_string(), - ) - .into_response()); - } - - #[cfg(feature = "parquet")] - if let Some(sqlx::types::Json(config)) = trigger.static_asset_config { - let build_static_response_f = async { - let (_, s3_resource_opt) = get_workspace_s3_resource( - &authed, - &db, - None, - &"NO_TOKEN".to_string(), // no token is provided in this case - &trigger.workspace_id, - config.storage, - ) - .await?; - let s3_resource = s3_resource_opt.ok_or(error::Error::internal_err( - "No files storage resource defined at the workspace level".to_string(), - ))?; - let s3_client = build_object_store_client(&s3_resource).await?; - - let path = if trigger.is_static_website { - let subpath = params - .get("wm_subpath") - .cloned() - .unwrap_or("index.html".to_string()); - tracing::info!("subpath: {}", subpath); - format!("{}/{}", config.s3.trim_end_matches('/'), subpath) - } else { - config.s3.clone() - }; - let path = object_store::path::Path::from(path); - let s3_object = s3_client.get(&path).await; - - let s3_object = match s3_object { - Err(object_store::Error::NotFound { .. }) if trigger.is_static_website => { - // fallback to index.html if the file is not found - let path = object_store::path::Path::from(format!( - "{}/index.html", - config.s3.trim_end_matches('/') - )); - s3_client.get(&path).await - } - r => r, - }; - - let s3_object = s3_object.map_err(|err| { - tracing::warn!("Error retrieving file from S3: {:?}", err); - error::Error::internal_err(format!("Error retrieving file: {}", err.to_string())) - })?; - - let mut response_headers = http::HeaderMap::new(); - if let Some(ref e_tag) = s3_object.meta.e_tag { - if let Some(if_none_match) = headers.get(IF_NONE_MATCH) { - if if_none_match == e_tag { - return Ok::<_, error::Error>(( - StatusCode::NOT_MODIFIED, - response_headers, - axum::body::Body::empty(), - )); - } - } - if let Ok(e_tag) = e_tag.parse() { - response_headers.insert("etag", e_tag); - } - } - response_headers.insert( - "content-type", - s3_object - .attributes - .get(&object_store::Attribute::ContentType) - .map(|s| s.parse().ok()) - .flatten() - .unwrap_or("application/octet-stream".parse().unwrap()), - ); - if !trigger.is_static_website { - response_headers.insert( - "content-disposition", - config.filename.as_ref().map_or_else( - || { - s3_object - .attributes - .get(&object_store::Attribute::ContentDisposition) - .map(|s| s.parse().ok()) - .flatten() - .unwrap_or("inline".parse().unwrap()) - }, - |filename| { - format!("inline; filename=\"{}\"", filename) - .parse() - .unwrap_or("inline".parse().unwrap()) - }, - ), - ); - } - - let body_stream = axum::body::Body::from_stream(s3_object.into_stream()); - Ok::<_, error::Error>((StatusCode::OK, response_headers, body_stream)) - }; - match build_static_response_f.await { - Ok((status, headers, body_stream)) => { - return Ok((status, headers, body_stream).into_response()) - } - Err(e) => return Err(e.into_response()), - } - } - - let runnable_format = get_runnable_format( - if trigger.is_flow { - RunnableId::from_flow_path(&trigger.script_path) - } else { - RunnableId::from_script_path(&trigger.script_path) - }, - &trigger.workspace_id, - &db, - &TriggerKind::Http, - ) - .await - .map_err(|e| e.into_response())?; - - let args = args - .to_args_from_format( - &trigger.route_path, - &called_path, - ¶ms, - runnable_format, - trigger.wrap_body, - ) - .map_err(|e| e.into_response())?; - - if trigger.is_async { - trigger_runnable( - &db, - Some(user_db), - authed, - &trigger.workspace_id, - &trigger.script_path, - trigger.is_flow, - args, - trigger.retry.as_ref(), - trigger.error_handler_path.as_deref(), - trigger.error_handler_args.as_ref(), - format!("http_trigger/{}", trigger.path), - ) - .await - .map_err(|e| e.into_response()) - } else { - trigger_runnable_and_wait_for_result( - &db, - Some(user_db), - authed, - &trigger.workspace_id, - &trigger.script_path, - trigger.is_flow, - args, - trigger.retry.as_ref(), - trigger.error_handler_path.as_deref(), - trigger.error_handler_args.as_ref(), - format!("http_trigger/{}", trigger.path), - ) - .await - .map_err(|e| e.into_response()) - } -} diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index b29d683a71..cbdc5eb5cc 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -48,7 +48,7 @@ use crate::{ auth::{OptTokened, Tokened}, concurrency_groups::join_concurrency_key, db::{ApiAuthed, DB}, - trigger_helpers::RunnableId, + triggers::trigger_helpers::RunnableId, users::{get_scope_tags, require_owner_of_path, OptAuthed}, utils::{check_scopes, content_plain, require_super_admin}, }; diff --git a/backend/windmill-api/src/kafka_triggers_oss.rs b/backend/windmill-api/src/kafka_triggers_oss.rs index 22cbf3785d..4227023648 100644 --- a/backend/windmill-api/src/kafka_triggers_oss.rs +++ b/backend/windmill-api/src/kafka_triggers_oss.rs @@ -5,19 +5,12 @@ pub use crate::kafka_triggers_ee::*; #[cfg(not(feature = "private"))] use crate::db::DB; #[cfg(not(feature = "private"))] -use axum::Router; -#[cfg(not(feature = "private"))] use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] #[cfg(not(feature = "private"))] pub struct KafkaResourceSecurity {} -#[cfg(not(feature = "private"))] -pub fn workspaced_service() -> Router { - Router::new() -} - #[cfg(not(feature = "private"))] pub fn start_kafka_consumers( _db: DB, @@ -54,7 +47,9 @@ pub struct KafkaTrigger { #[serde(skip_serializing_if = "Option::is_none")] pub error_handler_path: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub error_handler_args: Option>>>, + pub error_handler_args: Option< + sqlx::types::Json>>, + >, #[serde(skip_serializing_if = "Option::is_none")] pub retry: Option>, } diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index 2fd3c16cce..6d9fd79b60 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -89,12 +89,6 @@ mod flows; mod folders; mod granular_acls; mod groups; -#[cfg(feature = "http_trigger")] -mod http_trigger_args; -#[cfg(feature = "http_trigger")] -mod http_trigger_auth; -#[cfg(feature = "http_trigger")] -pub mod http_triggers; #[cfg(feature = "private")] pub mod indexer_ee; mod indexer_oss; @@ -106,6 +100,7 @@ mod integration; mod live_migrations; #[cfg(feature = "postgres_trigger")] mod postgres_triggers; +mod trigger_helpers; pub mod openapi; @@ -170,7 +165,6 @@ mod sqs_triggers_oss; #[cfg(feature = "private")] pub mod teams_approvals_ee; mod teams_approvals_oss; -mod trigger_helpers; mod static_assets; #[cfg(all(feature = "stripe", feature = "enterprise", feature = "private"))] @@ -182,7 +176,7 @@ pub mod teams_ee; mod teams_oss; mod token; mod tracing_init; -mod triggers; +pub mod triggers; mod users; #[cfg(feature = "private")] pub mod users_ee; @@ -381,101 +375,14 @@ pub async fn run_server( } }; - let kafka_triggers_service = { - #[cfg(all(feature = "enterprise", feature = "kafka"))] - { - kafka_triggers_oss::workspaced_service() - } - - #[cfg(not(all(feature = "enterprise", feature = "kafka")))] - { - Router::new() - } - }; - - let nats_triggers_service = { - #[cfg(all(feature = "enterprise", feature = "nats"))] - { - nats_triggers_oss::workspaced_service() - } - - #[cfg(not(all(feature = "enterprise", feature = "nats")))] - { - Router::new() - } - }; - - let mqtt_triggers_service = { - #[cfg(all(feature = "mqtt_trigger"))] - { - mqtt_triggers::workspaced_service() - } - - #[cfg(not(feature = "mqtt_trigger"))] - { - Router::new() - } - }; - - let gcp_triggers_service = { - #[cfg(all(feature = "enterprise", feature = "gcp_trigger"))] - { - gcp_triggers_oss::workspaced_service() - } - - #[cfg(not(all(feature = "enterprise", feature = "gcp_trigger")))] - { - Router::new() - } - }; - - let sqs_triggers_service = { - #[cfg(all(feature = "enterprise", feature = "sqs_trigger"))] - { - sqs_triggers_oss::workspaced_service() - } - - #[cfg(not(all(feature = "enterprise", feature = "sqs_trigger")))] - { - Router::new() - } - }; - - let websocket_triggers_service = { - #[cfg(feature = "websocket")] - { - websocket_triggers::workspaced_service() - } - - #[cfg(not(feature = "websocket"))] - Router::new() - }; - - let http_triggers_service = { - #[cfg(feature = "http_trigger")] - { - http_triggers::workspaced_service() - } - - #[cfg(not(feature = "http_trigger"))] - Router::new() - }; - + // Initialize HTTP trigger refresh loop #[cfg(feature = "http_trigger")] { let http_killpill_rx = killpill_rx.resubscribe(); - http_triggers::refresh_routers_loop(&db, http_killpill_rx).await; + triggers::http::refresh_routers_loop(&db, http_killpill_rx).await; } - let postgres_triggers_service = { - #[cfg(feature = "postgres_trigger")] - { - postgres_triggers::workspaced_service() - } - - #[cfg(not(feature = "postgres_trigger"))] - Router::new() - }; + let triggers_service = triggers::generate_trigger_routers(); if !*CLOUD_HOSTED && server_mode && !mcp_mode { #[cfg(feature = "websocket")] @@ -619,14 +526,7 @@ pub async fn run_server( .nest("/workspaces", workspaces::workspaced_service()) .nest("/oidc", oidc_oss::workspaced_service()) .nest("/openapi", openapi::openapi_service()) - .nest("/http_triggers", http_triggers_service) - .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("/gcp_triggers", gcp_triggers_service) - .nest("/postgres_triggers", postgres_triggers_service), + .merge(triggers_service), ) .nest("/workspaces", workspaces::global_service()) .nest( @@ -775,7 +675,7 @@ pub async fn run_server( { #[cfg(feature = "http_trigger")] { - http_triggers::routes_global_service() + triggers::http::handler::http_route_trigger_handler() } #[cfg(not(feature = "http_trigger"))] @@ -788,11 +688,19 @@ pub async fn run_server( .nest( "/gcp/w/:workspace_id", { - #[cfg(all(feature = "enterprise", feature = "gcp_trigger"))] + #[cfg(all( + feature = "enterprise", + feature = "gcp_trigger", + feature = "private" + ))] { - gcp_triggers_oss::gcp_push_route_handler() + triggers::gcp::handler_oss::gcp_push_route_handler() } - #[cfg(not(all(feature = "enterprise", feature = "gcp_trigger")))] + #[cfg(not(all( + feature = "enterprise", + feature = "gcp_trigger", + feature = "private" + )))] { Router::new() } diff --git a/backend/windmill-api/src/mqtt_triggers.rs b/backend/windmill-api/src/mqtt_triggers.rs index 5659c9b944..2b8c16fc6d 100644 --- a/backend/windmill-api/src/mqtt_triggers.rs +++ b/backend/windmill-api/src/mqtt_triggers.rs @@ -4,22 +4,11 @@ use crate::{ resources::try_get_resource_from_db_as, trigger_helpers::{trigger_runnable, TriggerJobArgs}, users::fetch_api_authed, - utils::check_scopes, }; -use windmill_git_sync::{handle_deployment_metadata, DeployedObject}; -use axum::{ - async_trait, - extract::{Path, Query}, - Extension, Json, -}; -use axum::{ - routing::{delete, get, post}, - Router, -}; +use axum::async_trait; use base64::{engine, prelude::*}; use bytes::Bytes; -use http::StatusCode; use itertools::Itertools; use rumqttc::{ v5::{ @@ -35,17 +24,15 @@ use rumqttc::{ TlsConfiguration, Transport, }; use serde::{Deserialize, Serialize}; -use sql_builder::{bind::Bind, SqlBuilder}; use sqlx::{FromRow, Type}; use std::collections::HashMap; use std::time::Duration; -use windmill_audit::{audit_oss::audit_log, ActionKind}; use windmill_common::{ db::UserDB, - error::{self, JsonResult}, + error::{to_anyhow, Error, Result}, triggers::TriggerKind, - utils::{not_found_if_none, paginate, report_critical_error, Pagination, StripPath}, - worker::{to_raw_value, CLOUD_HOSTED}, + utils::report_critical_error, + worker::to_raw_value, INSTANCE_NAME, }; @@ -53,34 +40,6 @@ use rand::seq::SliceRandom; use serde_json::value::RawValue; use sqlx::types::Json as SqlxJson; -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( payload: &[u8], trigger_info: HashMap>, @@ -124,6 +83,18 @@ async fn run_job( Ok(()) } +#[derive(Debug, Deserialize, Serialize)] +pub struct MqttV3Config { + clean_session: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct MqttV5Config { + clean_start: Option, + session_expiry_interval: Option, + topic_alias_maximum: Option, +} + #[derive(Clone, Debug, Deserialize, Serialize, Type)] #[serde(rename_all = "lowercase")] pub enum QualityOfService { @@ -152,18 +123,6 @@ impl From for V5QoS { } } -#[derive(Debug, Deserialize, Serialize)] -pub struct MqttV3Config { - clean_session: Option, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct MqttV5Config { - clean_start: Option, - session_expiry_interval: Option, - topic_alias_maximum: Option, -} - #[derive(Debug, Deserialize, Serialize, Type)] #[sqlx(type_name = "MQTT_CLIENT_VERSION")] #[sqlx(rename_all = "lowercase")] @@ -201,87 +160,6 @@ pub struct SubscribeTopic { 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, - error_handler_path: Option, - error_handler_args: Option>>>, - retry: Option>, -} - -#[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, - error_handler_path: Option, - error_handler_args: Option>>>, - retry: Option>, -} - -#[derive(Debug, Serialize, Deserialize, FromRow)] -pub struct MqttTrigger { - pub mqtt_resource_path: String, - pub subscribe_topics: Vec>, - pub v3_config: Option>, - pub v5_config: Option>, - pub client_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub client_version: Option, - pub path: String, - pub script_path: String, - pub is_flow: bool, - pub workspace_id: String, - pub edited_by: String, - pub email: String, - pub edited_at: chrono::DateTime, - pub extra_perms: Option, - pub error: Option, - pub server_id: Option, - pub last_server_ping: Option>, - pub enabled: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub error_handler_path: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub error_handler_args: Option>>>, - #[serde(skip_serializing_if = "Option::is_none")] - pub retry: Option>, -} - -#[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, -} - -const KEEP_ALIVE: u64 = 60; -const CLIENT_CONNECTION_TIMEOUT: u64 = 60; -const TOPIC_ALIAS_MAXIMUM: u16 = 65535; struct MqttClientBuilder<'client> { mqtt_resource: MqttResource, client_id: &'client str, @@ -310,14 +188,14 @@ impl<'client> MqttClientBuilder<'client> { } } - async fn build_client(&self) -> Result { + 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> { + fn get_tls_configuration(&self) -> Result> { let transport = match self.mqtt_resource.tls { Some(ref tls) if tls.enabled => { let transport = match tls.ca_certificate.trim().is_empty() { @@ -329,8 +207,9 @@ impl<'client> MqttClientBuilder<'client> { Some(client_certificate) if !client_certificate.trim().is_empty() => { - let client_certificate = - BASE64_STANDARD.decode(client_certificate)?; + let client_certificate = BASE64_STANDARD + .decode(client_certificate) + .map_err(to_anyhow)?; let password = tls .pkcs12_certificate_password .clone() @@ -351,7 +230,7 @@ impl<'client> MqttClientBuilder<'client> { Ok(transport) } - async fn build_v5_client(&self) -> Result { + async fn build_v5_client(&self) -> Result { let mut mqtt_options = V5MqttOptions::new( self.client_id, &self.mqtt_resource.broker, @@ -398,12 +277,15 @@ impl<'client> MqttClientBuilder<'client> { .map(|topic| Filter::new(topic.topic.clone(), topic.qos.clone().into())) .collect_vec(); - async_client.subscribe_many(subscribe_filters).await?; + async_client + .subscribe_many(subscribe_filters) + .await + .map_err(to_anyhow)?; } Ok(MqttClientResult::V5((V5MqttHandler, event_loop))) } - async fn build_v3_client(&self) -> Result { + async fn build_v3_client(&self) -> Result { let mut mqtt_options = V3MqttOptions::new( self.client_id, &self.mqtt_resource.broker, @@ -435,574 +317,62 @@ impl<'client> MqttClientBuilder<'client> { .map(|topic| SubscribeFilter::new(topic.topic.clone(), topic.qos.clone().into())) .collect_vec(); - async_client.subscribe_many(subscribe_filters).await?; + async_client + .subscribe_many(subscribe_filters) + .await + .map_err(to_anyhow)?; } Ok(MqttClientResult::V3((V3MqttHandler, event_loop))) } } -fn convert_disconnect_packet_into_string( - disconnect: rumqttc::v5::mqttbytes::v5::Disconnect, -) -> String { +#[derive(Debug, Serialize, Deserialize, FromRow)] +pub struct MqttTrigger { + pub mqtt_resource_path: String, + pub subscribe_topics: Vec>, + pub v3_config: Option>, + pub v5_config: Option>, + pub client_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub client_version: Option, + pub path: String, + pub script_path: String, + pub is_flow: bool, + pub workspace_id: String, + pub edited_by: String, + pub email: String, + pub edited_at: chrono::DateTime, + pub extra_perms: Option, + pub error: Option, + pub server_id: Option, + pub last_server_ping: Option>, + pub enabled: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_handler_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_handler_args: Option>>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub retry: Option>, +} + +const KEEP_ALIVE: u64 = 60; +const CLIENT_CONNECTION_TIMEOUT: u64 = 60; +const TOPIC_ALIAS_MAXIMUM: u16 = 65535; + +fn convert_disconnect_packet_into_err(disconnect: rumqttc::v5::mqttbytes::v5::Disconnect) -> Error { let err_message = disconnect .properties .map(|properties| properties.reason_string) .flatten(); let reason_code = disconnect.reason_code as u8; - format!( + anyhow::anyhow!( "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 occurred while trying to connect to mqtt broker after 30 seconds" - )) - })??; - - Ok(()) -} - -pub async fn create_mqtt_trigger( - authed: ApiAuthed, - Extension(db): Extension, - Extension(user_db): Extension, - Path(w_id): Path, - Json(new_mqtt_trigger): Json, -) -> error::Result<(StatusCode, String)> { - check_scopes(&authed, || { - format!("mqtt_triggers:write:{}", &new_mqtt_trigger.path) - })?; - 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, - error_handler_path, - error_handler_args, - retry, - } = 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, - error_handler_path, - error_handler_args, - retry - ) - VALUES ( - $1, - $2, - $3, - $4, - $5, - $6, - $7, - $8, - $9, - $10, - $11, - $12, - $13, - $14, - $15, - $16 - )"#, - 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, - error_handler_path, - error_handler_args as _, - retry as _ - ) - .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?; - - handle_deployment_metadata( - &authed.email, - &authed.username, - &db, - &w_id, - DeployedObject::MqttTrigger { path: path.to_string() }, - Some(format!("MQTT trigger '{}' created", path)), - true, - ) - .await?; - - Ok((StatusCode::CREATED, format!("{}", 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"); - sqlb.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", - "error_handler_path", - "error_handler_args", - "retry", - ]) - .order_by("edited_at", true) - .and_where("workspace_id = ?".bind(&w_id)) - .offset(offset) - .limit(per_page); - 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 committing 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 path = path.to_path(); - check_scopes(&authed, || format!("mqtt_triggers:read:{}", path))?; - - let mut tx = user_db.begin(&authed).await?; - 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, - error_handler_path, - error_handler_args as "error_handler_args: _", - retry as "retry: _" - 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(db): Extension, - Extension(user_db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, - Json(mqtt_trigger): Json, -) -> error::Result { - let workspace_path = path.to_path(); - check_scopes(&authed, || { - format!("mqtt_triggers:write:{}", workspace_path) - })?; - - let EditMqttTrigger { - mqtt_resource_path, - subscribe_topics, - script_path, - path, - is_flow, - v3_config, - v5_config, - client_version, - client_id, - error_handler_path, - error_handler_args, - retry, - } = 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, - error_handler_path = $14, - error_handler_args = $15, - retry = $16 - 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, - error_handler_path, - error_handler_args as _, - retry as _ - ) - .execute(&mut *tx) - .await?; - - audit_log( - &mut *tx, - &authed, - "mqtt_triggers.update", - ActionKind::Update, - &w_id, - Some(&path), - None, - ) - .await?; - - tx.commit().await?; - - handle_deployment_metadata( - &authed.email, - &authed.username, - &db, - &w_id, - DeployedObject::MqttTrigger { path: path.clone() }, - Some(format!("MQTT trigger '{}' updated", path)), - true, - ) - .await?; - - Ok(path.to_string()) -} - -pub async fn delete_mqtt_trigger( - authed: ApiAuthed, - Extension(db): Extension, - Extension(user_db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, -) -> error::Result { - let path = path.to_path(); - check_scopes(&authed, || format!("mqtt_triggers:write:{}", 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?; - - handle_deployment_metadata( - &authed.email, - &authed.username, - &db, - &w_id, - DeployedObject::MqttTrigger { path: path.to_string() }, - Some(format!("MQTT trigger '{}' deleted", path)), - true, - ) - .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(db): Extension, - Extension(user_db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, - Json(payload): Json, -) -> error::Result { - let path = path.to_path(); - check_scopes(&authed, || format!("mqtt_triggers:write:{}", path))?; - let mut tx = user_db.begin(&authed).await?; - - // 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?; - - handle_deployment_metadata( - &authed.email, - &authed.username, - &db, - &w_id, - DeployedObject::MqttTrigger { path: path.to_string() }, - Some(format!("MQTT trigger '{}' updated", path)), - true, - ) - .await?; - - Ok(format!( - "successfully updated mqtt trigger at path {} to status {}", - path, payload.enabled - )) + .into() } async fn loop_ping(db: &DB, mqtt: &MqttConfig, error: Option<&str>) { @@ -1026,7 +396,7 @@ trait MqttEvent { type Event; fn handle_publish_packet(publish_packet: Self::PublishPacket) -> PublishData; - fn handle_event(&self, event: Self::Event) -> Result, String>; + fn handle_event(&self, event: Self::Event) -> Result>; } struct V5MqttHandler; @@ -1046,7 +416,7 @@ impl MqttEvent for V5MqttHandler { ) } - fn handle_event(&self, event: Self::Event) -> Result, String> { + fn handle_event(&self, event: Self::Event) -> Result> { tracing::debug!("Inside V5 event"); match event { Self::Event::Incoming(packet) => match packet { @@ -1057,7 +427,7 @@ impl MqttEvent for V5MqttHandler { ))) } Self::IncomingPacket::Disconnect(disconnect) => { - return Err(convert_disconnect_packet_into_string(disconnect)); + return Err(convert_disconnect_packet_into_err(disconnect)); } packet => { tracing::debug!("Received = {:#?}", packet); @@ -1089,7 +459,7 @@ impl MqttEvent for V3MqttHandler { ) } - fn handle_event(&self, event: Self::Event) -> Result, String> { + fn handle_event(&self, event: Self::Event) -> Result> { tracing::debug!("Inside V3 event"); match event { Self::Event::Incoming(packet) => match packet { @@ -1116,67 +486,65 @@ const TIMEOUT_DURATION: u64 = 10; const CONNECTION_TIMEOUT: Duration = Duration::from_secs(TIMEOUT_DURATION); #[async_trait] -trait EventLoop { +pub trait EventLoop { type Event; type Error; - async fn poll(&mut self) -> Result; - async fn verify_connection(&mut self) -> Result<(), Error>; + async fn poll(&mut self) -> Result; + async fn verify_connection(&mut self) -> Result<()>; } #[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 poll(&mut self) -> Result { + self.poll().await.map_err(|err| to_anyhow(err).into()) } - async fn verify_connection(&mut self) -> Result<(), Error> { + async fn verify_connection(&mut self) -> Result<()> { let start = std::time::Instant::now(); while start.elapsed() < CONNECTION_TIMEOUT { - match self.poll().await? { + match self.poll().await.map_err(to_anyhow)? { 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), - ))); + return Err(convert_disconnect_packet_into_err(disconnect)); } _ => continue, } } - Err(Error::Common(error::Error::BadConfig(format!( + Err(Error::BadConfig(format!( "Timeout occurred while trying to connect to mqtt broker after {} seconds", TIMEOUT_DURATION - )))) + ))) } } #[async_trait] impl EventLoop for V3EventLoop { type Event = V3Event; - type Error = rumqttc::ConnectionError; + type Error = Error; - async fn poll(&mut self) -> Result { - self.poll().await + async fn poll(&mut self) -> Result { + self.poll().await.map_err(|err| to_anyhow(err).into()) } - async fn verify_connection(&mut self) -> Result<(), Error> { + async fn verify_connection(&mut self) -> Result<()> { let start = std::time::Instant::now(); while start.elapsed() < CONNECTION_TIMEOUT { - match self.poll().await? { + match self.poll().await.map_err(to_anyhow)? { Self::Event::Incoming(rumqttc::Packet::ConnAck(_)) => return Ok(()), _ => continue, } } - Err(Error::Common(error::Error::BadConfig(format!( + Err(Error::BadConfig(format!( "Timeout occurred while trying to connect to mqtt broker after {} seconds", TIMEOUT_DURATION - )))) + ))) } } @@ -1479,7 +847,7 @@ impl MqttTrigger { } } - async fn fetch_authed(&self, db: &DB) -> error::Result { + async fn fetch_authed(&self, db: &DB) -> Result { fetch_api_authed( self.edited_by.clone(), self.email.clone(), @@ -1722,7 +1090,7 @@ impl CaptureConfigForMqttTrigger { Some(()) } - async fn fetch_authed(&self, db: &DB) -> error::Result { + async fn fetch_authed(&self, db: &DB) -> Result { fetch_api_authed( self.owner.clone(), self.email.clone(), diff --git a/backend/windmill-api/src/nats_triggers_oss.rs b/backend/windmill-api/src/nats_triggers_oss.rs index 3e85ff45cf..53e8fd88ec 100644 --- a/backend/windmill-api/src/nats_triggers_oss.rs +++ b/backend/windmill-api/src/nats_triggers_oss.rs @@ -5,19 +5,12 @@ pub use crate::nats_triggers_ee::*; #[cfg(not(feature = "private"))] use crate::db::DB; #[cfg(not(feature = "private"))] -use axum::Router; -#[cfg(not(feature = "private"))] use serde::{Deserialize, Serialize}; #[cfg(not(feature = "private"))] #[derive(Serialize, Deserialize)] pub struct NatsResourceAuth {} -#[cfg(not(feature = "private"))] -pub fn workspaced_service() -> Router { - Router::new() -} - #[cfg(not(feature = "private"))] pub fn start_nats_consumers(_db: DB, mut _killpill_rx: tokio::sync::broadcast::Receiver<()>) -> () { // implementation is not open source diff --git a/backend/windmill-api/src/openapi.rs b/backend/windmill-api/src/openapi.rs index 95f73f95ba..6cc950523a 100644 --- a/backend/windmill-api/src/openapi.rs +++ b/backend/windmill-api/src/openapi.rs @@ -25,8 +25,11 @@ use crate::db::ApiAuthed; #[cfg(feature = "http_trigger")] use { crate::{ - http_trigger_args::HttpMethod, http_trigger_auth::ApiKeyAuthentication, - http_triggers::AuthenticationMethod, resources::try_get_resource_from_db_as, + resources::try_get_resource_from_db_as, + triggers::http::{ + http_trigger_args::HttpMethod, http_trigger_auth::ApiKeyAuthentication, + AuthenticationMethod, + }, }, itertools::Itertools, }; diff --git a/backend/windmill-api/src/postgres_triggers/mod.rs b/backend/windmill-api/src/postgres_triggers/mod.rs index 1e224df3dc..17d6d44678 100644 --- a/backend/windmill-api/src/postgres_triggers/mod.rs +++ b/backend/windmill-api/src/postgres_triggers/mod.rs @@ -1,47 +1,28 @@ use crate::{ db::{ApiAuthed, DB}, resources::try_get_resource_from_db_as, - trigger_helpers::{trigger_runnable, TriggerJobArgs}, - users::fetch_api_authed, }; -use chrono::Utc; -use itertools::Itertools; use native_tls::{Certificate, TlsConnector}; use pg_escape::quote_identifier; -use rand::Rng; use rust_postgres::{config::SslMode, Client, Config, NoTls}; use rust_postgres_native_tls::MakeTlsConnector; +use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; +use sqlx::FromRow; use std::collections::HashMap; -use axum::{ - routing::{delete, get, post}, - Router, -}; -pub use handler::PostgresTrigger; -use handler::{ - alter_publication, create_postgres_trigger, create_publication, create_slot, - create_template_script, delete_postgres_trigger, delete_publication, drop_slot_name, - exists_postgres_trigger, get_postgres_trigger, get_postgres_version, - get_postgres_version_internal, get_publication_info, get_template_script, - is_database_in_logical_level, list_database_publication, list_postgres_triggers, - list_slot_name, set_enabled, test_postgres_connection, update_postgres_trigger, Postgres, - Relations, -}; use windmill_common::{ db::UserDB, error::{to_anyhow, Error, Result}, + utils::empty_as_none, }; mod bool; mod converter; -mod handler; mod hex; -mod mapper; mod relation; mod replication_message; mod trigger; -pub use handler::PublicationData; pub use trigger::start_database; const ERROR_REPLICATION_SLOT_NOT_EXISTS: &str = r#"The replication slot associated with this trigger no longer exists. Recreate a new replication slot or select an existing one in the advanced tab, or delete and recreate a new trigger"#; @@ -88,6 +69,19 @@ fn build_tls_connector( ))) } +#[derive(FromRow, Serialize, Deserialize, Debug)] +pub struct Postgres { + pub user: String, + pub password: String, + pub host: String, + pub port: Option, + pub dbname: String, + #[serde(default)] + pub sslmode: String, + #[serde(default, deserialize_with = "empty_as_none")] + pub root_certificate_pem: Option, +} + pub async fn get_raw_postgres_connection( database: &Postgres, logical_mode: bool, @@ -174,124 +168,65 @@ pub async fn get_default_pg_connection( get_pg_connection(authed, user_db, db, postgres_resource_path, w_id, false).await } -pub async fn create_logical_replication_slot(tx: &Client, slot_name: &str) -> Result<()> { - tx.execute( - &format!("SELECT * FROM pg_create_logical_replication_slot($1, 'pgoutput')"), - &[&slot_name], - ) - .await - .map_err(to_anyhow)?; - Ok(()) +#[derive(FromRow, Deserialize, Serialize, Debug)] +pub struct PostgresTrigger { + pub path: String, + pub script_path: String, + pub is_flow: bool, + pub workspace_id: String, + pub edited_by: String, + pub email: String, + pub edited_at: chrono::DateTime, + #[serde(skip_serializing_if = "Option::is_none")] + pub extra_perms: Option, + pub postgres_resource_path: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub server_id: Option, + pub replication_slot_name: String, + pub publication_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_server_ping: Option>, + pub enabled: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_handler_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_handler_args: Option>>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub retry: Option>, } -async fn check_if_valid_publication_for_postgres_version( - pg_connection: &Client, - table_to_track: Option<&[Relations]>, -) -> Result { - let postgres_version = get_postgres_version_internal(pg_connection).await?; +pub async fn drop_logical_replication_slot(pg_connection: &Client, slot_name: &str) -> Result<()> { + let row = pg_connection + .query_opt( + r#" + SELECT + active_pid + FROM + pg_replication_slots + WHERE + slot_name = $1 + "#, + &[&slot_name], + ) + .await + .map_err(to_anyhow)?; - let pg_14 = postgres_version.starts_with("14"); - if pg_14 { - let unsupported_publication = table_to_track - .and_then(|relations| { - relations.iter().find(|relation| { - let invalid_relation = relation.table_to_track.iter().find(|table_to_track| { - table_to_track.where_clause.is_some() - || table_to_track.columns_name.is_some() - }); + let active_pid = row.map(|r| r.get::<_, Option>(0)).flatten(); - relation.table_to_track.is_empty() || invalid_relation.is_some() - }) - }) - .is_some(); - - if unsupported_publication { - return Err(Error::BadRequest( - "Your PostgreSQL database is running version 14, which does not support the following publication features: \ - - WHERE clause filtering, \ - - selective column tracking, and \ - - tracking all tables within a schema.\n\ - These features are only available in PostgreSQL 15 and above.".to_string(), - )); - } - } - Ok(pg_14) -} - -pub async fn create_pg_publication( - pg_connection: &Client, - publication_name: &str, - table_to_track: Option<&[Relations]>, - transaction_to_track: &[String], -) -> Result<()> { - let pg_14 = - check_if_valid_publication_for_postgres_version(pg_connection, table_to_track).await?; - let mut query = String::from("CREATE PUBLICATION "); - - query.push_str("e_identifier(publication_name)); - - match table_to_track { - Some(database_component) if !database_component.is_empty() => { - query.push_str(" FOR"); - let mut first = true; - for (i, schema) in database_component.iter().enumerate() { - if schema.table_to_track.is_empty() { - query.push_str(" TABLES IN SCHEMA "); - query.push_str("e_identifier(&schema.schema_name)); - } else { - if pg_14 && first { - query.push_str(" TABLE ONLY "); - first = false - } else if !pg_14 { - query.push_str(" TABLE ONLY "); - } - for (j, table) in schema.table_to_track.iter().enumerate() { - let table_name = quote_identifier(&table.table_name); - let schema_name = quote_identifier(&schema.schema_name); - let full_name = format!("{}.{}", &schema_name, &table_name); - query.push_str(&full_name); - if let Some(columns) = table.columns_name.as_ref() { - query.push_str(" ("); - let columns = columns - .iter() - .map(|column| quote_identifier(column)) - .join(", "); - query.push_str(&columns); - query.push_str(")"); - } - - if let Some(where_clause) = &table.where_clause { - query.push_str(" WHERE ("); - query.push_str(where_clause); - query.push(')'); - } - - if j + 1 != schema.table_to_track.len() { - query.push_str(", "); - } - } - } - if i < database_component.len() - 1 { - query.push_str(", "); - } - } - } - _ => { - query.push_str(" FOR ALL TABLES "); - } - }; - - if !transaction_to_track.is_empty() { - let transactions = || transaction_to_track.iter().join(", "); - query.push_str(" WITH (publish = '"); - query.push_str(&transactions()); - query.push_str("');"); + if let Some(pid) = active_pid { + pg_connection + .execute("SELECT pg_terminate_backend($1)", &[&pid]) + .await + .map_err(to_anyhow)?; } pg_connection - .execute(&query, &[]) + .execute("SELECT pg_drop_replication_slot($1)", &[&slot_name]) .await .map_err(to_anyhow)?; + Ok(()) } @@ -307,106 +242,3 @@ pub async fn drop_publication(pg_connection: &Client, publication_name: &str) -> Ok(()) } - -pub fn generate_random_string() -> String { - let timestamp = Utc::now().timestamp_millis().to_string(); - let mut rng = rand::rng(); - let charset = "abcdefghijklmnopqrstuvwxyz0123456789"; - - let random_part = (0..10) - .map(|_| { - charset - .chars() - .nth(rng.random_range(0..charset.len())) - .unwrap() - }) - .collect::(); - - format!("{}_{}", timestamp, random_part) -} - -fn publication_service() -> Router { - Router::new() - .route("/get/:publication_name/*path", get(get_publication_info)) - .route("/create/:publication_name/*path", post(create_publication)) - .route("/update/:publication_name/*path", post(alter_publication)) - .route( - "/delete/:publication_name/*path", - delete(delete_publication), - ) - .route("/list/*path", get(list_database_publication)) -} - -fn slot_service() -> Router { - Router::new() - .route("/list/*path", get(list_slot_name)) - .route("/create/*path", post(create_slot)) - .route("/delete/*path", delete(drop_slot_name)) -} - -fn postgres_service() -> Router { - Router::new().route("/version/*path", get(get_postgres_version)) -} - -pub fn workspaced_service() -> Router { - Router::new() - .route("/test", post(test_postgres_connection)) - .route("/create", post(create_postgres_trigger)) - .route("/list", get(list_postgres_triggers)) - .route("/get/*path", get(get_postgres_trigger)) - .route("/update/*path", post(update_postgres_trigger)) - .route("/delete/*path", delete(delete_postgres_trigger)) - .route("/exists/*path", get(exists_postgres_trigger)) - .route("/setenabled/*path", post(set_enabled)) - .route("/get_template_script/:id", get(get_template_script)) - .route("/create_template_script", post(create_template_script)) - .route( - "/is_valid_postgres_configuration/*path", - get(is_database_in_logical_level), - ) - .nest("/publication", publication_service()) - .nest("/slot", slot_service()) - .nest("/postgres", postgres_service()) -} - -async fn run_job( - payload: HashMap>, - db: &DB, - trigger: &PostgresTrigger, -) -> anyhow::Result<()> { - let args = PostgresTrigger::build_job_args( - &trigger.script_path, - trigger.is_flow, - &trigger.workspace_id, - db, - payload, - HashMap::new(), - ) - .await?; - - let authed = fetch_api_authed( - trigger.edited_by.clone(), - trigger.email.clone(), - &trigger.workspace_id, - db, - Some(format!("postgres-{}", trigger.path)), - ) - .await?; - - trigger_runnable( - db, - None, - authed, - &trigger.workspace_id, - &trigger.script_path, - trigger.is_flow, - args, - trigger.retry.as_ref(), - trigger.error_handler_path.as_deref(), - trigger.error_handler_args.as_ref(), - format!("postgres_trigger/{}", trigger.path), - ) - .await?; - - Ok(()) -} diff --git a/backend/windmill-api/src/postgres_triggers/trigger.rs b/backend/windmill-api/src/postgres_triggers/trigger.rs index 832fa997f3..6bd8877fec 100644 --- a/backend/windmill-api/src/postgres_triggers/trigger.rs +++ b/backend/windmill-api/src/postgres_triggers/trigger.rs @@ -9,10 +9,9 @@ use crate::{ LogicalReplicationMessage::{Begin, Commit, Delete, Insert, Relation, Type, Update}, ReplicationMessage, }, - run_job, }, resources::try_get_resource_from_db_as, - trigger_helpers::TriggerJobArgs, + trigger_helpers::{trigger_runnable, TriggerJobArgs}, users::fetch_api_authed, }; @@ -36,10 +35,9 @@ use windmill_common::{ }; use super::{ - drop_publication, get_default_pg_connection, get_raw_postgres_connection, - handler::{drop_logical_replication_slot, Postgres, PostgresTrigger}, - replication_message::PrimaryKeepAliveBody, - Error, ERROR_PUBLICATION_NAME_NOT_EXISTS, ERROR_REPLICATION_SLOT_NOT_EXISTS, + drop_logical_replication_slot, drop_publication, get_default_pg_connection, + get_raw_postgres_connection, replication_message::PrimaryKeepAliveBody, Error, Postgres, + PostgresTrigger, ERROR_PUBLICATION_NAME_NOT_EXISTS, ERROR_REPLICATION_SLOT_NOT_EXISTS, }; pub struct LogicalReplicationSettings { @@ -350,6 +348,48 @@ impl TriggerJobArgs>> for PostgresTrigger { } } +async fn run_job( + payload: HashMap>, + db: &DB, + trigger: &PostgresTrigger, +) -> anyhow::Result<()> { + let args = PostgresTrigger::build_job_args( + &trigger.script_path, + trigger.is_flow, + &trigger.workspace_id, + db, + payload, + HashMap::new(), + ) + .await?; + + let authed = fetch_api_authed( + trigger.edited_by.clone(), + trigger.email.clone(), + &trigger.workspace_id, + db, + Some(format!("postgres-{}", trigger.path)), + ) + .await?; + + trigger_runnable( + db, + None, + authed, + &trigger.workspace_id, + &trigger.script_path, + trigger.is_flow, + args, + trigger.retry.as_ref(), + trigger.error_handler_path.as_deref(), + trigger.error_handler_args.as_ref(), + format!("postgres_trigger/{}", trigger.path), + ) + .await?; + + Ok(()) +} + struct PgInfo<'a> { postgres_resource_path: &'a str, publication_name: &'a str, diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index f07c3b6760..cb29f323b5 100644 --- a/backend/windmill-api/src/resources.rs +++ b/backend/windmill-api/src/resources.rs @@ -1270,7 +1270,12 @@ async fn update_resource_type( feature = "mqtt_trigger", all( feature = "enterprise", - any(feature = "sqs_trigger", feature = "gcp_trigger") + any( + feature = "sqs_trigger", + feature = "gcp_trigger", + feature = "kafka", + feature = "nats" + ) ) ))] pub async fn try_get_resource_from_db_as( diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index 9f55d22d65..bf1447b51b 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -10,9 +10,8 @@ use crate::{ auth::AuthCache, db::{ApiAuthed, DB}, schedule::clear_schedule, - triggers::{ - get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail, - }, + triggers::{get_triggers_count_internal, TriggersCount}, + auth::{list_tokens_internal, TruncatedTokenWithEmail}, users::{maybe_refresh_folders, require_owner_of_path}, utils::{check_scopes, WithStarredInfoQuery}, webhook_util::{WebhookMessage, WebhookShared}, diff --git a/backend/windmill-api/src/sqs_triggers_oss.rs b/backend/windmill-api/src/sqs_triggers_oss.rs index 7d993bf1cc..49fffb43e9 100644 --- a/backend/windmill-api/src/sqs_triggers_oss.rs +++ b/backend/windmill-api/src/sqs_triggers_oss.rs @@ -5,17 +5,10 @@ pub use crate::sqs_triggers_ee::*; #[cfg(not(feature = "private"))] use crate::db::DB; #[cfg(not(feature = "private"))] -use axum::Router; -#[cfg(not(feature = "private"))] use serde::{Deserialize, Serialize}; #[cfg(not(feature = "private"))] use windmill_common::auth::aws::AwsAuthResourceType; -#[cfg(not(feature = "private"))] -pub fn workspaced_service() -> Router { - Router::new() -} - #[cfg(not(feature = "private"))] pub fn start_sqs(_db: DB, mut _killpill_rx: tokio::sync::broadcast::Receiver<()>) -> () { // implementation is not open source @@ -43,7 +36,9 @@ pub struct SqsTrigger { #[serde(skip_serializing_if = "Option::is_none")] pub error_handler_path: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub error_handler_args: Option>>>, + pub error_handler_args: Option< + sqlx::types::Json>>, + >, #[serde(skip_serializing_if = "Option::is_none")] pub retry: Option>, } diff --git a/backend/windmill-api/src/trigger_helpers.rs b/backend/windmill-api/src/trigger_helpers.rs index e8211d471c..3670622128 100644 --- a/backend/windmill-api/src/trigger_helpers.rs +++ b/backend/windmill-api/src/trigger_helpers.rs @@ -60,6 +60,7 @@ pub enum RunnableId { } impl RunnableId { + #[allow(dead_code)] pub fn from_script_hash(hash: ScriptHash) -> Self { Self::ScriptId(ScriptId::ScriptHash(hash)) } @@ -78,6 +79,7 @@ impl RunnableId { } #[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[allow(unused)] pub enum ScriptId { ScriptPath(String), ScriptHash(ScriptHash), diff --git a/backend/windmill-api/src/triggers.rs b/backend/windmill-api/src/triggers.rs deleted file mode 100644 index 82d189df5c..0000000000 --- a/backend/windmill-api/src/triggers.rs +++ /dev/null @@ -1,249 +0,0 @@ -use axum::Json; -use serde::{Deserialize, Serialize}; -use sqlx::FromRow; -use windmill_common::error::JsonResult; - -use crate::db::DB; - -#[derive(Serialize, Deserialize, Debug)] -pub struct TriggerPrimarySchedule { - schedule: String, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct TriggersCount { - primary_schedule: Option, - schedule_count: i64, - http_routes_count: i64, - webhook_count: i64, - email_count: i64, - websocket_count: i64, - kafka_count: i64, - nats_count: i64, - postgres_count: i64, - mqtt_count: i64, - sqs_count: i64, - gcp_count: i64, -} -pub(crate) async fn get_triggers_count_internal( - db: &DB, - w_id: &str, - path: &str, - is_flow: bool, -) -> JsonResult { - let primary_schedule = sqlx::query_scalar!( - "SELECT schedule FROM schedule WHERE path = $1 AND script_path = $1 AND is_flow = $2 AND workspace_id = $3", - path, - is_flow, - w_id - ) - .fetch_optional(db) - .await?; - - let schedule_count = sqlx::query_scalar!( - "SELECT COUNT(*) FROM schedule WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - path, - is_flow, - w_id - ) - .fetch_one(db) - .await? - .unwrap_or(0); - - let http_routes_count = sqlx::query_scalar!( - "SELECT COUNT(*) FROM http_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - path, - is_flow, - w_id - ) - .fetch_one(db) - .await? - .unwrap_or(0); - - let websocket_count = sqlx::query_scalar!( - "SELECT COUNT(*) FROM websocket_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - path, - is_flow, - w_id - ) - .fetch_one(db) - .await? - .unwrap_or(0); - - let kafka_count = sqlx::query_scalar!( - "SELECT COUNT(*) FROM kafka_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - path, - is_flow, - w_id - ) - .fetch_one(db) - .await? - .unwrap_or(0); - - let nats_count = sqlx::query_scalar!( - "SELECT COUNT(*) FROM nats_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - path, - is_flow, - w_id - ) - .fetch_one(db) - .await? - .unwrap_or(0); - - let postgres_count = sqlx::query_scalar!( - "SELECT COUNT(*) FROM postgres_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - path, - is_flow, - w_id - ) - .fetch_one(db) - .await? - .unwrap_or(0); - - let mqtt_count = sqlx::query_scalar!( - "SELECT COUNT(*) FROM mqtt_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - path, - is_flow, - w_id - ) - .fetch_one(db) - .await? - .unwrap_or(0); - - let sqs_count = sqlx::query_scalar!( - "SELECT COUNT(*) FROM sqs_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - path, - is_flow, - w_id - ) - .fetch_one(db) - .await? - .unwrap_or(0); - - let gcp_count = sqlx::query_scalar!( - "SELECT COUNT(*) FROM gcp_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", - path, - is_flow, - w_id - ) - .fetch_one(db) - .await? - .unwrap_or(0); - - let webhook_count = (if is_flow { - sqlx::query_scalar!( - "SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]", - w_id, - path, - ) - - } else { - sqlx::query_scalar!( - "SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:' || $2]::text[]", - w_id, - path, - ) - }).fetch_one(db) - .await? - .unwrap_or(0); - - let email_count = (if is_flow { - sqlx::query_scalar!( - "SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]", - w_id, - path, - ) - - } else { - sqlx::query_scalar!( - "SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]", - w_id, - path, - ) - }).fetch_one(db) - .await? - .unwrap_or(0); - - Ok(Json(TriggersCount { - primary_schedule: primary_schedule.map(|s| TriggerPrimarySchedule { schedule: s }), - schedule_count, - http_routes_count, - webhook_count, - email_count, - websocket_count, - kafka_count, - nats_count, - postgres_count, - mqtt_count, - gcp_count, - sqs_count, - })) -} - -#[derive(FromRow, Serialize)] -pub struct TruncatedTokenWithEmail { - pub label: Option, - pub token_prefix: Option, - pub expiration: Option>, - pub created_at: chrono::DateTime, - pub last_used_at: chrono::DateTime, - pub scopes: Option>, - pub email: Option, -} - -pub async fn list_tokens_internal( - db: &DB, - w_id: &str, - path: &str, - is_flow: bool, -) -> JsonResult> { - let tokens = if is_flow { - sqlx::query_as!( - TruncatedTokenWithEmail, - r#" - SELECT label, - concat(substring(token for 10)) AS token_prefix, - expiration, - created_at, - last_used_at, - scopes, - email - FROM token - WHERE workspace_id = $1 - AND ( - scopes @> ARRAY['jobs:run:flows:' || $2]::text[] - OR scopes @> ARRAY['run:flow/' || $2]::text[] - ) - "#, - w_id, - path - ) - .fetch_all(db) - .await? - } else { - sqlx::query_as!( - TruncatedTokenWithEmail, - r#" - SELECT label, - concat(substring(token for 10)) AS token_prefix, - expiration, - created_at, - last_used_at, - scopes, - email - FROM token - WHERE workspace_id = $1 - AND ( - scopes @> ARRAY['jobs:run:scripts:' || $2]::text[] - OR scopes @> ARRAY['run:script/' || $2]::text[] - ) - "#, - w_id, - path - ) - .fetch_all(db) - .await? - }; - - Ok(Json(tokens)) -} diff --git a/backend/windmill-api/src/triggers/gcp/handler_oss.rs b/backend/windmill-api/src/triggers/gcp/handler_oss.rs new file mode 100644 index 0000000000..825c7f71e0 --- /dev/null +++ b/backend/windmill-api/src/triggers/gcp/handler_oss.rs @@ -0,0 +1,64 @@ +#[cfg(feature = "private")] +pub use super::handler_ee::*; + +#[cfg(not(feature = "private"))] +use { + super::GcpTrigger, + crate::{ + db::{ApiAuthed, DB}, + triggers::{TriggerCrud, TriggerData}, + }, + axum::async_trait, + sqlx::PgConnection, + windmill_common::error::{Error, Result}, + windmill_git_sync::DeployedObject, +}; + +#[cfg(not(feature = "private"))] +#[async_trait] +impl TriggerCrud for GcpTrigger { + type Trigger = (); + type TriggerConfig = (); + type TriggerConfigRequest = (); + type TestConnectionConfig = (); + + const TABLE_NAME: &'static str = ""; + const TRIGGER_TYPE: &'static str = ""; + const SUPPORTS_ENABLED: bool = false; + const SUPPORTS_SERVER_STATE: bool = false; + const SUPPORTS_TEST_CONNECTION: bool = false; + const ROUTE_PREFIX: &'static str = "/gcp_triggers"; + const DEPLOYMENT_NAME: &'static str = ""; + const IS_CLOUD_HOSTED: bool = false; + + fn get_deployed_object(path: String) -> DeployedObject { + DeployedObject::GcpTrigger { path } + } + + async fn create_trigger( + &self, + _db: &DB, + _executor: &mut PgConnection, + _authed: &ApiAuthed, + _w_id: &str, + _trigger: TriggerData, + ) -> Result<()> { + Err(Error::BadRequest( + "GCP triggers are not available in open source version".to_string(), + )) + } + + async fn update_trigger( + &self, + _db: &DB, + _executor: &mut PgConnection, + _authed: &ApiAuthed, + _workspace_id: &str, + _path: &str, + _trigger: TriggerData, + ) -> Result<()> { + Err(Error::BadRequest( + "GCP triggers are not available in open source version".to_string(), + )) + } +} diff --git a/backend/windmill-api/src/triggers/gcp/mod.rs b/backend/windmill-api/src/triggers/gcp/mod.rs new file mode 100644 index 0000000000..f11ba94883 --- /dev/null +++ b/backend/windmill-api/src/triggers/gcp/mod.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "private")] +mod handler_ee; +pub mod handler_oss; + +#[cfg(feature = "private")] +mod mod_ee; +#[cfg(feature = "private")] +pub use mod_ee::*; + +#[derive(Clone, Copy)] +pub struct GcpTrigger; diff --git a/backend/windmill-api/src/triggers/handler.rs b/backend/windmill-api/src/triggers/handler.rs new file mode 100644 index 0000000000..7138b2661e --- /dev/null +++ b/backend/windmill-api/src/triggers/handler.rs @@ -0,0 +1,923 @@ +use crate::{ + db::ApiAuthed, + triggers::{StandardTriggerQuery, TriggerData}, +}; +use async_trait::async_trait; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use sql_builder::{bind::Bind, SqlBuilder}; +use sqlx::{FromRow, PgConnection}; +use std::fmt::Debug; +use windmill_common::{ + db::UserDB, + error::{Error, JsonResult, Result}, + utils::{paginate, Pagination, StripPath}, + worker::CLOUD_HOSTED, + DB, +}; +use windmill_git_sync::DeployedObject; + +use axum::{ + extract::{Path, Query}, + http::StatusCode, + routing::{delete, get, post}, + Extension, Json, Router, +}; +use std::sync::Arc; +use windmill_audit::{audit_oss::audit_log, ActionKind}; +use windmill_git_sync::handle_deployment_metadata; + +use crate::utils::check_scopes; + +#[async_trait] +pub trait TriggerCrud: Send + Sync + 'static { + type Trigger: Serialize + + DeserializeOwned + + for<'r> FromRow<'r, sqlx::postgres::PgRow> + + Send + + Sync + + Unpin; + + type TriggerConfig: Debug + + DeserializeOwned + + for<'r> FromRow<'r, sqlx::postgres::PgRow> + + Serialize + + Send + + Sync + + Unpin; + + type TriggerConfigRequest: Debug + DeserializeOwned + Serialize + Send + Sync; + type TestConnectionConfig: Debug + DeserializeOwned + Serialize + Send + Sync; + + const TABLE_NAME: &'static str; + const TRIGGER_TYPE: &'static str; + const SUPPORTS_ENABLED: bool; + const SUPPORTS_SERVER_STATE: bool; + const SUPPORTS_TEST_CONNECTION: bool; + const ROUTE_PREFIX: &'static str; + const DEPLOYMENT_NAME: &'static str; + const ADDITIONAL_SELECT_FIELDS: &[&'static str] = &[]; + const IS_ALLOWED_ON_CLOUD: bool; + + fn get_deployed_object(path: String) -> DeployedObject; + + async fn validate_new( + &self, + db: &DB, + workspace_id: &str, + new: &Self::TriggerConfigRequest, + ) -> Result<()> { + self.validate_config(db, new, workspace_id).await + } + + async fn validate_edit( + &self, + db: &DB, + workspace_id: &str, + edit: &Self::TriggerConfigRequest, + _path: &str, + ) -> Result<()> { + self.validate_config(db, edit, workspace_id).await + } + + async fn validate_config( + &self, + _db: &DB, + _config: &Self::TriggerConfigRequest, + _workspace_id: &str, + ) -> Result<()> { + Ok(()) + } + + fn scope_domain_name() -> &'static str { + &Self::ROUTE_PREFIX[1..] + } + + async fn create_trigger( + &self, + db: &DB, + tx: &mut PgConnection, + authed: &ApiAuthed, + w_id: &str, + trigger: TriggerData, + ) -> Result<()>; + + async fn update_trigger( + &self, + db: &DB, + tx: &mut PgConnection, + authed: &ApiAuthed, + workspace_id: &str, + path: &str, + trigger: TriggerData, + ) -> Result<()>; + + async fn test_connection( + &self, + _db: &DB, + _authed: &ApiAuthed, + _user_db: &UserDB, + _workspace_id: &str, + _config: Self::TestConnectionConfig, + ) -> Result<()> { + Err( + anyhow::anyhow!("Test connection not supported for this trigger type".to_string(),) + .into(), + ) + } + + fn additional_routes(&self) -> axum::Router { + axum::Router::new() + } + + async fn get_trigger_by_path( + &self, + tx: &mut PgConnection, + workspace_id: &str, + path: &str, + ) -> Result { + let mut fields = vec![ + "workspace_id", + "path", + "script_path", + "is_flow", + "edited_by", + "email", + "edited_at", + "extra_perms", + ]; + + if Self::SUPPORTS_SERVER_STATE { + fields.extend_from_slice(&["enabled", "server_id", "last_server_ping", "error"]); + } + + fields.extend_from_slice(&["error_handler_path", "error_handler_args", "retry"]); + fields.extend_from_slice(Self::ADDITIONAL_SELECT_FIELDS); + + let sql = format!( + r#"SELECT + {} + FROM + {} + WHERE + workspace_id = $1 AND + path = $2 + "#, + fields.join(", "), + Self::TABLE_NAME + ); + + sqlx::query_as(&sql) + .bind(workspace_id) + .bind(path) + .fetch_optional(&mut *tx) + .await? + .ok_or_else(|| Error::NotFound(format!("Trigger not found at path: {}", path))) + } + + async fn exists(&self, db: &DB, workspace_id: &str, path: &str) -> Result { + let exists = sqlx::query_scalar(&format!( + "SELECT EXISTS(SELECT 1 FROM {} WHERE workspace_id = $1 AND path = $2)", + Self::TABLE_NAME + )) + .bind(workspace_id) + .bind(path) + .fetch_one(db) + .await?; + + Ok(exists) + } + + async fn delete_by_path( + &self, + tx: &mut PgConnection, + workspace_id: &str, + path: &str, + ) -> Result { + let deleted = sqlx::query(&format!( + "DELETE FROM {} WHERE workspace_id = $1 AND path = $2", + Self::TABLE_NAME + )) + .bind(workspace_id) + .bind(path) + .execute(&mut *tx) + .await? + .rows_affected(); + + Ok(deleted > 0) + } + + async fn set_enabled( + &self, + authed: &ApiAuthed, + tx: &mut PgConnection, + workspace_id: &str, + path: &str, + enabled: bool, + ) -> Result { + if !Self::SUPPORTS_SERVER_STATE { + return Err(anyhow::anyhow!( + "Enable/disable not supported for this trigger type".to_string(), + ) + .into()); + } + + let updated = sqlx::query(&format!( + r#" + UPDATE + {} + SET + enabled = $1, + email = $2, + edited_by = $3, + edited_at = now(), + server_id = NULL, + error = NULL, + last_server_ping = NULL + WHERE + workspace_id = $4 AND + path = $5 + "#, + Self::TABLE_NAME + )) + .bind(enabled) + .bind(&authed.email) + .bind(&authed.username) + .bind(workspace_id) + .bind(path) + .execute(&mut *tx) + .await? + .rows_affected(); + + Ok(updated > 0) + } + + #[allow(unused)] + async fn trigger_count( + &self, + tx: &mut PgConnection, + workspace_id: &str, + is_flow: bool, + script_path: &str, + ) -> i64 { + let count = sqlx::query_scalar(&format!( + r#" + SELECT + COUNT(*) + FROM + {} + WHERE + workspace_id = $1 AND + is_flow = $2 AND + script_path = $3 + "#, + Self::TABLE_NAME + )) + .bind(workspace_id) + .bind(is_flow) + .bind(script_path) + .fetch_one(&mut *tx) + .await + .unwrap_or(0); + + count + } + + async fn list_triggers( + &self, + tx: &mut PgConnection, + workspace_id: &str, + query: Option<&StandardTriggerQuery>, + ) -> Result> { + let mut fields = vec![ + "workspace_id", + "path", + "script_path", + "is_flow", + "edited_by", + "email", + "edited_at", + "extra_perms", + ]; + + if Self::SUPPORTS_SERVER_STATE { + fields.extend_from_slice(&["enabled", "server_id", "last_server_ping", "error"]); + } + + fields.extend_from_slice(&["error_handler_path", "error_handler_args", "retry"]); + fields.extend_from_slice(Self::ADDITIONAL_SELECT_FIELDS); + + let mut sqlb = SqlBuilder::select_from(Self::TABLE_NAME); + + sqlb.fields(&fields) + .order_by("edited_at", true) + .and_where("workspace_id = ?".bind(&workspace_id)); + + if let Some(query) = query { + let (per_page, offset) = + paginate(Pagination { per_page: query.per_page, page: query.page }); + if let Some(path) = &query.path { + sqlb.and_where_eq("script_path", "?".bind(path)); + } + + if let Some(is_flow) = query.is_flow { + sqlb.and_where_eq("is_flow", "?".bind(&is_flow)); + } + + if let Some(path_start) = &query.path_start { + sqlb.and_where_like_left("path", path_start); + } + + sqlb.offset(offset).limit(per_page); + } + + let sql = sqlb + .sql() + .map_err(|e| Error::InternalErr(format!("SQL error: {}", e)))?; + + let triggers = sqlx::query_as(&sql).fetch_all(&mut *tx).await?; + + Ok(triggers) + } +} + +pub fn trigger_routes() -> Router { + let mut router = Router::new() + .route("/create", post(create_trigger::)) + .route("/list", get(list_triggers::)) + .route("/get/*path", get(get_trigger::)) + .route("/update/*path", post(update_trigger::)) + .route("/delete/*path", delete(delete_trigger::)) + .route("/exists/*path", get(exists_trigger::)); + + if T::SUPPORTS_ENABLED { + router = router.route("/setenabled/*path", post(set_enabled_trigger::)); + } + + if T::SUPPORTS_TEST_CONNECTION { + router = router.route("/test", post(test_connection::)); + } + + router +} + +async fn create_trigger( + Extension(handler): Extension>, + authed: ApiAuthed, + Extension(db): Extension, + Extension(user_db): Extension, + Path(workspace_id): Path, + Json(new_trigger): Json>, +) -> Result<(StatusCode, String)> { + check_scopes(&authed, || { + format!( + "{}:write:{}", + T::scope_domain_name(), + &new_trigger.base.path + ) + })?; + + if *CLOUD_HOSTED && !T::IS_ALLOWED_ON_CLOUD { + return Err(Error::BadRequest(format!( + "{} triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host", + T::TRIGGER_TYPE + ))); + } + + handler + .validate_new(&db, &workspace_id, &new_trigger.config) + .await?; + + let mut tx = user_db.begin(&authed).await?; + + let new_path = new_trigger.base.path.clone(); + + handler + .create_trigger(&db, &mut *tx, &authed, &workspace_id, new_trigger) + .await?; + + audit_log( + &mut *tx, + &authed, + &format!("{}_triggers.create", T::TRIGGER_TYPE), + ActionKind::Create, + &workspace_id, + Some(&new_path), + None, + ) + .await?; + + handle_deployment_metadata( + &authed.email, + &authed.username, + &db, + &workspace_id, + T::get_deployed_object(new_path.clone()), + Some(format!("{} '{}' created", T::DEPLOYMENT_NAME, new_path)), + true, + ) + .await?; + + tx.commit().await?; + + Ok((StatusCode::CREATED, new_path)) +} + +async fn list_triggers( + Extension(handler): Extension>, + authed: ApiAuthed, + Extension(user_db): Extension, + Path(workspace_id): Path, + Query(query): Query, +) -> JsonResult> { + let mut tx = user_db.begin(&authed).await?; + let triggers = handler + .list_triggers(&mut *tx, &workspace_id, Some(&query)) + .await?; + tx.commit().await?; + + Ok(Json(triggers)) +} + +async fn get_trigger( + Extension(handler): Extension>, + authed: ApiAuthed, + Extension(user_db): Extension, + Path((workspace_id, path)): Path<(String, StripPath)>, +) -> JsonResult { + let path = path.to_path(); + check_scopes(&authed, || { + format!("{}:read:{}", T::scope_domain_name(), &path) + })?; + + let mut tx = user_db.begin(&authed).await?; + let trigger = handler + .get_trigger_by_path(&mut *tx, &workspace_id, path) + .await?; + + tx.commit().await?; + + Ok(Json(trigger)) +} + +async fn update_trigger( + Extension(handler): Extension>, + authed: ApiAuthed, + Extension(db): Extension, + Extension(user_db): Extension, + Path((workspace_id, path)): Path<(String, StripPath)>, + Json(edit_trigger): Json>, +) -> Result { + let path = path.to_path(); + check_scopes(&authed, || { + format!( + "{}:write:{}", + T::scope_domain_name(), + &edit_trigger.base.path + ) + })?; + + handler + .validate_edit(&db, &workspace_id, &edit_trigger.config, path) + .await?; + + let mut tx = user_db.begin(&authed).await?; + + let new_path = edit_trigger.base.path.to_string(); + handler + .update_trigger(&db, &mut *tx, &authed, &workspace_id, path, edit_trigger) + .await?; + + audit_log( + &mut *tx, + &authed, + &format!("{}_triggers.update", T::TRIGGER_TYPE), + ActionKind::Update, + &workspace_id, + Some(&new_path), + None, + ) + .await?; + + handle_deployment_metadata( + &authed.email, + &authed.username, + &db, + &workspace_id, + T::get_deployed_object(new_path.clone()), + Some(format!("{} '{}' updated", T::DEPLOYMENT_NAME, new_path)), + true, + ) + .await?; + + tx.commit().await?; + + Ok(format!("Trigger '{}' updated", path)) +} + +async fn delete_trigger( + Extension(handler): Extension>, + authed: ApiAuthed, + Extension(user_db): Extension, + Path((workspace_id, path)): Path<(String, StripPath)>, +) -> Result { + let path = path.to_path(); + check_scopes(&authed, || { + format!("{}:write:{}", T::scope_domain_name(), &path) + })?; + + let mut tx = user_db.begin(&authed).await?; + let deleted = handler + .delete_by_path(&mut *tx, &workspace_id, path) + .await?; + + if !deleted { + return Err(Error::NotFound(format!( + "Trigger not found at path: {}", + path + ))); + } + + audit_log( + &mut *tx, + &authed, + &format!("{}_triggers.delete", T::TRIGGER_TYPE), + ActionKind::Delete, + &workspace_id, + Some(&path), + None, + ) + .await?; + + tx.commit().await?; + + Ok(format!("Trigger '{}' deleted", path)) +} + +async fn exists_trigger( + Extension(handler): Extension>, + authed: ApiAuthed, + Extension(db): Extension, + Path((workspace_id, path)): Path<(String, StripPath)>, +) -> JsonResult { + let path = path.to_path(); + check_scopes(&authed, || { + format!("{}:read:{}", T::scope_domain_name(), path) + })?; + let exists = handler.exists(&db, &workspace_id, path).await?; + + Ok(Json(exists)) +} + +#[derive(serde::Deserialize)] +struct SetEnabledPayload { + enabled: bool, +} + +async fn set_enabled_trigger( + Extension(handler): Extension>, + authed: ApiAuthed, + Extension(user_db): Extension, + Extension(db): Extension, + Path((workspace_id, path)): Path<(String, StripPath)>, + Json(payload): Json, +) -> Result { + let path = path.to_path(); + check_scopes(&authed, || format!("{}:write", T::scope_domain_name()))?; + + let mut tx = user_db.begin(&authed).await?; + let updated = handler + .set_enabled(&authed, &mut *tx, &workspace_id, path, payload.enabled) + .await?; + + if !updated { + return Err(Error::NotFound(format!( + "Trigger not found at path: {}", + path + ))); + } + + tx.commit().await?; + + handle_deployment_metadata( + &authed.email, + &authed.username, + &db, + &workspace_id, + T::get_deployed_object(path.to_owned()), + Some(format!("{} trigger '{}' updated", T::DEPLOYMENT_NAME, path)), + true, + ) + .await?; + + Ok(format!( + "Trigger '{}' {}", + path, + if payload.enabled { + "enabled" + } else { + "disabled" + } + )) +} + +async fn test_connection( + Extension(handler): Extension>, + authed: ApiAuthed, + Extension(db): Extension, + Extension(user_db): Extension, + Path(workspace_id): Path, + Json(config): Json, +) -> Result<()> { + let connect_f = async move { + handler + .test_connection(&db, &authed, &user_db, &workspace_id, config) + .await + }; + + tokio::time::timeout(tokio::time::Duration::from_secs(30), connect_f) + .await + .map_err(|_| { + Error::BadConfig(format!("Timeout connecting to service after 30 seconds")) + })??; + Ok(()) +} + +#[allow(unused)] +pub fn complete_trigger_routes(handler: T) -> Router { + let standard_routes = trigger_routes::(); + + let additional_routes = handler.additional_routes(); + + standard_routes + .merge(additional_routes) + .layer(Extension(Arc::new(handler))) +} + +pub fn generate_trigger_routers() -> Router { + #[allow(unused_mut)] + let mut router = Router::new(); + + #[cfg(feature = "http_trigger")] + { + use crate::triggers::http::handler::HttpTrigger; + + router = router.nest( + HttpTrigger::ROUTE_PREFIX, + complete_trigger_routes(HttpTrigger), + ); + } + + #[cfg(feature = "websocket")] + { + use crate::triggers::websocket::WebsocketTrigger; + + router = router.nest( + WebsocketTrigger::ROUTE_PREFIX, + complete_trigger_routes(WebsocketTrigger), + ); + } + + #[cfg(all(feature = "enterprise", feature = "kafka", feature = "private"))] + { + use crate::triggers::kafka::KafkaTrigger; + + router = router.nest( + KafkaTrigger::ROUTE_PREFIX, + complete_trigger_routes(KafkaTrigger), + ); + } + + #[cfg(all(feature = "enterprise", feature = "nats", feature = "private"))] + { + use crate::triggers::nats::NatsTrigger; + + router = router.nest( + NatsTrigger::ROUTE_PREFIX, + complete_trigger_routes(NatsTrigger), + ); + } + + #[cfg(feature = "mqtt_trigger")] + { + use crate::triggers::mqtt::MqttTrigger; + + router = router.nest( + MqttTrigger::ROUTE_PREFIX, + complete_trigger_routes(MqttTrigger), + ); + } + + #[cfg(all(feature = "enterprise", feature = "sqs_trigger", feature = "private"))] + { + use crate::triggers::sqs::SqsTrigger; + + router = router.nest( + SqsTrigger::ROUTE_PREFIX, + complete_trigger_routes(SqsTrigger), + ); + } + + #[cfg(all(feature = "enterprise", feature = "gcp_trigger", feature = "private"))] + { + use crate::triggers::gcp::GcpTrigger; + + router = router.nest( + GcpTrigger::ROUTE_PREFIX, + complete_trigger_routes(GcpTrigger), + ); + } + + #[cfg(feature = "postgres_trigger")] + { + use crate::triggers::postgres::PostgresTrigger; + + router = router.nest( + PostgresTrigger::ROUTE_PREFIX, + complete_trigger_routes(PostgresTrigger), + ); + } + + router +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct TriggerPrimarySchedule { + schedule: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct TriggersCount { + primary_schedule: Option, + schedule_count: i64, + http_routes_count: i64, + webhook_count: i64, + email_count: i64, + websocket_count: i64, + kafka_count: i64, + nats_count: i64, + postgres_count: i64, + mqtt_count: i64, + sqs_count: i64, + gcp_count: i64, +} + +pub async fn get_triggers_count_internal( + db: &DB, + w_id: &str, + path: &str, + is_flow: bool, +) -> JsonResult { + let primary_schedule = sqlx::query_scalar!( + "SELECT schedule FROM schedule WHERE path = $1 AND script_path = $1 AND is_flow = $2 AND workspace_id = $3", + path, + is_flow, + w_id + ) + .fetch_optional(db) + .await?; + + let schedule_count = sqlx::query_scalar!( + "SELECT COUNT(*) FROM schedule WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", + path, + is_flow, + w_id + ) + .fetch_one(db) + .await? + .unwrap_or(0); + + #[allow(unused)] + let mut tx = db.begin().await?; + + #[cfg(feature = "http_trigger")] + let http_routes_count = { + use crate::triggers::http::handler::HttpTrigger; + let count = HttpTrigger + .trigger_count(&mut tx, w_id, is_flow, path) + .await; + count + }; + #[cfg(not(feature = "http_trigger"))] + let http_routes_count = 0; + + #[cfg(feature = "websocket")] + let websocket_count = { + use crate::triggers::websocket::WebsocketTrigger; + let count = WebsocketTrigger + .trigger_count(&mut tx, w_id, is_flow, path) + .await; + count + }; + #[cfg(not(feature = "websocket"))] + let websocket_count = 0; + + #[cfg(all(feature = "kafka", feature = "enterprise", feature = "private"))] + let kafka_count = { + use crate::triggers::kafka::KafkaTrigger; + let count = KafkaTrigger + .trigger_count(&mut tx, w_id, is_flow, path) + .await; + count + }; + #[cfg(not(all(feature = "kafka", feature = "enterprise", feature = "private")))] + let kafka_count = 0; + + #[cfg(all(feature = "nats", feature = "enterprise", feature = "private"))] + let nats_count = { + use crate::triggers::nats::NatsTrigger; + let count = NatsTrigger + .trigger_count(&mut tx, w_id, is_flow, path) + .await; + count + }; + #[cfg(not(all(feature = "nats", feature = "enterprise", feature = "private")))] + let nats_count = 0; + + #[cfg(feature = "postgres_trigger")] + let postgres_count = { + use crate::triggers::postgres::PostgresTrigger; + let count = PostgresTrigger + .trigger_count(&mut tx, w_id, is_flow, path) + .await; + count + }; + #[cfg(not(feature = "postgres_trigger"))] + let postgres_count = 0; + + #[cfg(feature = "mqtt_trigger")] + let mqtt_count = { + use crate::triggers::mqtt::MqttTrigger; + let count = MqttTrigger + .trigger_count(&mut tx, w_id, is_flow, path) + .await; + count + }; + #[cfg(not(feature = "mqtt_trigger"))] + let mqtt_count = 0; + + #[cfg(all(feature = "sqs_trigger", feature = "enterprise", feature = "private"))] + let sqs_count = { + use crate::triggers::sqs::SqsTrigger; + let count = SqsTrigger.trigger_count(&mut tx, w_id, is_flow, path).await; + count + }; + #[cfg(not(all(feature = "sqs_trigger", feature = "enterprise", feature = "private")))] + let sqs_count = 0; + + #[cfg(all(feature = "gcp_trigger", feature = "enterprise", feature = "private"))] + let gcp_count = { + use crate::triggers::gcp::GcpTrigger; + let count = GcpTrigger.trigger_count(&mut tx, w_id, is_flow, path).await; + count + }; + #[cfg(not(all(feature = "gcp_trigger", feature = "enterprise", feature = "private")))] + let gcp_count = 0; + tx.commit().await?; + + let webhook_count = (if is_flow { + sqlx::query_scalar!( + "SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]", + w_id, + path, + ) + } else { + sqlx::query_scalar!( + "SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:' || $2]::text[]", + w_id, + path, + ) + }).fetch_one(db) + .await? + .unwrap_or(0); + + let email_count = (if is_flow { + sqlx::query_scalar!( + "SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]", + w_id, + path, + ) + } else { + sqlx::query_scalar!( + "SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]", + w_id, + path, + ) + }).fetch_one(db) + .await? + .unwrap_or(0); + + Ok(Json(TriggersCount { + primary_schedule: primary_schedule.map(|s| TriggerPrimarySchedule { schedule: s }), + schedule_count, + http_routes_count, + webhook_count, + email_count, + websocket_count, + kafka_count, + nats_count, + postgres_count, + mqtt_count, + gcp_count, + sqs_count, + })) +} diff --git a/backend/windmill-api/src/triggers/http/handler.rs b/backend/windmill-api/src/triggers/http/handler.rs new file mode 100644 index 0000000000..78ea4029c5 --- /dev/null +++ b/backend/windmill-api/src/triggers/http/handler.rs @@ -0,0 +1,1015 @@ +use super::{ + http_trigger_args::RawHttpTriggerArgs, AuthenticationMethod, HttpMethod, TriggerRoute, + HTTP_ACCESS_CACHE, HTTP_AUTH_CACHE, HTTP_ROUTERS_CACHE, +}; +use crate::{ + auth::{AuthCache, OptTokened}, + db::{ApiAuthed, DB}, + resources::try_get_resource_from_db_as, + triggers::{ + http::{ + refresh_routers, validate_authentication_method, HttpConfig, HttpConfigRequest, + RouteExists, ROUTE_PATH_KEY_RE, VALID_ROUTE_PATH_RE, + }, + trigger_helpers::{ + get_runnable_format, trigger_runnable, trigger_runnable_and_wait_for_result, RunnableId, + }, + Trigger, TriggerCrud, TriggerData, + }, + users::fetch_api_authed, + utils::ExpiringCacheEntry, +}; +use axum::{ + async_trait, + extract::Path, + response::{IntoResponse, Response}, + routing::{get, post}, + Extension, Json, Router, +}; +use http::{HeaderMap, StatusCode}; +use sqlx::PgConnection; +use std::{ + borrow::Cow, + collections::{HashMap, HashSet}, + sync::Arc, +}; +use tower_http::cors::CorsLayer; +use windmill_audit::{audit_oss::audit_log, ActionKind}; +use windmill_common::{ + db::UserDB, + error::{Error, Result}, + triggers::TriggerKind, + utils::{not_found_if_none, require_admin, StripPath}, + worker::CLOUD_HOSTED, +}; +use windmill_git_sync::handle_deployment_metadata; + +#[cfg(feature = "parquet")] +use { + crate::job_helpers_oss::get_workspace_s3_resource, + windmill_common::s3_helpers::build_object_store_client, +}; + +use windmill_git_sync::DeployedObject; + +pub async fn increase_trigger_version(tx: &mut PgConnection) -> Result<()> { + sqlx::query!("SELECT nextval('http_trigger_version_seq')") + .fetch_one(tx) + .await?; + Ok(()) +} + +pub fn generate_route_path_key(route_path: &str) -> String { + ROUTE_PATH_KEY_RE.replace_all(route_path, "/*").to_string() +} + +pub async fn route_path_key_exists( + route_path_key: &str, + http_method: &HttpMethod, + w_id: &str, + trigger_path: Option<&str>, + workspaced_route: Option, + db: &DB, +) -> Result { + let exists = if *CLOUD_HOSTED { + sqlx::query_scalar!( + r#" + SELECT EXISTS( + SELECT 1 + FROM http_trigger + WHERE + route_path_key = $1 + AND workspace_id = $2 + AND http_method = $3 + AND ($4::TEXT IS NULL OR path != $4) + ) + "#, + &route_path_key, + w_id, + http_method as &HttpMethod, + trigger_path + ) + .fetch_one(db) + .await? + .unwrap_or(false) + } else { + let route_path_key = match workspaced_route { + Some(true) => Cow::Owned(format!("{}/{}", w_id, route_path_key.trim_matches('/'))), + _ => Cow::Borrowed(route_path_key), + }; + + sqlx::query_scalar!( + r#" + SELECT EXISTS( + SELECT 1 + FROM http_trigger + WHERE + ((workspaced_route IS TRUE AND workspace_id || '/' || route_path_key = $1) + OR (workspaced_route IS FALSE AND route_path_key = $1)) + AND http_method = $2 + AND ($3::TEXT IS NULL OR path != $3) + ) + "#, + &route_path_key, + http_method as &HttpMethod, + trigger_path + ) + .fetch_one(db) + .await? + .unwrap_or(false) + }; + + Ok(exists) +} + +pub async fn exists_route( + Extension(db): Extension, + Path(w_id): Path, + Json(RouteExists { route_path, http_method, trigger_path, workspaced_route }): Json< + RouteExists, + >, +) -> Result> { + let route_path_key = generate_route_path_key(&route_path); + + let exists = route_path_key_exists( + &route_path_key, + &http_method, + &w_id, + trigger_path.as_deref(), + workspaced_route, + &db, + ) + .await?; + + Ok(Json(exists)) +} + +fn check_no_duplicates<'trigger>( + new_http_triggers: &[TriggerData], + route_path_key: &[String], +) -> Result<()> { + let mut seen = HashSet::with_capacity(new_http_triggers.len()); + + for (i, trigger) in new_http_triggers.iter().enumerate() { + if !seen.insert(( + &route_path_key[i], + trigger.config.http_method, + trigger.config.workspaced_route, + )) { + return Err(Error::BadRequest(format!( + "Duplicate HTTP route detected: '{}'. Each HTTP route must have a unique 'route_path'.", + &trigger.config.route_path + ))); + } + } + + Ok(()) +} + +pub async fn insert_new_trigger_into_db( + authed: &ApiAuthed, + tx: &mut PgConnection, + w_id: &str, + trigger: &TriggerData, + route_path_key: &str, +) -> Result<()> { + require_admin(authed.is_admin, &authed.username)?; + + sqlx::query!( + r#" + INSERT INTO http_trigger ( + workspace_id, + path, + route_path, + route_path_key, + workspaced_route, + authentication_resource_path, + wrap_body, + raw_string, + script_path, + summary, + description, + is_flow, + is_async, + authentication_method, + http_method, + static_asset_config, + edited_by, + email, + edited_at, + is_static_website, + error_handler_path, + error_handler_args, + retry + ) + VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), $19, $20, $21, $22 + ) + "#, + w_id, + trigger.base.path, + trigger.config.route_path, + route_path_key, + trigger.config.workspaced_route.unwrap_or(false), + trigger.config.authentication_resource_path, + trigger.config.wrap_body.unwrap_or(false), + trigger.config.raw_string.unwrap_or(false), + trigger.base.script_path, + trigger.config.summary, + trigger.config.description, + trigger.base.is_flow, + trigger.config.is_async, + trigger.config.authentication_method as _, + trigger.config.http_method as _, + trigger.config.static_asset_config as _, + &authed.username, + &authed.email, + trigger.config.is_static_website, + trigger.error_handling.error_handler_path, + trigger.error_handling.error_handler_args as _, + trigger.error_handling.retry as _ + ) + .execute(&mut *tx) + .await?; + Ok(()) +} + +pub async fn create_many_http_triggers( + authed: ApiAuthed, + Extension(db): Extension, + Extension(user_db): Extension, + Path(w_id): Path, + Json(new_http_triggers): Json>>, +) -> Result<(StatusCode, String)> { + require_admin(authed.is_admin, &authed.username)?; + + let handler = HttpTrigger; + + let error_wrapper = |route_path: &str, error: Error| -> Error { + anyhow::anyhow!( + "Error occurred for HTTP route at route path: {}, error: {}", + route_path, + error + ) + .into() + }; + + let mut route_path_keys = Vec::with_capacity(new_http_triggers.len()); + + for new_http_trigger in new_http_triggers.iter() { + handler + .validate_new(&db, &w_id, &new_http_trigger.config) + .await + .map_err(|err| error_wrapper(&new_http_trigger.config.route_path, err))?; + + let route_path_key = + check_if_route_exist(&db, &new_http_trigger.config, &w_id, None).await?; + + route_path_keys.push(route_path_key.clone()); + } + + check_no_duplicates(&new_http_triggers, &route_path_keys)?; + + let mut tx = user_db.begin(&authed).await?; + + for (new_http_trigger, route_path_key) in new_http_triggers.iter().zip(route_path_keys.iter()) { + insert_new_trigger_into_db(&authed, &mut tx, &w_id, new_http_trigger, route_path_key) + .await + .map_err(|err| error_wrapper(&new_http_trigger.config.route_path, err))?; + + audit_log( + &mut *tx, + &authed, + "http_trigger.create", + ActionKind::Create, + &w_id, + Some(&new_http_trigger.base.path), + None, + ) + .await + .map_err(|err| error_wrapper(&new_http_trigger.config.route_path, err.into()))?; + + increase_trigger_version(&mut tx) + .await + .map_err(|err| error_wrapper(&new_http_trigger.config.route_path, err.into()))?; + } + + tx.commit().await?; + + for http_trigger in new_http_triggers.into_iter() { + handle_deployment_metadata( + &authed.email, + &authed.username, + &db, + &w_id, + windmill_git_sync::DeployedObject::HttpTrigger { path: http_trigger.base.path.clone() }, + Some(format!("HTTP trigger '{}' created", http_trigger.base.path)), + true, + ) + .await + .map_err(|err| error_wrapper(&http_trigger.config.route_path, err.into()))?; + } + Ok((StatusCode::CREATED, "Created all HTTP routes".to_string())) +} + +async fn check_if_route_exist( + db: &DB, + config: &HttpConfigRequest, + workspace_id: &str, + trigger_path: Option<&str>, +) -> Result { + let route_path_key = ROUTE_PATH_KEY_RE.replace_all(&config.route_path, ":key"); + + let exists = route_path_key_exists( + &route_path_key, + &config.http_method, + workspace_id, + trigger_path, + config.workspaced_route, + db, + ) + .await?; + + if exists { + return Err(Error::BadRequest( + "A route already exists with this path".to_string(), + )); + } + + Ok(route_path_key.into_owned()) +} + +pub struct HttpTrigger; + +#[async_trait] +impl TriggerCrud for HttpTrigger { + type TriggerConfig = HttpConfig; + type Trigger = Trigger; + type TriggerConfigRequest = HttpConfigRequest; + type TestConnectionConfig = (); + + const TABLE_NAME: &'static str = "http_trigger"; + const TRIGGER_TYPE: &'static str = "http"; + const SUPPORTS_ENABLED: bool = false; + const SUPPORTS_SERVER_STATE: bool = false; + const SUPPORTS_TEST_CONNECTION: bool = false; + const ROUTE_PREFIX: &'static str = "/http_triggers"; + const DEPLOYMENT_NAME: &'static str = "HTTP trigger"; + const IS_ALLOWED_ON_CLOUD: bool = true; + const ADDITIONAL_SELECT_FIELDS: &[&'static str] = &[ + "route_path", + "route_path_key", + "is_async", + "authentication_method", + "http_method", + "summary", + "description", + "static_asset_config", + "is_static_website", + "authentication_resource_path", + "workspaced_route", + "wrap_body", + "raw_string", + ]; + + fn get_deployed_object(path: String) -> DeployedObject { + DeployedObject::HttpTrigger { path } + } + + fn additional_routes(&self) -> Router { + Router::new() + .route("/create_many", post(create_many_http_triggers)) + .route("/route_exists", post(exists_route)) + } + + async fn validate_new( + &self, + _db: &DB, + _workspace_id: &str, + new: &Self::TriggerConfigRequest, + ) -> Result<()> { + if *CLOUD_HOSTED && (new.is_static_website || new.static_asset_config.is_some()) { + return Err(Error::BadRequest( + "Static website and static asset are not supported on cloud".to_string(), + )); + } + + if !VALID_ROUTE_PATH_RE.is_match(&new.route_path) { + return Err(Error::BadRequest("Invalid route path".to_string())); + } + + validate_authentication_method(new.authentication_method, new.raw_string)?; + + Ok(()) + } + + async fn validate_edit( + &self, + _db: &DB, + _workspace_id: &str, + edit: &Self::TriggerConfigRequest, + _path: &str, + ) -> Result<()> { + if *CLOUD_HOSTED && (edit.is_static_website || edit.static_asset_config.is_some()) { + return Err(Error::BadRequest( + "Static website and static asset are not supported on cloud".to_string(), + )); + } + + validate_authentication_method(edit.authentication_method, edit.raw_string)?; + + Ok(()) + } + + async fn create_trigger( + &self, + db: &DB, + tx: &mut PgConnection, + authed: &ApiAuthed, + w_id: &str, + trigger: TriggerData, + ) -> Result<()> { + let route_path_key = check_if_route_exist(db, &trigger.config, &w_id, None).await?; + + insert_new_trigger_into_db(authed, tx, w_id, &trigger, &route_path_key).await?; + + increase_trigger_version(tx).await?; + + Ok(()) + } + + async fn update_trigger( + &self, + db: &DB, + tx: &mut PgConnection, + authed: &ApiAuthed, + workspace_id: &str, + path: &str, + trigger: TriggerData, + ) -> Result<()> { + if authed.is_admin { + if trigger.config.route_path.is_empty() { + return Err(Error::BadRequest("route_path is required".to_string())); + }; + + let route_path = &trigger.config.route_path; + if !VALID_ROUTE_PATH_RE.is_match(route_path) { + return Err(Error::BadRequest("Invalid route path".to_string())); + } + + let route_path_key = + check_if_route_exist(db, &trigger.config, workspace_id, Some(path)) + .await?; + + sqlx::query!( + r#" + UPDATE + http_trigger + SET + route_path = $1, + route_path_key = $2, + workspaced_route = $3, + wrap_body = $4, + raw_string = $5, + authentication_resource_path = $6, + script_path = $7, + path = $8, + is_flow = $9, + http_method = $10, + static_asset_config = $11, + edited_by = $12, + email = $13, + is_async = $14, + authentication_method = $15, + summary = $16, + description = $17, + edited_at = now(), + is_static_website = $18, + error_handler_path = $19, + error_handler_args = $20, + retry = $21 + WHERE + workspace_id = $22 AND + path = $23 + "#, + route_path, + &route_path_key, + trigger.config.workspaced_route, + trigger.config.wrap_body, + trigger.config.raw_string, + trigger.config.authentication_resource_path, + trigger.base.script_path, + trigger.base.path, + trigger.base.is_flow, + trigger.config.http_method as _, + trigger.config.static_asset_config as _, + &authed.username, + &authed.email, + trigger.config.is_async, + trigger.config.authentication_method as _, + trigger.config.summary, + trigger.config.description, + trigger.config.is_static_website, + trigger.error_handling.error_handler_path, + trigger.error_handling.error_handler_args as _, + trigger.error_handling.retry as _, + workspace_id, + path, + ) + .execute(&mut *tx) + .await?; + } else { + sqlx::query!( + r#" + UPDATE + http_trigger + SET + workspaced_route = $1, + wrap_body = $2, + raw_string = $3, + authentication_resource_path = $4, + script_path = $5, + path = $6, + is_flow = $7, + http_method = $8, + static_asset_config = $9, + edited_by = $10, + email = $11, + is_async = $12, + authentication_method = $13, + summary = $14, + description = $15, + edited_at = now(), + is_static_website = $16, + error_handler_path = $17, + error_handler_args = $18, + retry = $19 + WHERE + workspace_id = $20 AND + path = $21 + "#, + trigger.config.workspaced_route, + trigger.config.wrap_body, + trigger.config.raw_string, + trigger.config.authentication_resource_path, + trigger.base.script_path, + trigger.base.path, + trigger.base.is_flow, + trigger.config.http_method as _, + trigger.config.static_asset_config as _, + &authed.username, + &authed.email, + trigger.config.is_async, + trigger.config.authentication_method as _, + trigger.config.summary, + trigger.config.description, + trigger.config.is_static_website, + trigger.error_handling.error_handler_path, + trigger.error_handling.error_handler_args as _, + trigger.error_handling.retry as _, + workspace_id, + path, + ) + .execute(&mut *tx) + .await?; + } + + increase_trigger_version(tx).await?; + + Ok(()) + } + + async fn delete_by_path( + &self, + tx: &mut PgConnection, + workspace_id: &str, + path: &str, + ) -> Result { + let deleted = sqlx::query(&format!( + "DELETE FROM {} WHERE workspace_id = $1 AND path = $2", + Self::TABLE_NAME + )) + .bind(workspace_id) + .bind(path) + .execute(&mut *tx) + .await? + .rows_affected(); + + increase_trigger_version(tx).await?; + + Ok(deleted > 0) + } +} + +pub fn http_route_trigger_handler() -> Router { + let cors = CorsLayer::new() + .allow_methods([ + http::Method::GET, + http::Method::POST, + http::Method::DELETE, + http::Method::PUT, + http::Method::PATCH, + ]) + .allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION]) + .allow_origin(tower_http::cors::Any); + Router::new() + .route( + "/*path", + get(route_job) + .post(route_job) + .delete(route_job) + .put(route_job) + .patch(route_job) + .head(|| async { "" }), + ) + .layer(cors) +} + +async fn get_http_route_trigger( + route_path: &str, + auth_cache: &Arc, + token: Option<&String>, + db: &DB, + user_db: UserDB, + method: &http::Method, +) -> Result<(TriggerRoute, String, HashMap, ApiAuthed)> { + let http_method: HttpMethod = method.try_into()?; + + let requested_path = format!("/{}", route_path); + + let routers_cache = HTTP_ROUTERS_CACHE.read().await; + + let routers_cache = if routers_cache.routers.is_empty() { + tracing::warn!("HTTP routers are not loaded, loading from db"); + let (_, routers_cache) = refresh_routers(db).await?; + routers_cache + } else { + routers_cache + }; + + let router = routers_cache + .routers + .get(&http_method) + .ok_or(Error::internal_err( + "HTTP routers could not be loaded".to_string(), + ))?; + + let trigger_match = router.at(requested_path.as_str()).ok(); + + let matchit::Match { value: trigger, params } = + not_found_if_none(trigger_match, "Trigger", requested_path.as_str())?; + + let params: HashMap = params + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + + let username_override = if let AuthenticationMethod::Windmill = trigger.authentication_method { + let opt_authed = if let Some(token) = token { + auth_cache + .get_authed(Some(trigger.workspace_id.clone()), token) + .await + } else { + None + }; + if let Some(authed) = opt_authed { + // check that the user has access to the trigger + let cache_key = ( + trigger.workspace_id.clone(), + trigger.path.clone(), + authed.clone(), + ); + let exists = match HTTP_ACCESS_CACHE.get(&cache_key) { + Some(cache_entry) if cache_entry.expiry > std::time::Instant::now() => { + tracing::debug!("HTTP access cache hit for route {}", trigger.path); + true + } + _ => { + tracing::debug!("HTTP access cache miss for route {}", trigger.path); + let mut tx = user_db.begin(&authed).await?; + let exists = sqlx::query_scalar!( + r#" + SELECT EXISTS( + SELECT 1 + FROM + http_trigger + WHERE + workspace_id = $1 AND + path = $2 + ) + "#, + trigger.workspace_id, + trigger.path + ) + .fetch_one(&mut *tx) + .await? + .unwrap_or(false); + if exists { + HTTP_ACCESS_CACHE.insert( + cache_key, + ExpiringCacheEntry { + value: (), + expiry: std::time::Instant::now() + + std::time::Duration::from_secs(10), + }, + ); + } + exists + } + }; + if exists { + Some(authed.display_username().to_owned()) + } else { + return Err(Error::NotAuthorized("Unauthorized".to_string())); + } + } else { + return Err(Error::NotAuthorized("Requires authentication".to_string())); + } + } else { + None + }; + + let authed = fetch_api_authed( + trigger.edited_by.clone(), + trigger.email.clone(), + &trigger.workspace_id, + &db, + Some(username_override.unwrap_or(format!("HTTP-{}", trigger.path))), + ) + .await?; + + Ok((trigger.clone(), route_path.to_string(), params, authed)) +} + +async fn route_job( + Extension(db): Extension, + Extension(user_db): Extension, + Extension(auth_cache): Extension>, + OptTokened { token }: OptTokened, + Path(route_path): Path, + headers: HeaderMap, + args: RawHttpTriggerArgs, +) -> std::result::Result { + let route_path = route_path.to_path().trim_end_matches("/"); + let (trigger, called_path, params, authed) = get_http_route_trigger( + route_path, + &auth_cache, + token.as_ref(), + &db, + user_db.clone(), + &args.0.metadata.method, + ) + .await + .map_err(|e| e.into_response())?; + + if trigger.script_path.is_empty() && trigger.static_asset_config.is_none() { + return Err(Error::NotFound(format!( + "Runnable path of HTTP route at path: {}", + trigger.path + )) + .into_response()); + } + + let args = args + .process_args( + &authed, + &db, + &trigger.workspace_id, + match trigger.authentication_method { + AuthenticationMethod::CustomScript | AuthenticationMethod::Signature => true, + _ => trigger.raw_string, + }, + ) + .await + .map_err(|e| e.into_response())?; + + match trigger.authentication_method { + AuthenticationMethod::None + | AuthenticationMethod::Windmill + | AuthenticationMethod::CustomScript => {} + _ => { + let resource_path = match trigger.authentication_resource_path { + Some(resource_path) => resource_path, + None => { + return Err(Error::BadRequest( + "Missing authentication resource path".to_string(), + ) + .into_response()) + } + }; + + let cache_key = ( + trigger.workspace_id.clone(), + resource_path.clone(), + authed.clone(), + ); + + let authentication_method = match HTTP_AUTH_CACHE.get(&cache_key) { + Some(cache_entry) if cache_entry.expiry > std::time::Instant::now() => { + tracing::debug!("HTTP auth method cache hit for route {}", trigger.path); + cache_entry.value + } + _ => { + tracing::debug!("HTTP auth method cache miss for route {}", trigger.path); + let auth_method = try_get_resource_from_db_as::< + super::http_trigger_auth::AuthenticationMethod, + >( + &authed, + Some(user_db.clone()), + &db, + &resource_path, + &trigger.workspace_id, + ) + .await + .map_err(|e| e.into_response())?; + HTTP_AUTH_CACHE.insert( + cache_key, + ExpiringCacheEntry { + value: auth_method.clone(), + expiry: std::time::Instant::now() + std::time::Duration::from_secs(60), + }, + ); + auth_method + } + }; + + let raw_payload = args.0.metadata.raw_string.as_ref(); + + let response = authentication_method + .authenticate_http_request(&headers, raw_payload) + .map_err(|e| e.into_response())?; + + if let Some(response) = response { + return Ok(response); + } + } + } + + #[cfg(not(feature = "parquet"))] + if trigger.static_asset_config.is_some() { + return Err(Error::internal_err( + "Static asset configuration is not supported in this build".to_string(), + ) + .into_response()); + } + + #[cfg(feature = "parquet")] + if let Some(sqlx::types::Json(config)) = trigger.static_asset_config { + let build_static_response_f = async { + let (_, s3_resource_opt) = get_workspace_s3_resource( + &authed, + &db, + None, + &"NO_TOKEN".to_string(), // no token is provided in this case + &trigger.workspace_id, + config.storage, + ) + .await?; + let s3_resource = s3_resource_opt.ok_or(Error::internal_err( + "No files storage resource defined at the workspace level".to_string(), + ))?; + let s3_client = build_object_store_client(&s3_resource).await?; + + let path = if trigger.is_static_website { + let subpath = params + .get("wm_subpath") + .cloned() + .unwrap_or("index.html".to_string()); + tracing::info!("subpath: {}", subpath); + format!("{}/{}", config.s3.trim_end_matches('/'), subpath) + } else { + config.s3.clone() + }; + let path = object_store::path::Path::from(path); + let s3_object = s3_client.get(&path).await; + + let s3_object = match s3_object { + Err(object_store::Error::NotFound { .. }) if trigger.is_static_website => { + // fallback to index.html if the file is not found + let path = object_store::path::Path::from(format!( + "{}/index.html", + config.s3.trim_end_matches('/') + )); + s3_client.get(&path).await + } + r => r, + }; + + let s3_object = s3_object.map_err(|err| { + tracing::warn!("Error retrieving file from S3: {:?}", err); + Error::internal_err(format!("Error retrieving file: {}", err.to_string())) + })?; + + let mut response_headers = http::HeaderMap::new(); + if let Some(ref e_tag) = s3_object.meta.e_tag { + if let Some(if_none_match) = headers.get(http::header::IF_NONE_MATCH) { + if if_none_match == e_tag { + return Ok::<_, Error>(( + StatusCode::NOT_MODIFIED, + response_headers, + axum::body::Body::empty(), + )); + } + } + if let Ok(e_tag) = e_tag.parse() { + response_headers.insert("etag", e_tag); + } + } + response_headers.insert( + "content-type", + s3_object + .attributes + .get(&object_store::Attribute::ContentType) + .map(|s| s.parse().ok()) + .flatten() + .unwrap_or("application/octet-stream".parse().unwrap()), + ); + if !trigger.is_static_website { + response_headers.insert( + "content-disposition", + config.filename.as_ref().map_or_else( + || { + s3_object + .attributes + .get(&object_store::Attribute::ContentDisposition) + .map(|s| s.parse().ok()) + .flatten() + .unwrap_or("inline".parse().unwrap()) + }, + |filename| { + format!("inline; filename=\"{}\"", filename) + .parse() + .unwrap_or("inline".parse().unwrap()) + }, + ), + ); + } + + let body_stream = axum::body::Body::from_stream(s3_object.into_stream()); + Ok::<_, Error>((StatusCode::OK, response_headers, body_stream)) + }; + match build_static_response_f.await { + Ok((status, headers, body_stream)) => { + return Ok((status, headers, body_stream).into_response()) + } + Err(e) => return Err(e.into_response()), + } + } + + let runnable_format = get_runnable_format( + if trigger.is_flow { + RunnableId::from_flow_path(&trigger.script_path) + } else { + RunnableId::from_script_path(&trigger.script_path) + }, + &trigger.workspace_id, + &db, + &TriggerKind::Http, + ) + .await + .map_err(|e| e.into_response())?; + + let args = args + .to_args_from_format( + &trigger.route_path, + &called_path, + ¶ms, + runnable_format, + trigger.wrap_body, + ) + .map_err(|e| e.into_response())?; + + if trigger.is_async { + trigger_runnable( + &db, + Some(user_db), + authed, + &trigger.workspace_id, + &trigger.script_path, + trigger.is_flow, + args, + trigger.retry.as_ref(), + trigger.error_handler_path.as_deref(), + trigger.error_handler_args.as_ref(), + format!("http_trigger/{}", trigger.path), + ) + .await + .map_err(|e| e.into_response()) + } else { + trigger_runnable_and_wait_for_result( + &db, + Some(user_db), + authed, + &trigger.workspace_id, + &trigger.script_path, + trigger.is_flow, + args, + trigger.retry.as_ref(), + trigger.error_handler_path.as_deref(), + trigger.error_handler_args.as_ref(), + format!("http_trigger/{}", trigger.path), + ) + .await + .map_err(|e| e.into_response()) + } +} diff --git a/backend/windmill-api/src/http_trigger_args.rs b/backend/windmill-api/src/triggers/http/http_trigger_args.rs similarity index 100% rename from backend/windmill-api/src/http_trigger_args.rs rename to backend/windmill-api/src/triggers/http/http_trigger_args.rs diff --git a/backend/windmill-api/src/http_trigger_auth.rs b/backend/windmill-api/src/triggers/http/http_trigger_auth.rs similarity index 100% rename from backend/windmill-api/src/http_trigger_auth.rs rename to backend/windmill-api/src/triggers/http/http_trigger_auth.rs diff --git a/backend/windmill-api/src/triggers/http/mod.rs b/backend/windmill-api/src/triggers/http/mod.rs new file mode 100644 index 0000000000..2b29f0e644 --- /dev/null +++ b/backend/windmill-api/src/triggers/http/mod.rs @@ -0,0 +1,279 @@ +use std::collections::HashMap; + +use quick_cache::sync::Cache; +use serde::{Deserialize, Serialize}; +use sqlx::{types::Json as SqlxJson, FromRow}; +use tokio::sync::{RwLock, RwLockReadGuard}; +use windmill_common::{ + error::{Error, Result}, + flows::Retry, + s3_helpers::S3Object, + worker::CLOUD_HOSTED, + DB, +}; + +use crate::{db::ApiAuthed, utils::ExpiringCacheEntry}; + +pub mod handler; +pub mod http_trigger_args; +pub mod http_trigger_auth; + +lazy_static::lazy_static! { + static ref HTTP_ACCESS_CACHE: Cache<(String, String, ApiAuthed), ExpiringCacheEntry<()>> = Cache::new(100); + static ref HTTP_AUTH_CACHE: Cache<(String, String, ApiAuthed), ExpiringCacheEntry> = Cache::new(100); + + static ref HTTP_ROUTERS_CACHE: RwLock = RwLock::new(RoutersCache { + routers: HashMap::new(), + version: 0, + }); +} + +#[derive(Debug, Deserialize, Clone)] +pub struct TriggerRoute { + path: String, + script_path: String, + is_flow: bool, + route_path: String, + workspace_id: String, + is_async: bool, + authentication_method: AuthenticationMethod, + edited_by: String, + email: String, + static_asset_config: Option>, + is_static_website: bool, + authentication_resource_path: Option, + workspaced_route: bool, + wrap_body: bool, + raw_string: bool, + error_handler_path: Option, + error_handler_args: Option>>, + retry: Option>, +} + +pub struct RoutersCache { + routers: HashMap>, + version: i64, +} + +#[derive(Serialize, Deserialize, sqlx::Type, Debug, Clone, Copy, Hash, Eq, PartialEq)] +#[sqlx(type_name = "HTTP_METHOD", rename_all = "lowercase")] +#[serde(rename_all = "lowercase")] +pub enum HttpMethod { + Get, + Post, + Put, + Delete, + Patch, +} + +impl TryFrom<&http::Method> for HttpMethod { + type Error = Error; + fn try_from(method: &http::Method) -> Result { + match method { + &http::Method::GET => Ok(HttpMethod::Get), + &http::Method::POST => Ok(HttpMethod::Post), + &http::Method::PUT => Ok(HttpMethod::Put), + &http::Method::DELETE => Ok(HttpMethod::Delete), + &http::Method::PATCH => Ok(HttpMethod::Patch), + _ => Err(Error::BadRequest("Invalid HTTP method".to_string())), + } + } +} + +#[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Clone, Copy)] +#[sqlx(type_name = "AUTHENTICATION_METHOD", rename_all = "snake_case")] +#[serde(rename_all(serialize = "snake_case", deserialize = "snake_case"))] +pub enum AuthenticationMethod { + None, + Windmill, + ApiKey, + BasicHttp, + CustomScript, + Signature, +} + +#[derive(Debug, Clone, FromRow, Serialize, Deserialize)] +pub struct HttpConfig { + pub route_path: String, + pub route_path_key: String, + pub is_async: bool, + pub authentication_method: AuthenticationMethod, + pub http_method: HttpMethod, + pub summary: Option, + pub description: Option, + pub static_asset_config: Option>, + pub is_static_website: bool, + pub authentication_resource_path: Option, + pub workspaced_route: bool, + pub wrap_body: bool, + pub raw_string: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HttpConfigRequest { + #[serde(default)] + pub route_path: String, + pub is_async: bool, + pub authentication_method: AuthenticationMethod, + pub http_method: HttpMethod, + pub summary: Option, + pub description: Option, + pub static_asset_config: Option>, + pub is_static_website: bool, + pub authentication_resource_path: Option, + pub workspaced_route: Option, + pub wrap_body: Option, + pub raw_string: Option, +} + +// Regex patterns for route validation +lazy_static::lazy_static! { + static ref ROUTE_PATH_KEY_RE: regex::Regex = regex::Regex::new(r"/?:[-\w]+").unwrap(); + static ref VALID_ROUTE_PATH_RE: regex::Regex = regex::Regex::new(r"^:?[-\w]+(/:?[-\w]+)*$").unwrap(); +} + +#[derive(Deserialize)] +pub struct RouteExists { + pub route_path: String, + pub http_method: HttpMethod, + pub trigger_path: Option, + pub workspaced_route: Option, +} + +pub fn validate_authentication_method( + authentication_method: AuthenticationMethod, + raw_string: Option, +) -> Result<()> { + match (authentication_method, raw_string) { + (AuthenticationMethod::CustomScript, raw) if !raw.unwrap_or(false) => { + Err(Error::BadRequest( + "To use custom script authentication, please enable the raw body option." + .to_string(), + )) + } + _ => Ok(()), + } +} + +pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, RoutersCache>)> { + let version = sqlx::query_scalar!("SELECT last_value FROM http_trigger_version_seq",) + .fetch_one(db) + .await?; + let routers_cache = HTTP_ROUTERS_CACHE.read().await; + if routers_cache.version == 0 || version > routers_cache.version { + drop(routers_cache); + let mut routers = HashMap::new(); + + for http_method in [ + HttpMethod::Get, + HttpMethod::Post, + HttpMethod::Put, + HttpMethod::Patch, + HttpMethod::Delete, + ] { + let triggers = sqlx::query_as!( + TriggerRoute, + r#" + SELECT + path, + script_path, + is_flow, + route_path, + authentication_resource_path, + workspace_id, + is_async, + authentication_method AS "authentication_method: _", + edited_by, + email, + static_asset_config AS "static_asset_config: _", + wrap_body, + raw_string, + workspaced_route, + is_static_website, + error_handler_path, + error_handler_args as "error_handler_args: _", + retry as "retry: _" + FROM + http_trigger + WHERE + http_method = $1 + "#, + &http_method as &HttpMethod + ) + .fetch_all(db) + .await?; + + let mut router = matchit::Router::new(); + + for trigger in triggers { + let full_path = if trigger.workspaced_route || *CLOUD_HOSTED { + format!("/{}/{}", trigger.workspace_id, trigger.route_path) + } else { + format!("/{}", trigger.route_path) + }; + + if trigger.is_static_website { + router + .insert(format!("{}/*wm_subpath", full_path), trigger.clone()) + .unwrap_or_else(|e| { + tracing::warn!( + "Failed to consider HTTP route {}/*wm_subpath: {:?}", + full_path, + e, + ); + }); + } + router + .insert(full_path.clone(), trigger.clone()) + .unwrap_or_else(|e| { + tracing::warn!("Failed to consider HTTP route {}: {:?}", full_path, e,); + }); + } + + routers.insert(http_method, router); + } + + let mut routers_cache = HTTP_ROUTERS_CACHE.write().await; + *routers_cache = RoutersCache { routers, version }; + + Ok((true, routers_cache.downgrade())) + } else { + tracing::debug!("No HTTP routers refresh needed"); + Ok((false, routers_cache)) + } +} + +pub async fn refresh_routers_loop( + db: &DB, + mut killpill_rx: tokio::sync::broadcast::Receiver<()>, +) -> () { + match refresh_routers(db).await { + Ok(_) => { + tracing::info!("Loaded HTTP routers"); + } + Err(err) => { + tracing::error!("Error loading HTTP routers: {err:#}"); + } + }; + let db = db.clone(); + tokio::spawn(async move { + loop { + tokio::select! { + _ = killpill_rx.recv() => { + break; + } + _ = tokio::time::sleep(std::time::Duration::from_secs(60)) => { + match refresh_routers(&db).await { + Ok((true, _)) => { + tracing::info!("Refreshed HTTP routers"); + } + Err(err) => { + tracing::error!("Error refreshing HTTP routers: {err:#}"); + } + _ => {} + } + } + } + } + }); +} diff --git a/backend/windmill-api/src/triggers/kafka/handler_oss.rs b/backend/windmill-api/src/triggers/kafka/handler_oss.rs new file mode 100644 index 0000000000..3922b684ce --- /dev/null +++ b/backend/windmill-api/src/triggers/kafka/handler_oss.rs @@ -0,0 +1,68 @@ +#[cfg(not(feature = "private"))] +use crate::triggers::TriggerData; + +#[allow(unused)] +#[cfg(feature = "private")] +pub use super::handler_ee::*; + +#[cfg(not(feature = "private"))] +use { + super::KafkaTrigger, + crate::{ + db::{ApiAuthed, DB}, + triggers::TriggerCrud, + }, + axum::async_trait, + sqlx::PgConnection, + windmill_common::error::{Error, Result}, + windmill_git_sync::DeployedObject, +}; + +#[cfg(not(feature = "private"))] +#[async_trait] +impl TriggerCrud for KafkaTrigger { + type Trigger = (); + type TriggerConfig = (); + type TriggerConfigRequest = (); + type TestConnectionConfig = (); + + const TABLE_NAME: &'static str = ""; + const TRIGGER_TYPE: &'static str = ""; + const SUPPORTS_ENABLED: bool = false; + const SUPPORTS_SERVER_STATE: bool = false; + const SUPPORTS_TEST_CONNECTION: bool = false; + const ROUTE_PREFIX: &'static str = "/kafka_triggers"; + const DEPLOYMENT_NAME: &'static str = ""; + const IS_CLOUD_HOSTED: bool = false; + + fn get_deployed_object(path: String) -> DeployedObject { + DeployedObject::KafkaTrigger { path } + } + + async fn create_trigger( + &self, + _db: &DB, + _tx: &mut PgConnection, + _authed: &ApiAuthed, + _w_id: &str, + _trigger: TriggerData, + ) -> Result<()> { + Err(Error::BadRequest( + "Kafka triggers are not available in open source version".to_string(), + )) + } + + async fn update_trigger( + &self, + _db: &DB, + _executor: &mut PgConnection, + _authed: &ApiAuthed, + _workspace_id: &str, + _path: &str, + _trigger: TriggerData, + ) -> Result<()> { + Err(Error::BadRequest( + "Kafka triggers are not available in open source version".to_string(), + )) + } +} diff --git a/backend/windmill-api/src/triggers/kafka/mod.rs b/backend/windmill-api/src/triggers/kafka/mod.rs new file mode 100644 index 0000000000..700b76b056 --- /dev/null +++ b/backend/windmill-api/src/triggers/kafka/mod.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "private")] +mod handler_ee; +pub mod handler_oss; + +#[cfg(feature = "private")] +mod mod_ee; +#[cfg(feature = "private")] +pub use mod_ee::*; + +#[derive(Copy, Clone)] +pub struct KafkaTrigger; diff --git a/backend/windmill-api/src/triggers/mod.rs b/backend/windmill-api/src/triggers/mod.rs new file mode 100644 index 0000000000..2bfe6b0225 --- /dev/null +++ b/backend/windmill-api/src/triggers/mod.rs @@ -0,0 +1,140 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::{types::Json as SqlxJson, FromRow}; +use std::{collections::HashMap, fmt::Debug}; + +#[cfg(all(feature = "gcp_trigger", feature = "enterprise", feature = "private"))] +pub mod gcp; +#[cfg(feature = "http_trigger")] +pub mod http; +#[cfg(all(feature = "kafka", feature = "enterprise", feature = "private"))] +pub mod kafka; +#[cfg(feature = "mqtt_trigger")] +pub mod mqtt; +#[cfg(all(feature = "nats", feature = "enterprise", feature = "private"))] +pub mod nats; +#[cfg(feature = "postgres_trigger")] +pub mod postgres; +#[cfg(all(feature = "sqs_trigger", feature = "enterprise", feature = "private"))] +pub mod sqs; +#[cfg(feature = "websocket")] +pub mod websocket; + +mod handler; +pub mod trigger_helpers; + +#[allow(unused)] +pub(crate) use handler::TriggerCrud; +pub use handler::{generate_trigger_routers, get_triggers_count_internal, TriggersCount}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StandardTriggerQuery { + pub page: Option, + pub per_page: Option, + pub path: Option, + pub is_flow: Option, + pub path_start: Option, +} + +#[derive(Debug, FromRow, Clone, Serialize, Deserialize)] +pub struct BaseTrigger { + pub workspace_id: String, + pub path: String, + pub script_path: String, + pub is_flow: bool, + pub edited_by: String, + pub email: String, + pub edited_at: DateTime, + pub extra_perms: Option, +} + +#[derive(Debug, FromRow, Clone, Serialize, Deserialize)] +pub struct ServerState { + pub enabled: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub server_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_server_ping: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Debug, FromRow, Clone, Serialize, Deserialize)] +pub struct TriggerErrorHandling { + #[serde(skip_serializing_if = "Option::is_none")] + pub error_handler_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_handler_args: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub retry: Option>, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Trigger +where + T: for<'r> FromRow<'r, sqlx::postgres::PgRow>, +{ + #[serde(flatten)] + pub base: BaseTrigger, + + #[serde(flatten)] + pub config: T, + + #[serde(flatten)] + pub server_state: Option, + + #[serde(flatten)] + pub error_handling: TriggerErrorHandling, +} + +impl FromRow<'_, sqlx::postgres::PgRow> for Trigger +where + T: for<'r> FromRow<'r, sqlx::postgres::PgRow>, +{ + fn from_row(row: &sqlx::postgres::PgRow) -> std::result::Result { + Ok(Trigger { + base: BaseTrigger::from_row(row)?, + config: T::from_row(row)?, + server_state: ServerState::from_row(row).ok(), + error_handling: TriggerErrorHandling::from_row(row)?, + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BaseTriggerData { + pub path: String, + pub script_path: String, + pub is_flow: bool, + pub enabled: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TriggerData { + #[serde(flatten)] + pub base: BaseTriggerData, + + #[serde(flatten)] + pub config: T, + + #[serde(flatten)] + pub error_handling: TriggerErrorHandling, +} + +impl StandardTriggerQuery { + pub fn offset(&self) -> i64 { + let page = self.page.unwrap_or(0); + let per_page = self.per_page.unwrap_or(100); + (page * per_page) as i64 + } + + pub fn limit(&self) -> i64 { + self.per_page.unwrap_or(100) as i64 + } +} + +impl Default for StandardTriggerQuery { + fn default() -> Self { + Self { page: Some(0), per_page: Some(100), path: None, path_start: None, is_flow: None } + } +} diff --git a/backend/windmill-api/src/triggers/mqtt/handler.rs b/backend/windmill-api/src/triggers/mqtt/handler.rs new file mode 100644 index 0000000000..d8e314a836 --- /dev/null +++ b/backend/windmill-api/src/triggers/mqtt/handler.rs @@ -0,0 +1,238 @@ +use crate::{ + db::{ApiAuthed, DB}, + resources::try_get_resource_from_db_as, + triggers::{Trigger, TriggerCrud, TriggerData}, +}; +use axum::async_trait; +use itertools::Itertools; +use sqlx::{types::Json as SqlxJson, PgConnection}; +use windmill_common::{ + db::UserDB, + error::{Error, Result}, +}; +use windmill_git_sync::DeployedObject; + +use super::{ + MqttClientBuilder, MqttClientVersion, MqttConfig, MqttConfigRequest, MqttResource, MqttTrigger, + MqttV3Config, MqttV5Config, SubscribeTopic, TestMqttConfig, +}; + +#[async_trait] +impl TriggerCrud for MqttTrigger { + type TriggerConfig = MqttConfig; + type Trigger = Trigger; + type TriggerConfigRequest = MqttConfigRequest; + type TestConnectionConfig = TestMqttConfig; + + const TABLE_NAME: &'static str = "mqtt_trigger"; + const TRIGGER_TYPE: &'static str = "mqtt"; + const SUPPORTS_ENABLED: bool = true; + const SUPPORTS_SERVER_STATE: bool = true; + const SUPPORTS_TEST_CONNECTION: bool = true; + const ROUTE_PREFIX: &'static str = "/mqtt_triggers"; + const DEPLOYMENT_NAME: &'static str = "MQTT trigger"; + const ADDITIONAL_SELECT_FIELDS: &[&'static str] = &[ + "mqtt_resource_path", + "subscribe_topics", + "v3_config", + "v5_config", + "client_id", + "client_version", + ]; + const IS_ALLOWED_ON_CLOUD: bool = false; + + fn get_deployed_object(path: String) -> DeployedObject { + DeployedObject::MqttTrigger { path } + } + + async fn validate_config( + &self, + _db: &DB, + config: &Self::TriggerConfigRequest, + _workspace_id: &str, + ) -> Result<()> { + if config.mqtt_resource_path.trim().is_empty() { + return Err(Error::BadRequest( + "MQTT resource path cannot be empty".to_string(), + )); + } + + if config.subscribe_topics.is_empty() { + return Err(Error::BadRequest( + "At least one subscribe topic must be specified".to_string(), + )); + } + + Ok(()) + } + + async fn create_trigger( + &self, + _db: &DB, + tx: &mut PgConnection, + authed: &ApiAuthed, + w_id: &str, + trigger: TriggerData, + ) -> Result<()> { + let subscribe_topics = trigger + .config + .subscribe_topics + .into_iter() + .map(SqlxJson) + .collect_vec(); + let v3_config = trigger.config.v3_config.map(SqlxJson); + let v5_config = trigger.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, + error_handler_path, + error_handler_args, + retry + ) + VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16 + )"#, + trigger.config.mqtt_resource_path, + subscribe_topics.as_slice() as &[SqlxJson], + trigger.config.client_version as Option, + trigger.config.client_id, + v3_config as Option>, + v5_config as Option>, + w_id, + trigger.base.path, + trigger.base.script_path, + trigger.base.is_flow, + authed.email, + trigger.base.enabled.unwrap_or(true), + authed.username, + trigger.error_handling.error_handler_path, + trigger.error_handling.error_handler_args as _, + trigger.error_handling.retry as _ + ) + .execute(tx) + .await?; + + Ok(()) + } + + async fn update_trigger( + &self, + _db: &DB, + tx: &mut PgConnection, + authed: &ApiAuthed, + workspace_id: &str, + path: &str, + trigger: TriggerData, + ) -> Result<()> { + let subscribe_topics = trigger + .config + .subscribe_topics + .into_iter() + .map(SqlxJson) + .collect_vec(); + let v3_config = trigger.config.v3_config.map(SqlxJson); + let v5_config = trigger.config.v5_config.map(SqlxJson); + + // Important to set server_id to NULL to stop current mqtt listener + 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, + error_handler_path = $14, + error_handler_args = $15, + retry = $16 + WHERE + workspace_id = $12 AND + path = $13 + "#, + trigger.config.mqtt_resource_path, + subscribe_topics.as_slice() as &[SqlxJson], + trigger.config.client_version as Option, + trigger.config.client_id, + v3_config as Option>, + v5_config as Option>, + trigger.base.is_flow, + authed.username, + authed.email, + trigger.base.script_path, + trigger.base.path, + workspace_id, + path, + trigger.error_handling.error_handler_path, + trigger.error_handling.error_handler_args as _, + trigger.error_handling.retry as _ + ) + .execute(tx) + .await?; + + Ok(()) + } + + async fn test_connection( + &self, + db: &DB, + authed: &ApiAuthed, + user_db: &UserDB, + workspace_id: &str, + config: Self::TestConnectionConfig, + ) -> Result<()> { + let mqtt_resource = try_get_resource_from_db_as::( + authed, + Some(user_db.clone()), + db, + &config.mqtt_resource_path, + workspace_id, + ) + .await?; + + let connect_f = async { + let client_builder = MqttClientBuilder::new( + mqtt_resource, + Some(""), + vec![], + config.v3_config.as_ref(), + config.v5_config.as_ref(), + config.client_version.as_ref(), + ); + + client_builder.build_client().await.map_err(|err| { + Error::BadConfig(format!( + "Error connecting to mqtt broker: {}", + err.to_string() + )) + }) + }; + + connect_f.await?; + Ok(()) + } +} diff --git a/backend/windmill-api/src/triggers/mqtt/mod.rs b/backend/windmill-api/src/triggers/mqtt/mod.rs new file mode 100644 index 0000000000..1ac287b82a --- /dev/null +++ b/backend/windmill-api/src/triggers/mqtt/mod.rs @@ -0,0 +1,346 @@ +use base64::{engine, prelude::*}; +use itertools::Itertools; +use rumqttc::{ + v5::{ + mqttbytes::{ + v5::{ConnectProperties, Filter}, + QoS as V5QoS, + }, + AsyncClient as V5AsyncClient, EventLoop as V5EventLoop, MqttOptions as V5MqttOptions, + }, + AsyncClient as V3AsyncClient, EventLoop as V3EventLoop, MqttOptions as V3MqttOptions, + QoS as V3QoS, SubscribeFilter, TlsConfiguration, Transport, +}; +use crate::mqtt_triggers::EventLoop; +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use sqlx::{types::Json as SqlxJson, FromRow, Type}; +use std::{collections::HashMap, time::Duration}; +use windmill_common::{ + error::{to_anyhow, Error}, + triggers::TriggerKind, + worker::to_raw_value, +}; + +use crate::triggers::{ trigger_helpers::TriggerJobArgs}; + +pub mod handler; + +#[derive(Clone, Copy)] +pub struct MqttTrigger; + +impl TriggerJobArgs for MqttTrigger { + type Payload = Vec; + const TRIGGER_KIND: TriggerKind = TriggerKind::Mqtt; + + fn v1_payload_fn(payload: &Self::Payload) -> HashMap> { + HashMap::from([("payload".to_string(), to_raw_value(&payload))]) + } + + fn v2_payload_fn(payload: &Self::Payload) -> HashMap> { + let base64_payload = engine::general_purpose::STANDARD.encode(payload); + HashMap::from([("payload".to_string(), to_raw_value(&base64_payload))]) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, Type)] +#[serde(rename_all = "lowercase")] +pub enum QualityOfService { + Qos0, + Qos1, + Qos2, +} + +impl From for V3QoS { + fn from(value: QualityOfService) -> Self { + match value { + QualityOfService::Qos0 => V3QoS::AtMostOnce, + QualityOfService::Qos1 => V3QoS::AtLeastOnce, + QualityOfService::Qos2 => V3QoS::ExactlyOnce, + } + } +} + +impl From for V5QoS { + fn from(value: QualityOfService) -> Self { + match value { + QualityOfService::Qos0 => V5QoS::AtMostOnce, + QualityOfService::Qos1 => V5QoS::AtLeastOnce, + QualityOfService::Qos2 => V5QoS::ExactlyOnce, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct MqttV3Config { + clean_session: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct MqttV5Config { + clean_start: Option, + session_expiry_interval: Option, + topic_alias_maximum: Option, +} + +#[derive(Clone, 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 { + enabled: bool, + ca_certificate: String, + 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, Clone, FromRow, Serialize, Deserialize)] +pub struct MqttConfig { + pub mqtt_resource_path: String, + pub subscribe_topics: Vec>, + pub v3_config: Option>, + pub v5_config: Option>, + pub client_id: Option, + pub client_version: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MqttConfigRequest { + pub mqtt_resource_path: String, + pub subscribe_topics: Vec, + pub v3_config: Option, + pub v5_config: Option, + pub client_id: Option, + pub client_version: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TestMqttConfig { + pub mqtt_resource_path: String, + pub client_version: Option, + pub v3_config: Option, + pub v5_config: Option, +} + +// Constants +pub const KEEP_ALIVE: u64 = 60; +pub const CLIENT_CONNECTION_TIMEOUT: u64 = 60; +pub const TOPIC_ALIAS_MAXIMUM: u16 = 65535; +pub const TIMEOUT_DURATION: u64 = 10; +pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(TIMEOUT_DURATION); + +pub struct V3MqttHandler; +pub struct V5MqttHandler; + +pub enum MqttClientResult { + V3((V3MqttHandler, V3EventLoop)), + V5((V5MqttHandler, V5EventLoop)), +} + +#[derive(Debug, thiserror::Error)] +pub enum MqttError { + #[error("{0}")] + Common(#[from] 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), +} + +pub 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> { + pub 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, + } + } + + pub 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.enabled => { + let transport = match tls.ca_certificate.trim().is_empty() { + true => rumqttc::Transport::Tls(TlsConfiguration::Native), + false => 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) + if !client_certificate.trim().is_empty() => + { + let client_certificate = BASE64_STANDARD + .decode(client_certificate) + .map_err(to_anyhow)?; + 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_connection_timeout(CLIENT_CONNECTION_TIMEOUT); + + 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)); + mqtt_options.set_connect_properties(ConnectProperties { + session_expiry_interval: v5_config.session_expiry_interval, + receive_maximum: None, + max_packet_size: None, + topic_alias_max: v5_config.topic_alias_maximum.or(Some(TOPIC_ALIAS_MAXIMUM)), + request_response_info: None, + request_problem_info: None, + user_properties: vec![], + authentication_method: None, + authentication_data: None, + }); + } + + 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 + .map_err(to_anyhow)?; + } + 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 + .map_err(to_anyhow)?; + } + Ok(MqttClientResult::V3((V3MqttHandler, event_loop))) + } +} diff --git a/backend/windmill-api/src/triggers/nats/handler_oss.rs b/backend/windmill-api/src/triggers/nats/handler_oss.rs new file mode 100644 index 0000000000..6b03dafef0 --- /dev/null +++ b/backend/windmill-api/src/triggers/nats/handler_oss.rs @@ -0,0 +1,65 @@ +#[allow(unused)] +#[cfg(feature = "private")] +pub use super::handler_ee::*; + +#[cfg(not(feature = "private"))] +use { + super::NatsTrigger, + crate::{ + db::{ApiAuthed, DB}, + triggers::{TriggerCrud, TriggerData}, + }, + axum::async_trait, + sqlx::PgConnection, + windmill_common::error::{Error, Result}, + windmill_git_sync::DeployedObject, +}; + +#[cfg(not(feature = "private"))] +#[async_trait] +impl TriggerCrud for NatsTrigger { + type Trigger = (); + type TriggerConfig = (); + type TriggerConfigRequest = (); + type TestConnectionConfig = (); + + const TABLE_NAME: &'static str = ""; + const TRIGGER_TYPE: &'static str = ""; + const SUPPORTS_ENABLED: bool = false; + const SUPPORTS_SERVER_STATE: bool = false; + const SUPPORTS_TEST_CONNECTION: bool = false; + const ROUTE_PREFIX: &'static str = "/nats_triggers"; + const DEPLOYMENT_NAME: &'static str = ""; + const IS_CLOUD_HOSTED: bool = false; + + fn get_deployed_object(path: String) -> DeployedObject { + DeployedObject::NatsTrigger { path } + } + + async fn create_trigger( + &self, + _db: &DB, + _executor: &mut PgConnection, + _authed: &ApiAuthed, + _w_id: &str, + _trigger: TriggerData, + ) -> Result<()> { + Err(Error::BadRequest( + "NATS triggers are not available in open source version".to_string(), + )) + } + + async fn update_trigger( + &self, + _db: &DB, + _executor: &mut PgConnection, + _authed: &ApiAuthed, + _workspace_id: &str, + _path: &str, + _trigger: TriggerData, + ) -> Result<()> { + Err(Error::BadRequest( + "NATS triggers are not available in open source version".to_string(), + )) + } +} diff --git a/backend/windmill-api/src/triggers/nats/mod.rs b/backend/windmill-api/src/triggers/nats/mod.rs new file mode 100644 index 0000000000..b69f2eb3c8 --- /dev/null +++ b/backend/windmill-api/src/triggers/nats/mod.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "private")] +mod handler_ee; +pub mod handler_oss; + +#[cfg(feature = "private")] +mod mod_ee; +#[cfg(feature = "private")] +pub use mod_ee::*; + +#[derive(Copy, Clone)] +pub struct NatsTrigger; diff --git a/backend/windmill-api/src/postgres_triggers/handler.rs b/backend/windmill-api/src/triggers/postgres/handler.rs similarity index 50% rename from backend/windmill-api/src/postgres_triggers/handler.rs rename to backend/windmill-api/src/triggers/postgres/handler.rs index 71d53dcf8b..5dce0345c6 100644 --- a/backend/windmill-api/src/postgres_triggers/handler.rs +++ b/backend/windmill-api/src/triggers/postgres/handler.rs @@ -1,284 +1,319 @@ -use std::collections::{ - hash_map::Entry::{Occupied, Vacant}, - HashMap, -}; +use std::collections::HashMap; -use crate::{ - db::{ApiAuthed, DB}, - postgres_triggers::mapper::{Mapper, MappingInfo}, - utils::check_scopes, -}; use axum::{ - extract::{Path, Query}, - Extension, Json, + async_trait, + extract::Path, + routing::{delete, get, post}, + Extension, Json, Router, }; -use http::StatusCode; use itertools::Itertools; +use lazy_static::lazy_static; use pg_escape::{quote_identifier, quote_literal}; use quick_cache::sync::Cache; use rust_postgres::{types::Type, Client}; -use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::value::RawValue; -use sql_builder::{bind::Bind, SqlBuilder}; -use sqlx::FromRow; -use windmill_audit::{audit_oss::audit_log, ActionKind}; +use sqlx::PgConnection; +use uuid; use windmill_common::{ db::UserDB, - error::{self, to_anyhow, Error, JsonResult, Result}, - utils::{empty_as_none, not_found_if_none, paginate, Pagination, StripPath}, - worker::CLOUD_HOSTED, + error::{self, to_anyhow, Error, Result}, +}; +use windmill_git_sync::DeployedObject; + +use crate::{ + db::{ApiAuthed, DB}, + triggers::{postgres::PostgresTrigger, Trigger, TriggerCrud, TriggerData}, }; -use windmill_git_sync::{handle_deployment_metadata, DeployedObject}; use super::{ check_if_valid_publication_for_postgres_version, create_logical_replication_slot, create_pg_publication, drop_publication, generate_random_string, get_default_pg_connection, + mapper::{Mapper, MappingInfo}, + PostgresConfig, PostgresConfigRequest, PostgresPublicationReplication, PublicationData, + Relations, Slot, SlotList, TableToTrack, TemplateScript, TestPostgresConfig, ERROR_PUBLICATION_NAME_NOT_EXISTS, }; -use anyhow::anyhow; -use lazy_static::lazy_static; - -#[derive(FromRow, Serialize, Deserialize, Debug)] -pub struct Postgres { - pub user: String, - pub password: String, - pub host: String, - pub port: Option, - pub dbname: String, - #[serde(default)] - pub sslmode: String, - #[serde(default, deserialize_with = "empty_as_none")] - pub root_certificate_pem: Option, -} - -#[derive(Debug, Clone, FromRow, Serialize, Deserialize)] -pub struct TableToTrack { - pub table_name: String, - #[serde(default, deserialize_with = "empty_as_none")] - pub where_clause: Option, - #[serde(default, deserialize_with = "empty_as_none")] - pub columns_name: Option>, -} - -impl TableToTrack { - fn new( - table_name: String, - where_clause: Option, - columns_name: Option>, - ) -> TableToTrack { - TableToTrack { table_name, where_clause, columns_name } - } -} +// Lazy static template cache lazy_static! { pub static ref TEMPLATE: Cache = Cache::new(50); } -#[derive(Debug, Clone, FromRow, Serialize, Deserialize)] -pub struct Relations { - pub schema_name: String, - pub table_to_track: Vec, -} +#[async_trait] +impl TriggerCrud for PostgresTrigger { + type TriggerConfig = PostgresConfig; + type Trigger = Trigger; + type TriggerConfigRequest = PostgresConfigRequest; + type TestConnectionConfig = TestPostgresConfig; -impl Relations { - fn new(schema_name: String, table_to_track: Vec) -> Relations { - Relations { schema_name, table_to_track } + const TABLE_NAME: &'static str = "postgres_trigger"; + const TRIGGER_TYPE: &'static str = "postgres"; + const SUPPORTS_ENABLED: bool = true; + const SUPPORTS_SERVER_STATE: bool = true; + const SUPPORTS_TEST_CONNECTION: bool = true; + const ROUTE_PREFIX: &'static str = "/postgres_triggers"; + const DEPLOYMENT_NAME: &'static str = "PostgreSQL trigger"; + const ADDITIONAL_SELECT_FIELDS: &[&'static str] = &[ + "postgres_resource_path", + "replication_slot_name", + "publication_name", + "NULL::text AS basic_mode", + ]; + const IS_ALLOWED_ON_CLOUD: bool = false; + + fn get_deployed_object(path: String) -> DeployedObject { + DeployedObject::PostgresTrigger { path } } - fn add_new_table(&mut self, table_to_track: TableToTrack) { - self.table_to_track.push(table_to_track); - } -} -#[derive(Debug, Deserialize)] -pub struct EditPostgresTrigger { - replication_slot_name: String, - publication_name: String, - path: String, - script_path: String, - is_flow: bool, - postgres_resource_path: String, - publication: Option, - error_handler_path: Option, - error_handler_args: Option>>>, - retry: Option>, -} + async fn create_trigger( + &self, + db: &DB, + tx: &mut PgConnection, + authed: &ApiAuthed, + w_id: &str, + trigger: TriggerData, + ) -> Result<()> { + let Self::TriggerConfigRequest { + postgres_resource_path, + publication_name, + replication_slot_name, + publication, + } = trigger.config; -#[derive(Deserialize, Serialize, Debug)] -pub struct NewPostgresTrigger { - path: String, - script_path: String, - is_flow: bool, - enabled: bool, - postgres_resource_path: String, - replication_slot_name: Option, - publication_name: Option, - publication: Option, - error_handler_path: Option, - error_handler_args: Option>>>, - retry: Option>, -} + let (pub_name, slot_name) = + if publication_name.is_empty() && replication_slot_name.is_empty() { + if publication.is_none() { + return Err(Error::BadRequest("publication must be set".to_string())); + } -#[derive(Serialize, Deserialize)] -pub struct TestPostgres { - pub postgres_resource_path: String, -} + let PostgresPublicationReplication { publication_name, replication_slot_name } = + create_custom_slot_and_publication_inner( + authed.clone(), + UserDB::new(db.clone()), + &db, + &postgres_resource_path, + &w_id, + &publication.unwrap(), + ) + .await?; -pub async fn test_postgres_connection( - authed: ApiAuthed, - Extension(db): Extension, - Extension(user_db): Extension, - Path(workspace_id): Path, - Json(test_postgres): Json, -) -> Result<()> { - let connect_f = async { - get_default_pg_connection( - authed, - Some(user_db), - &db, - &test_postgres.postgres_resource_path, - &workspace_id, - ) - .await - .map_err(|err| { - error::Error::BadConfig(format!("Error connecting to postgres: {}", err.to_string())) - }) - }; - tokio::time::timeout(tokio::time::Duration::from_secs(30), connect_f) - .await - .map_err(|_| { - error::Error::BadConfig(format!("Timeout connecting to postgres after 30 seconds")) - })??; - - Ok(()) -} - -#[derive(Deserialize, Debug)] -pub enum Language { - #[serde(rename = "typescript", alias = "Typescript")] - Typescript, -} - -#[derive(Debug, Deserialize)] -pub struct TemplateScript { - postgres_resource_path: String, - #[serde(deserialize_with = "check_if_valid_relation")] - relations: Option>, - language: Language, -} - -fn check_if_valid_relation<'de, D>( - relations: D, -) -> std::result::Result>, D::Error> -where - D: Deserializer<'de>, -{ - let relations: Option> = Option::deserialize(relations)?; - let mut track_all_table_in_schema = false; - let mut track_specific_columns_in_table = false; - match relations { - Some(relations) => { - for relation in relations.iter() { - if relation.schema_name.is_empty() { - return Err(serde::de::Error::custom( - "Schema Name must not be empty".to_string(), + (publication_name, replication_slot_name) + } else { + if publication_name.is_empty() { + return Err(Error::BadRequest( + "Publication name must not be empty".to_string(), + )); + } else if replication_slot_name.is_empty() { + return Err(Error::BadRequest( + "Replication slot name must not be empty".to_string(), )); } + (publication_name, replication_slot_name) + }; - if !track_all_table_in_schema && relation.table_to_track.is_empty() { - track_all_table_in_schema = true; - continue; - } + sqlx::query!( + r#" + INSERT INTO postgres_trigger ( + workspace_id, + path, + postgres_resource_path, + replication_slot_name, + publication_name, + script_path, + is_flow, + enabled, + edited_by, + email, + edited_at, + error_handler_path, + error_handler_args, + retry + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, now(), $11, $12, $13 + ) + "#, + w_id, + trigger.base.path, + postgres_resource_path, + slot_name, + pub_name, + trigger.base.script_path, + trigger.base.is_flow, + trigger.base.enabled.unwrap_or(true), + authed.username, + authed.email, + trigger.error_handling.error_handler_path, + trigger.error_handling.error_handler_args as _, + trigger.error_handling.retry as _ + ) + .execute(tx) + .await?; + Ok(()) + } - for table_to_track in relation.table_to_track.iter() { - if table_to_track.table_name.trim().is_empty() { - return Err(serde::de::Error::custom( - "Table name must not be empty".to_string(), - )); - } + async fn update_trigger( + &self, + db: &DB, + tx: &mut PgConnection, + authed: &ApiAuthed, + w_id: &str, + path: &str, + trigger: TriggerData, + ) -> Result<()> { + let Self::TriggerConfigRequest { + replication_slot_name, + publication_name, + postgres_resource_path, + publication, + } = trigger.config; - if !track_specific_columns_in_table && table_to_track.columns_name.is_some() { - track_specific_columns_in_table = true; - } - } + let mut pg_connection = get_default_pg_connection( + authed.clone(), + Some(UserDB::new(db.clone())), + db, + &postgres_resource_path, + &w_id, + ) + .await + .map_err(to_anyhow)?; - if track_all_table_in_schema && track_specific_columns_in_table { - return Err(serde::de::Error::custom("Incompatible tracking options. Schema-level tracking and specific table tracking with column selection cannot be used together. Refer to the documentation for valid configurations.")); - } - } + let exists = + check_if_logical_replication_slot_exist(&mut pg_connection, &replication_slot_name) + .await?; - if !relations - .iter() - .map(|relation| relation.schema_name.as_str()) - .all_unique() - { - return Err(serde::de::Error::custom( - "You cannot choose a schema more than one time".to_string(), - )); - } + let remote_db_tx = pg_connection.transaction().await.map_err(to_anyhow)?; - Ok(Some(relations)) + if !exists { + tracing::debug!( + "Logical replication slot named: {} does not exists creating it...", + &replication_slot_name + ); + create_logical_replication_slot(remote_db_tx.client(), &replication_slot_name) + .await + .map_err(to_anyhow)?; } - None => Ok(None), + + if let Some(publication) = publication { + let publication_data = + get_publication_scope_and_transaction(remote_db_tx.client(), &publication_name) + .await + .map_err(to_anyhow)?; + + update_pg_publication( + remote_db_tx.client(), + &publication_name, + publication, + publication_data.map(|publication| publication.0), + ) + .await + .map_err(to_anyhow)?; + } + + remote_db_tx.commit().await.map_err(to_anyhow)?; + + sqlx::query!( + r#" + UPDATE postgres_trigger + SET + postgres_resource_path = $1, + replication_slot_name = $2, + publication_name = $3, + script_path = $4, + path = $5, + is_flow = $6, + edited_by = $7, + email = $8, + edited_at = now(), + server_id = NULL, + error = NULL, + error_handler_path = $11, + error_handler_args = $12, + retry = $13 + WHERE + workspace_id = $9 AND path = $10 + "#, + postgres_resource_path, + replication_slot_name, + publication_name, + trigger.base.script_path, + trigger.base.path, + trigger.base.is_flow, + authed.username, + authed.email, + w_id, + path, + trigger.error_handling.error_handler_path, + trigger.error_handling.error_handler_args as _, + trigger.error_handling.retry as _ + ) + .execute(tx) + .await?; + Ok(()) + } + + async fn test_connection( + &self, + db: &DB, + authed: &ApiAuthed, + user_db: &UserDB, + workspace_id: &str, + config: Self::TestConnectionConfig, + ) -> Result<()> { + let connect_f = async { + get_default_pg_connection( + authed.clone(), + Some(user_db.clone()), + db, + &config.postgres_resource_path, + workspace_id, + ) + .await + .map_err(|err| { + Error::BadConfig(format!("Error connecting to postgres: {}", err.to_string())) + })?; + + Ok::<(), Error>(()) + }; + + connect_f.await?; + Ok(()) + } + + fn additional_routes(&self) -> Router { + Router::new() + .route("/get_template_script/:id", get(get_template_script)) + .route("/create_template_script", post(create_template_script)) + .route( + "/is_valid_postgres_configuration/*path", + get(is_database_in_logical_level), + ) + .nest("/publication", publication_service()) + .nest("/slot", slot_service()) + .nest("/postgres", postgres_service()) } } -#[derive(FromRow, Deserialize, Serialize, Debug)] -pub struct PostgresTrigger { - pub path: String, - pub script_path: String, - pub is_flow: bool, - pub workspace_id: String, - pub edited_by: String, - pub email: String, - pub edited_at: chrono::DateTime, - #[serde(skip_serializing_if = "Option::is_none")] - pub extra_perms: Option, - pub postgres_resource_path: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub server_id: Option, - pub replication_slot_name: String, - pub publication_name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_server_ping: Option>, - pub enabled: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub error_handler_path: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub error_handler_args: Option>>>, - #[serde(skip_serializing_if = "Option::is_none")] - pub retry: Option>, +fn publication_service() -> Router { + Router::new() + .route("/get/:publication_name/*path", get(get_publication_info)) + .route("/create/:publication_name/*path", post(create_publication)) + .route("/update/:publication_name/*path", post(alter_publication)) + .route( + "/delete/:publication_name/*path", + delete(delete_publication), + ) + .route("/list/*path", get(list_database_publication)) } -#[derive(Deserialize, Serialize)] -pub struct ListPostgresTriggerQuery { - pub page: Option, - pub per_page: Option, - pub path: Option, - pub is_flow: Option, - pub path_start: Option, +fn slot_service() -> Router { + Router::new() + .route("/list/*path", get(list_slot_name)) + .route("/create/*path", post(create_slot)) + .route("/delete/*path", delete(drop_slot_name)) } -#[derive(Deserialize)] -pub struct SetEnabled { - pub enabled: bool, -} - -#[derive(Serialize, Deserialize)] -pub struct PostgresPublicationReplication { - publication_name: String, - replication_slot_name: String, -} - -impl PostgresPublicationReplication { - pub fn new( - publication_name: String, - replication_slot_name: String, - ) -> PostgresPublicationReplication { - PostgresPublicationReplication { publication_name, replication_slot_name } - } +fn postgres_service() -> Router { + Router::new().route("/version/*path", get(get_postgres_version)) } async fn check_if_logical_replication_slot_exist( @@ -367,264 +402,6 @@ pub async fn get_postgres_version( Ok(postgres_version) } -pub async fn create_postgres_trigger( - authed: ApiAuthed, - Extension(user_db): Extension, - Extension(db): Extension, - Path(w_id): Path, - Json(new_postgres_trigger): Json, -) -> Result<(StatusCode, String)> { - check_scopes(&authed, || { - format!("postgres_triggers:write:{}", new_postgres_trigger.path) - })?; - - if *CLOUD_HOSTED { - return Err(error::Error::BadRequest( - "Postgres triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(), - )); - } - - let NewPostgresTrigger { - postgres_resource_path, - path, - script_path, - enabled, - is_flow, - publication_name, - replication_slot_name, - publication, - error_handler_path, - error_handler_args, - retry, - } = new_postgres_trigger; - - if publication_name.is_none() && publication.is_none() { - return Err(error::Error::BadRequest( - "Publication data is missing".to_string(), - )); - } - let (pub_name, slot_name) = if publication_name.is_none() && replication_slot_name.is_none() { - if publication.is_none() { - return Err(Error::BadRequest("publication must be set".to_string())); - } - - let PostgresPublicationReplication { publication_name, replication_slot_name } = - create_custom_slot_and_publication_inner( - authed.clone(), - user_db.clone(), - &db, - &postgres_resource_path, - &w_id, - &publication.unwrap(), - ) - .await?; - - (publication_name, replication_slot_name) - } else { - if publication_name.is_none() { - return Err(Error::BadRequest("Missing publication name".to_string())); - } else if replication_slot_name.is_none() { - return Err(Error::BadRequest( - "Missing replication slot name".to_string(), - )); - } - (publication_name.unwrap(), replication_slot_name.unwrap()) - }; - - let mut tx = user_db.begin(&authed).await?; - - sqlx::query!( - r#" - INSERT INTO postgres_trigger ( - publication_name, - replication_slot_name, - workspace_id, - path, - script_path, - is_flow, - email, - enabled, - postgres_resource_path, - edited_by, - error_handler_path, - error_handler_args, - retry - ) - VALUES ( - $1, - $2, - $3, - $4, - $5, - $6, - $7, - $8, - $9, - $10, - $11, - $12, - $13 - )"#, - pub_name, - slot_name, - &w_id, - &path, - script_path, - is_flow, - &authed.email, - enabled, - postgres_resource_path, - &authed.username, - error_handler_path, - error_handler_args as _, - retry as _ - ) - .execute(&mut *tx) - .await?; - - audit_log( - &mut *tx, - &authed, - "postgres_triggers.create", - ActionKind::Create, - &w_id, - Some(path.as_str()), - None, - ) - .await?; - - tx.commit().await?; - - handle_deployment_metadata( - &authed.email, - &authed.username, - &db, - &w_id, - DeployedObject::PostgresTrigger { path: path.to_string() }, - Some(format!("Postgres trigger '{}' created", path)), - true, - ) - .await?; - - Ok((StatusCode::CREATED, path.to_string())) -} - -pub async fn list_postgres_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("postgres_trigger"); - sqlb.fields(&[ - "workspace_id", - "path", - "script_path", - "is_flow", - "edited_by", - "email", - "edited_at", - "server_id", - "last_server_ping", - "extra_perms", - "error", - "enabled", - "postgres_resource_path", - "replication_slot_name", - "publication_name", - "error_handler_path", - "error_handler_args", - "retry", - ]) - .order_by("edited_at", true) - .and_where("workspace_id = ?".bind(&w_id)) - .offset(offset) - .limit(per_page); - 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::<_, PostgresTrigger>(&sql) - .fetch_all(&mut *tx) - .await - .map_err(|e| { - tracing::debug!("Error fetching postgres_trigger: {:#?}", e); - windmill_common::error::Error::InternalErr("server error".to_string()) - })?; - tx.commit().await.map_err(|e| { - tracing::debug!("Error commiting postgres_trigger: {:#?}", e); - windmill_common::error::Error::InternalErr("server error".to_string()) - })?; - - Ok(Json(rows)) -} - -#[derive(Deserialize, Serialize, Debug)] -pub struct PublicationData { - #[serde(default, deserialize_with = "check_if_valid_relation")] - pub table_to_track: Option>, - #[serde(deserialize_with = "check_if_valid_transaction_type")] - pub transaction_to_track: Vec, -} - -fn check_if_valid_transaction_type<'de, D>( - transaction_type: D, -) -> std::result::Result, D::Error> -where - D: Deserializer<'de>, -{ - let mut transaction_type: Vec = Vec::deserialize(transaction_type)?; - if transaction_type.len() > 3 { - return Err(serde::de::Error::custom( - "More than 3 transaction type which is not authorized, you are only allowed to those 3 transaction types: Insert, Update and Delete" - .to_string(), - )); - } - transaction_type.sort_unstable(); - transaction_type.dedup(); - - for transaction in transaction_type.iter() { - match transaction.to_lowercase().as_ref() { - "insert" => {}, - "update" => {}, - "delete" => {}, - _ => { - return Err(serde::de::Error::custom( - "Only the following transaction types are allowed: Insert, Update and Delete (case insensitive)" - .to_string(), - )) - } - } - } - - Ok(transaction_type) -} - -impl PublicationData { - fn new( - table_to_track: Option>, - transaction_to_track: Vec, - ) -> PublicationData { - PublicationData { table_to_track, transaction_to_track } - } -} - -#[derive(FromRow, Debug, Serialize)] -pub struct SlotList { - slot_name: Option, - active: Option, -} - pub async fn list_slot_name( authed: ApiAuthed, Extension(user_db): Extension, @@ -666,11 +443,6 @@ pub async fn list_slot_name( Ok(Json(slots)) } -#[derive(Debug, Serialize, Deserialize)] -pub struct Slot { - name: String, -} - pub async fn create_slot( authed: ApiAuthed, Extension(user_db): Extension, @@ -1117,31 +889,32 @@ pub async fn get_tracked_relations( let columns: Option> = row.get("columns"); let where_clause: Option = row.get("where_clause"); - let schema_name = schema_name.ok_or_else::( || { - anyhow!( + let schema_name = schema_name.ok_or_else::(|| { + anyhow::anyhow!( "Unexpected NULL `schema_name` in publication entry (pubname: `{}`). This should never happen unless PostgreSQL internals are corrupted.", publication_name, - ).into() - } - )?; + ) + .into() + })?; let table_name = table_name.ok_or_else::(|| { - anyhow!( + anyhow::anyhow!( "Unexpected NULL `table_name` for schema `{}` in publication `{}`. This should never happen unless PostgreSQL internals are corrupted.", schema_name, publication_name, - ).into() + ) + .into() })?; let entry = table_to_track.entry(schema_name.clone()); - let table_to_track = TableToTrack::new(table_name, where_clause, columns); + let table_to_track_item = TableToTrack::new(table_name, where_clause, columns); match entry { - std::collections::hash_map::Entry::Occupied(mut occuped) => { - occuped.get_mut().add_new_table(table_to_track); + std::collections::hash_map::Entry::Occupied(mut occupied) => { + occupied.get_mut().add_new_table(table_to_track_item); } std::collections::hash_map::Entry::Vacant(vacant) => { - vacant.insert(Relations::new(schema_name, vec![table_to_track])); + vacant.insert(Relations::new(schema_name, vec![table_to_track_item])); } } } @@ -1149,331 +922,6 @@ pub async fn get_tracked_relations( Ok(table_to_track.into_values().collect_vec()) } -pub async fn get_postgres_trigger( - authed: ApiAuthed, - Extension(user_db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, -) -> JsonResult { - let path = path.to_path(); - check_scopes(&authed, || format!("postgres_triggers:read:{}", path))?; - let mut tx = user_db.begin(&authed).await?; - let trigger = sqlx::query_as!( - PostgresTrigger, - r#" - SELECT - workspace_id, - path, - script_path, - is_flow, - edited_by, - email, - edited_at, - server_id, - last_server_ping, - extra_perms, - error, - enabled, - replication_slot_name, - publication_name, - postgres_resource_path, - error_handler_path, - error_handler_args as "error_handler_args: _", - retry as "retry: _" - FROM - postgres_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, "Trigger", path)?; - - Ok(Json(trigger)) -} - -pub async fn update_postgres_trigger( - authed: ApiAuthed, - Extension(user_db): Extension, - Extension(db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, - Json(postgres_trigger): Json, -) -> Result { - let workspace_path = path.to_path(); - check_scopes(&authed, || { - format!("postgres_triggers:write:{}", workspace_path) - })?; - - let EditPostgresTrigger { - replication_slot_name, - publication_name, - script_path, - path, - is_flow, - postgres_resource_path, - publication, - error_handler_path, - error_handler_args, - retry, - } = postgres_trigger; - - let mut pg_connection = get_default_pg_connection( - authed.clone(), - Some(user_db.clone()), - &db, - &postgres_resource_path, - &w_id, - ) - .await - .map_err(to_anyhow)?; - - let exists = - check_if_logical_replication_slot_exist(&mut pg_connection, &replication_slot_name).await?; - - let tx = pg_connection.transaction().await.map_err(to_anyhow)?; - - if !exists { - tracing::debug!( - "Logical replication slot named: {} does not exists creating it...", - &replication_slot_name - ); - create_logical_replication_slot(tx.client(), &replication_slot_name) - .await - .map_err(to_anyhow)?; - } - - if let Some(publication) = publication { - let publication_data = - get_publication_scope_and_transaction(tx.client(), &publication_name) - .await - .map_err(to_anyhow)?; - - update_pg_publication( - tx.client(), - &publication_name, - publication, - publication_data.map(|publication| publication.0), - ) - .await - .map_err(to_anyhow)?; - } - - tx.commit().await.map_err(to_anyhow)?; - - let mut tx = user_db.begin(&authed).await?; - - sqlx::query!( - r#" - UPDATE postgres_trigger - SET - script_path = $1, - path = $2, - is_flow = $3, - edited_by = $4, - email = $5, - postgres_resource_path = $6, - replication_slot_name = $7, - publication_name = $8, - edited_at = now(), - error = NULL, - server_id = NULL, - error_handler_path = $11, - error_handler_args = $12, - retry = $13 - WHERE - workspace_id = $9 AND - path = $10 - "#, - script_path, - path, - is_flow, - &authed.username, - &authed.email, - postgres_resource_path, - replication_slot_name, - publication_name, - w_id, - workspace_path, - error_handler_path, - error_handler_args as _, - retry as _ - ) - .execute(&mut *tx) - .await?; - - audit_log( - &mut *tx, - &authed, - "postgres_triggers.update", - ActionKind::Update, - &w_id, - Some(&path), - None, - ) - .await?; - - tx.commit().await?; - - handle_deployment_metadata( - &authed.email, - &authed.username, - &db, - &w_id, - DeployedObject::PostgresTrigger { path: path.to_string() }, - Some(format!("Postgres trigger '{}' updated", path)), - true, - ) - .await?; - - Ok(workspace_path.to_string()) -} - -pub async fn delete_postgres_trigger( - authed: ApiAuthed, - Extension(db): Extension, - Extension(user_db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, -) -> Result { - let path = path.to_path(); - check_scopes(&authed, || format!("postgres_triggers:write:{}", path))?; - let mut tx = user_db.begin(&authed).await?; - sqlx::query!( - r#" - DELETE FROM postgres_trigger - WHERE - workspace_id = $1 AND - path = $2 - "#, - w_id, - path, - ) - .execute(&mut *tx) - .await?; - - audit_log( - &mut *tx, - &authed, - "postgres_triggers.delete", - ActionKind::Delete, - &w_id, - Some(path), - None, - ) - .await?; - - tx.commit().await?; - - handle_deployment_metadata( - &authed.email, - &authed.username, - &db, - &w_id, - DeployedObject::PostgresTrigger { path: path.to_string() }, - Some(format!("Postgres trigger '{}' deleted", path)), - true, - ) - .await?; - - Ok(format!("Postgres trigger {path} deleted")) -} - -pub async fn exists_postgres_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 postgres_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(db): Extension, - Extension(user_db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, - Json(payload): Json, -) -> Result { - let path = path.to_path(); - check_scopes(&authed, || format!("postgres_triggers:write:{}", path))?; - let mut tx = user_db.begin(&authed).await?; - - // important to set server_id, last_server_ping and error to NULL to stop current postgres listener - let one_o = sqlx::query_scalar!( - r#" - UPDATE postgres_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, "Postgres trigger", path)?; - - audit_log( - &mut *tx, - &authed, - "postgres_triggers.setenabled", - ActionKind::Update, - &w_id, - Some(path), - Some([("enabled", payload.enabled.to_string().as_ref())].into()), - ) - .await?; - - tx.commit().await?; - - handle_deployment_metadata( - &authed.email, - &authed.username, - &db, - &w_id, - DeployedObject::PostgresTrigger { path: path.to_string() }, - Some(format!("Postgres trigger '{}' updated", path)), - true, - ) - .await?; - - Ok(format!( - "succesfully updated postgres trigger at path {} to status {}", - path, payload.enabled - )) -} - pub async fn get_template_script(Path((_, id)): Path<(String, String)>) -> Result { let template = if let Some((_, template)) = TEMPLATE.remove(&id) { template @@ -1494,7 +942,11 @@ pub async fn create_template_script( let relations = match relations { Some(r) => r, - None => return Err(anyhow!("You must at least choose schema to fetch table from").into()), + None => { + return Err( + anyhow::anyhow!("You must at least choose schema to fetch table from").into(), + ) + } }; let pg_connection: Client = get_default_pg_connection( @@ -1596,15 +1048,17 @@ pub async fn create_template_script( MappingInfo::new(info.column_name, Type::from_oid(info.oid), info.is_nullable); match schema_map.entry(info.table_schema) { - Occupied(mut schema_entry) => match schema_entry.get_mut().entry(info.table_name) { - Occupied(mut table_entry) => { - table_entry.get_mut().push(mapped_info); + std::collections::hash_map::Entry::Occupied(mut schema_entry) => { + match schema_entry.get_mut().entry(info.table_name) { + std::collections::hash_map::Entry::Occupied(mut table_entry) => { + table_entry.get_mut().push(mapped_info); + } + std::collections::hash_map::Entry::Vacant(v) => { + v.insert(vec![mapped_info]); + } } - Vacant(v) => { - v.insert(vec![mapped_info]); - } - }, - Vacant(schema_vacant) => { + } + std::collections::hash_map::Entry::Vacant(schema_vacant) => { let mut table_map = HashMap::new(); table_map.insert(info.table_name, vec![mapped_info]); schema_vacant.insert(table_map); diff --git a/backend/windmill-api/src/postgres_triggers/mapper.rs b/backend/windmill-api/src/triggers/postgres/mapper.rs similarity index 99% rename from backend/windmill-api/src/postgres_triggers/mapper.rs rename to backend/windmill-api/src/triggers/postgres/mapper.rs index bbc7b9c011..a24fb65378 100644 --- a/backend/windmill-api/src/postgres_triggers/mapper.rs +++ b/backend/windmill-api/src/triggers/postgres/mapper.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use rust_postgres::types::Type; -use super::handler::Language; +use super::Language; fn postgres_to_typescript_type(postgres_type: Option) -> String { let data_type = match postgres_type { diff --git a/backend/windmill-api/src/triggers/postgres/mod.rs b/backend/windmill-api/src/triggers/postgres/mod.rs new file mode 100644 index 0000000000..d94b08f67d --- /dev/null +++ b/backend/windmill-api/src/triggers/postgres/mod.rs @@ -0,0 +1,547 @@ +use std::collections::HashMap; + +use crate::{ + db::{ApiAuthed, DB}, + resources::try_get_resource_from_db_as, + triggers::trigger_helpers::TriggerJobArgs, +}; +use chrono::Utc; +use itertools::Itertools; +use native_tls::{Certificate, TlsConnector}; +use pg_escape::quote_identifier; +use rand::Rng; +use rust_postgres::{config::SslMode, Client, Config, NoTls}; +use rust_postgres_native_tls::MakeTlsConnector; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::value::RawValue; +use sqlx::FromRow; +use windmill_common::{ + db::UserDB, + error::{to_anyhow, Error, Result}, + triggers::TriggerKind, + utils::empty_as_none, +}; + +pub mod handler; +mod mapper; + +#[derive(Clone, Copy)] +pub struct PostgresTrigger; + +impl TriggerJobArgs for PostgresTrigger { + type Payload = HashMap>; + const TRIGGER_KIND: TriggerKind = TriggerKind::Postgres; + fn v1_payload_fn(payload: &HashMap>) -> HashMap> { + payload.to_owned() + } +} + +#[derive(Debug, Clone, FromRow, Serialize, Deserialize)] +pub struct PostgresConfig { + pub postgres_resource_path: String, + pub replication_slot_name: String, + pub publication_name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub basic_mode: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PostgresConfigRequest { + postgres_resource_path: String, + #[serde(default)] + replication_slot_name: String, + #[serde(default)] + publication_name: String, + publication: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TestPostgresConfig { + pub postgres_resource_path: String, +} + +fn check_if_valid_relation<'de, D>( + relations: D, +) -> std::result::Result>, D::Error> +where + D: Deserializer<'de>, +{ + let relations: Option> = Option::deserialize(relations)?; + let mut track_all_table_in_schema = false; + let mut track_specific_columns_in_table = false; + match relations { + Some(relations) => { + for relation in relations.iter() { + if relation.schema_name.is_empty() { + return Err(serde::de::Error::custom( + "Schema Name must not be empty".to_string(), + )); + } + + if !track_all_table_in_schema && relation.table_to_track.is_empty() { + track_all_table_in_schema = true; + continue; + } + + for table_to_track in relation.table_to_track.iter() { + if table_to_track.table_name.trim().is_empty() { + return Err(serde::de::Error::custom( + "Table name must not be empty".to_string(), + )); + } + + if !track_specific_columns_in_table && table_to_track.columns_name.is_some() { + track_specific_columns_in_table = true; + } + } + + if track_all_table_in_schema && track_specific_columns_in_table { + return Err(serde::de::Error::custom("Incompatible tracking options. Schema-level tracking and specific table tracking with column selection cannot be used together. Refer to the documentation for valid configurations.")); + } + } + + if !relations + .iter() + .map(|relation| relation.schema_name.as_str()) + .all_unique() + { + return Err(serde::de::Error::custom( + "You cannot choose a schema more than one time".to_string(), + )); + } + + Ok(Some(relations)) + } + None => Ok(None), + } +} + +fn check_if_valid_transaction_type<'de, D>( + transaction_type: D, +) -> std::result::Result, D::Error> +where + D: Deserializer<'de>, +{ + let mut transaction_type: Vec = Vec::deserialize(transaction_type)?; + if transaction_type.len() > 3 { + return Err(serde::de::Error::custom( + "More than 3 transaction type which is not authorized, you are only allowed to those 3 transaction types: Insert, Update and Delete" + .to_string(), + )); + } + transaction_type.sort_unstable(); + transaction_type.dedup(); + + for transaction in transaction_type.iter() { + match transaction.to_lowercase().as_ref() { + "insert" => {}, + "update" => {}, + "delete" => {}, + _ => { + return Err(serde::de::Error::custom( + "Only the following transaction types are allowed: Insert, Update and Delete (case insensitive)" + .to_string(), + )) + } + } + } + + Ok(transaction_type) +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PublicationData { + #[serde(default, deserialize_with = "check_if_valid_relation")] + pub table_to_track: Option>, + #[serde(deserialize_with = "check_if_valid_transaction_type")] + pub transaction_to_track: Vec, +} + +impl PublicationData { + pub fn new( + table_to_track: Option>, + transaction_to_track: Vec, + ) -> PublicationData { + PublicationData { table_to_track, transaction_to_track } + } +} + +// Slot list struct +#[derive(FromRow, Debug, Serialize)] +pub struct SlotList { + pub slot_name: Option, + pub active: Option, +} + +// Slot struct +#[derive(Debug, Serialize, Deserialize)] +pub struct Slot { + pub name: String, +} + +// Template script struct +#[derive(Debug, Deserialize)] +pub struct TemplateScript { + pub postgres_resource_path: String, + #[serde(deserialize_with = "check_if_valid_relation")] + pub relations: Option>, + pub language: Language, +} + +// Language enum +#[derive(Deserialize, Debug)] +pub enum Language { + #[serde(rename = "typescript", alias = "Typescript")] + Typescript, +} + +// Test postgres struct +#[derive(Serialize, Deserialize)] +pub struct TestPostgres { + pub postgres_resource_path: String, +} + +// PostgreSQL publication replication struct +#[derive(Serialize, Deserialize)] +pub struct PostgresPublicationReplication { + pub publication_name: String, + pub replication_slot_name: String, +} + +impl PostgresPublicationReplication { + pub fn new( + publication_name: String, + replication_slot_name: String, + ) -> PostgresPublicationReplication { + PostgresPublicationReplication { publication_name, replication_slot_name } + } +} + +pub const ERROR_PUBLICATION_NAME_NOT_EXISTS: &str = r#"The publication associated with this trigger no longer exists. Recreate a new publication or select an existing one in the advanced tab, or delete and recreate a new trigger"#; + +#[derive(FromRow, Serialize, Deserialize, Debug)] +pub struct Postgres { + pub user: String, + pub password: String, + pub host: String, + pub port: Option, + pub dbname: String, + #[serde(default)] + pub sslmode: String, + #[serde(default, deserialize_with = "empty_as_none")] + pub root_certificate_pem: Option, +} + +#[derive(Debug, Clone, FromRow, Serialize, Deserialize)] +pub struct TableToTrack { + pub table_name: String, + #[serde(default, deserialize_with = "empty_as_none")] + pub where_clause: Option, + #[serde(default, deserialize_with = "empty_as_none")] + pub columns_name: Option>, +} + +impl TableToTrack { + pub fn new( + table_name: String, + where_clause: Option, + columns_name: Option>, + ) -> TableToTrack { + TableToTrack { table_name, where_clause, columns_name } + } +} + +#[derive(Debug, Clone, FromRow, Serialize, Deserialize)] +pub struct Relations { + pub schema_name: String, + pub table_to_track: Vec, +} + +impl Relations { + pub fn new(schema_name: String, table_to_track: Vec) -> Relations { + Relations { schema_name, table_to_track } + } + + pub fn add_new_table(&mut self, table_to_track: TableToTrack) { + self.table_to_track.push(table_to_track); + } +} + +fn build_tls_connector( + ssl_mode: SslMode, + root_certificate_pem: Option<&String>, +) -> Result> { + let get_tls_builder_for_verify = |root_certificate: Option<&String>| { + let mut builder = TlsConnector::builder(); + if let Some(root_certificate) = root_certificate { + let root_certificate_pem = + Certificate::from_pem(root_certificate.as_bytes()).map_err(to_anyhow)?; + builder.add_root_certificate(root_certificate_pem); + } + Ok::<_, Error>(builder) + }; + let connector = match ssl_mode { + SslMode::Disable => return Ok(None), + SslMode::Require | SslMode::Prefer => { + let mut builder = TlsConnector::builder(); + builder.danger_accept_invalid_certs(true); + builder.danger_accept_invalid_hostnames(true); + builder + } + + SslMode::VerifyCa => { + let mut builder = get_tls_builder_for_verify(root_certificate_pem)?; + builder.danger_accept_invalid_hostnames(true); + builder + } + + SslMode::VerifyFull => { + let builder = get_tls_builder_for_verify(root_certificate_pem)?; + builder + } + _ => unreachable!(), + }; + + Ok(Some(MakeTlsConnector::new( + connector.build().map_err(to_anyhow)?, + ))) +} + +pub async fn get_raw_postgres_connection( + database: &Postgres, + logical_mode: bool, +) -> Result { + let ssl_mode = match database.sslmode.as_ref() { + "disable" => SslMode::Disable, + "" | "prefer" | "allow" => SslMode::Prefer, + "require" => SslMode::Require, + "verify-ca" => SslMode::VerifyCa, + "verify-full" => SslMode::VerifyFull, + ssl_mode => { + return Err(Error::BadRequest( + format!("Invalid ssl mode for postgres: {}, please put a valid ssl_mode among the following available ssl mode: ['disable', 'allow', 'prefer', 'verify-ca', 'verify-full']", ssl_mode), + )) + } + }; + + let mut config = Config::new(); + config + .dbname(&database.dbname) + .host(&database.host) + .user(&database.user) + .ssl_mode(ssl_mode); + + if logical_mode { + config.replication_mode(rust_postgres::config::ReplicationMode::Logical); + } + + if let Some(port) = database.port { + config.port(port); + }; + + if !database.password.is_empty() { + config.password(&database.password); + } + + let connector = build_tls_connector(ssl_mode, database.root_certificate_pem.as_ref())?; + + let client = if let Some(connector) = connector { + let (client, connection) = config.connect(connector).await.map_err(to_anyhow)?; + tokio::spawn(async move { + if let Err(e) = connection.await { + tracing::debug!("{:#?}", e); + }; + tracing::info!("Successfully Connected into database"); + }); + client + } else { + let (client, connection) = config.connect(NoTls).await.map_err(to_anyhow)?; + tokio::spawn(async move { + if let Err(e) = connection.await { + tracing::debug!("{:#?}", e); + }; + tracing::info!("Successfully Connected into database"); + }); + client + }; + + Ok(client) +} + +pub async fn get_pg_connection( + authed: ApiAuthed, + user_db: Option, + db: &DB, + postgres_resource_path: &str, + w_id: &str, + logical_mode: bool, +) -> Result { + let database = + try_get_resource_from_db_as::(&authed, user_db, db, postgres_resource_path, w_id) + .await?; + + Ok(get_raw_postgres_connection(&database, logical_mode).await?) +} + +pub async fn get_default_pg_connection( + authed: ApiAuthed, + user_db: Option, + db: &DB, + postgres_resource_path: &str, + w_id: &str, +) -> Result { + get_pg_connection(authed, user_db, db, postgres_resource_path, w_id, false).await +} + +pub async fn create_logical_replication_slot(tx: &Client, slot_name: &str) -> Result<()> { + tx.execute( + &format!("SELECT * FROM pg_create_logical_replication_slot($1, 'pgoutput')"), + &[&slot_name], + ) + .await + .map_err(to_anyhow)?; + Ok(()) +} + +pub async fn check_if_valid_publication_for_postgres_version( + pg_connection: &Client, + table_to_track: Option<&[Relations]>, +) -> Result { + use crate::triggers::postgres::handler::get_postgres_version_internal; + + let postgres_version = get_postgres_version_internal(pg_connection).await?; + + let pg_14 = postgres_version.starts_with("14"); + if pg_14 { + let unsupported_publication = table_to_track + .and_then(|relations| { + relations.iter().find(|relation| { + let invalid_relation = relation.table_to_track.iter().find(|table_to_track| { + table_to_track.where_clause.is_some() + || table_to_track.columns_name.is_some() + }); + + relation.table_to_track.is_empty() || invalid_relation.is_some() + }) + }) + .is_some(); + + if unsupported_publication { + return Err(Error::BadRequest( + "Your PostgreSQL database is running version 14, which does not support the following publication features: \ + - WHERE clause filtering, \ + - selective column tracking, and \ + - tracking all tables within a schema.\n\ + These features are only available in PostgreSQL 15 and above.".to_string(), + )); + } + } + Ok(pg_14) +} + +pub async fn create_pg_publication( + pg_connection: &Client, + publication_name: &str, + table_to_track: Option<&[Relations]>, + transaction_to_track: &[String], +) -> Result<()> { + let pg_14 = + check_if_valid_publication_for_postgres_version(pg_connection, table_to_track).await?; + let mut query = String::from("CREATE PUBLICATION "); + + query.push_str("e_identifier(publication_name)); + + match table_to_track { + Some(database_component) if !database_component.is_empty() => { + query.push_str(" FOR"); + let mut first = true; + for (i, schema) in database_component.iter().enumerate() { + if schema.table_to_track.is_empty() { + query.push_str(" TABLES IN SCHEMA "); + query.push_str("e_identifier(&schema.schema_name)); + } else { + if pg_14 && first { + query.push_str(" TABLE ONLY "); + first = false + } else if !pg_14 { + query.push_str(" TABLE ONLY "); + } + for (j, table) in schema.table_to_track.iter().enumerate() { + let table_name = quote_identifier(&table.table_name); + let schema_name = quote_identifier(&schema.schema_name); + let full_name = format!("{}.{}", &schema_name, &table_name); + query.push_str(&full_name); + if let Some(columns) = table.columns_name.as_ref() { + query.push_str(" ("); + let columns = columns + .iter() + .map(|column| quote_identifier(column)) + .join(", "); + query.push_str(&columns); + query.push_str(")"); + } + + if let Some(where_clause) = &table.where_clause { + query.push_str(" WHERE ("); + query.push_str(where_clause); + query.push(')'); + } + + if j + 1 != schema.table_to_track.len() { + query.push_str(", "); + } + } + } + if i < database_component.len() - 1 { + query.push_str(", "); + } + } + } + _ => { + query.push_str(" FOR ALL TABLES "); + } + }; + + if !transaction_to_track.is_empty() { + let transactions = || transaction_to_track.iter().join(", "); + query.push_str(" WITH (publish = '"); + query.push_str(&transactions()); + query.push_str("');"); + } + + pg_connection + .execute(&query, &[]) + .await + .map_err(to_anyhow)?; + Ok(()) +} + +pub async fn drop_publication(pg_connection: &Client, publication_name: &str) -> Result<()> { + let mut query = String::from("DROP PUBLICATION IF EXISTS "); + let quoted_publication_name = quote_identifier(publication_name); + query.push_str("ed_publication_name); + + pg_connection + .execute(&query, &[]) + .await + .map_err(to_anyhow)?; + + Ok(()) +} + +pub fn generate_random_string() -> String { + let timestamp = Utc::now().timestamp_millis().to_string(); + let mut rng = rand::rng(); + let charset = "abcdefghijklmnopqrstuvwxyz0123456789"; + + let random_part = (0..10) + .map(|_| { + charset + .chars() + .nth(rng.random_range(0..charset.len())) + .unwrap() + }) + .collect::(); + + format!("{}_{}", timestamp, random_part) +} diff --git a/backend/windmill-api/src/triggers/sqs/handler_oss.rs b/backend/windmill-api/src/triggers/sqs/handler_oss.rs new file mode 100644 index 0000000000..53365def58 --- /dev/null +++ b/backend/windmill-api/src/triggers/sqs/handler_oss.rs @@ -0,0 +1,66 @@ +#[cfg(feature = "private")] +#[allow(unused)] +pub use super::handler_ee::*; + +#[cfg(not(feature = "private"))] +use { + super::SqsTrigger, + crate::{ + db::{ApiAuthed, DB}, + triggers::{Trigger, TriggerCrud, TriggerData}, + }, + axum::async_trait, + sqlx::PgConnection, + windmill_common::error::{Error, Result}, + windmill_git_sync::DeployedObject, +}; + +#[cfg(not(feature = "private"))] +#[async_trait] +#[cfg(not(feature = "private"))] +impl TriggerCrud for SqsTrigger { + type Trigger = Trigger; + type TriggerConfig = (); + type TriggerConfigRequest = (); + type TestConnectionConfig = (); + + const TABLE_NAME: &'static str = ""; + const TRIGGER_TYPE: &'static str = ""; + const SUPPORTS_ENABLED: bool = false; + const SUPPORTS_SERVER_STATE: bool = false; + const SUPPORTS_TEST_CONNECTION: bool = false; + const ROUTE_PREFIX: &'static str = "/sqs_triggers"; + const DEPLOYMENT_NAME: &'static str = ""; + const IS_CLOUD_HOSTED: bool = false; + + fn get_deployed_object(path: String) -> DeployedObject { + DeployedObject::SqsTrigger { path } + } + + async fn create_trigger( + &self, + _db: &DB, + _tx: &mut PgConnection, + _authed: &ApiAuthed, + _w_id: &str, + _trigger: TriggerData, + ) -> Result<()> { + Err(Error::BadRequest( + "SQS triggers are not available in open source version".to_string(), + )) + } + + async fn update_trigger( + &self, + _db: &DB, + _tx: &mut PgConnection, + _authed: &ApiAuthed, + _workspace_id: &str, + _path: &str, + _trigger: TriggerData, + ) -> Result<()> { + Err(Error::BadRequest( + "SQS triggers are not available in open source version".to_string(), + )) + } +} diff --git a/backend/windmill-api/src/triggers/sqs/mod.rs b/backend/windmill-api/src/triggers/sqs/mod.rs new file mode 100644 index 0000000000..74a93c12a5 --- /dev/null +++ b/backend/windmill-api/src/triggers/sqs/mod.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "private")] +mod handler_ee; +pub mod handler_oss; + +#[cfg(feature = "private")] +mod mod_ee; +#[cfg(feature = "private")] +pub use mod_ee::*; + +#[derive(Copy, Clone)] +pub struct SqsTrigger; diff --git a/backend/windmill-api/src/triggers/trigger_helpers.rs b/backend/windmill-api/src/triggers/trigger_helpers.rs new file mode 100644 index 0000000000..21a698b62d --- /dev/null +++ b/backend/windmill-api/src/triggers/trigger_helpers.rs @@ -0,0 +1,811 @@ +use anyhow::Context; +use axum::response::IntoResponse; +use http::StatusCode; +use serde::Deserialize; +use serde_json::value::RawValue; +use sqlx::types::Json; +use std::collections::HashMap; +use std::future::Future; +use uuid::Uuid; +use windmill_common::{ + db::UserDB, + error::Result, + flows::{FlowModuleValue, Retry}, + get_latest_deployed_hash_for_path, get_latest_flow_version_info_for_path, + jobs::{get_has_preprocessor_from_content_and_lang, script_path_to_payload, JobPayload}, + scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang}, + triggers::{ + HubOrWorkspaceId, RunnableFormat, RunnableFormatVersion, TriggerKind, + RUNNABLE_FORMAT_VERSION_CACHE, + }, + users::username_to_permissioned_as, + utils::StripPath, + worker::to_raw_value, + FlowVersionInfo, +}; +use windmill_queue::{push, PushArgs, PushArgsOwned, PushIsolationLevel}; + +#[cfg(feature = "enterprise")] +use crate::jobs::check_license_key_valid; +use crate::{ + db::{ApiAuthed, DB}, + jobs::{ + check_tag_available_for_workspace, delete_job_metadata_after_use, result_to_response, + run_flow_by_path_inner, run_script_by_path_inner, run_wait_result_internal, RunJobQuery, + }, + utils::check_scopes, + HTTP_CLIENT, +}; + +struct ScriptInfo { + has_preprocessor: Option, + language: ScriptLang, + content: String, + schema: Option>, +} + +#[derive(Debug, Deserialize)] +struct PropertyDefinition { + r#type: Option, +} + +#[derive(Debug, Deserialize)] +struct PartialSchema { + properties: Option>, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum RunnableId { + FlowPath(String), + ScriptId(ScriptId), + HubScript(String), +} + +impl RunnableId { + pub fn from_script_hash(hash: ScriptHash) -> Self { + Self::ScriptId(ScriptId::ScriptHash(hash)) + } + + pub fn from_script_path(path: &str) -> Self { + if path.starts_with("hub/") { + Self::HubScript(path.to_string()) + } else { + Self::ScriptId(ScriptId::ScriptPath(path.to_string())) + } + } + + pub fn from_flow_path(path: &str) -> Self { + Self::FlowPath(path.to_string()) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum ScriptId { + ScriptPath(String), + ScriptHash(ScriptHash), +} + +impl ScriptId { + async fn get_script_hash(self, workspace_id: &str, db: &DB) -> Result { + let hash = match self { + ScriptId::ScriptPath(path) => { + let info = get_latest_deployed_hash_for_path(db, workspace_id, &path).await?; + info.hash + } + ScriptId::ScriptHash(hash) => hash.0, + }; + + Ok(hash) + } +} + +async fn get_script_info( + db: &DB, + workspace_id: &str, + hash: i64, +) -> std::result::Result { + sqlx::query_as!(ScriptInfo, "SELECT has_preprocessor, language as \"language: _\", content, schema as \"schema: _\" FROM script WHERE workspace_id = $1 AND hash = $2", workspace_id, hash) + .fetch_one(db) + .await +} + +fn runnable_format_from_schema_without_preprocessor( + trigger_kind: &TriggerKind, + schema: Option>, +) -> RunnableFormat { + match trigger_kind { + TriggerKind::Mqtt + if schema.as_ref().is_some_and(|schema| { + schema.properties.as_ref().is_some_and(|properties| { + properties.iter().any(|(key, def)| { + key == "payload" && def.r#type.as_ref().is_some_and(|t| t == "array") + }) + }) + }) => + { + RunnableFormat { version: RunnableFormatVersion::V1, has_preprocessor: false } + } + TriggerKind::Kafka | TriggerKind::Nats + if schema.as_ref().is_some_and(|schema| { + schema + .properties + .as_ref() + .is_some_and(|properties| properties.keys().any(|key| key == "msg")) + }) => + { + RunnableFormat { version: RunnableFormatVersion::V1, has_preprocessor: false } + } + _ => RunnableFormat { version: RunnableFormatVersion::V2, has_preprocessor: false }, + } +} + +fn runnable_format_from_preprocessor_args( + args: Option>, +) -> RunnableFormat { + if let Some(args) = args { + if args.iter().any(|arg| arg.name == "wm_trigger") + || (args.len() > 0 && args.iter().all(|arg| arg.name != "event")) + { + RunnableFormat { version: RunnableFormatVersion::V1, has_preprocessor: true } + } else { + RunnableFormat { version: RunnableFormatVersion::V2, has_preprocessor: true } + } + } else { + RunnableFormat { version: RunnableFormatVersion::V2, has_preprocessor: true } + } +} + +enum PreprocessorInfo { + Preprocessor { content: String, language: ScriptLang }, + NoPreprocessor { schema: Option> }, +} + +#[derive(Debug, Deserialize)] +struct FlowInfo { + preprocessor_module: Option>, + schema: Option>, +} + +fn get_preprocessor_args_from_content_and_language( + content: &str, + language: &ScriptLang, +) -> Result>> { + let args = match language { + ScriptLang::Bun | ScriptLang::Bunnative | ScriptLang::Deno | ScriptLang::Nativets => { + let args = windmill_parser_ts::parse_deno_signature( + &content, + true, + false, + Some("preprocessor".to_string()), + )?; + Some(args.args) + } + ScriptLang::Python3 => { + let args = windmill_parser_py::parse_python_signature( + &content, + Some("preprocessor".to_string()), + false, + )?; + Some(args.args) + } + _ => None, + }; + Ok(args) +} + +pub async fn get_runnable_format( + runnable_id: RunnableId, + workspace_id: &str, + db: &DB, + trigger_kind: &TriggerKind, +) -> Result { + let (key, preprocessor_info) = match runnable_id { + RunnableId::HubScript(path) => { + let Some(version) = path.split("/").nth(1) else { + return Err(windmill_common::error::Error::internal_err( + "Invalid hub script path".to_string(), + )); + }; + + let version = match version.parse::() { + Ok(version) => version, + Err(_) => { + return Err(windmill_common::error::Error::internal_err( + "Invalid hub script version".to_string(), + )); + } + }; + + let key = (HubOrWorkspaceId::Hub, version, trigger_kind.clone()); + + let runnable_format = RUNNABLE_FORMAT_VERSION_CACHE.get(&key); + + if let Some(runnable_format) = runnable_format { + tracing::debug!("Using cached runnable format for hub script {path}"); + return Ok(runnable_format); + } + + let hub_script = + get_full_hub_script_by_path(StripPath(path.to_string()), &HTTP_CLIENT, Some(db)) + .await?; + + let has_preprocessor = get_has_preprocessor_from_content_and_lang( + &hub_script.content, + &hub_script.language, + )?; + + let partial_schema = serde_json::from_str(hub_script.schema.get())?; + + ( + key, + if has_preprocessor { + PreprocessorInfo::Preprocessor { + content: hub_script.content, + language: hub_script.language, + } + } else { + PreprocessorInfo::NoPreprocessor { + schema: Some(sqlx::types::Json(partial_schema)), + } + }, + ) + } + RunnableId::FlowPath(path) => { + let FlowVersionInfo { version, .. } = + get_latest_flow_version_info_for_path(db, workspace_id, &path, true).await?; + + let key = ( + HubOrWorkspaceId::WorkspaceId(workspace_id.to_string()), + version, + trigger_kind.clone(), + ); + + let runnable_format = RUNNABLE_FORMAT_VERSION_CACHE.get(&key); + + if let Some(runnable_format) = runnable_format { + tracing::debug!("Using cached runnable format for flow {path}"); + return Ok(runnable_format); + } + + let flow_info = sqlx::query_as!( + FlowInfo, + "SELECT + value->'preprocessor_module'->'value' as \"preprocessor_module: _\", + schema as \"schema: _\" + FROM flow_version + WHERE + path = $1 + AND workspace_id = $2 + ORDER BY created_at DESC + LIMIT 1", + path, + workspace_id, + ) + .fetch_one(db) + .await?; + + if let Some(preprocessor_module) = flow_info.preprocessor_module { + match preprocessor_module.0 { + FlowModuleValue::RawScript { content, language, .. } => { + (key, PreprocessorInfo::Preprocessor { content, language }) + } + FlowModuleValue::Script { path, hash, .. } => { + let hash = if let Some(hash) = hash { + hash.0 + } else { + let script_hash = + get_latest_deployed_hash_for_path(db, workspace_id, &path).await?; + script_hash.hash + }; + let script_info = get_script_info(db, workspace_id, hash).await?; + ( + key, + PreprocessorInfo::Preprocessor { + content: script_info.content, + language: script_info.language, + }, + ) + } + _ => { + return Err(windmill_common::error::Error::internal_err( + "Unsupported preprocessor module".to_string(), + )); + } + } + } else { + ( + key, + PreprocessorInfo::NoPreprocessor { schema: flow_info.schema }, + ) + } + } + RunnableId::ScriptId(script_id) => { + let hash = script_id.get_script_hash(workspace_id, db).await?; + let key = ( + HubOrWorkspaceId::WorkspaceId(workspace_id.to_string()), + hash, + trigger_kind.clone(), + ); + let runnable_format = RUNNABLE_FORMAT_VERSION_CACHE.get(&key); + + if let Some(runnable_format) = runnable_format { + tracing::debug!("Using cached runnable format for script {hash}"); + return Ok(runnable_format); + } + + let script_info = get_script_info(db, workspace_id, hash).await?; + + if script_info.has_preprocessor.unwrap_or(false) { + ( + key, + PreprocessorInfo::Preprocessor { + content: script_info.content, + language: script_info.language, + }, + ) + } else { + ( + key, + PreprocessorInfo::NoPreprocessor { schema: script_info.schema }, + ) + } + } + }; + + let runnable_format = match preprocessor_info { + PreprocessorInfo::Preprocessor { content, language } => { + let args = get_preprocessor_args_from_content_and_language(&content, &language)?; + runnable_format_from_preprocessor_args(args) + } + PreprocessorInfo::NoPreprocessor { schema } => { + runnable_format_from_schema_without_preprocessor(trigger_kind, schema) + } + }; + + RUNNABLE_FORMAT_VERSION_CACHE.insert(key, runnable_format); + + Ok(runnable_format) +} + +#[allow(dead_code)] + +pub trait TriggerJobArgs { + type Payload: Send + Sync; + const TRIGGER_KIND: TriggerKind; + + fn v1_payload_fn(payload: &Self::Payload) -> HashMap>; + fn v2_payload_fn(payload: &Self::Payload) -> HashMap> { + Self::v1_payload_fn(payload) + } + + fn build_job_args_v2( + has_preprocessor: bool, + payload: &Self::Payload, + info: HashMap>, + ) -> PushArgsOwned { + let mut args = Self::v2_payload_fn(payload); + if has_preprocessor { + args.insert( + "kind".to_string(), + to_raw_value(&Self::TRIGGER_KIND.to_key()), + ); + args.extend(info); + let args = HashMap::from([("event".to_string(), to_raw_value(&args))]); + PushArgsOwned { args, extra: None } + } else { + PushArgsOwned { args, extra: None } + } + } + + fn build_job_args_v1( + has_preprocessor: bool, + payload: &Self::Payload, + info: HashMap>, + ) -> PushArgsOwned { + let trigger_key = Self::TRIGGER_KIND.to_key(); + let args = Self::v1_payload_fn(payload); + let extra = if has_preprocessor { + Some(HashMap::from([( + "wm_trigger".to_string(), + to_raw_value(&serde_json::json!({ + "kind": trigger_key, + trigger_key: info + })), + )])) + } else { + None + }; + PushArgsOwned { args, extra } + } + + fn build_job_args( + runnable_path: &str, + is_flow: bool, + w_id: &str, + db: &DB, + payload: Self::Payload, + info: HashMap>, + ) -> impl Future> + Send { + async move { + let runnable_id = if is_flow { + RunnableId::from_flow_path(runnable_path) + } else { + RunnableId::from_script_path(runnable_path) + }; + Self::build_job_args_from_runnable_id(runnable_id, w_id, db, payload, info).await + } + } + + fn build_job_args_from_runnable_id( + runnable_id: RunnableId, + w_id: &str, + db: &DB, + payload: Self::Payload, + trigger_info: HashMap>, + ) -> impl Future> + Send { + async move { + let runnable_format = + get_runnable_format(runnable_id, w_id, db, &Self::TRIGGER_KIND).await?; + let job_args = match runnable_format { + RunnableFormat { version: RunnableFormatVersion::V1, has_preprocessor } => { + Self::build_job_args_v1(has_preprocessor, &payload, trigger_info) + } + RunnableFormat { version: RunnableFormatVersion::V2, has_preprocessor } => { + Self::build_job_args_v2(has_preprocessor, &payload, trigger_info) + } + }; + Ok(job_args) + } + } + + fn build_capture_payloads( + payload: &Self::Payload, + info: HashMap>, + ) -> (PushArgsOwned, PushArgsOwned) { + let main_args = Self::build_job_args_v2(false, payload, info.clone()); + let preprocessor_args = Self::build_job_args_v2(true, payload, info); + (main_args, preprocessor_args) + } +} + +#[allow(dead_code)] +async fn trigger_runnable_inner( + db: &DB, + user_db: Option, + authed: ApiAuthed, + workspace_id: &str, + runnable_path: &str, + is_flow: bool, + args: PushArgsOwned, + retry: Option<&sqlx::types::Json>, + error_handler_path: Option<&str>, + error_handler_args: Option<&sqlx::types::Json>>, + trigger_path: String, +) -> Result<(Uuid, Option, Option)> { + let error_handler_args = error_handler_args.map(|args| { + let args = args + .0 + .iter() + .map(|(key, value)| (key.to_owned(), to_raw_value(&value))) + .collect::>>(); + Json(args) + }); + + let user_db = user_db.unwrap_or_else(|| UserDB::new(db.clone())); + let (uuid, delete_after_use, early_return) = if is_flow { + let run_query = RunJobQuery::default(); + let path = StripPath(runnable_path.to_string()); + let (uuid, early_return) = run_flow_by_path_inner( + authed, + db.clone(), + user_db, + workspace_id.to_string(), + path, + run_query, + args, + ) + .await?; + (uuid, None, early_return) + } else { + let (uuid, delete_after_use) = trigger_script_internal( + db, + user_db, + authed, + workspace_id, + runnable_path, + args, + retry, + error_handler_path, + error_handler_args.as_ref(), + trigger_path, + ) + .await?; + (uuid, delete_after_use, None) + }; + + Ok((uuid, delete_after_use, early_return)) +} + +#[allow(dead_code)] +pub async fn trigger_runnable( + db: &DB, + user_db: Option, + authed: ApiAuthed, + workspace_id: &str, + runnable_path: &str, + is_flow: bool, + args: PushArgsOwned, + retry: Option<&sqlx::types::Json>, + error_handler_path: Option<&str>, + error_handler_args: Option<&sqlx::types::Json>>, + trigger_path: String, +) -> Result { + let (uuid, _, _) = trigger_runnable_inner( + db, + user_db, + authed, + workspace_id, + runnable_path, + is_flow, + args, + retry, + error_handler_path, + error_handler_args, + trigger_path, + ) + .await?; + Ok((StatusCode::CREATED, uuid.to_string()).into_response()) +} + +#[allow(dead_code)] +pub async fn trigger_runnable_and_wait_for_result( + db: &DB, + user_db: Option, + authed: ApiAuthed, + workspace_id: &str, + runnable_path: &str, + is_flow: bool, + args: PushArgsOwned, + retry: Option<&sqlx::types::Json>, + error_handler_path: Option<&str>, + error_handler_args: Option<&sqlx::types::Json>>, + trigger_path: String, +) -> Result { + let username = authed.username.clone(); + let (uuid, delete_after_use, early_return) = trigger_runnable_inner( + db, + user_db, + authed, + workspace_id, + runnable_path, + is_flow, + args, + retry, + error_handler_path, + error_handler_args, + trigger_path, + ) + .await?; + let (result, success) = + run_wait_result_internal(db, uuid, workspace_id.to_string(), early_return, &username) + .await?; + + if delete_after_use.unwrap_or(false) { + delete_job_metadata_after_use(&db, uuid).await?; + } + + result_to_response(result, success) +} + +#[allow(dead_code)] +pub async fn trigger_runnable_and_wait_for_raw_result( + db: &DB, + user_db: Option, + authed: ApiAuthed, + workspace_id: &str, + runnable_path: &str, + is_flow: bool, + args: PushArgsOwned, + retry: Option<&sqlx::types::Json>, + error_handler_path: Option<&str>, + error_handler_args: Option<&sqlx::types::Json>>, + trigger_path: String, +) -> Result> { + let username = authed.username.clone(); + let (uuid, delete_after_use, early_return) = trigger_runnable_inner( + db, + user_db, + authed, + workspace_id, + runnable_path, + is_flow, + args, + retry, + error_handler_path, + error_handler_args, + trigger_path, + ) + .await?; + + let (result, success) = + run_wait_result_internal(db, uuid, workspace_id.to_string(), early_return, &username) + .await + .with_context(|| { + format!( + "Error fetching job result for {} {}", + if is_flow { "flow" } else { "script" }, + runnable_path + ) + })?; + + if delete_after_use.unwrap_or(false) { + delete_job_metadata_after_use(&db, uuid).await?; + } + + if !success { + Err(windmill_common::error::Error::internal_err(format!( + "{} {runnable_path} failed: {:?}", + if is_flow { "Flow" } else { "Script" }, + result + ))) + } else { + Ok(result) + } +} + +async fn trigger_script_internal( + db: &DB, + user_db: UserDB, + authed: ApiAuthed, + workspace_id: &str, + script_path: &str, + args: PushArgsOwned, + retry: Option<&sqlx::types::Json>, + error_handler_path: Option<&str>, + error_handler_args: Option<&sqlx::types::Json>>>, + trigger_path: String, +) -> Result<(Uuid, Option)> { + if retry.is_none() && error_handler_path.is_none() { + let run_query = RunJobQuery::default(); + let path = StripPath(script_path.to_string()); + run_script_by_path_inner( + authed, + db.clone(), + user_db, + workspace_id.to_string(), + path, + run_query, + args, + ) + .await + } else { + trigger_script_with_retry_and_error_handler( + db, + user_db, + authed, + workspace_id, + script_path, + args, + retry, + error_handler_path, + error_handler_args, + trigger_path, + ) + .await + } +} + +async fn trigger_script_with_retry_and_error_handler( + db: &DB, + user_db: UserDB, + authed: ApiAuthed, + workspace_id: &str, + script_path: &str, + args: PushArgsOwned, + retry: Option<&sqlx::types::Json>, + error_handler_path: Option<&str>, + error_handler_args: Option<&sqlx::types::Json>>>, + trigger_path: String, +) -> Result<(Uuid, Option)> { + #[cfg(feature = "enterprise")] + check_license_key_valid().await?; + + check_scopes(&authed, || format!("jobs:run:scripts:{script_path}"))?; + + let retry = retry.map(|r| r.0.clone()); + let error_handler_path = error_handler_path.map(|p| p.to_string()); + let error_handler_args = error_handler_args.map(|args| args.0.clone()); + + let (job_payload, tag, delete_after_use, timeout, on_behalf_of) = { + let mut tx = user_db.clone().begin(&authed).await?; + script_path_to_payload(script_path, &mut *tx, &workspace_id, Some(false)).await? + }; + + check_tag_available_for_workspace(&db, &workspace_id, &tag, &authed).await?; + + let (email, permissioned_as, push_authed, tx) = + if let Some(on_behalf_of) = on_behalf_of.as_ref() { + ( + on_behalf_of.email.as_str(), + on_behalf_of.permissioned_as.clone(), + None, + PushIsolationLevel::IsolatedRoot(db.clone()), + ) + } else { + ( + authed.email.as_str(), + username_to_permissioned_as(&authed.username), + Some(authed.clone().into()), + PushIsolationLevel::Isolated(user_db, authed.clone().into()), + ) + }; + + let push_args = PushArgs { args: &args.args, extra: args.extra }; + + let retryable_job_payload = match job_payload { + JobPayload::ScriptHash { + hash, + path, + custom_concurrency_key, + concurrent_limit, + concurrency_time_window_s, + cache_ttl, + priority, + apply_preprocessor, + .. + } => JobPayload::SingleScriptFlow { + path, + hash, + args: HashMap::from(&push_args), + retry, + error_handler_path, + error_handler_args, + custom_concurrency_key, + concurrent_limit, + concurrency_time_window_s, + cache_ttl, + priority, + tag_override: tag.clone(), + apply_preprocessor, + trigger_path: Some(trigger_path), + }, + _ => { + return Err(windmill_common::error::Error::internal_err(format!( + "Unsupported job payload: {:?}", + job_payload + ))) + } + }; + + let (uuid, tx) = push( + &db, + tx, + &workspace_id, + retryable_job_payload, + push_args, + authed.display_username(), + email, + permissioned_as, + authed.token_prefix.as_deref(), + None, + None, + None, + None, + None, + None, + false, + false, + None, + true, + tag, + timeout, + None, + None, + push_authed.as_ref(), + false, + ) + .await?; + tx.commit().await?; + + Ok((uuid, delete_after_use)) +} diff --git a/backend/windmill-api/src/triggers/websocket/handler.rs b/backend/windmill-api/src/triggers/websocket/handler.rs new file mode 100644 index 0000000000..0a321a450c --- /dev/null +++ b/backend/windmill-api/src/triggers/websocket/handler.rs @@ -0,0 +1,258 @@ +use std::borrow::Cow; + +use crate::{ + db::{ApiAuthed, DB}, + triggers::{Trigger, TriggerCrud, TriggerData}, +}; +use axum::async_trait; +use itertools::Itertools; +use serde_json::value::RawValue; +use sqlx::{types::Json as SqlxJson, PgConnection}; +use tokio_tungstenite::connect_async; +use windmill_common::{ + db::UserDB, + error::{Error, Result}, +}; +use windmill_git_sync::DeployedObject; + +use super::{ + get_url_from_runnable_value, TestWebsocketConfig, WebsocketConfig, WebsocketConfigRequest, + WebsocketTrigger, +}; + +#[async_trait] +impl TriggerCrud for WebsocketTrigger { + type TriggerConfig = WebsocketConfig; + type Trigger = Trigger; + type TriggerConfigRequest = WebsocketConfigRequest; + type TestConnectionConfig = TestWebsocketConfig; + + const TABLE_NAME: &'static str = "websocket_trigger"; + const TRIGGER_TYPE: &'static str = "websocket"; + const SUPPORTS_ENABLED: bool = true; + const SUPPORTS_SERVER_STATE: bool = true; + const SUPPORTS_TEST_CONNECTION: bool = true; + const ROUTE_PREFIX: &'static str = "/websocket_triggers"; + const DEPLOYMENT_NAME: &'static str = "WebSocket trigger"; + const ADDITIONAL_SELECT_FIELDS: &[&'static str] = &[ + "url", + "filters", + "initial_messages", + "url_runnable_args", + "can_return_message", + ]; + const IS_ALLOWED_ON_CLOUD: bool = false; + + fn get_deployed_object(path: String) -> DeployedObject { + DeployedObject::WebsocketTrigger { path } + } + + async fn validate_config( + &self, + _db: &DB, + config: &Self::TriggerConfigRequest, + _workspace_id: &str, + ) -> Result<()> { + if config.url.trim().is_empty() { + return Err(Error::BadRequest( + "WebSocket URL cannot be empty".to_string(), + )); + } + + if let Some(args) = &config.url_runnable_args { + if !args.is_object() { + return Err(Error::BadRequest( + "url_runnable_args must be an object".to_string(), + )); + } + } + + Ok(()) + } + + async fn create_trigger( + &self, + _db: &DB, + tx: &mut PgConnection, + authed: &ApiAuthed, + w_id: &str, + trigger: TriggerData, + ) -> Result<()> { + let filters = trigger + .config + .filters + .into_iter() + .map(|v| SqlxJson(serde_json::value::to_raw_value(&v).unwrap())) + .collect_vec(); + let initial_messages = trigger + .config + .initial_messages + .unwrap_or_default() + .into_iter() + .map(|v| SqlxJson(serde_json::value::to_raw_value(&v).unwrap())) + .collect_vec(); + sqlx::query!( + r#" + INSERT INTO websocket_trigger ( + workspace_id, + path, + url, + script_path, + is_flow, + enabled, + filters, + initial_messages, + url_runnable_args, + edited_by, + can_return_message, + email, + edited_at, + error_handler_path, + error_handler_args, + retry + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13, $14, $15 + ) + "#, + w_id, + trigger.base.path, + trigger.config.url, + trigger.base.script_path, + trigger.base.is_flow, + trigger.base.enabled.unwrap_or(true), + &filters as _, + &initial_messages as _, + trigger + .config + .url_runnable_args + .map(|v| SqlxJson(serde_json::value::to_raw_value(&v).unwrap())) as _, + authed.username, + trigger.config.can_return_message, + authed.email, + trigger.error_handling.error_handler_path, + trigger.error_handling.error_handler_args as _, + trigger.error_handling.retry as _ + ) + .execute(&mut *tx) + .await?; + Ok(()) + } + + async fn update_trigger( + &self, + _db: &DB, + tx: &mut PgConnection, + authed: &ApiAuthed, + w_id: &str, + path: &str, + trigger: TriggerData, + ) -> Result<()> { + let filters = trigger + .config + .filters + .into_iter() + .map(|v| SqlxJson(serde_json::value::to_raw_value(&v).unwrap())) + .collect_vec(); + let initial_messages = trigger + .config + .initial_messages + .unwrap_or_default() + .into_iter() + .map(|v| SqlxJson(serde_json::value::to_raw_value(&v).unwrap())) + .collect_vec(); + + // important to update server_id to NULL to stop current websocket listener + sqlx::query!( + " + UPDATE + websocket_trigger + SET + url = $1, + script_path = $2, + path = $3, + is_flow = $4, + filters = $5, + initial_messages = $6, + url_runnable_args = $7, + edited_by = $8, + email = $9, + can_return_message = $10, + edited_at = now(), + server_id = NULL, + error = NULL, + error_handler_path = $13, + error_handler_args = $14, + retry = $15 + WHERE + workspace_id = $11 AND path = $12 + ", + trigger.config.url, + trigger.base.script_path, + trigger.base.path, + trigger.base.is_flow, + filters.as_slice() as &[SqlxJson>], + initial_messages.as_slice() as &[SqlxJson>], + trigger + .config + .url_runnable_args + .map(|v| SqlxJson(serde_json::value::to_raw_value(&v).unwrap())) + as Option>>, + &authed.username, + &authed.email, + trigger.config.can_return_message, + w_id, + path, + trigger.error_handling.error_handler_path, + trigger.error_handling.error_handler_args as _, + trigger.error_handling.retry as _ + ) + .execute(&mut *tx) + .await?; + + Ok(()) + } + + async fn test_connection( + &self, + db: &DB, + authed: &ApiAuthed, + _user_db: &UserDB, + workspace_id: &str, + config: Self::TestConnectionConfig, + ) -> Result<()> { + let url = &config.url; + + let connect_url: Cow = if url.starts_with("$") { + if url.starts_with("$flow:") || url.starts_with("$script:") { + let path = url.splitn(2, ':').nth(1).unwrap(); + Cow::Owned( + get_url_from_runnable_value( + path, + url.starts_with("$flow:"), + &db, + authed.clone(), + config.url_runnable_args.as_ref(), + &workspace_id, + ) + .await?, + ) + } else { + return Err(Error::BadConfig(format!( + "Invalid WebSocket runnable path: {}", + url + ))); + } + } else { + Cow::Borrowed(&url) + }; + + connect_async(connect_url.as_ref()).await.map_err(|err| { + Error::BadConfig(format!( + "Error connecting to WebSocket: {}", + err.to_string() + )) + })?; + + Ok(()) + } +} diff --git a/backend/windmill-api/src/triggers/websocket/mod.rs b/backend/windmill-api/src/triggers/websocket/mod.rs new file mode 100644 index 0000000000..c367811406 --- /dev/null +++ b/backend/windmill-api/src/triggers/websocket/mod.rs @@ -0,0 +1,121 @@ +use std::collections::HashMap; + +use crate::{ + db::ApiAuthed, + triggers::trigger_helpers::{trigger_runnable_and_wait_for_raw_result, TriggerJobArgs}, +}; +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use sqlx::{types::Json as SqlxJson, FromRow}; +use windmill_common::{ + error::{Error, Result}, + triggers::TriggerKind, + worker::to_raw_value, + DB, +}; +use windmill_queue::PushArgsOwned; + +mod handler; + +#[derive(Copy, Clone)] +pub struct WebsocketTrigger; + +impl TriggerJobArgs for WebsocketTrigger { + type Payload = String; + const TRIGGER_KIND: TriggerKind = TriggerKind::Websocket; + fn v1_payload_fn(payload: &Self::Payload) -> HashMap> { + HashMap::from([("msg".to_string(), to_raw_value(&payload))]) + } +} + +#[derive(Debug, Clone, FromRow, Serialize, Deserialize)] +pub struct WebsocketConfig { + pub url: String, + #[serde(default)] + pub filters: Vec>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub initial_messages: Option>>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub url_runnable_args: Option>>, + #[serde(default)] + pub can_return_message: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebsocketConfigRequest { + url: String, + filters: Vec, + initial_messages: Option>, + url_runnable_args: Option, + can_return_message: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TestWebsocketConfig { + url: String, + url_runnable_args: Option, +} + +pub fn value_to_args_hashmap( + args: Option<&serde_json::Value>, +) -> Result>> { + let args = if let Some(args) = args { + let args_map: Option> = + serde_json::from_value(args.clone()) + .map_err(|e| Error::BadRequest(format!("invalid json: {}", e)))?; + + args_map + .unwrap_or_else(HashMap::new) + .into_iter() + .map(|(k, v)| { + let raw_value = serde_json::value::to_raw_value(&v).map_err(|e| { + Error::BadRequest(format!("failed to convert to raw value: {}", e)) + })?; + Ok((k, raw_value)) + }) + .collect::>>>() + } else { + Ok(HashMap::new()) + }?; + Ok(args) +} + +pub async fn get_url_from_runnable_value( + path: &str, + is_flow: bool, + db: &DB, + authed: ApiAuthed, + args: Option<&serde_json::Value>, + workspace_id: &str, +) -> Result { + tracing::info!( + "Running {} {} to get WebSocket URL", + if is_flow { "flow" } else { "script" }, + path + ); + + let args = value_to_args_hashmap(args)?; + + let result = trigger_runnable_and_wait_for_raw_result( + db, + None, + authed, + workspace_id, + path, + is_flow, + PushArgsOwned { args, extra: None }, + None, + None, + None, + "".to_string(), // doesn't matter as no retry/error handler + ) + .await?; + + serde_json::from_str::(result.get()).map_err(|_| { + Error::BadConfig(format!( + "{} {} did not return a string", + if is_flow { "Flow" } else { "Script" }, + path, + )) + }) +} diff --git a/backend/windmill-api/src/utils.rs b/backend/windmill-api/src/utils.rs index ebbcad481c..2026099141 100644 --- a/backend/windmill-api/src/utils.rs +++ b/backend/windmill-api/src/utils.rs @@ -447,11 +447,12 @@ pub struct ExpiringCacheEntry { pub expiry: std::time::Instant, } +lazy_static::lazy_static! { + static ref DUCKLAKE_INSTANCE_PG_PASSWORD: std::sync::RwLock> = std::sync::RwLock::new(None); +} + #[cfg(all(feature = "kafka", feature = "enterprise", feature = "private"))] pub async fn update_rw_lock(lock: std::sync::Arc>, value: T) -> () { let mut w = lock.write().await; *w = value; } -lazy_static::lazy_static! { - static ref DUCKLAKE_INSTANCE_PG_PASSWORD: std::sync::RwLock> = std::sync::RwLock::new(None); -} diff --git a/backend/windmill-api/src/websocket_triggers.rs b/backend/windmill-api/src/websocket_triggers.rs index a75652e555..43e234a8c7 100644 --- a/backend/windmill-api/src/websocket_triggers.rs +++ b/backend/windmill-api/src/websocket_triggers.rs @@ -1,11 +1,6 @@ use anyhow::Context; -use axum::{ - extract::{Path, Query}, - routing::{delete, get, post}, - Extension, Json, Router, -}; + use futures::{stream::SplitSink, SinkExt, StreamExt}; -use http::StatusCode; use itertools::Itertools; use rand::seq::SliceRandom; use serde::{ @@ -13,24 +8,18 @@ use serde::{ Deserialize, Deserializer, Serialize, }; use serde_json::{value::RawValue, Value}; -use sql_builder::{bind::Bind, SqlBuilder}; use sqlx::prelude::FromRow; use sqlx::types::Json as SqlxJson; use std::{collections::HashMap, fmt}; use tokio::net::TcpStream; use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream}; -use windmill_audit::{audit_oss::audit_log, ActionKind}; use windmill_common::{ - db::UserDB, - error::{self, to_anyhow, JsonResult}, + error::{self, to_anyhow}, triggers::TriggerKind, - utils::{ - empty_as_none, not_found_if_none, paginate, report_critical_error, Pagination, StripPath, - }, - worker::{to_raw_value, CLOUD_HOSTED}, + utils::report_critical_error, + worker::to_raw_value, INSTANCE_NAME, }; -use windmill_git_sync::handle_deployment_metadata; use windmill_queue::PushArgsOwned; use crate::{ @@ -38,40 +27,10 @@ use crate::{ db::{ApiAuthed, DB}, trigger_helpers::{trigger_runnable, trigger_runnable_and_wait_for_raw_result, TriggerJobArgs}, users::fetch_api_authed, - utils::check_scopes, }; use std::borrow::Cow; -pub fn workspaced_service() -> Router { - Router::new() - .route("/create", post(create_websocket_trigger)) - .route("/list", get(list_websocket_triggers)) - .route("/get/*path", get(get_websocket_trigger)) - .route("/update/*path", post(update_websocket_trigger)) - .route("/delete/*path", delete(delete_websocket_trigger)) - .route("/exists/*path", get(exists_websocket_trigger)) - .route("/setenabled/*path", post(set_enabled)) - .route("/test", post(test_websocket_connection)) -} - -#[derive(Deserialize)] -struct NewWebsocketTrigger { - path: String, - url: String, - script_path: String, - is_flow: bool, - enabled: Option, - filters: Vec>, - initial_messages: Option>>, - url_runnable_args: Option>, - can_return_message: bool, - #[serde(default, deserialize_with = "empty_as_none")] - error_handler_path: Option, - error_handler_args: Option>>>, - retry: Option>, -} - #[derive(Deserialize)] pub struct JsonFilter { key: String, @@ -123,496 +82,6 @@ pub struct WebsocketTrigger { pub retry: Option>, } -#[derive(Deserialize)] -struct EditWebsocketTrigger { - path: String, - url: String, - script_path: String, - is_flow: bool, - filters: Vec>, - initial_messages: Option>>, - url_runnable_args: Option>, - can_return_message: bool, - error_handler_path: Option, - error_handler_args: Option>>>, - retry: Option>, -} - -#[derive(Deserialize)] -pub struct ListWebsocketTriggerQuery { - pub page: Option, - pub per_page: Option, - pub path: Option, - pub is_flow: Option, - pub path_start: Option, -} - -async fn list_websocket_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("websocket_trigger"); - - sqlb.fields(&[ - "workspace_id", - "path", - "url", - "script_path", - "is_flow", - "edited_by", - "email", - "edited_at", - "server_id", - "last_server_ping", - "extra_perms", - "error", - "enabled", - "filters", - "initial_messages", - "url_runnable_args", - "can_return_message", - "error_handler_path", - "error_handler_args", - "retry", - ]) - .order_by("edited_at", true) - .and_where("workspace_id = ?".bind(&w_id)) - .offset(offset) - .limit(per_page); - 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::internal_err(e.to_string()))?; - let rows = sqlx::query_as::<_, WebsocketTrigger>(&sql) - .fetch_all(&mut *tx) - .await?; - tx.commit().await?; - - Ok(Json(rows)) -} - -async fn get_websocket_trigger( - authed: ApiAuthed, - Extension(user_db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, -) -> error::JsonResult { - let path = path.to_path(); - check_scopes(&authed, || format!("websocket_triggers:read:{}", path))?; - let mut tx = user_db.begin(&authed).await?; - let trigger = sqlx::query_as::<_, WebsocketTrigger>( - r#" - SELECT - workspace_id, - path, - url, - script_path, - is_flow, - edited_by, - email, - edited_at, - server_id, - last_server_ping, - extra_perms, - error, - enabled, - filters, - initial_messages, - url_runnable_args, - can_return_message, - error_handler_path, - error_handler_args, - retry - FROM - websocket_trigger - WHERE - workspace_id = $1 AND path = $2 - "#, - ) - .bind(w_id) - .bind(path) - .fetch_optional(&mut *tx) - .await?; - - tx.commit().await?; - - let trigger = not_found_if_none(trigger, "Trigger", path)?; - - Ok(Json(trigger)) -} - -async fn create_websocket_trigger( - authed: ApiAuthed, - Extension(db): Extension, - Extension(user_db): Extension, - Path(w_id): Path, - Json(ct): Json, -) -> error::Result<(StatusCode, String)> { - check_scopes(&authed, || format!("websocket_triggers:write:{}", ct.path))?; - - if *CLOUD_HOSTED { - return Err(error::Error::BadRequest( - "WebSocket triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(), - )); - } - - let mut tx = user_db.begin(&authed).await?; - - let filters = ct.filters.into_iter().map(SqlxJson).collect_vec(); - let initial_messages = ct - .initial_messages - .unwrap_or_default() - .into_iter() - .map(SqlxJson) - .collect_vec(); - sqlx::query!( - r#" - INSERT INTO websocket_trigger ( - workspace_id, - path, - url, - script_path, - is_flow, - enabled, - filters, - initial_messages, - url_runnable_args, - edited_by, - can_return_message, - email, - edited_at, - error_handler_path, - error_handler_args, - retry - ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13, $14, $15 - ) - "#, - w_id, - ct.path, - ct.url, - ct.script_path, - ct.is_flow, - ct.enabled.unwrap_or(true), - &filters as _, - &initial_messages as _, - ct.url_runnable_args.map(SqlxJson) as _, - authed.username, - ct.can_return_message, - authed.email, - ct.error_handler_path, - ct.error_handler_args as _, - ct.retry as _ - ) - .execute(&mut *tx) - .await?; - - audit_log( - &mut *tx, - &authed, - "websocket_triggers.create", - ActionKind::Create, - &w_id, - Some(ct.path.as_str()), - None, - ) - .await?; - - tx.commit().await?; - - handle_deployment_metadata( - &authed.email, - &authed.username, - &db, - &w_id, - windmill_git_sync::DeployedObject::WebsocketTrigger { path: ct.path.clone() }, - Some(format!("WebSocket trigger '{}' created", ct.path)), - true, - ) - .await?; - - Ok((StatusCode::CREATED, format!("{}", ct.path))) -} - -async fn update_websocket_trigger( - authed: ApiAuthed, - Extension(db): Extension, - Extension(user_db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, - Json(ct): Json, -) -> error::Result { - let path = path.to_path(); - check_scopes(&authed, || format!("websocket_triggers:write:{}", path))?; - let mut tx = user_db.begin(&authed).await?; - - let filters = ct.filters.into_iter().map(SqlxJson).collect_vec(); - let initial_messages = ct - .initial_messages - .unwrap_or_default() - .into_iter() - .map(SqlxJson) - .collect_vec(); - - // important to update server_id to NULL to stop current websocket listener - sqlx::query!( - " - UPDATE - websocket_trigger - SET - url = $1, - script_path = $2, - path = $3, - is_flow = $4, - filters = $5, - initial_messages = $6, - url_runnable_args = $7, - edited_by = $8, - email = $9, - can_return_message = $10, - edited_at = now(), - server_id = NULL, - error = NULL, - error_handler_path = $13, - error_handler_args = $14, - retry = $15 - WHERE - workspace_id = $11 AND path = $12 - ", - ct.url, - ct.script_path, - ct.path, - ct.is_flow, - filters.as_slice() as &[SqlxJson>], - initial_messages.as_slice() as &[SqlxJson>], - ct.url_runnable_args.map(SqlxJson) as Option>>, - &authed.username, - &authed.email, - ct.can_return_message, - w_id, - path, - ct.error_handler_path, - ct.error_handler_args as _, - ct.retry as _ - ) - .execute(&mut *tx) - .await?; - - audit_log( - &mut *tx, - &authed, - "websocket_triggers.update", - ActionKind::Update, - &w_id, - Some(&ct.path), - None, - ) - .await?; - - tx.commit().await?; - - handle_deployment_metadata( - &authed.email, - &authed.username, - &db, - &w_id, - windmill_git_sync::DeployedObject::WebsocketTrigger { path: ct.path.clone() }, - Some(format!("WebSocket trigger '{}' updated", ct.path)), - true, - ) - .await?; - - Ok(ct.path.to_string()) -} - -#[derive(Deserialize)] -pub struct SetEnabled { - pub enabled: bool, -} - -pub async fn set_enabled( - authed: ApiAuthed, - Extension(db): Extension, - Extension(user_db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, - Json(payload): Json, -) -> error::Result { - let path = path.to_path(); - check_scopes(&authed, || format!("websocket_triggers:write:{}", path))?; - let mut tx = user_db.begin(&authed).await?; - - // important to set server_id, last_server_ping and error to NULL to stop current websocket listener - let one_o = sqlx::query_scalar!( - "UPDATE websocket_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?; - - not_found_if_none(one_o.flatten(), "WebSocket trigger", path)?; - - audit_log( - &mut *tx, - &authed, - "websocket_triggers.setenabled", - ActionKind::Update, - &w_id, - Some(path), - Some([("enabled", payload.enabled.to_string().as_ref())].into()), - ) - .await?; - - tx.commit().await?; - - handle_deployment_metadata( - &authed.email, - &authed.username, - &db, - &w_id, - windmill_git_sync::DeployedObject::WebsocketTrigger { path: path.to_string() }, - Some(format!("WebSocket trigger '{}' updated", path)), - true, - ) - .await?; - - Ok(format!( - "succesfully updated WebSocket trigger at path {} to status {}", - path, payload.enabled - )) -} - -async fn delete_websocket_trigger( - authed: ApiAuthed, - Extension(db): Extension, - Extension(user_db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, -) -> error::Result { - let path = path.to_path(); - check_scopes(&authed, || format!("websocket_triggers:write:{}", path))?; - - let mut tx = user_db.begin(&authed).await?; - sqlx::query!( - "DELETE FROM websocket_trigger WHERE workspace_id = $1 AND path = $2", - w_id, - path, - ) - .execute(&mut *tx) - .await?; - - audit_log( - &mut *tx, - &authed, - "websocket_triggers.delete", - ActionKind::Delete, - &w_id, - Some(path), - None, - ) - .await?; - - tx.commit().await?; - - handle_deployment_metadata( - &authed.email, - &authed.username, - &db, - &w_id, - windmill_git_sync::DeployedObject::WebsocketTrigger { path: path.to_string() }, - Some(format!("WebSocket trigger '{}' deleted", path)), - true, - ) - .await?; - - Ok(format!("WebSocket trigger {path} deleted")) -} - -async fn exists_websocket_trigger( - Extension(db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, -) -> JsonResult { - let path = path.to_path(); - let exists = sqlx::query_scalar!( - "SELECT EXISTS(SELECT 1 FROM websocket_trigger WHERE path = $1 AND workspace_id = $2)", - path, - w_id, - ) - .fetch_one(&db) - .await? - .unwrap_or(false); - Ok(Json(exists)) -} - -#[derive(Debug, Deserialize)] -struct TestWebsocket { - url: String, - url_runnable_args: Option>, -} - -async fn test_websocket_connection( - authed: ApiAuthed, - Extension(db): Extension, - Path(workspace_id): Path, - Json(test_websocket): Json, -) -> error::Result<()> { - let url = test_websocket.url; - - let connect_f = async { - let connect_url: Cow = if url.starts_with("$") { - if url.starts_with("$flow:") || url.starts_with("$script:") { - let path = url.splitn(2, ':').nth(1).unwrap(); - Cow::Owned( - get_url_from_runnable( - path, - url.starts_with("$flow:"), - &db, - authed, - test_websocket.url_runnable_args.as_ref(), - &workspace_id, - ) - .await?, - ) - } else { - return Err(error::Error::BadConfig(format!( - "Invalid WebSocket runnable path: {}", - url - ))); - } - } else { - Cow::Borrowed(&url) - }; - - connect_async(connect_url.as_ref()).await.map_err(|err| { - error::Error::BadConfig(format!( - "Error connecting to WebSocket: {}", - err.to_string() - )) - })?; - - Ok(()) - }; - - tokio::time::timeout(tokio::time::Duration::from_secs(30), connect_f) - .await - .map_err(|_| { - error::Error::BadConfig(format!("Timeout connecting to WebSocket after 30 seconds")) - })??; - - Ok(()) -} - async fn listen_to_unlistened_websockets( db: &DB, killpill_rx: &tokio::sync::broadcast::Receiver<()>, diff --git a/backend/windmill-api/src/workspaces_export.rs b/backend/windmill-api/src/workspaces_export.rs index 1b1f107be6..7758e024b7 100644 --- a/backend/windmill-api/src/workspaces_export.rs +++ b/backend/windmill-api/src/workspaces_export.rs @@ -17,6 +17,24 @@ use crate::{ resources::{Resource, ResourceType}, }; +#[cfg(any( + feature = "http_trigger", + feature = "websocket", + feature = "postgres_trigger", + feature = "mqtt_trigger", + all( + feature = "enterprise", + any( + feature = "kafka", + feature = "sqs_trigger", + feature = "gcp_trigger", + feature = "nats", + ), + feature = "private" + ) +))] +use crate::triggers::TriggerCrud; + use axum::{ extract::{Extension, Path, Query}, response::IntoResponse, @@ -537,357 +555,137 @@ pub(crate) async fn tarball_workspace( if include_triggers.unwrap_or(false) { #[cfg(feature = "http_trigger")] { - let http_triggers = sqlx::query_as!( - crate::http_triggers::HttpTrigger, - r#" - SELECT - workspace_id, - workspaced_route, - path, - route_path, - route_path_key, - authentication_resource_path, - script_path, - is_flow, - summary, - description, - edited_by, - edited_at, - email, - extra_perms, - is_async, - authentication_method AS "authentication_method: _", - http_method AS "http_method: _", - static_asset_config AS "static_asset_config: _", - is_static_website, - wrap_body, - raw_string, - error_handler_path, - error_handler_args as "error_handler_args: _", - retry as "retry: _" - FROM - http_trigger - WHERE - workspace_id = $1 - "#, - &w_id - ) - .fetch_all(&mut *tx) - .await?; + use crate::triggers::http::handler::HttpTrigger; + let handler = HttpTrigger; + let http_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?; for trigger in http_triggers { let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap(); archive - .write_to_archive(&trigger_str, &format!("{}.http_trigger.json", trigger.path)) + .write_to_archive( + &trigger_str, + &format!("{}.http_trigger.json", trigger.base.path), + ) .await?; } } #[cfg(feature = "websocket")] { - let websocket_triggers = sqlx::query_as!( - crate::websocket_triggers::WebsocketTrigger, - r#" - SELECT - workspace_id, - path, - url, - script_path, - is_flow, - edited_by, - email, - edited_at, - server_id, - last_server_ping, - extra_perms, - error, - enabled, - filters AS "filters: _", - initial_messages AS "initial_messages: _", - url_runnable_args AS "url_runnable_args: _", - can_return_message, - error_handler_path, - error_handler_args as "error_handler_args: _", - retry as "retry: _" - FROM - websocket_trigger - WHERE - workspace_id = $1 - "#, - &w_id - ) - .fetch_all(&mut *tx) - .await?; + use crate::triggers::websocket::WebsocketTrigger; + let handler = WebsocketTrigger; + let websocket_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?; for trigger in websocket_triggers { let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap(); archive .write_to_archive( &trigger_str, - &format!("{}.websocket_trigger.json", trigger.path), + &format!("{}.websocket_trigger.json", trigger.base.path), ) .await?; } } - #[cfg(all(feature = "enterprise", feature = "kafka"))] + #[cfg(all(feature = "enterprise", feature = "kafka", feature = "private"))] { - let kafka_triggers = sqlx::query_as!( - crate::kafka_triggers_oss::KafkaTrigger, - r#"SELECT - workspace_id, - path, - kafka_resource_path, - group_id, - topics, - script_path, - is_flow, - edited_by, - email, - edited_at, - server_id, - last_server_ping, - extra_perms, - error, - enabled, - error_handler_path, - error_handler_args as "error_handler_args: _", - retry as "retry: _" - FROM kafka_trigger - WHERE workspace_id = $1"#, - &w_id - ) - .fetch_all(&mut *tx) - .await?; + use crate::triggers::kafka::KafkaTrigger; + let handler = KafkaTrigger; + let kafka_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?; for trigger in kafka_triggers { let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap(); archive .write_to_archive( &trigger_str, - &format!("{}.kafka_trigger.json", trigger.path), + &format!("{}.kafka_trigger.json", trigger.base.path), ) .await?; } } - #[cfg(all(feature = "enterprise", feature = "sqs_trigger"))] + #[cfg(all(feature = "enterprise", feature = "sqs_trigger", feature = "private"))] { - let sqs_triggers = sqlx::query_as!( - crate::sqs_triggers_oss::SqsTrigger, - r#" - SELECT - aws_auth_resource_type AS "aws_auth_resource_type: _", - aws_resource_path, - message_attributes, - queue_url, - workspace_id, - path, - script_path, - is_flow, - edited_by, - email, - edited_at, - server_id, - last_server_ping, - extra_perms, - error, - enabled, - error_handler_path, - error_handler_args as "error_handler_args: _", - retry as "retry: _" - FROM - sqs_trigger - WHERE - workspace_id = $1 - "#, - &w_id - ) - .fetch_all(&mut *tx) - .await?; + use crate::triggers::sqs::SqsTrigger; + let handler = SqsTrigger; + let sqs_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?; for trigger in sqs_triggers { let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap(); archive - .write_to_archive(&trigger_str, &format!("{}.sqs_trigger.json", trigger.path)) + .write_to_archive( + &trigger_str, + &format!("{}.sqs_trigger.json", trigger.base.path), + ) .await?; } } - #[cfg(all(feature = "enterprise", feature = "gcp_trigger"))] + #[cfg(all(feature = "enterprise", feature = "gcp_trigger", feature = "private"))] { - let gcp_triggers = sqlx::query_as!( - crate::gcp_triggers_oss::GcpTrigger, - r#" - SELECT - gcp_resource_path, - subscription_id, - topic_id, - workspace_id, - delivery_type AS "delivery_type: _", - delivery_config AS "delivery_config: _", - subscription_mode AS "subscription_mode: _", - path, - script_path, - is_flow, - edited_by, - email, - edited_at, - server_id, - last_server_ping, - extra_perms, - error, - enabled, - error_handler_path, - error_handler_args as "error_handler_args: _", - retry as "retry: _", - auto_acknowledge_msg - FROM - gcp_trigger - WHERE - workspace_id = $1 - "#, - &w_id - ) - .fetch_all(&mut *tx) - .await?; + use crate::triggers::gcp::GcpTrigger; + let handler = GcpTrigger; + let gcp_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?; for trigger in gcp_triggers { let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap(); archive - .write_to_archive(&trigger_str, &format!("{}.gcp_trigger.json", trigger.path)) + .write_to_archive( + &trigger_str, + &format!("{}.gcp_trigger.json", trigger.base.path), + ) .await?; } } - #[cfg(all(feature = "enterprise", feature = "nats"))] + #[cfg(all(feature = "enterprise", feature = "nats", feature = "private"))] { - let nats_triggers = sqlx::query_as!( - crate::nats_triggers_oss::NatsTrigger, - r#" - SELECT - workspace_id, - path, - nats_resource_path, - subjects, - stream_name, - consumer_name, - use_jetstream, - script_path, - is_flow, - edited_by, - email, - edited_at, - server_id, - last_server_ping, - extra_perms, - error, - enabled, - error_handler_path, - error_handler_args as "error_handler_args: _", - retry as "retry: _" - FROM - nats_trigger - WHERE - workspace_id = $1 - "#, - &w_id - ) - .fetch_all(&mut *tx) - .await?; + use crate::triggers::nats::NatsTrigger; + let handler = NatsTrigger; + let nats_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?; for trigger in nats_triggers { let trigger_str: &String = &to_string_without_metadata(&trigger, false, None).unwrap(); archive - .write_to_archive(&trigger_str, &format!("{}.nats_trigger.json", trigger.path)) + .write_to_archive( + &trigger_str, + &format!("{}.nats_trigger.json", trigger.base.path), + ) .await?; } } #[cfg(feature = "postgres_trigger")] { - let postgres_triggers = sqlx::query_as!( - crate::postgres_triggers::PostgresTrigger, - r#" - SELECT - workspace_id, - path, - script_path, - is_flow, - edited_by, - email, - edited_at, - server_id, - last_server_ping, - extra_perms, - error, - enabled, - replication_slot_name, - publication_name, - postgres_resource_path, - error_handler_path, - error_handler_args as "error_handler_args: _", - retry as "retry: _" - FROM - postgres_trigger - WHERE - workspace_id = $1 - "#, - &w_id - ) - .fetch_all(&mut *tx) - .await?; + use crate::triggers::postgres::PostgresTrigger; + let handler = PostgresTrigger; + let postgres_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?; for trigger in postgres_triggers { let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap(); archive .write_to_archive( &trigger_str, - &format!("{}.postgres_trigger.json", trigger.path), + &format!("{}.postgres_trigger.json", trigger.base.path), ) .await?; } } - #[cfg(all(feature = "enterprise", feature = "mqtt_trigger"))] + #[cfg(feature = "mqtt_trigger")] { - let mqtt_triggers = sqlx::query_as!( - crate::mqtt_triggers::MqttTrigger, - r#" - SELECT - mqtt_resource_path, - subscribe_topics as "subscribe_topics: _", - v3_config as "v3_config: _", - v5_config as "v5_config: _", - 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, - error_handler_path, - error_handler_args as "error_handler_args: _", - retry as "retry: _" - FROM - mqtt_trigger - "#, - ) - .fetch_all(&mut *tx) - .await?; + use crate::triggers::mqtt::MqttTrigger; + let handler = MqttTrigger; + let mqtt_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?; for trigger in mqtt_triggers { let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap(); archive - .write_to_archive(&trigger_str, &format!("{}.mqtt_trigger.json", trigger.path)) + .write_to_archive( + &trigger_str, + &format!("{}.mqtt_trigger.json", trigger.base.path), + ) .await?; } } diff --git a/backend/windmill-common/src/cache.rs b/backend/windmill-common/src/cache.rs index f43d14845b..4890b9715b 100644 --- a/backend/windmill-common/src/cache.rs +++ b/backend/windmill-common/src/cache.rs @@ -539,7 +539,7 @@ pub mod flow { } pub mod script { - use crate::{worker::Connection, DB}; + use crate::{db::DB, worker::Connection}; use super::*; diff --git a/backend/windmill-common/src/flows.rs b/backend/windmill-common/src/flows.rs index 60583ef50f..5a905c65ca 100644 --- a/backend/windmill-common/src/flows.rs +++ b/backend/windmill-common/src/flows.rs @@ -20,11 +20,11 @@ use sqlx::types::JsonRawValue; use crate::{ assets::AssetWithAltAccessType, cache, + db::DB, error::Error, more_serde::{default_empty_string, default_id, default_null, default_true, is_default}, scripts::{Schema, ScriptHash, ScriptLang}, worker::{to_raw_value, Connection}, - DB, }; #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] diff --git a/backend/windmill-common/src/indexer.rs b/backend/windmill-common/src/indexer.rs index e581196af8..45a924748d 100644 --- a/backend/windmill-common/src/indexer.rs +++ b/backend/windmill-common/src/indexer.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{error, DB}; +use crate::{db::DB, error}; pub const BYTES_PER_MB: u64 = 1_048_576; pub const BYTES_PER_KB: u64 = 1024; diff --git a/backend/windmill-common/src/jobs.rs b/backend/windmill-common/src/jobs.rs index ba5c6c006e..0c53426eab 100644 --- a/backend/windmill-common/src/jobs.rs +++ b/backend/windmill-common/src/jobs.rs @@ -5,7 +5,7 @@ use futures_core::Stream; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; -use sqlx::{types::Json, Pool, Postgres}; +use sqlx::{types::Json, Postgres}; use tokio::io::AsyncReadExt; use uuid::Uuid; @@ -17,6 +17,7 @@ pub const EMAIL_ERROR_HANDLER_USER_EMAIL: &str = "email_error_handler@windmill.d use crate::{ apps::AppScriptId, auth::is_super_admin_email, + db::DB, error::{self, to_anyhow, Error}, flow_status::{FlowStatus, RestartedFrom}, flows::{FlowNodeId, FlowValue, Retry}, @@ -28,6 +29,42 @@ use crate::{ FlowVersionInfo, ScriptHashInfo, }; +#[derive(sqlx::Type, Serialize, Deserialize, Debug, Clone)] +#[sqlx(type_name = "JOB_TRIGGER_KIND", rename_all = "lowercase")] +#[serde(rename_all = "lowercase")] +pub enum JobTriggerKind { + Webhook, + Http, + Websocket, + Kafka, + Email, + Nats, + Mqtt, + Sqs, + Postgres, + Schedule, + Gcp, +} + +impl std::fmt::Display for JobTriggerKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let kind = match self { + JobTriggerKind::Webhook => "webhook", + JobTriggerKind::Http => "http", + JobTriggerKind::Websocket => "websocket", + JobTriggerKind::Kafka => "kafka", + JobTriggerKind::Email => "email", + JobTriggerKind::Nats => "nats", + JobTriggerKind::Mqtt => "mqtt", + JobTriggerKind::Sqs => "sqs", + JobTriggerKind::Postgres => "postgres", + JobTriggerKind::Schedule => "schedule", + JobTriggerKind::Gcp => "gcp", + }; + write!(f, "{}", kind) + } +} + #[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Copy, Clone)] #[sqlx(type_name = "JOB_KIND", rename_all = "lowercase")] #[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))] @@ -397,8 +434,6 @@ pub struct RawCode { type Tag = String; -pub type DB = Pool; - #[derive(Clone, Debug)] pub struct OnBehalfOf { pub email: String, diff --git a/backend/windmill-common/src/lib.rs b/backend/windmill-common/src/lib.rs index adf58871ba..ed35ab84bf 100644 --- a/backend/windmill-common/src/lib.rs +++ b/backend/windmill-common/src/lib.rs @@ -22,7 +22,7 @@ use tokio::sync::broadcast; use ee_oss::CriticalErrorChannel; use error::Error; use scripts::ScriptLang; -use sqlx::{Pool, Postgres}; +use sqlx::Postgres; pub mod agent_workers; pub mod apps; @@ -55,7 +55,7 @@ pub mod job_s3_helpers_oss; pub mod oidc_ee; #[cfg(all(feature = "enterprise", feature = "openidconnect"))] pub mod oidc_oss; - +pub mod triggers; pub mod jobs; pub mod jwt; pub mod more_serde; @@ -81,7 +81,6 @@ pub mod utils; pub mod variables; pub mod worker; pub mod workspaces; -pub mod triggers; pub mod result_stream; pub mod stream; @@ -422,7 +421,7 @@ pub async fn connect( type Tag = String; -pub type DB = Pool; +pub use db::DB; #[derive(Clone)] pub struct ExpiringLatestVersionId { diff --git a/backend/windmill-common/src/triggers.rs b/backend/windmill-common/src/triggers.rs index df68d0543c..08356f24ad 100644 --- a/backend/windmill-common/src/triggers.rs +++ b/backend/windmill-common/src/triggers.rs @@ -1,31 +1,8 @@ +use lazy_static::lazy_static; use quick_cache::sync::Cache; use serde::{Deserialize, Serialize}; use std::fmt; -use strum::EnumIter; - -#[derive(Eq, PartialEq, Hash)] -pub enum HubOrWorkspaceId { - Hub, - WorkspaceId(String), -} - -type RunnableFormatCacheKey = (HubOrWorkspaceId, i64, TriggerKind); - -#[derive(Debug, Clone, Eq, PartialEq, Hash, Copy)] -pub struct RunnableFormat { - pub version: RunnableFormatVersion, - pub has_preprocessor: bool, -} - -#[derive(Debug, Clone, Eq, PartialEq, Hash, Copy)] -pub enum RunnableFormatVersion { - V1, - V2, -} - -lazy_static::lazy_static! { - pub static ref RUNNABLE_FORMAT_VERSION_CACHE: Cache = Cache::new(1000); -} +use strum_macros::EnumIter; #[derive(sqlx::Type, Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, EnumIter)] #[sqlx(type_name = "TRIGGER_KIND", rename_all = "lowercase")] @@ -77,3 +54,28 @@ impl fmt::Display for TriggerKind { write!(f, "{}", s) } } + +#[derive(Eq, PartialEq, Hash)] +pub enum HubOrWorkspaceId { + Hub, + WorkspaceId(String), +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Copy)] +pub struct RunnableFormat { + pub version: RunnableFormatVersion, + pub has_preprocessor: bool, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Copy)] +pub enum RunnableFormatVersion { + V1, + V2, +} + +pub type RunnableFormatCacheKey = (HubOrWorkspaceId, i64, TriggerKind); + +lazy_static! { + pub static ref RUNNABLE_FORMAT_VERSION_CACHE: Cache = + Cache::new(1000); +} diff --git a/backend/windmill-git-sync/src/lib.rs b/backend/windmill-git-sync/src/lib.rs index 55ab86af3f..df63736acf 100644 --- a/backend/windmill-git-sync/src/lib.rs +++ b/backend/windmill-git-sync/src/lib.rs @@ -6,16 +6,13 @@ * LICENSE-AGPL for a copy of the license. */ -use sqlx::{Pool, Postgres}; - -use windmill_common::scripts::ScriptHash; +use windmill_common::{scripts::ScriptHash, DB}; #[cfg(feature = "private")] pub mod git_sync_ee; pub mod git_sync_oss; pub use git_sync_oss::handle_deployment_metadata; -pub type DB = Pool; #[derive(Clone, Debug)] pub enum DeployedObject { @@ -69,7 +66,11 @@ impl DeployedObject { pub fn get_ignore_regex_filter(&self) -> bool { match self { - Self::User { .. } | Self::Group { .. } | Self::ResourceType { .. } | Self::Settings { .. } | Self::Key { .. } => true, + Self::User { .. } + | Self::Group { .. } + | Self::ResourceType { .. } + | Self::Settings { .. } + | Self::Key { .. } => true, _ => false, } } diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index d84e5cdc97..fcefc458f7 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -33,7 +33,7 @@ use windmill_common::add_time; use windmill_common::auth::JobPerms; #[cfg(feature = "benchmark")] use windmill_common::bench::BenchmarkIter; -use windmill_common::jobs::EMAIL_ERROR_HANDLER_USER_EMAIL; +use windmill_common::jobs::{JobTriggerKind, EMAIL_ERROR_HANDLER_USER_EMAIL}; use windmill_common::utils::now_from_db; use windmill_common::worker::{Connection, SCRIPT_TOKEN_EXPIRY}; #[cfg(feature = "enterprise")] @@ -2257,42 +2257,6 @@ async fn handle_successful_schedule<'a, 'c, T: Serialize + Send + Sync>( Ok(()) } -#[derive(sqlx::Type, Serialize, Deserialize, Debug, Clone)] -#[sqlx(type_name = "JOB_TRIGGER_KIND", rename_all = "lowercase")] -#[serde(rename_all = "lowercase")] -pub enum JobTriggerKind { - Webhook, - Http, - Websocket, - Kafka, - Email, - Nats, - Mqtt, - Sqs, - Postgres, - Schedule, - Gcp, -} - -impl std::fmt::Display for JobTriggerKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let kind = match self { - JobTriggerKind::Webhook => "webhook", - JobTriggerKind::Http => "http", - JobTriggerKind::Websocket => "websocket", - JobTriggerKind::Kafka => "kafka", - JobTriggerKind::Email => "email", - JobTriggerKind::Nats => "nats", - JobTriggerKind::Mqtt => "mqtt", - JobTriggerKind::Sqs => "sqs", - JobTriggerKind::Postgres => "postgres", - JobTriggerKind::Schedule => "schedule", - JobTriggerKind::Gcp => "gcp", - }; - write!(f, "{}", kind) - } -} - #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)] pub struct MiniPulledJob { pub workspace_id: String, diff --git a/backend/windmill-worker/src/ai_executor.rs b/backend/windmill-worker/src/ai_executor.rs index b9a7cfcd88..398d8f876d 100644 --- a/backend/windmill-worker/src/ai_executor.rs +++ b/backend/windmill-worker/src/ai_executor.rs @@ -9,11 +9,12 @@ use windmill_common::{ auth::get_job_perms, cache, client::AuthedClient, + db::DB, error::{self, to_anyhow, Error}, flow_status::AgentAction, flows::{FlowModule, FlowModuleValue}, get_latest_hash_for_path, - jobs::{JobKind, DB}, + jobs::JobKind, scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang}, utils::{StripPath, HTTP_CLIENT}, worker::{to_raw_value, Connection}, diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index 36f255a836..405ed49307 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -48,7 +48,7 @@ use windmill_common::utils::WarnAfterExt; use windmill_common::worker::to_raw_value; use windmill_common::{ add_time, get_latest_flow_version_info_for_path, get_script_info_for_hash, FlowVersionInfo, - ScriptHashInfo, + ScriptHashInfo, DB, }; use windmill_common::{ error::{self, to_anyhow, Error}, @@ -66,8 +66,6 @@ use windmill_queue::{ MiniPulledJob, PushArgs, PushIsolationLevel, SameWorkerPayload, WrappedError, }; -type DB = sqlx::Pool; - use windmill_audit::audit_oss::{audit_log, AuditAuthor}; use windmill_audit::ActionKind; use windmill_queue::{canceled_job_to_result, push};