feat: option to prefix http route with workspace id (#5461)

* add new column

* handled worskpaced route

* update .sqlx

* update: rename migration file, rewrite query for checking existing route path key, add copy button url for each route trigger

* update .sqlx

* update sql query

* update .sqlx

* fix: prevent precedence on query

* nits

* update .sqlx

* update query

* Update http_triggers.rs

* nits

---------

Co-authored-by: HugoCasa <hugo@casademont.ch>
This commit is contained in:
dieriba
2025-03-11 16:24:22 +01:00
committed by GitHub
parent f9c21b6812
commit 61a5ceaba3
28 changed files with 592 additions and 294 deletions
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, static_asset_config as \"static_asset_config: _\", is_static_website FROM http_trigger WHERE http_method = $1",
"query": "\n SELECT \n path, \n script_path, \n is_flow, \n route_path, \n workspace_id, \n is_async, \n requires_auth, \n edited_by, \n email, \n static_asset_config AS \"static_asset_config: _\",\n workspaced_route,\n is_static_website\n FROM \n http_trigger \n WHERE \n http_method = $1\n ",
"describe": {
"columns": [
{
@@ -55,6 +55,11 @@
},
{
"ordinal": 10,
"name": "workspaced_route",
"type_info": "Bool"
},
{
"ordinal": 11,
"name": "is_static_website",
"type_info": "Bool"
}
@@ -88,8 +93,9 @@
false,
false,
true,
false,
false
]
},
"hash": "1961d15ae075072bd5f677c95f9b4dac7f747585d98aa8bcf60dcfb4c8124028"
"hash": "01b87cdd552c2d5a3cd475eb705110394fe6c8abcc1fa93291fac6f929043d20"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND http_method = $2 AND ($3::TEXT IS NULL OR path != $3))",
"query": "\n SELECT EXISTS(\n SELECT 1 \n FROM http_trigger \n WHERE \n ((workspaced_route IS TRUE AND workspace_id || '/' || route_path_key = $1) \n OR (workspaced_route IS FALSE AND route_path_key = $1))\n AND http_method = $2 \n AND ($3::TEXT IS NULL OR path != $3)\n )\n ",
"describe": {
"columns": [
{
@@ -33,5 +33,5 @@
null
]
},
"hash": "63b5f03741be97d0e8763dd070649ebb6ec02aa083d7e175c1a02a38935a4024"
"hash": "0d8153986cea6166820f601f80d8e67156408b08360d628300b28221ea995a58"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, static_asset_config as \"static_asset_config: _\", is_static_website FROM http_trigger WHERE workspace_id = $1 AND http_method = $2",
"query": "\n SELECT \n path, \n script_path, \n is_flow, \n route_path, \n workspace_id, \n is_async, \n requires_auth, \n edited_by, \n email,\n static_asset_config AS \"static_asset_config: _\",\n workspaced_route,\n is_static_website \n FROM \n http_trigger \n WHERE \n workspace_id = $1 AND \n http_method = $2\n ",
"describe": {
"columns": [
{
@@ -55,6 +55,11 @@
},
{
"ordinal": 10,
"name": "workspaced_route",
"type_info": "Bool"
},
{
"ordinal": 11,
"name": "is_static_website",
"type_info": "Bool"
}
@@ -89,8 +94,9 @@
false,
false,
true,
false,
false
]
},
"hash": "8ff25d890d3f7019c5c6f2b47f29f4c7b2c91094036d9b5037ef9a4ac986ee53"
"hash": "300b41622dbcdf908d9e173eee2133239d5806fc50bba2d3fe9d8a6e11627655"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as \"http_method: _\", edited_by, email, edited_at, extra_perms, is_async, requires_auth, static_asset_config as \"static_asset_config: _\", is_static_website\n FROM http_trigger\n WHERE workspace_id = $1 AND path = $2",
"query": "\n SELECT \n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n script_path, \n is_flow, \n http_method as \"http_method: _\", \n edited_by, \n email, \n edited_at, \n extra_perms, \n is_async, \n requires_auth, \n static_asset_config as \"static_asset_config: _\", \n is_static_website\n FROM \n http_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
"describe": {
"columns": [
{
@@ -25,16 +25,21 @@
},
{
"ordinal": 4,
"name": "workspaced_route",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 5,
"ordinal": 6,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "http_method: _",
"type_info": {
"Custom": {
@@ -52,42 +57,42 @@
}
},
{
"ordinal": 7,
"ordinal": 8,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"ordinal": 11,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 11,
"ordinal": 12,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 12,
"ordinal": 13,
"name": "requires_auth",
"type_info": "Bool"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "static_asset_config: _",
"type_info": "Jsonb"
},
{
"ordinal": 14,
"ordinal": 15,
"name": "is_static_website",
"type_info": "Bool"
}
@@ -112,9 +117,10 @@
false,
false,
false,
false,
true,
false
]
},
"hash": "197321abfe4667256761884970334f58c1b3edfcc1e863ec4316c9742a1ac7c8"
"hash": "4353a7de6b4de1df6e19d95a4db8ae89c8d75034a9210233aed2f0414690e37d"
}
@@ -0,0 +1,38 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n http_trigger \n SET \n script_path = $1, \n path = $2, \n is_flow = $3, \n http_method = $4,\n static_asset_config = $5, \n edited_by = $6, \n email = $7, \n is_async = $8, \n requires_auth = $9, \n edited_at = now(), \n is_static_website = $10\n WHERE \n workspace_id = $11 AND \n path = $12\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Bool",
{
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
},
"Jsonb",
"Varchar",
"Varchar",
"Bool",
"Bool",
"Bool",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "5d3401f608b27937285e418f7bd64bd112977c995bf266680c54f58ad57fd4b6"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND workspace_id = $2 AND http_method = $3 AND ($4::TEXT IS NULL OR path != $4))",
"query": "\n SELECT EXISTS(\n SELECT 1 \n FROM http_trigger \n WHERE \n route_path_key = $1\n AND workspace_id = $2 \n AND http_method = $3 \n AND ($4::TEXT IS NULL OR path != $4)\n )\n ",
"describe": {
"columns": [
{
@@ -34,5 +34,5 @@
null
]
},
"hash": "a96ff22bc78b74d7234550a12d9fb5c555c1187b276f1353dae1b2ac0670a92a"
"hash": "679a9159a5fca976a3de99fe26806faded2cc63e8f16c201e99ab1725dcff294"
}
@@ -0,0 +1,41 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n http_trigger \n SET \n route_path = $1, \n route_path_key = $2, \n workspaced_route = $3, \n script_path = $4, \n path = $5, \n is_flow = $6, \n http_method = $7, \n static_asset_config = $8, \n edited_by = $9, \n email = $10, \n is_async = $11, \n requires_auth = $12, \n edited_at = now(), \n is_static_website = $13\n WHERE \n workspace_id = $14 AND \n path = $15\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Bool",
{
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
},
"Jsonb",
"Varchar",
"Varchar",
"Bool",
"Bool",
"Bool",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "78e6342696052d0e1f79fd721f5942d4da686a65ab11eefdca16367d2db67963"
}
@@ -1,38 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, static_asset_config = $5, edited_by = $6, email = $7, is_async = $8, requires_auth = $9, edited_at = now(), is_static_website = $10\n WHERE workspace_id = $11 AND path = $12",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Bool",
{
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
},
"Jsonb",
"Varchar",
"Varchar",
"Bool",
"Bool",
"Bool",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "89c08575afb31b70984f6b2b7dd4297af93b5b83ffdfb3ec91eba5df7ad3fd95"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM http_trigger \n WHERE workspace_id = $1 \n AND path = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "a269c388056eabe4b045948f451ea74ffbb4c0ed7e694f8f03d92f2a7c118af9"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE path = $1 AND workspace_id = $2)",
"query": "SELECT EXISTS(\n SELECT 1 FROM http_trigger \n WHERE path = $1 AND workspace_id = $2\n )",
"describe": {
"columns": [
{
@@ -19,5 +19,5 @@
null
]
},
"hash": "9a050ab74cfc13a4b855408e61ebcb0a9d27e5563fa7af5e00a4916b03a62ddb"
"hash": "adb0090afd3ce918d8b80ff51d9f6104a430a11d7c5cb9447025d11506585708"
}
@@ -1,39 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, static_asset_config, edited_by, email, edited_at, is_static_website) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Bool",
"Bool",
{
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
},
"Jsonb",
"Varchar",
"Varchar",
"Bool"
]
},
"nullable": []
},
"hash": "b41cef713e822bbd89b49b1f35cc662539d5e4af1dd0b5923d8ee17b772c5677"
}
@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1 AND path = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "b57a188ca137162a2848ebf81fa3aeca9a14ac628c61a358e2c6612c57249b0f"
}
@@ -1,40 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE http_trigger \n SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, static_asset_config = $7, edited_by = $8, email = $9, is_async = $10, requires_auth = $11, edited_at = now(), is_static_website = $12\n WHERE workspace_id = $13 AND path = $14",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Bool",
{
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
},
"Jsonb",
"Varchar",
"Varchar",
"Bool",
"Bool",
"Bool",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "ccb7aea162fa8781675d547fb82f2996dede4de49c5bd9cca2928209dc40b8f1"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM http_trigger WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "dc9a906d6c6156a84fccf4e3a2a7c08d8ed4984409b669162f0a1fc1aa48e188"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, edited_by, edited_at, email, extra_perms, is_async, requires_auth, http_method as \"http_method: _\", static_asset_config as \"static_asset_config: _\", is_static_website FROM http_trigger\n WHERE workspace_id = $1",
"query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n script_path, \n is_flow, \n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n requires_auth, \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website \n FROM http_trigger\n WHERE workspace_id = $1\n ",
"describe": {
"columns": [
{
@@ -10,61 +10,66 @@
},
{
"ordinal": 1,
"name": "workspaced_route",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"ordinal": 3,
"name": "route_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"ordinal": 4,
"name": "route_path_key",
"type_info": "Varchar"
},
{
"ordinal": 4,
"ordinal": 5,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 5,
"ordinal": 6,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 10,
"ordinal": 11,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 11,
"ordinal": 12,
"name": "requires_auth",
"type_info": "Bool"
},
{
"ordinal": 12,
"ordinal": 13,
"name": "http_method: _",
"type_info": {
"Custom": {
@@ -82,12 +87,12 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "static_asset_config: _",
"type_info": "Jsonb"
},
{
"ordinal": 14,
"ordinal": 15,
"name": "is_static_website",
"type_info": "Bool"
}
@@ -111,9 +116,10 @@
false,
false,
false,
false,
true,
false
]
},
"hash": "d8aa1a5813fedbb22128112fcdf9d16769811cd5d5ad3b66362cc5bb1750ae6b"
"hash": "e44ea5870a8eba1fe3d22d4dfe7734669464a56e470935ce2924723003f3f92b"
}
@@ -0,0 +1,40 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO http_trigger (\n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n script_path, \n is_flow, \n is_async, \n requires_auth, \n http_method, \n static_asset_config, \n edited_by, \n email, \n edited_at, \n is_static_website\n ) \n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Bool",
"Bool",
"Bool",
{
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
},
"Jsonb",
"Varchar",
"Varchar",
"Bool"
]
},
"nullable": []
},
"hash": "eb4779976677308e8ad7c6427bfb45146478f70956919c01718623f0828e0383"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT EXISTS(\n SELECT 1 \n FROM \n http_trigger \n WHERE \n workspace_id = $1 AND \n path = $2\n )\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "f632d08a8d3df691fff9f57fdf926f787287c3cd181a6853056077edba10473d"
}
@@ -0,0 +1,3 @@
-- Add down migration script here
ALTER TABLE http_trigger
DROP COLUMN workspaced_route;
@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE http_trigger
ADD COLUMN workspaced_route BOOLEAN NOT NULL DEFAULT false;
+8
View File
@@ -8116,6 +8116,8 @@ paths:
enum: ["get", "post", "put", "delete", "patch"]
trigger_path:
type: string
workspaced_route:
type: boolean
required:
- route_path
- http_method
@@ -13767,6 +13769,8 @@ components:
type: boolean
is_static_website:
type: boolean
workspaced_route:
type: boolean
required:
- route_path
@@ -13784,6 +13788,8 @@ components:
type: string
route_path:
type: string
workspaced_route:
type: boolean
static_asset_config:
type: object
properties:
@@ -13831,6 +13837,8 @@ components:
type: string
route_path:
type: string
workspaced_route:
type: boolean
static_asset_config:
type: object
properties:
+245 -44
View File
@@ -34,6 +34,7 @@ use windmill_common::{
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
worker::{to_raw_value, CLOUD_HOSTED},
};
use std::borrow::Cow;
lazy_static::lazy_static! {
static ref ROUTE_PATH_KEY_RE: regex::Regex = regex::Regex::new(r"/?:[-\w]+").unwrap();
@@ -100,7 +101,7 @@ impl TryFrom<&http::Method> for HttpMethod {
}
}
#[derive(Deserialize)]
#[derive(Debug, Deserialize)]
struct NewTrigger {
path: String,
route_path: String,
@@ -110,6 +111,7 @@ struct NewTrigger {
requires_auth: bool,
http_method: HttpMethod,
static_asset_config: Option<sqlx::types::Json<S3Object>>,
workspaced_route: Option<bool>,
is_static_website: bool,
}
@@ -131,6 +133,7 @@ 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>,
}
#[derive(Deserialize)]
@@ -143,6 +146,7 @@ struct EditTrigger {
requires_auth: bool,
http_method: HttpMethod,
static_asset_config: Option<sqlx::types::Json<S3Object>>,
workspaced_route: Option<bool>,
is_static_website: bool,
}
@@ -164,7 +168,24 @@ async fn list_triggers(
let mut tx = user_db.begin(&authed).await?;
let (per_page, offset) = paginate(Pagination { per_page: lst.per_page, page: lst.page });
let mut sqlb = SqlBuilder::select_from("http_trigger")
.field("*")
.fields(&[
"workspace_id",
"path",
"route_path",
"route_path_key",
"workspaced_route",
"script_path",
"is_flow",
"http_method",
"edited_by",
"email",
"edited_at",
"extra_perms",
"is_async",
"requires_auth",
"static_asset_config",
"is_static_website",
])
.order_by("edited_at", true)
.and_where("workspace_id = ?".bind(&w_id))
.offset(offset)
@@ -199,9 +220,30 @@ async fn get_trigger(
let path = path.to_path();
let trigger = sqlx::query_as!(
HttpTrigger,
r#"SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as "http_method: _", edited_by, email, edited_at, extra_perms, is_async, requires_auth, static_asset_config as "static_asset_config: _", is_static_website
FROM http_trigger
WHERE workspace_id = $1 AND path = $2"#,
r#"
SELECT
workspace_id,
path,
route_path,
route_path_key,
workspaced_route,
script_path,
is_flow,
http_method as "http_method: _",
edited_by,
email,
edited_at,
extra_perms,
is_async,
requires_auth,
static_asset_config as "static_asset_config: _",
is_static_website
FROM
http_trigger
WHERE
workspace_id = $1 AND
path = $2
"#,
w_id,
path,
)
@@ -230,9 +272,16 @@ async fn create_trigger(
// route path key is extracted from the route path to check for uniqueness
// it replaces /?:{key} with :key
// it will also remove the leading / if present, not an issue as we only allow : after slashes
let route_path_key = ROUTE_PATH_KEY_RE.replace_all(ct.route_path.as_str(), ":key");
let exists = route_path_key_exists(&route_path_key, &ct.http_method, &w_id, None, &db).await?;
let route_path_key = ROUTE_PATH_KEY_RE.replace_all(&ct.route_path, ":key");
let exists = route_path_key_exists(
&route_path_key,
&ct.http_method,
&w_id,
None,
ct.workspaced_route,
&db,
)
.await?;
if exists {
return Err(error::Error::BadRequest(
"A route already exists with this path".to_string(),
@@ -247,11 +296,33 @@ async fn create_trigger(
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, static_asset_config, edited_by, email, edited_at, is_static_website) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13)",
r#"
INSERT INTO http_trigger (
workspace_id,
path,
route_path,
route_path_key,
workspaced_route,
script_path,
is_flow,
is_async,
requires_auth,
http_method,
static_asset_config,
edited_by,
email,
edited_at,
is_static_website
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14
)
"#,
w_id,
ct.path,
ct.route_path,
&route_path_key,
ct.workspaced_route,
ct.script_path,
ct.is_flow,
ct.is_async,
@@ -260,9 +331,10 @@ async fn create_trigger(
ct.static_asset_config as _,
&authed.username,
&authed.email,
ct.is_static_website,
ct.is_static_website
)
.execute(&mut *tx).await?;
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
@@ -288,7 +360,6 @@ async fn update_trigger(
Json(ct): Json<EditTrigger>,
) -> error::Result<String> {
let path = path.to_path();
if *CLOUD_HOSTED && (ct.is_static_website || ct.static_asset_config.is_some()) {
return Err(error::Error::BadRequest(
"Static website and static asset are not supported on cloud".to_string(),
@@ -309,9 +380,15 @@ async fn update_trigger(
let route_path_key = ROUTE_PATH_KEY_RE.replace_all(&route_path, ":key");
let exists =
route_path_key_exists(&route_path_key, &ct.http_method, &w_id, Some(&path), &db)
.await?;
let exists = route_path_key_exists(
&route_path_key,
&ct.http_method,
&w_id,
Some(&path),
ct.workspaced_route,
&db,
)
.await?;
if exists {
return Err(error::Error::BadRequest(
"A route already exists with this path".to_string(),
@@ -319,13 +396,32 @@ async fn update_trigger(
}
tx = user_db.begin(&authed).await?;
sqlx::query!(
"UPDATE http_trigger
SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, static_asset_config = $7, edited_by = $8, email = $9, is_async = $10, requires_auth = $11, edited_at = now(), is_static_website = $12
WHERE workspace_id = $13 AND path = $14",
r#"
UPDATE
http_trigger
SET
route_path = $1,
route_path_key = $2,
workspaced_route = $3,
script_path = $4,
path = $5,
is_flow = $6,
http_method = $7,
static_asset_config = $8,
edited_by = $9,
email = $10,
is_async = $11,
requires_auth = $12,
edited_at = now(),
is_static_website = $13
WHERE
workspace_id = $14 AND
path = $15
"#,
route_path,
&route_path_key,
ct.workspaced_route,
ct.script_path,
ct.path,
ct.is_flow,
@@ -339,12 +435,30 @@ async fn update_trigger(
w_id,
path,
)
.execute(&mut *tx).await?;
.execute(&mut *tx)
.await?;
} else {
tx = user_db.begin(&authed).await?;
sqlx::query!(
"UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, static_asset_config = $5, edited_by = $6, email = $7, is_async = $8, requires_auth = $9, edited_at = now(), is_static_website = $10
WHERE workspace_id = $11 AND path = $12",
r#"
UPDATE
http_trigger
SET
script_path = $1,
path = $2,
is_flow = $3,
http_method = $4,
static_asset_config = $5,
edited_by = $6,
email = $7,
is_async = $8,
requires_auth = $9,
edited_at = now(),
is_static_website = $10
WHERE
workspace_id = $11 AND
path = $12
"#,
ct.script_path,
ct.path,
ct.is_flow,
@@ -358,7 +472,8 @@ async fn update_trigger(
w_id,
path,
)
.execute(&mut *tx).await?;
.execute(&mut *tx)
.await?;
}
audit_log(
@@ -386,9 +501,11 @@ async fn delete_trigger(
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"DELETE FROM http_trigger WHERE workspace_id = $1 AND path = $2",
"DELETE FROM http_trigger
WHERE workspace_id = $1
AND path = $2",
w_id,
path,
path
)
.execute(&mut *tx)
.await?;
@@ -415,13 +532,17 @@ async fn exists_trigger(
) -> JsonResult<bool> {
let path = path.to_path();
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM http_trigger WHERE path = $1 AND workspace_id = $2)",
"SELECT EXISTS(
SELECT 1 FROM http_trigger
WHERE path = $1 AND workspace_id = $2
)",
path,
w_id,
w_id
)
.fetch_one(&db)
.await?
.unwrap_or(false);
Ok(Json(exists))
}
@@ -430,6 +551,7 @@ struct RouteExists {
route_path: String,
http_method: HttpMethod,
trigger_path: Option<String>,
workspaced_route: Option<bool>,
}
async fn route_path_key_exists(
@@ -437,37 +559,65 @@ async fn route_path_key_exists(
http_method: &HttpMethod,
w_id: &str,
trigger_path: Option<&str>,
workspaced_route: Option<bool>,
db: &DB,
) -> error::Result<bool> {
let exists = if *CLOUD_HOSTED {
sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND workspace_id = $2 AND http_method = $3 AND ($4::TEXT IS NULL OR path != $4))",
&route_path_key,
w_id,
http_method as &HttpMethod,
trigger_path
)
.fetch_one(db)
.await?
.unwrap_or(false)
} else {
sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND http_method = $2 AND ($3::TEXT IS NULL OR path != $3))",
r#"
SELECT EXISTS(
SELECT 1
FROM http_trigger
WHERE
route_path_key = $1
AND workspace_id = $2
AND http_method = $3
AND ($4::TEXT IS NULL OR path != $4)
)
"#,
&route_path_key,
w_id,
http_method as &HttpMethod,
trigger_path
)
.fetch_one(db)
.await?
.unwrap_or(false)
} else {
let route_path_key = match workspaced_route {
Some(true) => Cow::Owned(format!("{}/{}", w_id, route_path_key.trim_matches('/'))),
_ => Cow::Borrowed(route_path_key),
};
sqlx::query_scalar!(
r#"
SELECT EXISTS(
SELECT 1
FROM http_trigger
WHERE
((workspaced_route IS TRUE AND workspace_id || '/' || route_path_key = $1)
OR (workspaced_route IS FALSE AND route_path_key = $1))
AND http_method = $2
AND ($3::TEXT IS NULL OR path != $3)
)
"#,
&route_path_key,
http_method as &HttpMethod,
trigger_path
)
.fetch_one(db)
.await?
.unwrap_or(false)
};
Ok(exists)
}
async fn exists_route(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(RouteExists { route_path, http_method, trigger_path }): Json<RouteExists>,
Json(RouteExists { route_path, http_method, trigger_path, workspaced_route }): Json<
RouteExists,
>,
) -> JsonResult<bool> {
let route_path_key = ROUTE_PATH_KEY_RE.replace_all(route_path.as_str(), ":key");
@@ -476,6 +626,7 @@ async fn exists_route(
&http_method,
&w_id,
trigger_path.as_deref(),
workspaced_route,
&db,
)
.await?;
@@ -494,6 +645,7 @@ struct TriggerRoute {
edited_by: String,
email: String,
static_asset_config: Option<sqlx::types::Json<S3Object>>,
workspaced_route: Option<bool>,
is_static_website: bool,
}
@@ -514,7 +666,26 @@ async fn get_http_route_trigger(
let route_path = StripPath(splitted.collect::<Vec<_>>().join("/"));
let triggers = sqlx::query_as!(
TriggerRoute,
r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, static_asset_config as "static_asset_config: _", is_static_website FROM http_trigger WHERE workspace_id = $1 AND http_method = $2"#,
r#"
SELECT
path,
script_path,
is_flow,
route_path,
workspace_id,
is_async,
requires_auth,
edited_by,
email,
static_asset_config AS "static_asset_config: _",
workspaced_route,
is_static_website
FROM
http_trigger
WHERE
workspace_id = $1 AND
http_method = $2
"#,
w_id,
http_method as HttpMethod
)
@@ -524,7 +695,25 @@ async fn get_http_route_trigger(
} else {
let triggers = sqlx::query_as!(
TriggerRoute,
r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, static_asset_config as "static_asset_config: _", is_static_website FROM http_trigger WHERE http_method = $1"#,
r#"
SELECT
path,
script_path,
is_flow,
route_path,
workspace_id,
is_async,
requires_auth,
edited_by,
email,
static_asset_config AS "static_asset_config: _",
workspaced_route,
is_static_website
FROM
http_trigger
WHERE
http_method = $1
"#,
http_method as HttpMethod
)
.fetch_all(db)
@@ -535,7 +724,10 @@ async fn get_http_route_trigger(
let mut router = matchit::Router::new();
for (idx, trigger) in triggers.iter().enumerate() {
let route_path = trigger.route_path.clone();
let route_path = match trigger.workspaced_route {
Some(true) => format!("{}/{}", &trigger.workspace_id, &trigger.route_path),
_ => trigger.route_path.clone(),
};
if trigger.is_static_website {
router
.insert(format!("/{}/*wm_subpath", route_path), idx)
@@ -583,7 +775,16 @@ async fn get_http_route_trigger(
// check that the user has access to the trigger
let mut tx = user_db.begin(&authed).await?;
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1 AND path = $2)",
r#"
SELECT EXISTS(
SELECT 1
FROM
http_trigger
WHERE
workspace_id = $1 AND
path = $2
)
"#,
trigger.workspace_id,
trigger.path
)
+27 -11
View File
@@ -534,13 +534,32 @@ pub(crate) async fn tarball_workspace(
#[cfg(feature = "http_trigger")]
{
let http_triggers = sqlx::query_as!(
crate::http_triggers::HttpTrigger,
"SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, edited_by, edited_at, email, extra_perms, is_async, requires_auth, http_method as \"http_method: _\", static_asset_config as \"static_asset_config: _\", is_static_website FROM http_trigger
WHERE workspace_id = $1",
&w_id
)
.fetch_all(&mut *tx)
.await?;
crate::http_triggers::HttpTrigger,
r#"
SELECT
workspace_id,
workspaced_route,
path,
route_path,
route_path_key,
script_path,
is_flow,
edited_by,
edited_at,
email,
extra_perms,
is_async,
requires_auth,
http_method AS "http_method: _",
static_asset_config AS "static_asset_config: _",
is_static_website
FROM http_trigger
WHERE workspace_id = $1
"#,
&w_id
)
.fetch_all(&mut *tx)
.await?;
for trigger in http_triggers {
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
@@ -608,10 +627,7 @@ pub(crate) async fn tarball_workspace(
for trigger in sqs_triggers {
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
archive
.write_to_archive(
&trigger_str,
&format!("{}.sqs_trigger.json", trigger.path),
)
.write_to_archive(&trigger_str, &format!("{}.sqs_trigger.json", trigger.path))
.await?;
}
}
+1 -1
View File
@@ -111,7 +111,7 @@ pub struct S3AwsOidcResource {
pub audience: Option<String>,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct S3Object {
pub s3: String,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -16,12 +16,14 @@
import CaptureTable from '../CaptureTable.svelte'
import ClipboardPanel from '../../details/ClipboardPanel.svelte'
import { isCloudHosted } from '$lib/cloud'
import Toggle from '$lib/components/Toggle.svelte'
import { isObject } from '$lib/utils'
import { getHttpRoute } from './utils'
export let initialTriggerPath: string | undefined = undefined
export let dirtyRoutePath: boolean = false
export let route_path: string | undefined
export let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' | undefined
export let route_path: string = ''
export let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' = 'post'
export let can_write: boolean = false
export let static_asset_config: { s3: string; storage?: string; filename?: string } | undefined =
undefined
@@ -29,6 +31,7 @@
export let headless: boolean = false
export let captureInfo: CaptureInfo | undefined = undefined
export let captureTable: CaptureTable | undefined = undefined
export let workspaced_route: boolean = false
export let isValid = false
export let runnableArgs: any = {}
let validateTimeout: NodeJS.Timeout | undefined = undefined
@@ -36,7 +39,8 @@
let routeError: string = ''
async function validateRoute(
routePath: string | undefined,
method: typeof http_method
method: typeof http_method,
workspaced_route: boolean
): Promise<void> {
if (validateTimeout) {
clearTimeout(validateTimeout)
@@ -44,7 +48,7 @@
validateTimeout = setTimeout(async () => {
if (!routePath || !method || !/^:?[-\w]+(\/:?[-\w]+)*$/.test(routePath)) {
routeError = 'Endpoint not valid'
} else if (await routeExists(routePath, method)) {
} else if (await routeExists(routePath, method, workspaced_route)) {
routeError = 'Endpoint already taken'
} else {
routeError = ''
@@ -52,32 +56,23 @@
validateTimeout = undefined
}, 500)
}
async function routeExists(route_path: string, method: Exclude<typeof http_method, undefined>) {
async function routeExists(route_path: string, method: Exclude<typeof http_method, undefined>, workspaced_route: boolean) {
return await HttpTriggerService.existsRoute({
workspace: $workspaceStore!,
requestBody: {
route_path,
http_method: method,
trigger_path: initialTriggerPath
trigger_path: initialTriggerPath,
workspaced_route: workspaced_route
}
})
}
$: validateRoute(route_path, http_method)
$: validateRoute(route_path, http_method, workspaced_route)
$: isValid = routeError === ''
function getHttpRoute(route_path: string | undefined) {
return `${location.origin}${base}/api/r/${isCloudHosted() ? $workspaceStore + '/' : ''}${
route_path ?? ''
}`
}
$: fullRoute = getHttpRoute(route_path)
$: !http_method && (http_method = 'post')
$: route_path === undefined && (route_path = '')
$: fullRoute = getHttpRoute(route_path, workspaced_route, $workspaceStore ?? '')
</script>
<div>
@@ -182,10 +177,26 @@
}}
/>
</div>
<div class="text-red-600 dark:text-red-400 text-2xs mt-1.5"
>{dirtyRoutePath ? routeError : ''}</div
>
{#if !isCloudHosted()}
<div class="mt-1">
<Toggle
size="sm"
checked={workspaced_route}
on:change={async () => {
workspaced_route = !workspaced_route
dirtyRoutePath = true
}}
options={{
right: 'Prefix with workspace',
rightTooltip:
'Prefixes the route with the workspace ID (e.g., {base_url}/api/r/{workspace_id}/{route}). Note: deploying the HTTP trigger to another workspace updates the route workspace prefix accordingly.'
}}
/>
</div>
{/if}
</div>
</div>
</Section>
@@ -34,6 +34,22 @@
let script_path = ''
let initialScriptPath = ''
let fixedScriptPath = ''
let path: string = ''
let pathError = ''
let isValid = false
let dirtyRoutePath = false
let is_async = false
let requires_auth = false
let route_path = ''
let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' = 'post'
let static_asset_config: { s3: string; storage?: string; filename?: string } | undefined =
undefined
let is_static_website = false
let s3FilePicker: S3FilePicker
let s3FileUploadRawMode = false
let s3Editor: SimpleEditor | undefined = undefined
let workspaced_route: boolean = false
let drawerLoading = true
export async function openEdit(ePath: string, isFlow: boolean) {
@@ -78,27 +94,12 @@
initialPath = ''
dirtyPath = false
is_static_website = false
workspaced_route = false
} finally {
drawerLoading = false
}
}
let path: string = ''
let pathError = ''
let isValid = false
let dirtyRoutePath = false
let is_async = false
let requires_auth = false
let route_path = ''
let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' = 'post'
let static_asset_config: { s3: string; storage?: string; filename?: string } | undefined =
undefined
let is_static_website = false
let s3FilePicker: S3FilePicker
let s3FileUploadRawMode = false
let s3Editor: SimpleEditor | undefined = undefined
const dispatch = createEventDispatcher()
let can_write = true
@@ -109,13 +110,13 @@
})
script_path = s.script_path
initialScriptPath = s.script_path
is_flow = s.is_flow
path = s.path
route_path = s.route_path
http_method = s.http_method ?? 'post'
is_async = s.is_async
requires_auth = s.requires_auth
workspaced_route = s.workspaced_route ?? false
if (!isCloudHosted()) {
static_asset_config = s.static_asset_config
s3FileUploadRawMode = !!static_asset_config
@@ -139,7 +140,8 @@
route_path: $userStore?.is_admin || $userStore?.is_super_admin ? route_path : undefined,
http_method,
static_asset_config,
is_static_website
is_static_website,
workspaced_route
}
})
sendUserToast(`Route ${path} updated`)
@@ -155,7 +157,8 @@
route_path,
http_method,
static_asset_config,
is_static_website
is_static_website,
workspaced_route
}
})
sendUserToast(`Route ${path} created`)
@@ -357,9 +360,7 @@
btnClasses="ml-4 mt-2"
color="dark"
size="xs"
href={itemKind === 'flow'
? '/flows/add?hub=62'
: '/scripts/add?hub=hub%2F11627'}
href={itemKind === 'flow' ? '/flows/add?hub=62' : '/scripts/add?hub=hub%2F11627'}
target="_blank">Create from template</Button
>
{/if}
@@ -373,6 +374,7 @@
bind:isValid
bind:dirtyRoutePath
bind:http_method
bind:workspaced_route
{can_write}
bind:static_asset_config
/>
@@ -0,0 +1,8 @@
import { base } from "$lib/base";
import { isCloudHosted } from "$lib/cloud";
export function getHttpRoute(route_path: string, workspaced_route: boolean, workspace_id: string) {
return `${location.origin}${base}/api/r/${
isCloudHosted() || workspaced_route ? workspace_id + '/' : ''
}${route_path}`
}
@@ -1,5 +1,5 @@
export function load() {
return {
stuff: { title: 'HTTP routes' }
stuff: { title: 'HTTP triggers' }
}
}
@@ -5,7 +5,13 @@
type HttpTrigger,
type WorkspaceDeployUISettings
} from '$lib/gen'
import { canWrite, displayDate, getLocalSetting, storeLocalSetting } from '$lib/utils'
import {
canWrite,
copyToClipboard,
displayDate,
getLocalSetting,
storeLocalSetting
} from '$lib/utils'
import { base } from '$app/paths'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Button, Skeleton } from '$lib/components/common'
@@ -15,7 +21,7 @@
import ShareModal from '$lib/components/ShareModal.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { userStore, workspaceStore, userWorkspaces, enterpriseLicense } from '$lib/stores'
import { Route, Code, Eye, Pen, Plus, Share, Trash, FileUp } from 'lucide-svelte'
import { Route, Code, Eye, Pen, Plus, Share, Trash, FileUp, ClipboardCopy } from 'lucide-svelte'
import { goto } from '$lib/navigation'
import SearchItems from '$lib/components/SearchItems.svelte'
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
@@ -28,6 +34,8 @@
import RouteEditor from '$lib/components/triggers/http/RouteEditor.svelte'
import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
import { ALL_DEPLOYABLE, isDeployable } from '$lib/utils_deployable'
import { isCloudHosted } from '$lib/cloud'
import { getHttpRoute } from '$lib/components/triggers/http/utils'
type TriggerW = HttpTrigger & { canWrite: boolean }
@@ -229,7 +237,7 @@
<div class="text-center text-sm text-tertiary mt-2"> No routes </div>
{:else if items?.length}
<div class="border rounded-md divide-y">
{#each items.slice(0, nbDisplayed) as { path, edited_by, edited_at, script_path, route_path, is_flow, extra_perms, canWrite, marked, http_method, static_asset_config } (path)}
{#each items.slice(0, nbDisplayed) as { workspace_id, workspaced_route, path, edited_by, edited_at, script_path, route_path, is_flow, extra_perms, canWrite, marked, http_method, static_asset_config } (path)}
{@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
<div
@@ -250,7 +258,10 @@
{@html marked}
</span>
{:else}
{http_method.toUpperCase()} /{route_path}
{http_method.toUpperCase()}
{isCloudHosted() || workspaced_route
? workspace_id + '/' + route_path
: route_path}
{/if}
</div>
<div class="text-secondary text-xs truncate text-left font-light">
@@ -270,6 +281,15 @@
</div>
<div class="flex gap-2 items-center justify-end">
<Button
on:click={() =>
copyToClipboard(getHttpRoute(route_path, workspaced_route ?? false, workspace_id))}
color="dark"
size="xs"
startIcon={{ icon: ClipboardCopy }}
>
Copy URL
</Button>
<Button
on:click={() => routeEditor?.openEdit(path, is_flow)}
size="xs"