From 61a5ceaba38787dc146a36b443bbd3f78e26102b Mon Sep 17 00:00:00 2001 From: dieriba Date: Tue, 11 Mar 2025 16:24:22 +0100 Subject: [PATCH] feat: option to prefix http route with workspace id (#5461) * add new column * handled worskpaced route * update .sqlx * update: rename migration file, rewrite query for checking existing route path key, add copy button url for each route trigger * update .sqlx * update sql query * update .sqlx * fix: prevent precedence on query * nits * update .sqlx * update query * Update http_triggers.rs * nits --------- Co-authored-by: HugoCasa --- ...10394fe6c8abcc1fa93291fac6f929043d20.json} | 10 +- ...e67156408b08360d628300b28221ea995a58.json} | 4 +- ...33239d5806fc50bba2d3fe9d8a6e11627655.json} | 10 +- ...ae89c8d75034a9210233aed2f0414690e37d.json} | 30 +- ...64bd112977c995bf266680c54f58ad57fd4b6.json | 38 +++ ...6faded2cc63e8f16c201e99ab1725dcff294.json} | 4 +- ...942d4da686a65ab11eefdca16367d2db67963.json | 41 +++ ...4297af93b5b83ffdfb3ec91eba5df7ad3fd95.json | 38 --- ...ea74ffbb4c0ed7e694f8f03d92f2a7c118af9.json | 15 + ...6104a430a11d7c5cb9447025d11506585708.json} | 4 +- ...c662539d5e4af1dd0b5923d8ee17b772c5677.json | 39 --- ...3aeca9a14ac628c61a358e2c6612c57249b0f.json | 23 -- ...f2996dede4de49c5bd9cca2928209dc40b8f1.json | 40 --- ...7c08d8ed4984409b669162f0a1fc1aa48e188.json | 15 - ...34669464a56e470935ce2924723003f3f92b.json} | 36 ++- ...b45146478f70956919c01718623f0828e0383.json | 40 +++ ...26f787287c3cd181a6853056077edba10473d.json | 23 ++ ...ced_route_column_for_http_trigger.down.sql | 3 + ...paced_route_column_for_http_trigger.up.sql | 3 + backend/windmill-api/openapi.yaml | 8 + backend/windmill-api/src/http_triggers.rs | 289 +++++++++++++++--- backend/windmill-api/src/workspaces_export.rs | 38 ++- backend/windmill-common/src/s3_helpers.rs | 2 +- .../http/RouteEditorConfigSection.svelte | 49 +-- .../triggers/http/RouteEditorInner.svelte | 46 +-- .../src/lib/components/triggers/http/utils.ts | 8 + .../routes/(root)/(logged)/routes/+page.js | 2 +- .../(root)/(logged)/routes/+page.svelte | 28 +- 28 files changed, 592 insertions(+), 294 deletions(-) rename backend/.sqlx/{query-1961d15ae075072bd5f677c95f9b4dac7f747585d98aa8bcf60dcfb4c8124028.json => query-01b87cdd552c2d5a3cd475eb705110394fe6c8abcc1fa93291fac6f929043d20.json} (69%) rename backend/.sqlx/{query-63b5f03741be97d0e8763dd070649ebb6ec02aa083d7e175c1a02a38935a4024.json => query-0d8153986cea6166820f601f80d8e67156408b08360d628300b28221ea995a58.json} (52%) rename backend/.sqlx/{query-8ff25d890d3f7019c5c6f2b47f29f4c7b2c91094036d9b5037ef9a4ac986ee53.json => query-300b41622dbcdf908d9e173eee2133239d5806fc50bba2d3fe9d8a6e11627655.json} (68%) rename backend/.sqlx/{query-197321abfe4667256761884970334f58c1b3edfcc1e863ec4316c9742a1ac7c8.json => query-4353a7de6b4de1df6e19d95a4db8ae89c8d75034a9210233aed2f0414690e37d.json} (72%) create mode 100644 backend/.sqlx/query-5d3401f608b27937285e418f7bd64bd112977c995bf266680c54f58ad57fd4b6.json rename backend/.sqlx/{query-a96ff22bc78b74d7234550a12d9fb5c555c1187b276f1353dae1b2ac0670a92a.json => query-679a9159a5fca976a3de99fe26806faded2cc63e8f16c201e99ab1725dcff294.json} (58%) create mode 100644 backend/.sqlx/query-78e6342696052d0e1f79fd721f5942d4da686a65ab11eefdca16367d2db67963.json delete mode 100644 backend/.sqlx/query-89c08575afb31b70984f6b2b7dd4297af93b5b83ffdfb3ec91eba5df7ad3fd95.json create mode 100644 backend/.sqlx/query-a269c388056eabe4b045948f451ea74ffbb4c0ed7e694f8f03d92f2a7c118af9.json rename backend/.sqlx/{query-9a050ab74cfc13a4b855408e61ebcb0a9d27e5563fa7af5e00a4916b03a62ddb.json => query-adb0090afd3ce918d8b80ff51d9f6104a430a11d7c5cb9447025d11506585708.json} (57%) delete mode 100644 backend/.sqlx/query-b41cef713e822bbd89b49b1f35cc662539d5e4af1dd0b5923d8ee17b772c5677.json delete mode 100644 backend/.sqlx/query-b57a188ca137162a2848ebf81fa3aeca9a14ac628c61a358e2c6612c57249b0f.json delete mode 100644 backend/.sqlx/query-ccb7aea162fa8781675d547fb82f2996dede4de49c5bd9cca2928209dc40b8f1.json delete mode 100644 backend/.sqlx/query-dc9a906d6c6156a84fccf4e3a2a7c08d8ed4984409b669162f0a1fc1aa48e188.json rename backend/.sqlx/{query-d8aa1a5813fedbb22128112fcdf9d16769811cd5d5ad3b66362cc5bb1750ae6b.json => query-e44ea5870a8eba1fe3d22d4dfe7734669464a56e470935ce2924723003f3f92b.json} (69%) create mode 100644 backend/.sqlx/query-eb4779976677308e8ad7c6427bfb45146478f70956919c01718623f0828e0383.json create mode 100644 backend/.sqlx/query-f632d08a8d3df691fff9f57fdf926f787287c3cd181a6853056077edba10473d.json create mode 100644 backend/migrations/20250309165536_add_workspaced_route_column_for_http_trigger.down.sql create mode 100644 backend/migrations/20250309165536_add_workspaced_route_column_for_http_trigger.up.sql create mode 100644 frontend/src/lib/components/triggers/http/utils.ts diff --git a/backend/.sqlx/query-1961d15ae075072bd5f677c95f9b4dac7f747585d98aa8bcf60dcfb4c8124028.json b/backend/.sqlx/query-01b87cdd552c2d5a3cd475eb705110394fe6c8abcc1fa93291fac6f929043d20.json similarity index 69% rename from backend/.sqlx/query-1961d15ae075072bd5f677c95f9b4dac7f747585d98aa8bcf60dcfb4c8124028.json rename to backend/.sqlx/query-01b87cdd552c2d5a3cd475eb705110394fe6c8abcc1fa93291fac6f929043d20.json index 418be57757..cae9fdd79f 100644 --- a/backend/.sqlx/query-1961d15ae075072bd5f677c95f9b4dac7f747585d98aa8bcf60dcfb4c8124028.json +++ b/backend/.sqlx/query-01b87cdd552c2d5a3cd475eb705110394fe6c8abcc1fa93291fac6f929043d20.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, static_asset_config as \"static_asset_config: _\", is_static_website FROM http_trigger WHERE http_method = $1", + "query": "\n SELECT \n path, \n script_path, \n is_flow, \n route_path, \n workspace_id, \n is_async, \n requires_auth, \n edited_by, \n email, \n static_asset_config AS \"static_asset_config: _\",\n workspaced_route,\n is_static_website\n FROM \n http_trigger \n WHERE \n http_method = $1\n ", "describe": { "columns": [ { @@ -55,6 +55,11 @@ }, { "ordinal": 10, + "name": "workspaced_route", + "type_info": "Bool" + }, + { + "ordinal": 11, "name": "is_static_website", "type_info": "Bool" } @@ -88,8 +93,9 @@ false, false, true, + false, false ] }, - "hash": "1961d15ae075072bd5f677c95f9b4dac7f747585d98aa8bcf60dcfb4c8124028" + "hash": "01b87cdd552c2d5a3cd475eb705110394fe6c8abcc1fa93291fac6f929043d20" } diff --git a/backend/.sqlx/query-63b5f03741be97d0e8763dd070649ebb6ec02aa083d7e175c1a02a38935a4024.json b/backend/.sqlx/query-0d8153986cea6166820f601f80d8e67156408b08360d628300b28221ea995a58.json similarity index 52% rename from backend/.sqlx/query-63b5f03741be97d0e8763dd070649ebb6ec02aa083d7e175c1a02a38935a4024.json rename to backend/.sqlx/query-0d8153986cea6166820f601f80d8e67156408b08360d628300b28221ea995a58.json index 4d914355db..1d2f993f5d 100644 --- a/backend/.sqlx/query-63b5f03741be97d0e8763dd070649ebb6ec02aa083d7e175c1a02a38935a4024.json +++ b/backend/.sqlx/query-0d8153986cea6166820f601f80d8e67156408b08360d628300b28221ea995a58.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND http_method = $2 AND ($3::TEXT IS NULL OR path != $3))", + "query": "\n SELECT EXISTS(\n SELECT 1 \n FROM http_trigger \n WHERE \n ((workspaced_route IS TRUE AND workspace_id || '/' || route_path_key = $1) \n OR (workspaced_route IS FALSE AND route_path_key = $1))\n AND http_method = $2 \n AND ($3::TEXT IS NULL OR path != $3)\n )\n ", "describe": { "columns": [ { @@ -33,5 +33,5 @@ null ] }, - "hash": "63b5f03741be97d0e8763dd070649ebb6ec02aa083d7e175c1a02a38935a4024" + "hash": "0d8153986cea6166820f601f80d8e67156408b08360d628300b28221ea995a58" } diff --git a/backend/.sqlx/query-8ff25d890d3f7019c5c6f2b47f29f4c7b2c91094036d9b5037ef9a4ac986ee53.json b/backend/.sqlx/query-300b41622dbcdf908d9e173eee2133239d5806fc50bba2d3fe9d8a6e11627655.json similarity index 68% rename from backend/.sqlx/query-8ff25d890d3f7019c5c6f2b47f29f4c7b2c91094036d9b5037ef9a4ac986ee53.json rename to backend/.sqlx/query-300b41622dbcdf908d9e173eee2133239d5806fc50bba2d3fe9d8a6e11627655.json index de933d99e1..b2c551759d 100644 --- a/backend/.sqlx/query-8ff25d890d3f7019c5c6f2b47f29f4c7b2c91094036d9b5037ef9a4ac986ee53.json +++ b/backend/.sqlx/query-300b41622dbcdf908d9e173eee2133239d5806fc50bba2d3fe9d8a6e11627655.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, static_asset_config as \"static_asset_config: _\", is_static_website FROM http_trigger WHERE workspace_id = $1 AND http_method = $2", + "query": "\n SELECT \n path, \n script_path, \n is_flow, \n route_path, \n workspace_id, \n is_async, \n requires_auth, \n edited_by, \n email,\n static_asset_config AS \"static_asset_config: _\",\n workspaced_route,\n is_static_website \n FROM \n http_trigger \n WHERE \n workspace_id = $1 AND \n http_method = $2\n ", "describe": { "columns": [ { @@ -55,6 +55,11 @@ }, { "ordinal": 10, + "name": "workspaced_route", + "type_info": "Bool" + }, + { + "ordinal": 11, "name": "is_static_website", "type_info": "Bool" } @@ -89,8 +94,9 @@ false, false, true, + false, false ] }, - "hash": "8ff25d890d3f7019c5c6f2b47f29f4c7b2c91094036d9b5037ef9a4ac986ee53" + "hash": "300b41622dbcdf908d9e173eee2133239d5806fc50bba2d3fe9d8a6e11627655" } diff --git a/backend/.sqlx/query-197321abfe4667256761884970334f58c1b3edfcc1e863ec4316c9742a1ac7c8.json b/backend/.sqlx/query-4353a7de6b4de1df6e19d95a4db8ae89c8d75034a9210233aed2f0414690e37d.json similarity index 72% rename from backend/.sqlx/query-197321abfe4667256761884970334f58c1b3edfcc1e863ec4316c9742a1ac7c8.json rename to backend/.sqlx/query-4353a7de6b4de1df6e19d95a4db8ae89c8d75034a9210233aed2f0414690e37d.json index bfc823c911..49ede3a762 100644 --- a/backend/.sqlx/query-197321abfe4667256761884970334f58c1b3edfcc1e863ec4316c9742a1ac7c8.json +++ b/backend/.sqlx/query-4353a7de6b4de1df6e19d95a4db8ae89c8d75034a9210233aed2f0414690e37d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as \"http_method: _\", edited_by, email, edited_at, extra_perms, is_async, requires_auth, static_asset_config as \"static_asset_config: _\", is_static_website\n FROM http_trigger\n WHERE workspace_id = $1 AND path = $2", + "query": "\n SELECT \n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n script_path, \n is_flow, \n http_method as \"http_method: _\", \n edited_by, \n email, \n edited_at, \n extra_perms, \n is_async, \n requires_auth, \n static_asset_config as \"static_asset_config: _\", \n is_static_website\n FROM \n http_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ", "describe": { "columns": [ { @@ -25,16 +25,21 @@ }, { "ordinal": 4, + "name": "workspaced_route", + "type_info": "Bool" + }, + { + "ordinal": 5, "name": "script_path", "type_info": "Varchar" }, { - "ordinal": 5, + "ordinal": 6, "name": "is_flow", "type_info": "Bool" }, { - "ordinal": 6, + "ordinal": 7, "name": "http_method: _", "type_info": { "Custom": { @@ -52,42 +57,42 @@ } }, { - "ordinal": 7, + "ordinal": 8, "name": "edited_by", "type_info": "Varchar" }, { - "ordinal": 8, + "ordinal": 9, "name": "email", "type_info": "Varchar" }, { - "ordinal": 9, + "ordinal": 10, "name": "edited_at", "type_info": "Timestamptz" }, { - "ordinal": 10, + "ordinal": 11, "name": "extra_perms", "type_info": "Jsonb" }, { - "ordinal": 11, + "ordinal": 12, "name": "is_async", "type_info": "Bool" }, { - "ordinal": 12, + "ordinal": 13, "name": "requires_auth", "type_info": "Bool" }, { - "ordinal": 13, + "ordinal": 14, "name": "static_asset_config: _", "type_info": "Jsonb" }, { - "ordinal": 14, + "ordinal": 15, "name": "is_static_website", "type_info": "Bool" } @@ -112,9 +117,10 @@ false, false, false, + false, true, false ] }, - "hash": "197321abfe4667256761884970334f58c1b3edfcc1e863ec4316c9742a1ac7c8" + "hash": "4353a7de6b4de1df6e19d95a4db8ae89c8d75034a9210233aed2f0414690e37d" } diff --git a/backend/.sqlx/query-5d3401f608b27937285e418f7bd64bd112977c995bf266680c54f58ad57fd4b6.json b/backend/.sqlx/query-5d3401f608b27937285e418f7bd64bd112977c995bf266680c54f58ad57fd4b6.json new file mode 100644 index 0000000000..b962770bd9 --- /dev/null +++ b/backend/.sqlx/query-5d3401f608b27937285e418f7bd64bd112977c995bf266680c54f58ad57fd4b6.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE \n http_trigger \n SET \n script_path = $1, \n path = $2, \n is_flow = $3, \n http_method = $4,\n static_asset_config = $5, \n edited_by = $6, \n email = $7, \n is_async = $8, \n requires_auth = $9, \n edited_at = now(), \n is_static_website = $10\n WHERE \n workspace_id = $11 AND \n path = $12\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Bool", + { + "Custom": { + "name": "http_method", + "kind": { + "Enum": [ + "get", + "post", + "put", + "delete", + "patch" + ] + } + } + }, + "Jsonb", + "Varchar", + "Varchar", + "Bool", + "Bool", + "Bool", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "5d3401f608b27937285e418f7bd64bd112977c995bf266680c54f58ad57fd4b6" +} diff --git a/backend/.sqlx/query-a96ff22bc78b74d7234550a12d9fb5c555c1187b276f1353dae1b2ac0670a92a.json b/backend/.sqlx/query-679a9159a5fca976a3de99fe26806faded2cc63e8f16c201e99ab1725dcff294.json similarity index 58% rename from backend/.sqlx/query-a96ff22bc78b74d7234550a12d9fb5c555c1187b276f1353dae1b2ac0670a92a.json rename to backend/.sqlx/query-679a9159a5fca976a3de99fe26806faded2cc63e8f16c201e99ab1725dcff294.json index 060d87a941..1ce9e58cd4 100644 --- a/backend/.sqlx/query-a96ff22bc78b74d7234550a12d9fb5c555c1187b276f1353dae1b2ac0670a92a.json +++ b/backend/.sqlx/query-679a9159a5fca976a3de99fe26806faded2cc63e8f16c201e99ab1725dcff294.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "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))", + "query": "\n SELECT EXISTS(\n SELECT 1 \n FROM http_trigger \n WHERE \n route_path_key = $1\n AND workspace_id = $2 \n AND http_method = $3 \n AND ($4::TEXT IS NULL OR path != $4)\n )\n ", "describe": { "columns": [ { @@ -34,5 +34,5 @@ null ] }, - "hash": "a96ff22bc78b74d7234550a12d9fb5c555c1187b276f1353dae1b2ac0670a92a" + "hash": "679a9159a5fca976a3de99fe26806faded2cc63e8f16c201e99ab1725dcff294" } diff --git a/backend/.sqlx/query-78e6342696052d0e1f79fd721f5942d4da686a65ab11eefdca16367d2db67963.json b/backend/.sqlx/query-78e6342696052d0e1f79fd721f5942d4da686a65ab11eefdca16367d2db67963.json new file mode 100644 index 0000000000..19b8c3e0d7 --- /dev/null +++ b/backend/.sqlx/query-78e6342696052d0e1f79fd721f5942d4da686a65ab11eefdca16367d2db67963.json @@ -0,0 +1,41 @@ +{ + "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 script_path = $4, \n path = $5, \n is_flow = $6, \n http_method = $7, \n static_asset_config = $8, \n edited_by = $9, \n email = $10, \n is_async = $11, \n requires_auth = $12, \n edited_at = now(), \n is_static_website = $13\n WHERE \n workspace_id = $14 AND \n path = $15\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Bool", + "Varchar", + "Varchar", + "Bool", + { + "Custom": { + "name": "http_method", + "kind": { + "Enum": [ + "get", + "post", + "put", + "delete", + "patch" + ] + } + } + }, + "Jsonb", + "Varchar", + "Varchar", + "Bool", + "Bool", + "Bool", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "78e6342696052d0e1f79fd721f5942d4da686a65ab11eefdca16367d2db67963" +} diff --git a/backend/.sqlx/query-89c08575afb31b70984f6b2b7dd4297af93b5b83ffdfb3ec91eba5df7ad3fd95.json b/backend/.sqlx/query-89c08575afb31b70984f6b2b7dd4297af93b5b83ffdfb3ec91eba5df7ad3fd95.json deleted file mode 100644 index a463eafdb7..0000000000 --- a/backend/.sqlx/query-89c08575afb31b70984f6b2b7dd4297af93b5b83ffdfb3ec91eba5df7ad3fd95.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, static_asset_config = $5, edited_by = $6, email = $7, is_async = $8, requires_auth = $9, edited_at = now(), is_static_website = $10\n WHERE workspace_id = $11 AND path = $12", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Bool", - { - "Custom": { - "name": "http_method", - "kind": { - "Enum": [ - "get", - "post", - "put", - "delete", - "patch" - ] - } - } - }, - "Jsonb", - "Varchar", - "Varchar", - "Bool", - "Bool", - "Bool", - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "89c08575afb31b70984f6b2b7dd4297af93b5b83ffdfb3ec91eba5df7ad3fd95" -} diff --git a/backend/.sqlx/query-a269c388056eabe4b045948f451ea74ffbb4c0ed7e694f8f03d92f2a7c118af9.json b/backend/.sqlx/query-a269c388056eabe4b045948f451ea74ffbb4c0ed7e694f8f03d92f2a7c118af9.json new file mode 100644 index 0000000000..e81acc3584 --- /dev/null +++ b/backend/.sqlx/query-a269c388056eabe4b045948f451ea74ffbb4c0ed7e694f8f03d92f2a7c118af9.json @@ -0,0 +1,15 @@ +{ + "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-9a050ab74cfc13a4b855408e61ebcb0a9d27e5563fa7af5e00a4916b03a62ddb.json b/backend/.sqlx/query-adb0090afd3ce918d8b80ff51d9f6104a430a11d7c5cb9447025d11506585708.json similarity index 57% rename from backend/.sqlx/query-9a050ab74cfc13a4b855408e61ebcb0a9d27e5563fa7af5e00a4916b03a62ddb.json rename to backend/.sqlx/query-adb0090afd3ce918d8b80ff51d9f6104a430a11d7c5cb9447025d11506585708.json index a0c5bd7642..bbe9b8bb86 100644 --- a/backend/.sqlx/query-9a050ab74cfc13a4b855408e61ebcb0a9d27e5563fa7af5e00a4916b03a62ddb.json +++ b/backend/.sqlx/query-adb0090afd3ce918d8b80ff51d9f6104a430a11d7c5cb9447025d11506585708.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE path = $1 AND workspace_id = $2)", + "query": "SELECT EXISTS(\n SELECT 1 FROM http_trigger \n WHERE path = $1 AND workspace_id = $2\n )", "describe": { "columns": [ { @@ -19,5 +19,5 @@ null ] }, - "hash": "9a050ab74cfc13a4b855408e61ebcb0a9d27e5563fa7af5e00a4916b03a62ddb" + "hash": "adb0090afd3ce918d8b80ff51d9f6104a430a11d7c5cb9447025d11506585708" } diff --git a/backend/.sqlx/query-b41cef713e822bbd89b49b1f35cc662539d5e4af1dd0b5923d8ee17b772c5677.json b/backend/.sqlx/query-b41cef713e822bbd89b49b1f35cc662539d5e4af1dd0b5923d8ee17b772c5677.json deleted file mode 100644 index 8475175c69..0000000000 --- a/backend/.sqlx/query-b41cef713e822bbd89b49b1f35cc662539d5e4af1dd0b5923d8ee17b772c5677.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, static_asset_config, edited_by, email, edited_at, is_static_website) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Bool", - "Bool", - "Bool", - { - "Custom": { - "name": "http_method", - "kind": { - "Enum": [ - "get", - "post", - "put", - "delete", - "patch" - ] - } - } - }, - "Jsonb", - "Varchar", - "Varchar", - "Bool" - ] - }, - "nullable": [] - }, - "hash": "b41cef713e822bbd89b49b1f35cc662539d5e4af1dd0b5923d8ee17b772c5677" -} diff --git a/backend/.sqlx/query-b57a188ca137162a2848ebf81fa3aeca9a14ac628c61a358e2c6612c57249b0f.json b/backend/.sqlx/query-b57a188ca137162a2848ebf81fa3aeca9a14ac628c61a358e2c6612c57249b0f.json deleted file mode 100644 index a730d4b588..0000000000 --- a/backend/.sqlx/query-b57a188ca137162a2848ebf81fa3aeca9a14ac628c61a358e2c6612c57249b0f.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1 AND path = $2)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "exists", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "b57a188ca137162a2848ebf81fa3aeca9a14ac628c61a358e2c6612c57249b0f" -} diff --git a/backend/.sqlx/query-ccb7aea162fa8781675d547fb82f2996dede4de49c5bd9cca2928209dc40b8f1.json b/backend/.sqlx/query-ccb7aea162fa8781675d547fb82f2996dede4de49c5bd9cca2928209dc40b8f1.json deleted file mode 100644 index f4bc3f2174..0000000000 --- a/backend/.sqlx/query-ccb7aea162fa8781675d547fb82f2996dede4de49c5bd9cca2928209dc40b8f1.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE http_trigger \n SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, static_asset_config = $7, edited_by = $8, email = $9, is_async = $10, requires_auth = $11, edited_at = now(), is_static_website = $12\n WHERE workspace_id = $13 AND path = $14", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Bool", - { - "Custom": { - "name": "http_method", - "kind": { - "Enum": [ - "get", - "post", - "put", - "delete", - "patch" - ] - } - } - }, - "Jsonb", - "Varchar", - "Varchar", - "Bool", - "Bool", - "Bool", - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "ccb7aea162fa8781675d547fb82f2996dede4de49c5bd9cca2928209dc40b8f1" -} diff --git a/backend/.sqlx/query-dc9a906d6c6156a84fccf4e3a2a7c08d8ed4984409b669162f0a1fc1aa48e188.json b/backend/.sqlx/query-dc9a906d6c6156a84fccf4e3a2a7c08d8ed4984409b669162f0a1fc1aa48e188.json deleted file mode 100644 index 03b0c561e1..0000000000 --- a/backend/.sqlx/query-dc9a906d6c6156a84fccf4e3a2a7c08d8ed4984409b669162f0a1fc1aa48e188.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM http_trigger WHERE workspace_id = $1 AND path = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "dc9a906d6c6156a84fccf4e3a2a7c08d8ed4984409b669162f0a1fc1aa48e188" -} diff --git a/backend/.sqlx/query-d8aa1a5813fedbb22128112fcdf9d16769811cd5d5ad3b66362cc5bb1750ae6b.json b/backend/.sqlx/query-e44ea5870a8eba1fe3d22d4dfe7734669464a56e470935ce2924723003f3f92b.json similarity index 69% rename from backend/.sqlx/query-d8aa1a5813fedbb22128112fcdf9d16769811cd5d5ad3b66362cc5bb1750ae6b.json rename to backend/.sqlx/query-e44ea5870a8eba1fe3d22d4dfe7734669464a56e470935ce2924723003f3f92b.json index 99c0212031..2a0c89ea25 100644 --- a/backend/.sqlx/query-d8aa1a5813fedbb22128112fcdf9d16769811cd5d5ad3b66362cc5bb1750ae6b.json +++ b/backend/.sqlx/query-e44ea5870a8eba1fe3d22d4dfe7734669464a56e470935ce2924723003f3f92b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, edited_by, edited_at, email, extra_perms, is_async, requires_auth, http_method as \"http_method: _\", static_asset_config as \"static_asset_config: _\", is_static_website FROM http_trigger\n WHERE workspace_id = $1", + "query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n script_path, \n is_flow, \n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n requires_auth, \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website \n FROM http_trigger\n WHERE workspace_id = $1\n ", "describe": { "columns": [ { @@ -10,61 +10,66 @@ }, { "ordinal": 1, + "name": "workspaced_route", + "type_info": "Bool" + }, + { + "ordinal": 2, "name": "path", "type_info": "Varchar" }, { - "ordinal": 2, + "ordinal": 3, "name": "route_path", "type_info": "Varchar" }, { - "ordinal": 3, + "ordinal": 4, "name": "route_path_key", "type_info": "Varchar" }, { - "ordinal": 4, + "ordinal": 5, "name": "script_path", "type_info": "Varchar" }, { - "ordinal": 5, + "ordinal": 6, "name": "is_flow", "type_info": "Bool" }, { - "ordinal": 6, + "ordinal": 7, "name": "edited_by", "type_info": "Varchar" }, { - "ordinal": 7, + "ordinal": 8, "name": "edited_at", "type_info": "Timestamptz" }, { - "ordinal": 8, + "ordinal": 9, "name": "email", "type_info": "Varchar" }, { - "ordinal": 9, + "ordinal": 10, "name": "extra_perms", "type_info": "Jsonb" }, { - "ordinal": 10, + "ordinal": 11, "name": "is_async", "type_info": "Bool" }, { - "ordinal": 11, + "ordinal": 12, "name": "requires_auth", "type_info": "Bool" }, { - "ordinal": 12, + "ordinal": 13, "name": "http_method: _", "type_info": { "Custom": { @@ -82,12 +87,12 @@ } }, { - "ordinal": 13, + "ordinal": 14, "name": "static_asset_config: _", "type_info": "Jsonb" }, { - "ordinal": 14, + "ordinal": 15, "name": "is_static_website", "type_info": "Bool" } @@ -111,9 +116,10 @@ false, false, false, + false, true, false ] }, - "hash": "d8aa1a5813fedbb22128112fcdf9d16769811cd5d5ad3b66362cc5bb1750ae6b" + "hash": "e44ea5870a8eba1fe3d22d4dfe7734669464a56e470935ce2924723003f3f92b" } diff --git a/backend/.sqlx/query-eb4779976677308e8ad7c6427bfb45146478f70956919c01718623f0828e0383.json b/backend/.sqlx/query-eb4779976677308e8ad7c6427bfb45146478f70956919c01718623f0828e0383.json new file mode 100644 index 0000000000..f1b390b511 --- /dev/null +++ b/backend/.sqlx/query-eb4779976677308e8ad7c6427bfb45146478f70956919c01718623f0828e0383.json @@ -0,0 +1,40 @@ +{ + "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 script_path, \n is_flow, \n is_async, \n requires_auth, \n http_method, \n static_asset_config, \n edited_by, \n email, \n edited_at, \n is_static_website\n ) \n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Bool", + "Varchar", + "Bool", + "Bool", + "Bool", + { + "Custom": { + "name": "http_method", + "kind": { + "Enum": [ + "get", + "post", + "put", + "delete", + "patch" + ] + } + } + }, + "Jsonb", + "Varchar", + "Varchar", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "eb4779976677308e8ad7c6427bfb45146478f70956919c01718623f0828e0383" +} diff --git a/backend/.sqlx/query-f632d08a8d3df691fff9f57fdf926f787287c3cd181a6853056077edba10473d.json b/backend/.sqlx/query-f632d08a8d3df691fff9f57fdf926f787287c3cd181a6853056077edba10473d.json new file mode 100644 index 0000000000..8fae751d85 --- /dev/null +++ b/backend/.sqlx/query-f632d08a8d3df691fff9f57fdf926f787287c3cd181a6853056077edba10473d.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT EXISTS(\n SELECT 1 \n FROM \n http_trigger \n WHERE \n workspace_id = $1 AND \n path = $2\n )\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "f632d08a8d3df691fff9f57fdf926f787287c3cd181a6853056077edba10473d" +} diff --git a/backend/migrations/20250309165536_add_workspaced_route_column_for_http_trigger.down.sql b/backend/migrations/20250309165536_add_workspaced_route_column_for_http_trigger.down.sql new file mode 100644 index 0000000000..937853bec6 --- /dev/null +++ b/backend/migrations/20250309165536_add_workspaced_route_column_for_http_trigger.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here +ALTER TABLE http_trigger +DROP COLUMN workspaced_route; \ No newline at end of file diff --git a/backend/migrations/20250309165536_add_workspaced_route_column_for_http_trigger.up.sql b/backend/migrations/20250309165536_add_workspaced_route_column_for_http_trigger.up.sql new file mode 100644 index 0000000000..8bb8c8e2ff --- /dev/null +++ b/backend/migrations/20250309165536_add_workspaced_route_column_for_http_trigger.up.sql @@ -0,0 +1,3 @@ +-- Add up migration script here +ALTER TABLE http_trigger +ADD COLUMN workspaced_route BOOLEAN NOT NULL DEFAULT false; \ No newline at end of file diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index c11360803e..260bb0b3fd 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -8116,6 +8116,8 @@ paths: enum: ["get", "post", "put", "delete", "patch"] trigger_path: type: string + workspaced_route: + type: boolean required: - route_path - http_method @@ -13767,6 +13769,8 @@ components: type: boolean is_static_website: type: boolean + workspaced_route: + type: boolean required: - route_path @@ -13784,6 +13788,8 @@ components: type: string route_path: type: string + workspaced_route: + type: boolean static_asset_config: type: object properties: @@ -13831,6 +13837,8 @@ components: type: string route_path: type: string + workspaced_route: + type: boolean static_asset_config: type: object properties: diff --git a/backend/windmill-api/src/http_triggers.rs b/backend/windmill-api/src/http_triggers.rs index 722a639842..9f7cb25425 100644 --- a/backend/windmill-api/src/http_triggers.rs +++ b/backend/windmill-api/src/http_triggers.rs @@ -34,6 +34,7 @@ use windmill_common::{ utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath}, worker::{to_raw_value, CLOUD_HOSTED}, }; +use std::borrow::Cow; lazy_static::lazy_static! { static ref ROUTE_PATH_KEY_RE: regex::Regex = regex::Regex::new(r"/?:[-\w]+").unwrap(); @@ -100,7 +101,7 @@ impl TryFrom<&http::Method> for HttpMethod { } } -#[derive(Deserialize)] +#[derive(Debug, Deserialize)] struct NewTrigger { path: String, route_path: String, @@ -110,6 +111,7 @@ struct NewTrigger { requires_auth: bool, http_method: HttpMethod, static_asset_config: Option>, + workspaced_route: Option, is_static_website: bool, } @@ -131,6 +133,7 @@ pub struct HttpTrigger { #[serde(skip_serializing_if = "Option::is_none")] pub static_asset_config: Option>, pub is_static_website: bool, + pub workspaced_route: Option, } #[derive(Deserialize)] @@ -143,6 +146,7 @@ struct EditTrigger { requires_auth: bool, http_method: HttpMethod, static_asset_config: Option>, + workspaced_route: Option, is_static_website: bool, } @@ -164,7 +168,24 @@ async fn list_triggers( 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") - .field("*") + .fields(&[ + "workspace_id", + "path", + "route_path", + "route_path_key", + "workspaced_route", + "script_path", + "is_flow", + "http_method", + "edited_by", + "email", + "edited_at", + "extra_perms", + "is_async", + "requires_auth", + "static_asset_config", + "is_static_website", + ]) .order_by("edited_at", true) .and_where("workspace_id = ?".bind(&w_id)) .offset(offset) @@ -199,9 +220,30 @@ async fn get_trigger( let path = path.to_path(); let trigger = sqlx::query_as!( HttpTrigger, - r#"SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as "http_method: _", edited_by, email, edited_at, extra_perms, is_async, requires_auth, static_asset_config as "static_asset_config: _", is_static_website - FROM http_trigger - WHERE workspace_id = $1 AND path = $2"#, + r#" + SELECT + workspace_id, + path, + route_path, + route_path_key, + workspaced_route, + script_path, + is_flow, + http_method as "http_method: _", + edited_by, + email, + edited_at, + extra_perms, + is_async, + requires_auth, + static_asset_config as "static_asset_config: _", + is_static_website + FROM + http_trigger + WHERE + workspace_id = $1 AND + path = $2 + "#, w_id, path, ) @@ -230,9 +272,16 @@ async fn create_trigger( // 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(ct.route_path.as_str(), ":key"); - - let exists = route_path_key_exists(&route_path_key, &ct.http_method, &w_id, None, &db).await?; + let route_path_key = ROUTE_PATH_KEY_RE.replace_all(&ct.route_path, ":key"); + let exists = route_path_key_exists( + &route_path_key, + &ct.http_method, + &w_id, + None, + ct.workspaced_route, + &db, + ) + .await?; if exists { return Err(error::Error::BadRequest( "A route already exists with this path".to_string(), @@ -247,11 +296,33 @@ async fn create_trigger( let mut tx = user_db.begin(&authed).await?; sqlx::query!( - "INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, static_asset_config, edited_by, email, edited_at, is_static_website) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13)", + r#" + INSERT INTO http_trigger ( + workspace_id, + path, + route_path, + route_path_key, + workspaced_route, + script_path, + is_flow, + is_async, + requires_auth, + http_method, + static_asset_config, + edited_by, + email, + edited_at, + is_static_website + ) + VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14 + ) + "#, w_id, ct.path, ct.route_path, &route_path_key, + ct.workspaced_route, ct.script_path, ct.is_flow, ct.is_async, @@ -260,9 +331,10 @@ async fn create_trigger( ct.static_asset_config as _, &authed.username, &authed.email, - ct.is_static_website, + ct.is_static_website ) - .execute(&mut *tx).await?; + .execute(&mut *tx) + .await?; audit_log( &mut *tx, @@ -288,7 +360,6 @@ async fn update_trigger( Json(ct): Json, ) -> error::Result { let path = path.to_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(), @@ -309,9 +380,15 @@ async fn update_trigger( 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), &db) - .await?; + 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(), @@ -319,13 +396,32 @@ async fn update_trigger( } tx = user_db.begin(&authed).await?; - sqlx::query!( - "UPDATE http_trigger - SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, static_asset_config = $7, edited_by = $8, email = $9, is_async = $10, requires_auth = $11, edited_at = now(), is_static_website = $12 - WHERE workspace_id = $13 AND path = $14", + r#" + UPDATE + http_trigger + SET + route_path = $1, + route_path_key = $2, + workspaced_route = $3, + script_path = $4, + path = $5, + is_flow = $6, + http_method = $7, + static_asset_config = $8, + edited_by = $9, + email = $10, + is_async = $11, + requires_auth = $12, + edited_at = now(), + is_static_website = $13 + WHERE + workspace_id = $14 AND + path = $15 + "#, route_path, &route_path_key, + ct.workspaced_route, ct.script_path, ct.path, ct.is_flow, @@ -339,12 +435,30 @@ async fn update_trigger( w_id, path, ) - .execute(&mut *tx).await?; + .execute(&mut *tx) + .await?; } else { tx = user_db.begin(&authed).await?; sqlx::query!( - "UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, static_asset_config = $5, edited_by = $6, email = $7, is_async = $8, requires_auth = $9, edited_at = now(), is_static_website = $10 - WHERE workspace_id = $11 AND path = $12", + r#" + UPDATE + http_trigger + SET + script_path = $1, + path = $2, + is_flow = $3, + http_method = $4, + static_asset_config = $5, + edited_by = $6, + email = $7, + is_async = $8, + requires_auth = $9, + edited_at = now(), + is_static_website = $10 + WHERE + workspace_id = $11 AND + path = $12 + "#, ct.script_path, ct.path, ct.is_flow, @@ -358,7 +472,8 @@ async fn update_trigger( w_id, path, ) - .execute(&mut *tx).await?; + .execute(&mut *tx) + .await?; } audit_log( @@ -386,9 +501,11 @@ async fn delete_trigger( let path = path.to_path(); let mut tx = user_db.begin(&authed).await?; sqlx::query!( - "DELETE FROM http_trigger WHERE workspace_id = $1 AND path = $2", + "DELETE FROM http_trigger + WHERE workspace_id = $1 + AND path = $2", w_id, - path, + path ) .execute(&mut *tx) .await?; @@ -415,13 +532,17 @@ async fn exists_trigger( ) -> 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)", + "SELECT EXISTS( + SELECT 1 FROM http_trigger + WHERE path = $1 AND workspace_id = $2 + )", path, - w_id, + w_id ) .fetch_one(&db) .await? .unwrap_or(false); + Ok(Json(exists)) } @@ -430,6 +551,7 @@ struct RouteExists { route_path: String, http_method: HttpMethod, trigger_path: Option, + workspaced_route: Option, } async fn route_path_key_exists( @@ -437,37 +559,65 @@ async fn route_path_key_exists( http_method: &HttpMethod, w_id: &str, trigger_path: Option<&str>, + workspaced_route: Option, db: &DB, ) -> error::Result { let exists = if *CLOUD_HOSTED { sqlx::query_scalar!( - "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 { - sqlx::query_scalar!( - "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND http_method = $2 AND ($3::TEXT IS NULL OR path != $3))", + 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 }): Json, + 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"); @@ -476,6 +626,7 @@ async fn exists_route( &http_method, &w_id, trigger_path.as_deref(), + workspaced_route, &db, ) .await?; @@ -494,6 +645,7 @@ struct TriggerRoute { edited_by: String, email: String, static_asset_config: Option>, + workspaced_route: Option, is_static_website: bool, } @@ -514,7 +666,26 @@ async fn get_http_route_trigger( let route_path = StripPath(splitted.collect::>().join("/")); let triggers = sqlx::query_as!( TriggerRoute, - r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, static_asset_config as "static_asset_config: _", is_static_website FROM http_trigger WHERE workspace_id = $1 AND http_method = $2"#, + r#" + SELECT + path, + script_path, + is_flow, + route_path, + workspace_id, + is_async, + requires_auth, + edited_by, + email, + static_asset_config AS "static_asset_config: _", + workspaced_route, + is_static_website + FROM + http_trigger + WHERE + workspace_id = $1 AND + http_method = $2 + "#, w_id, http_method as HttpMethod ) @@ -524,7 +695,25 @@ async fn get_http_route_trigger( } else { let triggers = sqlx::query_as!( TriggerRoute, - r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, static_asset_config as "static_asset_config: _", is_static_website FROM http_trigger WHERE http_method = $1"#, + r#" + SELECT + path, + script_path, + is_flow, + route_path, + workspace_id, + is_async, + requires_auth, + edited_by, + email, + static_asset_config AS "static_asset_config: _", + workspaced_route, + is_static_website + FROM + http_trigger + WHERE + http_method = $1 + "#, http_method as HttpMethod ) .fetch_all(db) @@ -535,7 +724,10 @@ async fn get_http_route_trigger( let mut router = matchit::Router::new(); for (idx, trigger) in triggers.iter().enumerate() { - let route_path = trigger.route_path.clone(); + let route_path = match trigger.workspaced_route { + Some(true) => format!("{}/{}", &trigger.workspace_id, &trigger.route_path), + _ => trigger.route_path.clone(), + }; if trigger.is_static_website { router .insert(format!("/{}/*wm_subpath", route_path), idx) @@ -583,7 +775,16 @@ async fn get_http_route_trigger( // check that the user has access to the trigger let mut tx = user_db.begin(&authed).await?; let exists = sqlx::query_scalar!( - "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1 AND path = $2)", + r#" + SELECT EXISTS( + SELECT 1 + FROM + http_trigger + WHERE + workspace_id = $1 AND + path = $2 + ) + "#, trigger.workspace_id, trigger.path ) diff --git a/backend/windmill-api/src/workspaces_export.rs b/backend/windmill-api/src/workspaces_export.rs index 8c6293bddb..f2c574de45 100644 --- a/backend/windmill-api/src/workspaces_export.rs +++ b/backend/windmill-api/src/workspaces_export.rs @@ -534,13 +534,32 @@ pub(crate) async fn tarball_workspace( #[cfg(feature = "http_trigger")] { let http_triggers = sqlx::query_as!( - crate::http_triggers::HttpTrigger, - "SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, edited_by, edited_at, email, extra_perms, is_async, requires_auth, http_method as \"http_method: _\", static_asset_config as \"static_asset_config: _\", is_static_website FROM http_trigger - WHERE workspace_id = $1", - &w_id - ) - .fetch_all(&mut *tx) - .await?; + crate::http_triggers::HttpTrigger, + r#" + SELECT + workspace_id, + workspaced_route, + path, + route_path, + route_path_key, + script_path, + is_flow, + edited_by, + edited_at, + email, + extra_perms, + is_async, + requires_auth, + http_method AS "http_method: _", + static_asset_config AS "static_asset_config: _", + is_static_website + FROM http_trigger + WHERE workspace_id = $1 + "#, + &w_id + ) + .fetch_all(&mut *tx) + .await?; for trigger in http_triggers { let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap(); @@ -608,10 +627,7 @@ pub(crate) async fn tarball_workspace( 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.path)) .await?; } } diff --git a/backend/windmill-common/src/s3_helpers.rs b/backend/windmill-common/src/s3_helpers.rs index 01507f5b6a..2c5a76a2e1 100644 --- a/backend/windmill-common/src/s3_helpers.rs +++ b/backend/windmill-common/src/s3_helpers.rs @@ -111,7 +111,7 @@ pub struct S3AwsOidcResource { pub audience: Option, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct S3Object { pub s3: String, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/frontend/src/lib/components/triggers/http/RouteEditorConfigSection.svelte b/frontend/src/lib/components/triggers/http/RouteEditorConfigSection.svelte index 4837d1d561..4e386d5b55 100644 --- a/frontend/src/lib/components/triggers/http/RouteEditorConfigSection.svelte +++ b/frontend/src/lib/components/triggers/http/RouteEditorConfigSection.svelte @@ -16,12 +16,14 @@ import CaptureTable from '../CaptureTable.svelte' import ClipboardPanel from '../../details/ClipboardPanel.svelte' import { isCloudHosted } from '$lib/cloud' + import Toggle from '$lib/components/Toggle.svelte' import { isObject } from '$lib/utils' + import { getHttpRoute } from './utils' export let initialTriggerPath: string | undefined = undefined export let dirtyRoutePath: boolean = false - export let route_path: string | undefined - export let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' | undefined + export let route_path: string = '' + export let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' = 'post' export let can_write: boolean = false export let static_asset_config: { s3: string; storage?: string; filename?: string } | undefined = undefined @@ -29,6 +31,7 @@ export let headless: boolean = false export let captureInfo: CaptureInfo | undefined = undefined export let captureTable: CaptureTable | undefined = undefined + export let workspaced_route: boolean = false export let isValid = false export let runnableArgs: any = {} let validateTimeout: NodeJS.Timeout | undefined = undefined @@ -36,7 +39,8 @@ let routeError: string = '' async function validateRoute( routePath: string | undefined, - method: typeof http_method + method: typeof http_method, + workspaced_route: boolean ): Promise { if (validateTimeout) { clearTimeout(validateTimeout) @@ -44,7 +48,7 @@ validateTimeout = setTimeout(async () => { if (!routePath || !method || !/^:?[-\w]+(\/:?[-\w]+)*$/.test(routePath)) { routeError = 'Endpoint not valid' - } else if (await routeExists(routePath, method)) { + } else if (await routeExists(routePath, method, workspaced_route)) { routeError = 'Endpoint already taken' } else { routeError = '' @@ -52,32 +56,23 @@ validateTimeout = undefined }, 500) } - - async function routeExists(route_path: string, method: Exclude) { + async function routeExists(route_path: string, method: Exclude, workspaced_route: boolean) { return await HttpTriggerService.existsRoute({ workspace: $workspaceStore!, requestBody: { route_path, http_method: method, - trigger_path: initialTriggerPath + trigger_path: initialTriggerPath, + workspaced_route: workspaced_route } }) } - $: validateRoute(route_path, http_method) + $: validateRoute(route_path, http_method, workspaced_route) $: isValid = routeError === '' - function getHttpRoute(route_path: string | undefined) { - return `${location.origin}${base}/api/r/${isCloudHosted() ? $workspaceStore + '/' : ''}${ - route_path ?? '' - }` - } - - $: fullRoute = getHttpRoute(route_path) - - $: !http_method && (http_method = 'post') - $: route_path === undefined && (route_path = '') + $: fullRoute = getHttpRoute(route_path, workspaced_route, $workspaceStore ?? '')
@@ -182,10 +177,26 @@ }} />
-
{dirtyRoutePath ? routeError : ''}
+ {#if !isCloudHosted()} +
+ { + workspaced_route = !workspaced_route + dirtyRoutePath = true + }} + options={{ + right: 'Prefix with workspace', + rightTooltip: + 'Prefixes the route with the workspace ID (e.g., {base_url}/api/r/{workspace_id}/{route}). Note: deploying the HTTP trigger to another workspace updates the route workspace prefix accordingly.' + }} + /> +
+ {/if} diff --git a/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte b/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte index 7199455250..918e6678fc 100644 --- a/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte +++ b/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte @@ -34,6 +34,22 @@ let script_path = '' let initialScriptPath = '' let fixedScriptPath = '' + let path: string = '' + let pathError = '' + let isValid = false + let dirtyRoutePath = false + let is_async = false + let requires_auth = false + let route_path = '' + let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' = 'post' + let static_asset_config: { s3: string; storage?: string; filename?: string } | undefined = + undefined + let is_static_website = false + + let s3FilePicker: S3FilePicker + let s3FileUploadRawMode = false + let s3Editor: SimpleEditor | undefined = undefined + let workspaced_route: boolean = false let drawerLoading = true export async function openEdit(ePath: string, isFlow: boolean) { @@ -78,27 +94,12 @@ initialPath = '' dirtyPath = false is_static_website = false + workspaced_route = false } finally { drawerLoading = false } } - let path: string = '' - let pathError = '' - let isValid = false - let dirtyRoutePath = false - let is_async = false - let requires_auth = false - let route_path = '' - let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' = 'post' - let static_asset_config: { s3: string; storage?: string; filename?: string } | undefined = - undefined - let is_static_website = false - - let s3FilePicker: S3FilePicker - let s3FileUploadRawMode = false - let s3Editor: SimpleEditor | undefined = undefined - const dispatch = createEventDispatcher() let can_write = true @@ -109,13 +110,13 @@ }) script_path = s.script_path initialScriptPath = s.script_path - is_flow = s.is_flow path = s.path route_path = s.route_path http_method = s.http_method ?? 'post' is_async = s.is_async requires_auth = s.requires_auth + workspaced_route = s.workspaced_route ?? false if (!isCloudHosted()) { static_asset_config = s.static_asset_config s3FileUploadRawMode = !!static_asset_config @@ -139,7 +140,8 @@ route_path: $userStore?.is_admin || $userStore?.is_super_admin ? route_path : undefined, http_method, static_asset_config, - is_static_website + is_static_website, + workspaced_route } }) sendUserToast(`Route ${path} updated`) @@ -155,7 +157,8 @@ route_path, http_method, static_asset_config, - is_static_website + is_static_website, + workspaced_route } }) sendUserToast(`Route ${path} created`) @@ -357,9 +360,7 @@ btnClasses="ml-4 mt-2" color="dark" size="xs" - href={itemKind === 'flow' - ? '/flows/add?hub=62' - : '/scripts/add?hub=hub%2F11627'} + href={itemKind === 'flow' ? '/flows/add?hub=62' : '/scripts/add?hub=hub%2F11627'} target="_blank">Create from template {/if} @@ -373,6 +374,7 @@ bind:isValid bind:dirtyRoutePath bind:http_method + bind:workspaced_route {can_write} bind:static_asset_config /> diff --git a/frontend/src/lib/components/triggers/http/utils.ts b/frontend/src/lib/components/triggers/http/utils.ts new file mode 100644 index 0000000000..bf0a244c0e --- /dev/null +++ b/frontend/src/lib/components/triggers/http/utils.ts @@ -0,0 +1,8 @@ +import { base } from "$lib/base"; +import { isCloudHosted } from "$lib/cloud"; + +export function getHttpRoute(route_path: string, workspaced_route: boolean, workspace_id: string) { + return `${location.origin}${base}/api/r/${ + isCloudHosted() || workspaced_route ? workspace_id + '/' : '' + }${route_path}` +} \ No newline at end of file diff --git a/frontend/src/routes/(root)/(logged)/routes/+page.js b/frontend/src/routes/(root)/(logged)/routes/+page.js index 919dd87a1e..f4fd63c8ba 100644 --- a/frontend/src/routes/(root)/(logged)/routes/+page.js +++ b/frontend/src/routes/(root)/(logged)/routes/+page.js @@ -1,5 +1,5 @@ export function load() { return { - stuff: { title: 'HTTP routes' } + stuff: { title: 'HTTP triggers' } } } diff --git a/frontend/src/routes/(root)/(logged)/routes/+page.svelte b/frontend/src/routes/(root)/(logged)/routes/+page.svelte index 03a87d7b86..a43cfdd86f 100644 --- a/frontend/src/routes/(root)/(logged)/routes/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/routes/+page.svelte @@ -5,7 +5,13 @@ type HttpTrigger, type WorkspaceDeployUISettings } from '$lib/gen' - import { canWrite, displayDate, getLocalSetting, storeLocalSetting } from '$lib/utils' + import { + canWrite, + copyToClipboard, + displayDate, + getLocalSetting, + storeLocalSetting + } from '$lib/utils' import { base } from '$app/paths' import CenteredPage from '$lib/components/CenteredPage.svelte' import { Button, Skeleton } from '$lib/components/common' @@ -15,7 +21,7 @@ import ShareModal from '$lib/components/ShareModal.svelte' import Toggle from '$lib/components/Toggle.svelte' import { userStore, workspaceStore, userWorkspaces, enterpriseLicense } from '$lib/stores' - import { Route, Code, Eye, Pen, Plus, Share, Trash, FileUp } from 'lucide-svelte' + import { Route, Code, Eye, Pen, Plus, Share, Trash, FileUp, ClipboardCopy } from 'lucide-svelte' import { goto } from '$lib/navigation' import SearchItems from '$lib/components/SearchItems.svelte' import NoItemFound from '$lib/components/home/NoItemFound.svelte' @@ -28,6 +34,8 @@ import RouteEditor from '$lib/components/triggers/http/RouteEditor.svelte' import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte' import { ALL_DEPLOYABLE, isDeployable } from '$lib/utils_deployable' + import { isCloudHosted } from '$lib/cloud' + import { getHttpRoute } from '$lib/components/triggers/http/utils' type TriggerW = HttpTrigger & { canWrite: boolean } @@ -229,7 +237,7 @@
No routes
{:else if items?.length}
- {#each items.slice(0, nbDisplayed) as { path, edited_by, edited_at, script_path, route_path, is_flow, extra_perms, canWrite, marked, http_method, static_asset_config } (path)} + {#each items.slice(0, nbDisplayed) as { workspace_id, workspaced_route, path, edited_by, edited_at, script_path, route_path, is_flow, extra_perms, canWrite, marked, http_method, static_asset_config } (path)} {@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
{:else} - {http_method.toUpperCase()} /{route_path} + {http_method.toUpperCase()} + {isCloudHosted() || workspaced_route + ? workspace_id + '/' + route_path + : route_path} {/if}
@@ -270,6 +281,15 @@
+