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 <hugo@casademont.ch>
This commit is contained in:
dieriba
2025-03-13 21:39:54 +01:00
committed by GitHub
parent 5694a3adf5
commit df4b6db0d4
21 changed files with 377 additions and 205 deletions
@@ -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
]
},
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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
]
},
@@ -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"
}
@@ -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"
}
@@ -15,7 +15,7 @@
]
},
"nullable": [
null
true
]
},
"hash": "ddf2eccb78a310ed00c7d8b9c3f05d394a7cbcf0038c72a78add5c7b02ef5927"
@@ -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"
}
+1 -1
View File
@@ -11,5 +11,5 @@
"remote.autoForwardPorts": true,
"conventionalCommits.scopes": [
"restructring triggers, decoding trigger message on work"
],
]
}
@@ -0,0 +1,4 @@
-- Add down migration script here
ALTER TABLE http_trigger
DROP COLUMN wrap_body,
DROP COLUMN raw_string;
@@ -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;
+15
View File
@@ -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
+128 -118
View File
@@ -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<Multipart>,
@@ -163,6 +163,130 @@ async fn req_to_string<S: Send + Sync>(
.map_err(|e| Error::BadRequest(format!("invalid utf8: {}", e)).into_response())
}
pub async fn try_from_request_body<S>(
request: Request,
_state: &S,
use_raw: Option<bool>,
wrap_body: Option<bool>,
) -> Result<WebhookArgs, Response>
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::<RequestQuery>::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<String, Option<String>> = 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::<HashMap<_, _>>();
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<S> FromRequest<S, axum::body::Body> for WebhookArgs
where
@@ -170,124 +294,10 @@ where
{
type Rejection = Response;
async fn from_request(
req: Request<axum::body::Body>,
_state: &S,
) -> Result<Self, Self::Rejection> {
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::<RequestQuery>::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<Self, Self::Rejection> {
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<String, Option<String>> = 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::<HashMap<_, _>>();
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)
}
}
+64 -28
View File
@@ -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<sqlx::types::Json<S3Object>>,
workspaced_route: Option<bool>,
is_static_website: bool,
wrap_body: Option<bool>,
raw_string: Option<bool>,
}
#[derive(FromRow, Serialize)]
@@ -133,7 +135,9 @@ pub struct HttpTrigger {
#[serde(skip_serializing_if = "Option::is_none")]
pub static_asset_config: Option<sqlx::types::Json<S3Object>>,
pub is_static_website: bool,
pub workspaced_route: Option<bool>,
pub workspaced_route: bool,
pub wrap_body: bool,
pub raw_string: bool,
}
#[derive(Deserialize)]
@@ -148,6 +152,8 @@ struct EditTrigger {
static_asset_config: Option<sqlx::types::Json<S3Object>>,
workspaced_route: Option<bool>,
is_static_website: bool,
wrap_body: Option<bool>,
raw_string: Option<bool>,
}
#[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<sqlx::types::Json<S3Object>>,
workspaced_route: Option<bool>,
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<HashMap<String, String>>,
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(
+1 -1
View File
@@ -58,7 +58,7 @@ use windmill_common::error::AppError;
mod ai;
mod apps;
mod args;
pub mod args;
mod audit;
mod auth;
mod capture;
@@ -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
"#,
+1 -1
View File
@@ -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) {
@@ -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<typeof http_method, undefined>, workspaced_route: boolean) {
async function routeExists(
route_path: string,
method: Exclude<typeof http_method, undefined>,
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 = '')
</script>
<div>
@@ -185,7 +192,7 @@
<Toggle
size="sm"
checked={workspaced_route}
on:change={async () => {
on:change={() => {
workspaced_route = !workspaced_route
dirtyRoutePath = true
}}
@@ -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 @@
</ToggleButtonGroup>
</svelte:fragment>
</Label>
<Label label="Raw string" class="w-full">
<svelte:fragment slot="header">
<Tooltip
>Provides the raw JSON payload as a string under the 'raw_string' key, useful for
signature verification and other use cases.</Tooltip
>
</svelte:fragment>
<svelte:fragment slot="action">
<Toggle
checked={raw_string}
on:change={() => {
raw_string = !raw_string
}}
/>
</svelte:fragment>
</Label>
<Label label="Wrap body" class="w-full">
<svelte:fragment slot="header">
<Tooltip
>Wraps the payload in an object under the 'body' key, useful for handling
unknown payloads.</Tooltip
>
</svelte:fragment>
<svelte:fragment slot="action">
<Toggle
checked={wrap_body}
on:change={() => {
wrap_body = !wrap_body
}}
/>
</svelte:fragment>
</Label>
</div>
</Section>
{/if}
@@ -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 ?? ''}`
}