Files
windmill/backend/windmill-trigger-http/src/handler.rs
Ruben Fiszel c4c9ef5fd7 feat: add optional labels to scripts, flows, apps, schedules, triggers (#8609)
* feat: add optional labels to scripts, flows, apps, raw apps, schedules, and triggers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update sqlx cache, make labels optional in openapi, regenerate system prompts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add minimal labels input UI to script, flow, and schedule editors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reduce gap between summary and labels input

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add labels to script/flow detail pages and summary/path popover

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: move labels inside SummaryPathDisplay trigger for clickable area, reduce gap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: display labels inline to the right of summary, not below

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: increase gap between summary and labels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add labels to resources/variables, make labels nullable, add home page label filter badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add labels to workspace export/import, resources, variables + test coverage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: make migration idempotent, regenerate sqlx cache after merge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: pass labels in script create and flow create/update API calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add labels input UI to resource and variable editors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove negative margin from LabelsInput to prevent overlap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add top and left margin to LabelsInput for better spacing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reduce left margin on LabelsInput

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: widen label input to w-32

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use inline-flex so LabelsInput doesn't stretch full width

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove flex-wrap so label input stays on same line as badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add label filter presets to resources, variables, and schedules search

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use max-w-32 on label input to prevent stretching

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: pull labels closer to summary with negative top margin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: increase negative margin to pull labels even closer to summary

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: pass labels in schedule create/update API calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use COALESCE to preserve existing labels when not provided in schedule/flow update

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels to CreateResource, EditResource, CreateVariable, EditVariable in OpenAPI spec

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: display label badges on resource and variable list pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: display label badges on schedule and all trigger list pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add folder and label presets to schedules search filter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: apply user_folders_only filter on all workspaces including admins

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add label presets to resources and variables search filters

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: derive folder presets from loaded items, not all workspace folders

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add label query parameter to resource and variable list endpoints in OpenAPI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: display label filter badges inline with folder filters on home page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "feat: display label filter badges inline with folder filters on home page"

This reverts commit 6767a50aa6.

* feat: support comma-separated label filters (allowMultiple) in all list endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: append label presets with comma for allowMultiple filters instead of duplicating key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: hide label presets that are already in the comma-separated filter value

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace unsafe manual SQL ARRAY construction with parameterized queries, add labels to ScriptWDraft

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: complete down migration, add labels to Resource/Variable OpenAPI schemas, remove type cast, add label length validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels field to Schedule test fixture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels field to Rust client struct constructions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: regenerate sqlx cache with --all-features for EE builds

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate sqlx cache and package-lock after merge with main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: squash two migrations into one, use IF NOT EXISTS for idempotency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: track label changes in SummaryPathDisplay to enable save button

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use JSON string comparison for label dirty tracking in popover

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: navigate to script by path after save from popover to load new version

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update initialLabels after save so subsequent label changes enable save again

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use onchange callback for label dirty tracking instead of derived comparison

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reload script by path after label save to fetch new version

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: propagate script/flow labels to jobs at push time

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: show script/flow labels on runs page, merge with wm_labels for completed jobs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: change job labels type from JSONB to text[], show labels on job detail page, fix type mismatch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels to QueuedJob struct, fix get_job queries to return v2_job.labels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace +Label text with icon only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add tag icon before labels on job detail page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: move tag icon inside badge on job detail page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use blue badge with tag icon in RunBadges, remove duplicate labels from JobDetailHeader

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: set icon position to left so tag icon renders in badge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: render Tag icon inline in badge children instead of via icon prop

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: retry icon prop with small badge and position left

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add hover tooltip showing "Label: X" on job label badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: include v2_job.labels in runs page label filter and broad search

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate sqlx cache and system prompts after merge with main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels to EE JobPayload constructions, regenerate sqlx cache with --all-features

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: regenerate sqlx cache CE-only (without EE symlinks that cause conflicts)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update remaining wm_labels JSONB queries to use text[] merge expression

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: simplify job labels to just read v2_job.labels (wm_labels already merged at completion)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: consistent label badge spacing with gap-0.5 wrapper and px-0.5 on badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels: None to test utils JobPayload construction

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels to all test fixture JobPayload/NewFlow/EditApp constructions, regenerate sqlx cache

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: fix vertical content shift by fixing container and input height to h-5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: npm_check errors - unused imports, combinedItems order, flow.labels type, badge px-1 padding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove unused FolderService imports, fix label badge alignment in RunBadges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restore deleted service imports in variables page, remove empty loadFolders

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger CI with updated ee-repo-ref

* chore: update ee-repo-ref to merged EE companion PR

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger fresh CI run for updated ee-repo-ref

* fix: match label badge size with other badges in RunBadges using {large} prop

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove icon from RunBadges label badge to fix vertical alignment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: shorten "Job kind" to "Kind" in run badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add small inline tag icon (10px, -mt-px) to label badge without disrupting height

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add "Label: X" hover tooltip to all label badges, show hidden labels on +N hover

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add tag icon and "Label: X" tooltip to home page label filter badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: show LabelsInput even when path is hidden in ResourceEditor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add labels input to new resource creation drawer (AppConnectInner)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* iterate

* fix: add LabelsInput to all resource creation steps in AppConnectInner

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reduce LabelsInput top margin from -mt-3 to -mt-1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: increase negative margin to -mt-2 for tighter spacing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: split the difference with -mt-1.5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: adjust to -mt-1 for label spacing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: per-site label spacing via class prop instead of global negative margin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: make label badges clickable to toggle label filter on resources, variables, schedules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use proper array indexOf for label filter toggle, set undefined correctly on removal

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use delete instead of undefined to properly clear label filter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add /labels/list endpoint and autocomplete dropdown to LabelsInput

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use inline preventDefault for Svelte 5 event handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add "Create new" option in label autocomplete, regenerate sqlx cache with update_sqlx.sh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add GIN indexes on labels column for all 16 tables

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove CONCURRENTLY from GIN index creation in migration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add comprehensive label coverage for pull, edit, removal across all item types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: simplify job label filters to only use v2_job.labels, remove wm_labels back-compat

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add integration tests for job label propagation, display, and filtering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review findings — missing labels in fetch_script_for_update, app rename, escape key bug

- Add `labels` to SELECT in `fetch_script_for_update` to prevent lost labels on script clone
- Pass `labels` in app branch of `moveRenameManager.ts` so app renames preserve labels
- Clear `inputValue` before `adding = false` in LabelsInput escape handler to prevent accidental label add via onblur
- Fix `test_job_label_filter` to complete jobs via SQL (label filtering only works on completed jobs)
- Add `test_wm_labels_from_result_merged_with_static_labels` integration test using Bun

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:39:32 +00:00

620 lines
19 KiB
Rust

use super::{
validate_authentication_method, HttpConfig, HttpConfigRequest, HttpMethod, HttpTrigger,
RouteExists, ROUTE_PATH_KEY_RE, VALID_ROUTE_PATH_RE,
};
use async_trait::async_trait;
use axum::{extract::Path, routing::post, Extension, Json, Router};
use http::StatusCode;
use sqlx::PgConnection;
use std::collections::HashSet;
use windmill_api_auth::ApiAuthed;
use windmill_audit::{audit_oss::audit_log, ActionKind};
use windmill_common::global_settings::HTTP_ROUTE_WORKSPACED_ROUTE;
use windmill_common::{
db::UserDB,
error::{Error, Result},
utils::require_admin,
worker::CLOUD_HOSTED,
DB,
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_trigger::{Trigger, TriggerCrud, TriggerData};
pub async fn increase_trigger_version(tx: &mut PgConnection) -> Result<()> {
sqlx::query!("SELECT nextval('http_trigger_version_seq')")
.fetch_one(tx)
.await?;
Ok(())
}
pub fn generate_route_path_key(route_path: &str) -> String {
ROUTE_PATH_KEY_RE
.replace_all(route_path, "${1}${2}key")
.to_string()
}
pub async fn route_path_key_exists(
route_path_key: &str,
http_method: &HttpMethod,
w_id: &str,
trigger_path: Option<&str>,
workspaced_route: Option<bool>,
db: &DB,
) -> Result<bool> {
let exists = if *CLOUD_HOSTED {
sqlx::query_scalar!(
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 http_route_workspaced = *HTTP_ROUTE_WORKSPACED_ROUTE.read().await;
let effective_workspaced = workspaced_route.unwrap_or(false) || http_route_workspaced;
let route_path_key = if effective_workspaced {
std::borrow::Cow::Owned(format!("{}/{}", w_id, route_path_key.trim_matches('/')))
} else {
std::borrow::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)
}
pub async fn exists_route(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(RouteExists { route_path, http_method, trigger_path, workspaced_route }): Json<
RouteExists,
>,
) -> Result<Json<bool>> {
let route_path_key = generate_route_path_key(&route_path);
let exists = route_path_key_exists(
&route_path_key,
&http_method,
&w_id,
trigger_path.as_deref(),
workspaced_route,
&db,
)
.await?;
Ok(Json(exists))
}
fn check_no_duplicates(
new_http_triggers: &[TriggerData<HttpConfigRequest>],
route_path_key: &[String],
) -> Result<()> {
let mut seen = HashSet::with_capacity(new_http_triggers.len());
for (i, trigger) in new_http_triggers.iter().enumerate() {
if !seen.insert((
&route_path_key[i],
trigger.config.http_method,
trigger.config.workspaced_route,
)) {
return Err(Error::BadRequest(format!(
"Duplicate HTTP route detected: '{}'. Each HTTP route must have a unique 'route_path'.",
&trigger.config.route_path
)));
}
}
Ok(())
}
pub async fn insert_new_trigger_into_db(
authed: &ApiAuthed,
_db: &DB,
tx: &mut PgConnection,
w_id: &str,
trigger: &TriggerData<HttpConfigRequest>,
route_path_key: &str,
) -> Result<()> {
require_admin(authed.is_admin, &authed.username)?;
let http_route_workspaced = *HTTP_ROUTE_WORKSPACED_ROUTE.read().await;
let effective_workspaced =
trigger.config.workspaced_route.unwrap_or(false) || http_route_workspaced;
let request_type = trigger.config.request_type;
let resolved_edited_by = trigger.base.resolve_edited_by(authed);
let resolved_permissioned_as = trigger.base.resolve_permissioned_as(authed);
sqlx::query!(
r#"
INSERT INTO http_trigger (
workspace_id,
path,
route_path,
route_path_key,
workspaced_route,
authentication_resource_path,
wrap_body,
raw_string,
script_path,
summary,
description,
is_flow,
mode,
request_type,
authentication_method,
http_method,
static_asset_config,
edited_by,
permissioned_as,
edited_at,
is_static_website,
error_handler_path,
error_handler_args,
retry
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, now(), $20, $21, $22, $23
)
"#,
w_id,
trigger.base.path,
trigger.config.route_path,
route_path_key,
effective_workspaced,
trigger.config.authentication_resource_path,
trigger.config.wrap_body.unwrap_or(false),
trigger.config.raw_string.unwrap_or(false),
trigger.base.script_path,
trigger.config.summary,
trigger.config.description,
trigger.base.is_flow,
trigger.base.mode() as _,
request_type as _,
trigger.config.authentication_method as _,
trigger.config.http_method as _,
trigger.config.static_asset_config as _,
&resolved_edited_by,
resolved_permissioned_as,
trigger.config.is_static_website,
trigger.error_handling.error_handler_path,
trigger.error_handling.error_handler_args as _,
trigger.error_handling.retry as _
)
.execute(&mut *tx)
.await?;
Ok(())
}
pub async fn create_many_http_triggers(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(new_http_triggers): Json<Vec<TriggerData<HttpConfigRequest>>>,
) -> Result<(StatusCode, String)> {
require_admin(authed.is_admin, &authed.username)?;
let handler = HttpTrigger;
let error_wrapper = |route_path: &str, error: Error| -> Error {
anyhow::anyhow!(
"Error occurred for HTTP route at route path: {}, error: {}",
route_path,
error
)
.into()
};
let mut route_path_keys = Vec::with_capacity(new_http_triggers.len());
for new_http_trigger in new_http_triggers.iter() {
handler
.validate_new(&db, &w_id, &new_http_trigger.config)
.await
.map_err(|err| error_wrapper(&new_http_trigger.config.route_path, err))?;
let route_path_key =
check_if_route_exist(&db, &new_http_trigger.config, &w_id, None).await?;
route_path_keys.push(route_path_key.clone());
}
check_no_duplicates(&new_http_triggers, &route_path_keys)?;
let mut tx = user_db.begin(&authed).await?;
for (new_http_trigger, route_path_key) in new_http_triggers.iter().zip(route_path_keys.iter()) {
insert_new_trigger_into_db(
&authed,
&db,
&mut tx,
&w_id,
new_http_trigger,
route_path_key,
)
.await
.map_err(|err| error_wrapper(&new_http_trigger.config.route_path, err))?;
if let Some(labels) = &new_http_trigger.base.labels {
sqlx::query!(
"UPDATE http_trigger SET labels = $1 WHERE workspace_id = $2 AND path = $3",
labels as &[String],
&w_id,
&new_http_trigger.base.path
)
.execute(&mut *tx)
.await
.map_err(|err| error_wrapper(&new_http_trigger.config.route_path, err.into()))?;
}
audit_log(
&mut *tx,
&authed,
"http_trigger.create",
ActionKind::Create,
&w_id,
Some(&new_http_trigger.base.path),
None,
)
.await
.map_err(|err| error_wrapper(&new_http_trigger.config.route_path, err.into()))?;
increase_trigger_version(&mut tx)
.await
.map_err(|err| error_wrapper(&new_http_trigger.config.route_path, err.into()))?;
}
tx.commit().await?;
for http_trigger in new_http_triggers.into_iter() {
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::HttpTrigger { path: http_trigger.base.path.clone(), parent_path: None },
Some(format!("HTTP trigger '{}' created", http_trigger.base.path)),
true,
None,
)
.await
.map_err(|err| error_wrapper(&http_trigger.config.route_path, err.into()))?;
}
Ok((StatusCode::CREATED, "Created all HTTP routes".to_string()))
}
async fn check_if_route_exist(
db: &DB,
config: &HttpConfigRequest,
workspace_id: &str,
trigger_path: Option<&str>,
) -> Result<String> {
let route_path_key = generate_route_path_key(&config.route_path);
let exists = route_path_key_exists(
&route_path_key,
&config.http_method,
workspace_id,
trigger_path,
config.workspaced_route,
db,
)
.await?;
if exists {
return Err(Error::BadRequest(
"A route already exists with this path".to_string(),
));
}
Ok(route_path_key)
}
#[async_trait]
impl TriggerCrud for HttpTrigger {
type TriggerConfig = HttpConfig;
type Trigger = Trigger<Self::TriggerConfig>;
type TriggerConfigRequest = HttpConfigRequest;
type TestConnectionConfig = ();
const TABLE_NAME: &'static str = "http_trigger";
const TRIGGER_TYPE: &'static str = "http";
const SUPPORTS_SERVER_STATE: bool = false;
const SUPPORTS_TEST_CONNECTION: bool = false;
const ROUTE_PREFIX: &'static str = "/http_triggers";
const DEPLOYMENT_NAME: &'static str = "HTTP trigger";
const IS_ALLOWED_ON_CLOUD: bool = true;
const ADDITIONAL_SELECT_FIELDS: &[&'static str] = &[
"route_path",
"route_path_key",
"request_type",
"authentication_method",
"http_method",
"summary",
"description",
"static_asset_config",
"is_static_website",
"authentication_resource_path",
"workspaced_route",
"wrap_body",
"raw_string",
];
fn get_deployed_object(path: String, parent_path: Option<String>) -> DeployedObject {
DeployedObject::HttpTrigger { path, parent_path }
}
fn additional_routes(&self) -> Router {
Router::new()
.route("/create_many", post(create_many_http_triggers))
.route("/route_exists", post(exists_route))
}
async fn validate_new(
&self,
_db: &DB,
_workspace_id: &str,
new: &Self::TriggerConfigRequest,
) -> Result<()> {
if *CLOUD_HOSTED && (new.is_static_website || new.static_asset_config.is_some()) {
return Err(Error::BadRequest(
"Static website and static asset are not supported on cloud".to_string(),
));
}
if !VALID_ROUTE_PATH_RE.is_match(&new.route_path) {
return Err(Error::BadRequest("Invalid route path".to_string()));
}
validate_authentication_method(new.authentication_method, new.raw_string)?;
Ok(())
}
async fn validate_edit(
&self,
_db: &DB,
_workspace_id: &str,
edit: &Self::TriggerConfigRequest,
_path: &str,
) -> Result<()> {
if *CLOUD_HOSTED && (edit.is_static_website || edit.static_asset_config.is_some()) {
return Err(Error::BadRequest(
"Static website and static asset are not supported on cloud".to_string(),
));
}
validate_authentication_method(edit.authentication_method, edit.raw_string)?;
Ok(())
}
async fn create_trigger(
&self,
db: &DB,
tx: &mut PgConnection,
authed: &ApiAuthed,
w_id: &str,
trigger: TriggerData<Self::TriggerConfigRequest>,
) -> Result<()> {
let route_path_key = check_if_route_exist(db, &trigger.config, &w_id, None).await?;
insert_new_trigger_into_db(authed, db, tx, w_id, &trigger, &route_path_key).await?;
increase_trigger_version(tx).await?;
Ok(())
}
async fn update_trigger(
&self,
db: &DB,
tx: &mut PgConnection,
authed: &ApiAuthed,
workspace_id: &str,
path: &str,
trigger: TriggerData<Self::TriggerConfigRequest>,
) -> Result<()> {
let resolved_edited_by = trigger.base.resolve_edited_by(authed);
let resolved_permissioned_as = trigger.base.resolve_permissioned_as(authed);
if authed.is_admin {
if trigger.config.route_path.is_empty() {
return Err(Error::BadRequest("route_path is required".to_string()));
};
let route_path = &trigger.config.route_path;
if !VALID_ROUTE_PATH_RE.is_match(route_path) {
return Err(Error::BadRequest("Invalid route path".to_string()));
}
let route_path_key =
check_if_route_exist(db, &trigger.config, workspace_id, Some(path)).await?;
let http_route_workspaced = *HTTP_ROUTE_WORKSPACED_ROUTE.read().await;
let effective_workspaced =
trigger.config.workspaced_route.unwrap_or(false) || http_route_workspaced;
let request_type = trigger.config.request_type;
sqlx::query!(
r#"
UPDATE
http_trigger
SET
route_path = $1,
route_path_key = $2,
workspaced_route = $3,
wrap_body = $4,
raw_string = $5,
authentication_resource_path = $6,
script_path = $7,
path = $8,
is_flow = $9,
mode = $10,
http_method = $11,
static_asset_config = $12,
edited_by = $13,
permissioned_as = $14,
request_type = $15,
authentication_method = $16,
summary = $17,
description = $18,
edited_at = now(),
is_static_website = $19,
error_handler_path = $20,
error_handler_args = $21,
retry = $22
WHERE
workspace_id = $23 AND
path = $24
"#,
route_path,
&route_path_key,
Some(effective_workspaced),
trigger.config.wrap_body,
trigger.config.raw_string,
trigger.config.authentication_resource_path,
trigger.base.script_path,
trigger.base.path,
trigger.base.is_flow,
trigger.base.mode() as _,
trigger.config.http_method as _,
trigger.config.static_asset_config as _,
&resolved_edited_by,
resolved_permissioned_as,
request_type as _,
trigger.config.authentication_method as _,
trigger.config.summary,
trigger.config.description,
trigger.config.is_static_website,
trigger.error_handling.error_handler_path,
trigger.error_handling.error_handler_args as _,
trigger.error_handling.retry as _,
workspace_id,
path,
)
.execute(&mut *tx)
.await?;
} else {
let request_type = trigger.config.request_type;
sqlx::query!(
r#"
UPDATE
http_trigger
SET
wrap_body = $1,
raw_string = $2,
authentication_resource_path = $3,
script_path = $4,
path = $5,
is_flow = $6,
mode = $7,
http_method = $8,
static_asset_config = $9,
edited_by = $10,
permissioned_as = $11,
request_type = $12,
authentication_method = $13,
summary = $14,
description = $15,
edited_at = now(),
is_static_website = $16,
error_handler_path = $17,
error_handler_args = $18,
retry = $19
WHERE
workspace_id = $20 AND
path = $21
"#,
trigger.config.wrap_body,
trigger.config.raw_string,
trigger.config.authentication_resource_path,
trigger.base.script_path,
trigger.base.path,
trigger.base.is_flow,
trigger.base.mode() as _,
trigger.config.http_method as _,
trigger.config.static_asset_config as _,
&resolved_edited_by,
resolved_permissioned_as,
request_type as _,
trigger.config.authentication_method as _,
trigger.config.summary,
trigger.config.description,
trigger.config.is_static_website,
trigger.error_handling.error_handler_path,
trigger.error_handling.error_handler_args as _,
trigger.error_handling.retry as _,
workspace_id,
path,
)
.execute(&mut *tx)
.await?;
}
increase_trigger_version(tx).await?;
Ok(())
}
async fn set_trigger_mode_extra_action(&self, tx: &mut PgConnection) -> Result<()> {
increase_trigger_version(tx).await
}
async fn delete_by_path(
&self,
tx: &mut PgConnection,
workspace_id: &str,
path: &str,
) -> Result<bool> {
let deleted = sqlx::query(&format!(
"DELETE FROM {} WHERE workspace_id = $1 AND path = $2",
Self::TABLE_NAME
))
.bind(workspace_id)
.bind(path)
.execute(&mut *tx)
.await?
.rows_affected();
increase_trigger_version(tx).await?;
Ok(deleted > 0)
}
}