diff --git a/backend/openapi.yaml b/backend/openapi.yaml index 645b297744..7f744a5c9f 100644 --- a/backend/openapi.yaml +++ b/backend/openapi.yaml @@ -379,10 +379,10 @@ paths: schema: type: string - /workspaces/validate_id: + /workspaces/exists: post: - summary: validate id - operationId: validateId + summary: exists workspace + operationId: existsWorkspace tags: - workspace requestBody: @@ -403,12 +403,12 @@ paths: content: text/plain: schema: - type: string + type: boolean - /workspaces/validate_username: + /workspaces/exists_username: post: - summary: validate username - operationId: validateUsername + summary: exists username + operationId: existsUsername tags: - workspace requestBody: @@ -431,7 +431,7 @@ paths: content: text/plain: schema: - type: string + type: boolean /users/email: get: @@ -934,6 +934,23 @@ paths: schema: $ref: "#/components/schemas/ListableVariable" + /w/{workspace}/variables/exists/{path}: + get: + summary: does variable exists at path + operationId: existsVariable + tags: + - variable + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: variable + content: + application/json: + schema: + type: boolean + /w/{workspace}/variables/list: get: summary: list variables @@ -1304,6 +1321,23 @@ paths: schema: $ref: "#/components/schemas/Resource" + /w/{workspace}/resources/exists/{path}: + get: + summary: does resource exists + operationId: existsResource + tags: + - resource + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: does resource exists + content: + application/json: + schema: + type: boolean + /w/{workspace}/resources/list: get: summary: list resources @@ -1410,6 +1444,23 @@ paths: schema: $ref: "#/components/schemas/ResourceType" + /w/{workspace}/resources/type/exists/{path}: + get: + summary: does resource_type exists + operationId: existsResourceType + tags: + - resource + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: does resource_type exist + content: + application/json: + schema: + type: boolean + /w/{workspace}/resources/type/list: get: summary: list resource_types @@ -1731,6 +1782,23 @@ paths: schema: $ref: "#/components/schemas/Script" + /w/{workspace}/scripts/exists/p/{path}: + get: + summary: exists script by path + operationId: existsScriptByPath + tags: + - script + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + responses: + "200": + description: does it exists + content: + application/json: + schema: + type: boolean + /w/{workspace}/scripts/get/h/{hash}: get: summary: get script by hash @@ -1906,6 +1974,23 @@ paths: schema: $ref: "#/components/schemas/Flow" + /w/{workspace}/flows/exists/{path}: + get: + summary: exists flow by path + operationId: existsFlowByPath + tags: + - flow + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + responses: + "200": + description: flow details + content: + application/json: + schema: + type: boolean + /w/{workspace}/flows/create: post: summary: create flow @@ -2462,6 +2547,23 @@ paths: schema: $ref: "#/components/schemas/Schedule" + /w/{workspace}/schedules/exists/{path}: + get: + summary: does schedule exists + operationId: existsSchedule + tags: + - schedule + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: schedule deleted + content: + application/json: + schema: + type: boolean + /w/{workspace}/schedules/list: get: summary: list schedules diff --git a/backend/sqlx-data.json b/backend/sqlx-data.json index b56520d5c4..544cda8d09 100644 --- a/backend/sqlx-data.json +++ b/backend/sqlx-data.json @@ -62,6 +62,27 @@ ] } }, + "0784bb86a503f02b9ef247a2b83a82ddfa49632552b223a9f4536a449b0a1eb8": { + "query": "SELECT EXISTS(SELECT 1 FROM resource WHERE path = $1 AND workspace_id = $2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + } + }, "09246e9ff5b2beb61ab51a5f73d980f7638904d5a18a415e52d5e1c94dffd0aa": { "query": "SELECT SUM(duration) FROM completed_job WHERE created_by = $1 AND created_at > NOW() - INTERVAL '1200 seconds' AND workspace_id = $2", "describe": { @@ -681,6 +702,27 @@ "nullable": [] } }, + "355dcb2cbebd13f0e3bdd4929b9e431b0e6d72716d1c4f9ab6af6adce5b5e4b3": { + "query": "SELECT EXISTS(SELECT 1 FROM resource_type WHERE name = $1 AND workspace_id = $2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + } + }, "37d3ee8009055e869941e548a6d5a352053a5d7782f662c34b94706488abccb6": { "query": "UPDATE queue SET running = false WHERE last_ping < $1 RETURNING id", "describe": { @@ -1691,6 +1733,27 @@ ] } }, + "88a3f58a1a315200fdd2e4bb8638246ee21818f8aaaf56f6e9d7ddce1490d886": { + "query": "SELECT EXISTS(SELECT 1 FROM variable WHERE path = $1 AND workspace_id = $2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + } + }, "88b7589a6416da8be4b26af3bf30fcfcd6aeae7bc5a37e9a735cabbe2691c570": { "query": "SELECT login_type::TEXT FROM password WHERE email = $1", "describe": { @@ -2095,6 +2158,27 @@ ] } }, + "a38059dc3574da498ce986c916b6d385b1f18d5bd659ef13c43fafa9daff6bda": { + "query": "SELECT EXISTS(SELECT 1 FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter'))", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + } + }, "a98b2d68f023f46ab91167d3147416df672c2aed2ba5ab70e98a9da5fa47255a": { "query": "INSERT INTO workspace_settings\n (workspace_id)\n VALUES ($1)", "describe": { @@ -2931,6 +3015,27 @@ ] } }, + "e587299612077e6de39b5a501aa8a736cae4a21f11853f8ae66a610b4e65f49e": { + "query": "SELECT EXISTS(SELECT 1 FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')))", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + } + }, "e94abd39ec51b7e0c48c190d47ed766fd4f401187c3b60b3e599426c95232f7f": { "query": "UPDATE queue SET last_ping = $1 WHERE id = $2", "describe": { @@ -2944,6 +3049,27 @@ "nullable": [] } }, + "ee99cb974fb35d2d92fa96cecb53b8ad3a729ac710f362dc55a576613b66f9be": { + "query": "SELECT EXISTS(SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + } + }, "f056b5f3e66a764748925f1bfd3180923fde8c7fdf69088d0e4a5555cc049545": { "query": "SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2", "describe": { diff --git a/backend/src/flow.rs b/backend/src/flow.rs index ecaac534c5..7b9d322c99 100644 --- a/backend/src/flow.rs +++ b/backend/src/flow.rs @@ -20,7 +20,7 @@ use sqlx::FromRow; use crate::{ audit::{audit_log, ActionKind}, - db::UserDB, + db::{UserDB, DB}, error::{Error, JsonResult, Result}, jobs::RawCode, scripts::Schema, @@ -35,6 +35,7 @@ pub fn workspaced_service() -> Router { .route("/update/*path", post(update_flow)) .route("/archive/*path", post(archive_flow_by_path)) .route("/get/*path", get(get_flow_by_path)) + .route("/exists/*path", get(exists_flow_by_path)) } #[derive(FromRow, Serialize)] @@ -271,6 +272,24 @@ async fn get_flow_by_path( Ok(Json(flow)) } +async fn exists_flow_by_path( + Extension(db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult { + let path = path.to_path(); + + let exists = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter'))", + path, + w_id + ) + .fetch_one(&db) + .await? + .unwrap_or(false); + + Ok(Json(exists)) +} + async fn archive_flow_by_path( authed: Authed, Extension(user_db): Extension, diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 149a1c5b7d..0ed7cd57ec 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -79,9 +79,10 @@ pub async fn initialize_tracing() -> anyhow::Result<()> { tracing_init::initialize_tracing().await } -#[derive(Clone)] struct BaseUrl(String); +struct CloudHosted(bool); + pub async fn run_server( db: DB, addr: SocketAddr, @@ -115,7 +116,10 @@ pub async fn run_server( .layer(Extension(user_db)) .layer(Extension(auth_cache.clone())) .layer(Extension(basic_clients)) - .layer(Extension(BaseUrl(base_url.to_string()))) + .layer(Extension(Arc::new(BaseUrl(base_url.to_string())))) + .layer(Extension(Arc::new(CloudHosted( + std::env::var("CLOUD_HOSTED").is_ok(), + )))) .layer(Extension(http_client)) .layer(CookieManagerLayer::new()); // build our application with a route diff --git a/backend/src/oauth2.rs b/backend/src/oauth2.rs index 73a625fbce..87e2b84159 100644 --- a/backend/src/oauth2.rs +++ b/backend/src/oauth2.rs @@ -597,7 +597,7 @@ async fn slack_command( SlackSig { sig, ts }: SlackSig, Extension(slack_verifier): Extension>>, Extension(db): Extension, - Extension(base_url): Extension, + Extension(base_url): Extension>, body: Bytes, ) -> error::Result { let form: SlackCommand = serde_urlencoded::from_bytes(&body) @@ -651,7 +651,7 @@ async fn slack_command( ) .await?; tx.commit().await?; - let url = base_url.0; + let url = base_url.0.to_owned(); return Ok(format!("Job launched. See details at {url}/run/{uuid}")); } } diff --git a/backend/src/resources.rs b/backend/src/resources.rs index 2d35bdd923..1f343abb9e 100644 --- a/backend/src/resources.rs +++ b/backend/src/resources.rs @@ -26,6 +26,7 @@ pub fn workspaced_service() -> Router { Router::new() .route("/list", get(list_resources)) .route("/get/*path", get(get_resource)) + .route("/exists/*path", get(exists_resource)) .route("/get_value/*path", get(get_resource_value)) .route("/update/*path", post(update_resource)) .route("/delete/*path", delete(delete_resource)) @@ -33,6 +34,7 @@ pub fn workspaced_service() -> Router { .route("/type/list", get(list_resource_types)) .route("/type/listnames", get(list_resource_types_names)) .route("/type/get/:name", get(get_resource_type)) + .route("/type/exists/:name", get(exists_resource_type)) .route("/type/update/:name", post(update_resource_type)) .route("/type/delete/:name", delete(delete_resource_type)) .route("/type/create", post(create_resource_type)) @@ -150,6 +152,24 @@ async fn get_resource( Ok(Json(resource)) } +async fn exists_resource( + Extension(db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult { + let path = path.to_path(); + + let exists = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM resource WHERE path = $1 AND workspace_id = $2)", + path, + w_id + ) + .fetch_one(&db) + .await? + .unwrap_or(false); + + Ok(Json(exists)) +} + async fn get_resource_value( authed: Authed, Extension(user_db): Extension, @@ -325,6 +345,22 @@ async fn get_resource_type( Ok(Json(resource_type)) } +async fn exists_resource_type( + Extension(db): Extension, + Path((w_id, name)): Path<(String, String)>, +) -> JsonResult { + let exists = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM resource_type WHERE name = $1 AND workspace_id = $2)", + name, + w_id + ) + .fetch_one(&db) + .await? + .unwrap_or(false); + + Ok(Json(exists)) +} + async fn create_resource_type( authed: Authed, Extension(user_db): Extension, diff --git a/backend/src/schedule.rs b/backend/src/schedule.rs index 588999808c..a56aea8b82 100644 --- a/backend/src/schedule.rs +++ b/backend/src/schedule.rs @@ -9,7 +9,7 @@ use std::str::FromStr; use crate::{ audit::{audit_log, ActionKind}, - db::UserDB, + db::{UserDB, DB}, error::{self, JsonResult, Result}, jobs::{self, push, JobPayload}, users::Authed, @@ -30,6 +30,7 @@ pub fn workspaced_service() -> Router { Router::new() .route("/list", get(list_schedule)) .route("/get/*path", get(get_schedule)) + .route("/exists/*path", get(exists_schedule)) .route("/create", post(create_schedule)) .route("/update/*path", post(edit_schedule)) .route("/setenabled/*path", post(set_enabled)) @@ -284,6 +285,24 @@ async fn get_schedule( Ok(Json(schedule)) } +async fn exists_schedule( + Extension(db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult { + let path = path.to_path(); + + let exists = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2)", + path, + w_id + ) + .fetch_one(&db) + .await? + .unwrap_or(false); + + Ok(Json(exists)) +} + #[derive(Deserialize)] pub struct PreviewPayload { pub schedule: String, diff --git a/backend/src/scripts.rs b/backend/src/scripts.rs index e52ef741b4..e0b6dae599 100644 --- a/backend/src/scripts.rs +++ b/backend/src/scripts.rs @@ -17,7 +17,7 @@ use crate::{ utils::{require_admin, Pagination, StripPath}, }; use axum::{ - extract::{Extension, Path, Query}, + extract::{Extension, Host, Path, Query}, routing::{get, post}, Json, Router, }; @@ -51,6 +51,7 @@ pub fn workspaced_service() -> Router { .route("/create", post(create_script)) .route("/archive/p/*path", post(archive_script_by_path)) .route("/get/p/*path", get(get_script_by_path)) + .route("/exists/p/*path", get(exists_script_by_path)) .route("/archive/h/:hash", post(archive_script_by_hash)) .route("/delete/h/:hash", post(delete_script_by_hash)) .route("/get/h/:hash", get(get_script_by_hash)) @@ -263,6 +264,7 @@ async fn list_hub_scripts( Authed { email, username, .. }: Authed, + Host(host): Host, ) -> JsonResult> { let http_client = reqwest::ClientBuilder::new() .user_agent("windmill/beta") @@ -272,6 +274,7 @@ async fn list_hub_scripts( .get("https://hub.windmill.dev/searchData?approved=true") .header("X-email", email.unwrap_or_else(|| "".to_string())) .header("X-username", username) + .header("X-hostname", host) .send() .await .map_err(to_anyhow)? @@ -540,6 +543,24 @@ async fn get_script_by_path( Ok(Json(script)) } +async fn exists_script_by_path( + Extension(db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult { + let path = path.to_path(); + + let exists = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') AND + created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')))", + path, w_id + ) + .fetch_one(&db) + .await? + .unwrap_or(false); + + Ok(Json(exists)) +} + async fn get_script_by_hash_internal<'c>( db: &mut Transaction<'c, Postgres>, workspace_id: &str, diff --git a/backend/src/variables.rs b/backend/src/variables.rs index 6ee61e0201..7f21e3f413 100644 --- a/backend/src/variables.rs +++ b/backend/src/variables.rs @@ -32,6 +32,7 @@ pub fn workspaced_service() -> Router { .route("/list", get(list_variables)) .route("/list_contextual", get(list_contextual_variables)) .route("/get/*path", get(get_variable)) + .route("/exists/*path", get(exists_variable)) .route("/update/*path", post(update_variable)) .route("/delete/*path", delete(delete_variable)) .route("/create", post(create_variable)) @@ -254,6 +255,24 @@ async fn get_variable( Ok(Json(r)) } +async fn exists_variable( + Extension(db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult { + let path = path.to_path(); + + let exists = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM variable WHERE path = $1 AND workspace_id = $2)", + path, + w_id + ) + .fetch_one(&db) + .await? + .unwrap_or(false); + + Ok(Json(exists)) +} + async fn create_variable( authed: Authed, Extension(user_db): Extension, diff --git a/backend/src/workspaces.rs b/backend/src/workspaces.rs index 504dc041b6..d4afb1db87 100644 --- a/backend/src/workspaces.rs +++ b/backend/src/workspaces.rs @@ -41,10 +41,7 @@ pub fn global_service() -> Router { .route("/users", get(user_workspaces)) .route("/create", post(create_workspace)) .route("/exists", post(exists_workspace)) - .route("/validate_username", post(validate_username)) - .route("/validate_id", post(validate_id)) - - + .route("/exists_username", post(exists_username)) } #[derive(FromRow, Serialize)] @@ -485,7 +482,7 @@ async fn delete_invite( )) } -async fn validate_username( +async fn exists_username( Extension(db): Extension, Json(vu): Json, ) -> Result { @@ -506,27 +503,6 @@ async fn validate_username( Ok("valid username".to_string()) } - -async fn validate_id( - Extension(db): Extension, - Json(wi): Json, -) -> Result { - - let exists = sqlx::query_scalar!( - "SELECT EXISTS(SELECT 1 FROM workspace WHERE id = $1)", - wi.id - ) - .fetch_one(&db) - .await? - .unwrap_or(true); - - if exists { - return Err(Error::BadRequest("id already taken".to_string())) - } - - Ok("valid workspace".to_string()) -} - #[derive(Serialize)] struct ScriptMetadata { summary: String, diff --git a/frontend/src/lib/components/AppConnect.svelte b/frontend/src/lib/components/AppConnect.svelte index db75a60108..cd9508cf78 100644 --- a/frontend/src/lib/components/AppConnect.svelte +++ b/frontend/src/lib/components/AppConnect.svelte @@ -36,6 +36,9 @@ let step = 1 let no_back = false + + let pathError = '' + export function open() { step = 1 value = '' @@ -72,27 +75,18 @@ } else if (step == 1 && !manual) { window.location.href = `/api/oauth/connect/${resource_type}?scopes=${scopes.join('+')}` } else { - let exists = true - try { - await VariableService.getVariable({ - workspace: $workspaceStore!, - path - }) - } catch (e) { - exists = false - } + let exists = await VariableService.existsVariable({ + workspace: $workspaceStore!, + path + }) if (exists) { throw Error(`Variable at path ${path} already exists. Delete it or pick another path`) } - exists = true - try { - await ResourceService.getResource({ - workspace: $workspaceStore!, - path - }) - } catch (e) { - exists = false - } + exists = await ResourceService.existsResource({ + workspace: $workspaceStore!, + path + }) + if (exists) { throw Error(`Resource at path ${path} already exists. Delete it or pick another path`) } @@ -235,7 +229,12 @@ {/if} {:else} - +
  • 1. A secret variable containing the token {step == 3 ? 'Connect' : 'Next'} diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 609f0a9ddf..227cbffa74 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -3,7 +3,7 @@ import { page } from '$app/stores' import { FlowService, ScriptService, type Flow } from '$lib/gen' import { clearPreviewResults, hubScripts, workspaceStore } from '$lib/stores' - import { sendUserToast } from '$lib/utils' + import { pathIsEmpty, sendUserToast } from '$lib/utils' import { onMount } from 'svelte' import SvelteMarkdown from 'svelte-markdown' import FlowEditor from './FlowEditor.svelte' @@ -18,6 +18,8 @@ let mode: FlowMode + let pathError = '' + $: step = Number($page.url.searchParams.get('step')) || 1 async function loadSearchData() { @@ -92,6 +94,7 @@ }}>Step 1: Metadata