From df4b6db0d438f6ef9acf28b04431522b47438f80 Mon Sep 17 00:00:00 2001 From: dieriba Date: Thu, 13 Mar 2025 21:39:54 +0100 Subject: [PATCH] feat: add raw string option and wrap option for http trigger (#5467) * 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 * handling two new option in http route 'wrap_body' and 'raw_string' * update sqlx and add wrap_body and raw_string to http route * fix: undefined route path * nits: remove unnecessay ref as type implement copy * nits * fix: undefined protection --------- Co-authored-by: HugoCasa --- ...8234ca7d1efeee9661f3901f298da375e73f7.json | 30 +-- ...f9a8108c436cba1d2b770e9104d1c8590bcb.json} | 16 +- ...2079c7274ca0827dc2a26b29394c12b3f7fc.json} | 6 +- ...462df32172ad0d67321495c94bf2d59010f5.json} | 18 +- ...dcc40f463cbc52d94ed9315cf9a547d4c89f2.json | 30 +-- ...a6ff8553b9c196ca64ea2752df8cfe30c30c.json} | 16 +- ...c57cc51c7ea256058daf170a1ec8573ed102.json} | 18 +- ...05d394a7cbcf0038c72a78add5c7b02ef5927.json | 2 +- ...9b3abe9d04b86ad50c0f62903a1a79479a60.json} | 6 +- backend/.vscode/settings.json | 2 +- ...le_format_response_of_http_routes.down.sql | 4 + ...ndle_format_response_of_http_routes.up.sql | 4 + backend/windmill-api/openapi.yaml | 15 ++ backend/windmill-api/src/args.rs | 246 +++++++++--------- backend/windmill-api/src/http_triggers.rs | 92 +++++-- backend/windmill-api/src/lib.rs | 2 +- backend/windmill-api/src/workspaces_export.rs | 4 +- frontend/src/lib/components/ArgInput.svelte | 2 +- .../http/RouteEditorConfigSection.svelte | 15 +- .../triggers/http/RouteEditorInner.svelte | 50 +++- .../src/lib/components/triggers/http/utils.ts | 4 +- 21 files changed, 377 insertions(+), 205 deletions(-) rename backend/.sqlx/{query-4353a7de6b4de1df6e19d95a4db8ae89c8d75034a9210233aed2f0414690e37d.json => query-157090e958a907a09554c2491cabf9a8108c436cba1d2b770e9104d1c8590bcb.json} (83%) rename backend/.sqlx/{query-eb4779976677308e8ad7c6427bfb45146478f70956919c01718623f0828e0383.json => query-22888025706d457da65207ea9d592079c7274ca0827dc2a26b29394c12b3f7fc.json} (60%) rename backend/.sqlx/{query-01b87cdd552c2d5a3cd475eb705110394fe6c8abcc1fa93291fac6f929043d20.json => query-54798bea6f75ab6d3c680b1085f0462df32172ad0d67321495c94bf2d59010f5.json} (78%) rename backend/.sqlx/{query-e44ea5870a8eba1fe3d22d4dfe7734669464a56e470935ce2924723003f3f92b.json => query-5824982a58edd5bb2666daac49b2a6ff8553b9c196ca64ea2752df8cfe30c30c.json} (87%) rename backend/.sqlx/{query-300b41622dbcdf908d9e173eee2133239d5806fc50bba2d3fe9d8a6e11627655.json => query-6860deb7064e57b9e7646438145dc57cc51c7ea256058daf170a1ec8573ed102.json} (77%) rename backend/.sqlx/{query-78e6342696052d0e1f79fd721f5942d4da686a65ab11eefdca16367d2db67963.json => query-fd724083c4ef6ec0f1cede5813c89b3abe9d04b86ad50c0f62903a1a79479a60.json} (55%) create mode 100644 backend/migrations/20250311162140_add_columns_to_handle_format_response_of_http_routes.down.sql create mode 100644 backend/migrations/20250311162140_add_columns_to_handle_format_response_of_http_routes.up.sql diff --git a/backend/.sqlx/query-08f288d2781d823e109a9e5b8848234ca7d1efeee9661f3901f298da375e73f7.json b/backend/.sqlx/query-08f288d2781d823e109a9e5b8848234ca7d1efeee9661f3901f298da375e73f7.json index 4bcf3c6ce3..2be39fce26 100644 --- a/backend/.sqlx/query-08f288d2781d823e109a9e5b8848234ca7d1efeee9661f3901f298da375e73f7.json +++ b/backend/.sqlx/query-08f288d2781d823e109a9e5b8848234ca7d1efeee9661f3901f298da375e73f7.json @@ -130,28 +130,28 @@ }, { "ordinal": 25, - "name": "ai_models", - "type_info": "VarcharArray" - }, - { - "ordinal": 26, - "name": "code_completion_model", - "type_info": "Varchar" - }, - { - "ordinal": 27, "name": "teams_command_script", "type_info": "Text" }, { - "ordinal": 28, + "ordinal": 26, "name": "teams_team_id", "type_info": "Text" }, { - "ordinal": 29, + "ordinal": 27, "name": "teams_team_name", "type_info": "Text" + }, + { + "ordinal": 28, + "name": "ai_models", + "type_info": "VarcharArray" + }, + { + "ordinal": 29, + "name": "code_completion_model", + "type_info": "Varchar" } ], "parameters": { @@ -185,10 +185,10 @@ true, true, true, + true, + true, + true, false, - true, - true, - true, true ] }, diff --git a/backend/.sqlx/query-4353a7de6b4de1df6e19d95a4db8ae89c8d75034a9210233aed2f0414690e37d.json b/backend/.sqlx/query-157090e958a907a09554c2491cabf9a8108c436cba1d2b770e9104d1c8590bcb.json similarity index 83% rename from backend/.sqlx/query-4353a7de6b4de1df6e19d95a4db8ae89c8d75034a9210233aed2f0414690e37d.json rename to backend/.sqlx/query-157090e958a907a09554c2491cabf9a8108c436cba1d2b770e9104d1c8590bcb.json index 49ede3a762..df4a83bdb4 100644 --- a/backend/.sqlx/query-4353a7de6b4de1df6e19d95a4db8ae89c8d75034a9210233aed2f0414690e37d.json +++ b/backend/.sqlx/query-157090e958a907a09554c2491cabf9a8108c436cba1d2b770e9104d1c8590bcb.json @@ -1,6 +1,6 @@ { "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 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 ", + "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 wrap_body,\n raw_string\n FROM \n http_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ", "describe": { "columns": [ { @@ -95,6 +95,16 @@ "ordinal": 15, "name": "is_static_website", "type_info": "Bool" + }, + { + "ordinal": 16, + "name": "wrap_body", + "type_info": "Bool" + }, + { + "ordinal": 17, + "name": "raw_string", + "type_info": "Bool" } ], "parameters": { @@ -119,8 +129,10 @@ false, false, true, + false, + false, false ] }, - "hash": "4353a7de6b4de1df6e19d95a4db8ae89c8d75034a9210233aed2f0414690e37d" + "hash": "157090e958a907a09554c2491cabf9a8108c436cba1d2b770e9104d1c8590bcb" } diff --git a/backend/.sqlx/query-eb4779976677308e8ad7c6427bfb45146478f70956919c01718623f0828e0383.json b/backend/.sqlx/query-22888025706d457da65207ea9d592079c7274ca0827dc2a26b29394c12b3f7fc.json similarity index 60% rename from backend/.sqlx/query-eb4779976677308e8ad7c6427bfb45146478f70956919c01718623f0828e0383.json rename to backend/.sqlx/query-22888025706d457da65207ea9d592079c7274ca0827dc2a26b29394c12b3f7fc.json index f1b390b511..f4e752cc71 100644 --- a/backend/.sqlx/query-eb4779976677308e8ad7c6427bfb45146478f70956919c01718623f0828e0383.json +++ b/backend/.sqlx/query-22888025706d457da65207ea9d592079c7274ca0827dc2a26b29394c12b3f7fc.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 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 ", + "query": "\n INSERT INTO http_trigger (\n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n wrap_body,\n raw_string,\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, $14, $15, now(), $16\n )\n ", "describe": { "columns": [], "parameters": { @@ -10,6 +10,8 @@ "Varchar", "Varchar", "Bool", + "Bool", + "Bool", "Varchar", "Bool", "Bool", @@ -36,5 +38,5 @@ }, "nullable": [] }, - "hash": "eb4779976677308e8ad7c6427bfb45146478f70956919c01718623f0828e0383" + "hash": "22888025706d457da65207ea9d592079c7274ca0827dc2a26b29394c12b3f7fc" } diff --git a/backend/.sqlx/query-01b87cdd552c2d5a3cd475eb705110394fe6c8abcc1fa93291fac6f929043d20.json b/backend/.sqlx/query-54798bea6f75ab6d3c680b1085f0462df32172ad0d67321495c94bf2d59010f5.json similarity index 78% rename from backend/.sqlx/query-01b87cdd552c2d5a3cd475eb705110394fe6c8abcc1fa93291fac6f929043d20.json rename to backend/.sqlx/query-54798bea6f75ab6d3c680b1085f0462df32172ad0d67321495c94bf2d59010f5.json index cae9fdd79f..cafdebb2c3 100644 --- a/backend/.sqlx/query-01b87cdd552c2d5a3cd475eb705110394fe6c8abcc1fa93291fac6f929043d20.json +++ b/backend/.sqlx/query-54798bea6f75ab6d3c680b1085f0462df32172ad0d67321495c94bf2d59010f5.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "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 ", + "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 wrap_body,\n raw_string,\n workspaced_route,\n is_static_website\n FROM \n http_trigger \n WHERE \n http_method = $1\n ", "describe": { "columns": [ { @@ -55,11 +55,21 @@ }, { "ordinal": 10, - "name": "workspaced_route", + "name": "wrap_body", "type_info": "Bool" }, { "ordinal": 11, + "name": "raw_string", + "type_info": "Bool" + }, + { + "ordinal": 12, + "name": "workspaced_route", + "type_info": "Bool" + }, + { + "ordinal": 13, "name": "is_static_website", "type_info": "Bool" } @@ -94,8 +104,10 @@ false, true, false, + false, + false, false ] }, - "hash": "01b87cdd552c2d5a3cd475eb705110394fe6c8abcc1fa93291fac6f929043d20" + "hash": "54798bea6f75ab6d3c680b1085f0462df32172ad0d67321495c94bf2d59010f5" } diff --git a/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json b/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json index 14685a8bfa..9243288c9d 100644 --- a/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json +++ b/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json @@ -130,28 +130,28 @@ }, { "ordinal": 25, - "name": "ai_models", - "type_info": "VarcharArray" - }, - { - "ordinal": 26, - "name": "code_completion_model", - "type_info": "Varchar" - }, - { - "ordinal": 27, "name": "teams_command_script", "type_info": "Text" }, { - "ordinal": 28, + "ordinal": 26, "name": "teams_team_id", "type_info": "Text" }, { - "ordinal": 29, + "ordinal": 27, "name": "teams_team_name", "type_info": "Text" + }, + { + "ordinal": 28, + "name": "ai_models", + "type_info": "VarcharArray" + }, + { + "ordinal": 29, + "name": "code_completion_model", + "type_info": "Varchar" } ], "parameters": { @@ -185,10 +185,10 @@ true, true, true, + true, + true, + true, false, - true, - true, - true, true ] }, diff --git a/backend/.sqlx/query-e44ea5870a8eba1fe3d22d4dfe7734669464a56e470935ce2924723003f3f92b.json b/backend/.sqlx/query-5824982a58edd5bb2666daac49b2a6ff8553b9c196ca64ea2752df8cfe30c30c.json similarity index 87% rename from backend/.sqlx/query-e44ea5870a8eba1fe3d22d4dfe7734669464a56e470935ce2924723003f3f92b.json rename to backend/.sqlx/query-5824982a58edd5bb2666daac49b2a6ff8553b9c196ca64ea2752df8cfe30c30c.json index 2a0c89ea25..c54aa8f20d 100644 --- a/backend/.sqlx/query-e44ea5870a8eba1fe3d22d4dfe7734669464a56e470935ce2924723003f3f92b.json +++ b/backend/.sqlx/query-5824982a58edd5bb2666daac49b2a6ff8553b9c196ca64ea2752df8cfe30c30c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "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 ", + "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 wrap_body,\n raw_string\n FROM http_trigger\n WHERE workspace_id = $1\n ", "describe": { "columns": [ { @@ -95,6 +95,16 @@ "ordinal": 15, "name": "is_static_website", "type_info": "Bool" + }, + { + "ordinal": 16, + "name": "wrap_body", + "type_info": "Bool" + }, + { + "ordinal": 17, + "name": "raw_string", + "type_info": "Bool" } ], "parameters": { @@ -118,8 +128,10 @@ false, false, true, + false, + false, false ] }, - "hash": "e44ea5870a8eba1fe3d22d4dfe7734669464a56e470935ce2924723003f3f92b" + "hash": "5824982a58edd5bb2666daac49b2a6ff8553b9c196ca64ea2752df8cfe30c30c" } diff --git a/backend/.sqlx/query-300b41622dbcdf908d9e173eee2133239d5806fc50bba2d3fe9d8a6e11627655.json b/backend/.sqlx/query-6860deb7064e57b9e7646438145dc57cc51c7ea256058daf170a1ec8573ed102.json similarity index 77% rename from backend/.sqlx/query-300b41622dbcdf908d9e173eee2133239d5806fc50bba2d3fe9d8a6e11627655.json rename to backend/.sqlx/query-6860deb7064e57b9e7646438145dc57cc51c7ea256058daf170a1ec8573ed102.json index b2c551759d..be5ab7a673 100644 --- a/backend/.sqlx/query-300b41622dbcdf908d9e173eee2133239d5806fc50bba2d3fe9d8a6e11627655.json +++ b/backend/.sqlx/query-6860deb7064e57b9e7646438145dc57cc51c7ea256058daf170a1ec8573ed102.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "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 ", + "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 wrap_body,\n raw_string,\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,11 +55,21 @@ }, { "ordinal": 10, - "name": "workspaced_route", + "name": "wrap_body", "type_info": "Bool" }, { "ordinal": 11, + "name": "raw_string", + "type_info": "Bool" + }, + { + "ordinal": 12, + "name": "workspaced_route", + "type_info": "Bool" + }, + { + "ordinal": 13, "name": "is_static_website", "type_info": "Bool" } @@ -95,8 +105,10 @@ false, true, false, + false, + false, false ] }, - "hash": "300b41622dbcdf908d9e173eee2133239d5806fc50bba2d3fe9d8a6e11627655" + "hash": "6860deb7064e57b9e7646438145dc57cc51c7ea256058daf170a1ec8573ed102" } diff --git a/backend/.sqlx/query-ddf2eccb78a310ed00c7d8b9c3f05d394a7cbcf0038c72a78add5c7b02ef5927.json b/backend/.sqlx/query-ddf2eccb78a310ed00c7d8b9c3f05d394a7cbcf0038c72a78add5c7b02ef5927.json index c2dfed73a2..5bfff47576 100644 --- a/backend/.sqlx/query-ddf2eccb78a310ed00c7d8b9c3f05d394a7cbcf0038c72a78add5c7b02ef5927.json +++ b/backend/.sqlx/query-ddf2eccb78a310ed00c7d8b9c3f05d394a7cbcf0038c72a78add5c7b02ef5927.json @@ -15,7 +15,7 @@ ] }, "nullable": [ - null + true ] }, "hash": "ddf2eccb78a310ed00c7d8b9c3f05d394a7cbcf0038c72a78add5c7b02ef5927" diff --git a/backend/.sqlx/query-78e6342696052d0e1f79fd721f5942d4da686a65ab11eefdca16367d2db67963.json b/backend/.sqlx/query-fd724083c4ef6ec0f1cede5813c89b3abe9d04b86ad50c0f62903a1a79479a60.json similarity index 55% rename from backend/.sqlx/query-78e6342696052d0e1f79fd721f5942d4da686a65ab11eefdca16367d2db67963.json rename to backend/.sqlx/query-fd724083c4ef6ec0f1cede5813c89b3abe9d04b86ad50c0f62903a1a79479a60.json index 19b8c3e0d7..a24ed36971 100644 --- a/backend/.sqlx/query-78e6342696052d0e1f79fd721f5942d4da686a65ab11eefdca16367d2db67963.json +++ b/backend/.sqlx/query-fd724083c4ef6ec0f1cede5813c89b3abe9d04b86ad50c0f62903a1a79479a60.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 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 ", + "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 script_path = $6, \n path = $7, \n is_flow = $8, \n http_method = $9, \n static_asset_config = $10, \n edited_by = $11, \n email = $12, \n is_async = $13, \n requires_auth = $14, \n edited_at = now(), \n is_static_website = $15\n WHERE \n workspace_id = $16 AND \n path = $17\n ", "describe": { "columns": [], "parameters": { @@ -8,6 +8,8 @@ "Varchar", "Varchar", "Bool", + "Bool", + "Bool", "Varchar", "Varchar", "Bool", @@ -37,5 +39,5 @@ }, "nullable": [] }, - "hash": "78e6342696052d0e1f79fd721f5942d4da686a65ab11eefdca16367d2db67963" + "hash": "fd724083c4ef6ec0f1cede5813c89b3abe9d04b86ad50c0f62903a1a79479a60" } diff --git a/backend/.vscode/settings.json b/backend/.vscode/settings.json index ab8340f5c8..6ebd36cf07 100644 --- a/backend/.vscode/settings.json +++ b/backend/.vscode/settings.json @@ -11,5 +11,5 @@ "remote.autoForwardPorts": true, "conventionalCommits.scopes": [ "restructring triggers, decoding trigger message on work" - ], + ] } diff --git a/backend/migrations/20250311162140_add_columns_to_handle_format_response_of_http_routes.down.sql b/backend/migrations/20250311162140_add_columns_to_handle_format_response_of_http_routes.down.sql new file mode 100644 index 0000000000..8e428e5290 --- /dev/null +++ b/backend/migrations/20250311162140_add_columns_to_handle_format_response_of_http_routes.down.sql @@ -0,0 +1,4 @@ +-- Add down migration script here +ALTER TABLE http_trigger +DROP COLUMN wrap_body, +DROP COLUMN raw_string; \ No newline at end of file diff --git a/backend/migrations/20250311162140_add_columns_to_handle_format_response_of_http_routes.up.sql b/backend/migrations/20250311162140_add_columns_to_handle_format_response_of_http_routes.up.sql new file mode 100644 index 0000000000..13b1f4bd58 --- /dev/null +++ b/backend/migrations/20250311162140_add_columns_to_handle_format_response_of_http_routes.up.sql @@ -0,0 +1,4 @@ +-- Add up migration script here +ALTER TABLE http_trigger +ADD COLUMN wrap_body BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN raw_string 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 5a40693fc7..4716daeb12 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -13772,6 +13772,10 @@ components: type: boolean workspaced_route: type: boolean + wrap_body: + type: boolean + raw_string: + type: boolean required: - route_path @@ -13779,6 +13783,9 @@ components: - requires_auth - http_method - is_static_website + - workspaced_route + - wrap_body + - raw_string NewHttpTrigger: type: object @@ -13818,6 +13825,10 @@ components: type: boolean is_static_website: type: boolean + wrap_body: + type: boolean + raw_string: + type: boolean required: - path @@ -13867,6 +13878,10 @@ components: type: boolean is_static_website: type: boolean + wrap_body: + type: boolean + raw_string: + type: boolean required: - path - script_path diff --git a/backend/windmill-api/src/args.rs b/backend/windmill-api/src/args.rs index a482745f27..fd2ceefabe 100644 --- a/backend/windmill-api/src/args.rs +++ b/backend/windmill-api/src/args.rs @@ -23,7 +23,7 @@ use crate::db::ApiAuthed; #[cfg(feature = "parquet")] use crate::job_helpers_ee::{get_random_file_name, upload_file_internal}; -#[derive(Default)] +#[derive(Debug, Default)] pub struct WebhookArgs { pub args: PushArgsOwned, pub multipart: Option, @@ -163,6 +163,130 @@ async fn req_to_string( .map_err(|e| Error::BadRequest(format!("invalid utf8: {}", e)).into_response()) } +pub async fn try_from_request_body( + request: Request, + _state: &S, + use_raw: Option, + wrap_body: Option, +) -> Result +where + S: Send + Sync, +{ + let (content_type, mut extra, use_raw, wrap_body) = { + let headers_map = request.headers(); + let content_type_header = headers_map.get(CONTENT_TYPE); + let content_type = content_type_header.and_then(|value| value.to_str().ok()); + let uri = request.uri(); + let query = Query::::try_from_uri(uri).unwrap().0; + let mut extra = build_extra(&headers_map, query.include_header); + let query_decode = DecodeQueries::from_uri(uri); + if let Some(DecodeQueries(queries)) = query_decode { + extra.extend(queries); + } + let raw = query.raw.unwrap_or(use_raw.unwrap_or(false)); + let wrap_body = query.wrap_body.unwrap_or(wrap_body.unwrap_or(false)); + (content_type, extra, raw, wrap_body) + }; + + let no_content_type = content_type.is_none(); + if no_content_type || content_type.unwrap().starts_with("application/json") { + let bytes = Bytes::from_request(request, _state) + .await + .map_err(IntoResponse::into_response)?; + if no_content_type && bytes.is_empty() { + if use_raw { + extra.insert("raw_string".to_string(), to_raw_value(&"".to_string())); + } + let mut args = HashMap::new(); + if wrap_body { + args.insert("body".to_string(), to_raw_value(&serde_json::json!({}))); + } + return Ok(WebhookArgs { + args: PushArgsOwned { extra: Some(extra), args: args }, + ..Default::default() + }); + } + let str = String::from_utf8(bytes.to_vec()) + .map_err(|e| Error::BadRequest(format!("invalid utf8: {}", e)).into_response())?; + + PushArgsOwned::from_json(extra, use_raw, wrap_body, str) + .await + .map(|args| WebhookArgs { args, ..Default::default() }) + } else if content_type + .unwrap() + .starts_with("application/cloudevents+json") + { + let str = req_to_string(request, _state).await?; + + PushArgsOwned::from_ce_json(extra, use_raw, str) + .await + .map(|args| WebhookArgs { args, ..Default::default() }) + } else if content_type + .unwrap() + .starts_with("application/cloudevents-batch+json") + { + Err( + Error::BadRequest(format!("Cloud events batching is not supported yet")) + .into_response(), + ) + } else if content_type.unwrap().starts_with("text/plain") { + let str = req_to_string(request, _state).await?; + extra.insert("raw_string".to_string(), to_raw_value(&str)); + Ok(WebhookArgs { + args: PushArgsOwned { extra: Some(extra), args: HashMap::new() }, + ..Default::default() + }) + } else if content_type + .unwrap() + .starts_with("application/x-www-form-urlencoded") + { + let bytes = Bytes::from_request(request, _state) + .await + .map_err(IntoResponse::into_response)?; + + if use_raw { + let raw_string = String::from_utf8(bytes.to_vec()) + .map_err(|e| Error::BadRequest(format!("invalid utf8: {}", e)).into_response())?; + extra.insert("raw_string".to_string(), to_raw_value(&raw_string)); + } + + let payload: HashMap> = serde_urlencoded::from_bytes(&bytes) + .map_err(|e| { + Error::BadRequest(format!("invalid urlencoded data: {}", e)).into_response() + })?; + let payload = payload + .into_iter() + .map(|(k, v)| (k, to_raw_value(&v))) + .collect::>(); + + return Ok(WebhookArgs { + args: PushArgsOwned { extra: Some(extra), args: payload }, + ..Default::default() + }); + } else if content_type.unwrap().starts_with("application/xml") + || content_type.unwrap().starts_with("text/xml") + { + let str = req_to_string(request, _state).await?; + extra.insert("raw_string".to_string(), to_raw_value(&str)); + Ok(WebhookArgs { + args: PushArgsOwned { extra: Some(extra), args: HashMap::new() }, + ..Default::default() + }) + } else if content_type.unwrap().starts_with("multipart/form-data") { + let multipart = Multipart::from_request(request, _state) + .await + .map_err(IntoResponse::into_response)?; + + Ok(WebhookArgs { + args: PushArgsOwned { extra: Some(extra), args: HashMap::new() }, + multipart: Some(multipart), + wrap_body: Some(wrap_body), + }) + } else { + Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response()) + } +} + #[axum::async_trait] impl FromRequest for WebhookArgs where @@ -170,124 +294,10 @@ where { type Rejection = Response; - async fn from_request( - req: Request, - _state: &S, - ) -> Result { - let (content_type, mut extra, use_raw, wrap_body) = { - let headers_map = req.headers(); - let content_type_header = headers_map.get(CONTENT_TYPE); - let content_type = content_type_header.and_then(|value| value.to_str().ok()); - let uri = req.uri(); - let query = Query::::try_from_uri(uri).unwrap().0; - let mut extra = build_extra(&headers_map, query.include_header); - let query_decode = DecodeQueries::from_uri(uri); - if let Some(DecodeQueries(queries)) = query_decode { - extra.extend(queries); - } - let raw = query.raw.as_ref().is_some_and(|x| *x); - let wrap_body = query.wrap_body.as_ref().is_some_and(|x| *x); - (content_type, extra, raw, wrap_body) - }; + async fn from_request(request: Request, _state: &S) -> Result { + let args = try_from_request_body(request, _state, None, None).await?; - let no_content_type = content_type.is_none(); - if no_content_type || content_type.unwrap().starts_with("application/json") { - let bytes = Bytes::from_request(req, _state) - .await - .map_err(IntoResponse::into_response)?; - if no_content_type && bytes.is_empty() { - if use_raw { - extra.insert("raw_string".to_string(), to_raw_value(&"".to_string())); - } - let mut args = HashMap::new(); - if wrap_body { - args.insert("body".to_string(), to_raw_value(&serde_json::json!({}))); - } - return Ok(Self { - args: PushArgsOwned { extra: Some(extra), args: args }, - ..Default::default() - }); - } - let str = String::from_utf8(bytes.to_vec()) - .map_err(|e| Error::BadRequest(format!("invalid utf8: {}", e)).into_response())?; - - PushArgsOwned::from_json(extra, use_raw, wrap_body, str) - .await - .map(|args| Self { args, ..Default::default() }) - } else if content_type - .unwrap() - .starts_with("application/cloudevents+json") - { - let str = req_to_string(req, _state).await?; - - PushArgsOwned::from_ce_json(extra, use_raw, str) - .await - .map(|args| Self { args, ..Default::default() }) - } else if content_type - .unwrap() - .starts_with("application/cloudevents-batch+json") - { - Err( - Error::BadRequest(format!("Cloud events batching is not supported yet")) - .into_response(), - ) - } else if content_type.unwrap().starts_with("text/plain") { - let str = req_to_string(req, _state).await?; - extra.insert("raw_string".to_string(), to_raw_value(&str)); - Ok(Self { - args: PushArgsOwned { extra: Some(extra), args: HashMap::new() }, - ..Default::default() - }) - } else if content_type - .unwrap() - .starts_with("application/x-www-form-urlencoded") - { - let bytes = Bytes::from_request(req, _state) - .await - .map_err(IntoResponse::into_response)?; - - if use_raw { - let raw_string = String::from_utf8(bytes.to_vec()).map_err(|e| { - Error::BadRequest(format!("invalid utf8: {}", e)).into_response() - })?; - extra.insert("raw_string".to_string(), to_raw_value(&raw_string)); - } - - let payload: HashMap> = serde_urlencoded::from_bytes(&bytes) - .map_err(|e| { - Error::BadRequest(format!("invalid urlencoded data: {}", e)).into_response() - })?; - let payload = payload - .into_iter() - .map(|(k, v)| (k, to_raw_value(&v))) - .collect::>(); - - return Ok(Self { - args: PushArgsOwned { extra: Some(extra), args: payload }, - ..Default::default() - }); - } else if content_type.unwrap().starts_with("application/xml") - || content_type.unwrap().starts_with("text/xml") - { - let str = req_to_string(req, _state).await?; - extra.insert("raw_string".to_string(), to_raw_value(&str)); - Ok(Self { - args: PushArgsOwned { extra: Some(extra), args: HashMap::new() }, - ..Default::default() - }) - } else if content_type.unwrap().starts_with("multipart/form-data") { - let multipart = Multipart::from_request(req, _state) - .await - .map_err(IntoResponse::into_response)?; - - Ok(Self { - args: PushArgsOwned { extra: Some(extra), args: HashMap::new() }, - multipart: Some(multipart), - wrap_body: Some(wrap_body), - }) - } else { - Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response()) - } + Ok(args) } } diff --git a/backend/windmill-api/src/http_triggers.rs b/backend/windmill-api/src/http_triggers.rs index 9f7cb25425..9432fb0e7e 100644 --- a/backend/windmill-api/src/http_triggers.rs +++ b/backend/windmill-api/src/http_triggers.rs @@ -1,7 +1,7 @@ #[cfg(feature = "parquet")] use crate::job_helpers_ee::get_workspace_s3_resource; use crate::{ - args::WebhookArgs, + args::try_from_request_body, auth::{AuthCache, OptTokened}, db::{ApiAuthed, DB}, jobs::{ @@ -11,7 +11,7 @@ use crate::{ users::fetch_api_authed, }; use axum::{ - extract::{Path, Query}, + extract::{Path, Query, Request}, response::IntoResponse, routing::{delete, get, post}, Extension, Json, Router, @@ -22,6 +22,7 @@ use http::{HeaderMap, StatusCode}; use serde::{Deserialize, Serialize}; use sql_builder::{bind::Bind, SqlBuilder}; use sqlx::prelude::FromRow; +use std::borrow::Cow; use std::{collections::HashMap, sync::Arc}; use tower_http::cors::CorsLayer; use windmill_audit::{audit_ee::audit_log, ActionKind}; @@ -34,7 +35,6 @@ 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(); @@ -113,6 +113,8 @@ struct NewTrigger { static_asset_config: Option>, workspaced_route: Option, is_static_website: bool, + wrap_body: Option, + raw_string: Option, } #[derive(FromRow, Serialize)] @@ -133,7 +135,9 @@ pub struct HttpTrigger { #[serde(skip_serializing_if = "Option::is_none")] pub static_asset_config: Option>, pub is_static_website: bool, - pub workspaced_route: Option, + pub workspaced_route: bool, + pub wrap_body: bool, + pub raw_string: bool, } #[derive(Deserialize)] @@ -148,6 +152,8 @@ struct EditTrigger { static_asset_config: Option>, workspaced_route: Option, is_static_website: bool, + wrap_body: Option, + raw_string: Option, } #[derive(Deserialize)] @@ -174,6 +180,8 @@ async fn list_triggers( "route_path", "route_path_key", "workspaced_route", + "wrap_body", + "raw_string", "script_path", "is_flow", "http_method", @@ -237,7 +245,9 @@ async fn get_trigger( is_async, requires_auth, static_asset_config as "static_asset_config: _", - is_static_website + is_static_website, + wrap_body, + raw_string FROM http_trigger WHERE @@ -303,6 +313,8 @@ async fn create_trigger( route_path, route_path_key, workspaced_route, + wrap_body, + raw_string, script_path, is_flow, is_async, @@ -315,14 +327,16 @@ async fn create_trigger( is_static_website ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, now(), $16 ) "#, w_id, ct.path, ct.route_path, &route_path_key, - ct.workspaced_route, + ct.workspaced_route.unwrap_or(false), + ct.wrap_body.unwrap_or(false), + ct.raw_string.unwrap_or(false), ct.script_path, ct.is_flow, ct.is_async, @@ -404,24 +418,28 @@ async fn update_trigger( 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, + wrap_body = $4, + raw_string = $5, + script_path = $6, + path = $7, + is_flow = $8, + http_method = $9, + static_asset_config = $10, + edited_by = $11, + email = $12, + is_async = $13, + requires_auth = $14, edited_at = now(), - is_static_website = $13 + is_static_website = $15 WHERE - workspace_id = $14 AND - path = $15 + workspace_id = $16 AND + path = $17 "#, route_path, &route_path_key, ct.workspaced_route, + ct.wrap_body, + ct.raw_string, ct.script_path, ct.path, ct.is_flow, @@ -606,7 +624,7 @@ async fn route_path_key_exists( ) .fetch_one(db) .await? - .unwrap_or(false) + .unwrap_or(false) }; Ok(exists) @@ -645,8 +663,10 @@ struct TriggerRoute { edited_by: String, email: String, static_asset_config: Option>, - workspaced_route: Option, is_static_website: bool, + workspaced_route: bool, + wrap_body: bool, + raw_string: bool, } async fn get_http_route_trigger( @@ -678,6 +698,8 @@ async fn get_http_route_trigger( edited_by, email, static_asset_config AS "static_asset_config: _", + wrap_body, + raw_string, workspaced_route, is_static_website FROM @@ -707,6 +729,8 @@ async fn get_http_route_trigger( edited_by, email, static_asset_config AS "static_asset_config: _", + wrap_body, + raw_string, workspaced_route, is_static_website FROM @@ -725,7 +749,7 @@ async fn get_http_route_trigger( for (idx, trigger) in triggers.iter().enumerate() { let route_path = match trigger.workspaced_route { - Some(true) => format!("{}/{}", &trigger.workspace_id, &trigger.route_path), + true => format!("{}/{}", &trigger.workspace_id, &trigger.route_path), _ => trigger.route_path.clone(), }; if trigger.is_static_website { @@ -853,7 +877,7 @@ async fn route_job( Query(query): Query>, method: http::Method, headers: HeaderMap, - args: WebhookArgs, + request: Request, ) -> impl IntoResponse { let route_path = route_path.to_path().trim_end_matches("/"); let (trigger, called_path, params, authed) = match get_http_route_trigger( @@ -870,11 +894,22 @@ async fn route_job( Err(e) => return e.into_response(), }; - let mut args = match args - .to_push_args_owned(&authed, &db, &trigger.workspace_id) - .await - { - Ok(args) => args, + let result = try_from_request_body( + request, + &db, + Some(trigger.raw_string), + Some(trigger.wrap_body), + ) + .await; + + let mut args = match result { + Ok(args) => match args + .to_push_args_owned(&authed, &db, &trigger.workspace_id) + .await + { + Ok(args) => args, + Err(e) => return e.into_response(), + }, Err(e) => return e.into_response(), }; @@ -990,6 +1025,7 @@ async fn route_job( } let extra = args.extra.get_or_insert_with(HashMap::new); + extra.insert( "wm_trigger".to_string(), build_http_trigger_extra( diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index 10a1189db9..86a42aa8ba 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -58,7 +58,7 @@ use windmill_common::error::AppError; mod ai; mod apps; -mod args; +pub mod args; mod audit; mod auth; mod capture; diff --git a/backend/windmill-api/src/workspaces_export.rs b/backend/windmill-api/src/workspaces_export.rs index 9cb02fc567..449b5e3256 100644 --- a/backend/windmill-api/src/workspaces_export.rs +++ b/backend/windmill-api/src/workspaces_export.rs @@ -554,7 +554,9 @@ pub(crate) async fn tarball_workspace( requires_auth, http_method AS "http_method: _", static_asset_config AS "static_asset_config: _", - is_static_website + is_static_website, + wrap_body, + raw_string FROM http_trigger WHERE workspace_id = $1 "#, diff --git a/frontend/src/lib/components/ArgInput.svelte b/frontend/src/lib/components/ArgInput.svelte index dbce58f111..8bf6c53f1a 100644 --- a/frontend/src/lib/components/ArgInput.svelte +++ b/frontend/src/lib/components/ArgInput.svelte @@ -796,7 +796,7 @@ on:selected={({ detail }) => { oneOfSelected = detail const prevValueKeys = Object.keys( - oneOf.find((o) => o.title == detail)?.properties ?? {} + oneOf?.find((o) => o.title == detail)?.properties ?? {} ) const toKeep = {} for (const key of prevValueKeys) { diff --git a/frontend/src/lib/components/triggers/http/RouteEditorConfigSection.svelte b/frontend/src/lib/components/triggers/http/RouteEditorConfigSection.svelte index 4e386d5b55..2a5c42df4a 100644 --- a/frontend/src/lib/components/triggers/http/RouteEditorConfigSection.svelte +++ b/frontend/src/lib/components/triggers/http/RouteEditorConfigSection.svelte @@ -22,8 +22,8 @@ export let initialTriggerPath: string | undefined = undefined export let dirtyRoutePath: boolean = false - export let route_path: string = '' - export let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' = 'post' + export let route_path: string | undefined + export let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' | undefined export let can_write: boolean = false export let static_asset_config: { s3: string; storage?: string; filename?: string } | undefined = undefined @@ -56,7 +56,11 @@ validateTimeout = undefined }, 500) } - async function routeExists(route_path: string, method: Exclude, workspaced_route: boolean) { + async function routeExists( + route_path: string, + method: Exclude, + workspaced_route: boolean + ) { return await HttpTriggerService.existsRoute({ workspace: $workspaceStore!, requestBody: { @@ -73,6 +77,9 @@ $: isValid = routeError === '' $: fullRoute = getHttpRoute(route_path, workspaced_route, $workspaceStore ?? '') + + $: !http_method && (http_method = 'post') + $: route_path === undefined && (route_path = '')
@@ -185,7 +192,7 @@ { + on:change={() => { workspaced_route = !workspaced_route dirtyRoutePath = true }} diff --git a/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte b/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte index 918e6678fc..e32991a29a 100644 --- a/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte +++ b/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte @@ -23,6 +23,7 @@ import RouteEditorConfigSection from './RouteEditorConfigSection.svelte' import SimpleEditor from '$lib/components/SimpleEditor.svelte' import { isCloudHosted } from '$lib/cloud' + import Tooltip from '$lib/components/Tooltip.svelte' let is_flow: boolean = false let initialPath = '' let edit = true @@ -50,7 +51,8 @@ let s3FileUploadRawMode = false let s3Editor: SimpleEditor | undefined = undefined let workspaced_route: boolean = false - + let raw_string = false + let wrap_body = false let drawerLoading = true export async function openEdit(ePath: string, isFlow: boolean) { drawerLoading = true @@ -108,6 +110,7 @@ workspace: $workspaceStore!, path: initialPath }) + script_path = s.script_path initialScriptPath = s.script_path is_flow = s.is_flow @@ -116,7 +119,10 @@ http_method = s.http_method ?? 'post' is_async = s.is_async requires_auth = s.requires_auth - workspaced_route = s.workspaced_route ?? false + workspaced_route = s.workspaced_route + wrap_body = s.wrap_body + raw_string = s.raw_string + if (!isCloudHosted()) { static_asset_config = s.static_asset_config s3FileUploadRawMode = !!static_asset_config @@ -141,7 +147,9 @@ http_method, static_asset_config, is_static_website, - workspaced_route + workspaced_route, + wrap_body, + raw_string } }) sendUserToast(`Route ${path} updated`) @@ -158,7 +166,9 @@ http_method, static_asset_config, is_static_website, - workspaced_route + workspaced_route, + wrap_body, + raw_string } }) sendUserToast(`Route ${path} created`) @@ -433,6 +443,38 @@ + +
{/if} diff --git a/frontend/src/lib/components/triggers/http/utils.ts b/frontend/src/lib/components/triggers/http/utils.ts index bf0a244c0e..fbcaa55114 100644 --- a/frontend/src/lib/components/triggers/http/utils.ts +++ b/frontend/src/lib/components/triggers/http/utils.ts @@ -1,8 +1,8 @@ import { base } from "$lib/base"; import { isCloudHosted } from "$lib/cloud"; -export function getHttpRoute(route_path: string, workspaced_route: boolean, workspace_id: string) { +export function getHttpRoute(route_path: string | undefined, workspaced_route: boolean, workspace_id: string) { return `${location.origin}${base}/api/r/${ isCloudHosted() || workspaced_route ? workspace_id + '/' : '' - }${route_path}` + }${route_path ?? ''}` } \ No newline at end of file