mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 16:00:38 +00:00
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:
+15
-15
@@ -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
|
||||
]
|
||||
},
|
||||
|
||||
+14
-2
@@ -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"
|
||||
}
|
||||
+4
-2
@@ -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"
|
||||
}
|
||||
+15
-3
@@ -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"
|
||||
}
|
||||
+15
-15
@@ -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
|
||||
]
|
||||
},
|
||||
|
||||
+14
-2
@@ -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"
|
||||
}
|
||||
+15
-3
@@ -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"
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ddf2eccb78a310ed00c7d8b9c3f05d394a7cbcf0038c72a78add5c7b02ef5927"
|
||||
|
||||
+4
-2
@@ -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"
|
||||
}
|
||||
Vendored
+1
-1
@@ -11,5 +11,5 @@
|
||||
"remote.autoForwardPorts": true,
|
||||
"conventionalCommits.scopes": [
|
||||
"restructring triggers, decoding trigger message on work"
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE http_trigger
|
||||
DROP COLUMN wrap_body,
|
||||
DROP COLUMN raw_string;
|
||||
+4
@@ -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;
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
"#,
|
||||
|
||||
@@ -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 ?? ''}`
|
||||
}
|
||||
Reference in New Issue
Block a user