From 40183ce4e42f648d9eb6e2765fb141e16eba908e Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Thu, 10 Nov 2022 01:58:43 +0100 Subject: [PATCH] feat: deprecate previous_result in favor of results per id --- backend/Cargo.lock | 2 + .../parsers/windmill-parser-bash/Cargo.toml | 3 +- backend/parsers/windmill-parser-py/Cargo.toml | 1 + backend/parsers/windmill-parser-py/src/lib.rs | 8 +- backend/sqlx-data.json | 21 ++ backend/tests/worker.rs | 133 +++++----- backend/windmill-common/src/flow_status.rs | 21 ++ backend/windmill-queue/src/jobs.rs | 42 +++- backend/windmill-worker/Cargo.toml | 2 +- backend/windmill-worker/src/js_eval.rs | 100 ++++---- backend/windmill-worker/src/worker_flow.rs | 92 ++----- frontend/src/lib/components/ArgInput.svelte | 1 - .../lib/components/FlowPreviewContent.svelte | 4 +- frontend/src/lib/components/FlowViewer.svelte | 2 +- .../src/lib/components/TestJobLoader.svelte | 3 - .../flows/content/DynamicInputHelpBox.svelte | 8 +- .../flows/content/FlowBranchOneWrapper.svelte | 6 +- .../flows/content/FlowBranchesWrapper.svelte | 6 +- .../components/flows/content/FlowInput.svelte | 2 +- .../flows/content/FlowInputs.svelte | 8 +- .../components/flows/content/FlowLoop.svelte | 10 +- .../flows/content/FlowModuleComponent.svelte | 25 +- .../flows/content/FlowModuleEarlyStop.svelte | 30 +-- .../flows/content/FlowModuleSuspend.svelte | 9 +- .../flows/content/FlowModuleWrapper.svelte | 2 +- .../src/lib/components/flows/flowStore.ts | 24 +- .../flows/map/FlowBranchAllMap.svelte | 8 +- .../flows/map/FlowBranchOneMap.svelte | 20 +- .../flows/map/FlowInputsItem.svelte | 2 +- .../lib/components/flows/previousResults.ts | 111 +++++---- .../flows/propPicker/PropPickerWrapper.svelte | 35 ++- frontend/src/lib/components/flows/utils.ts | 8 +- .../propertyPicker/ObjectViewer.svelte | 6 +- .../propertyPicker/PropPicker.svelte | 233 ++++++++++-------- .../propertyPicker/PropPickerResult.svelte | 16 ++ .../lib/components/propertyPicker/utils.ts | 7 +- frontend/src/lib/utils.ts | 61 ----- .../src/routes/flows/get/[...path].svelte | 2 +- 38 files changed, 547 insertions(+), 527 deletions(-) create mode 100644 frontend/src/lib/components/propertyPicker/PropPickerResult.svelte diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 07631b1a0e..24e5f1edda 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -4159,6 +4159,7 @@ version = "1.45.0" dependencies = [ "anyhow", "itertools", + "lazy_static", "phf 0.11.1", "regex", "unicode-general-category", @@ -4184,6 +4185,7 @@ version = "1.45.0" dependencies = [ "anyhow", "itertools", + "lazy_static", "phf 0.11.1", "regex", "rustpython-parser", diff --git a/backend/parsers/windmill-parser-bash/Cargo.toml b/backend/parsers/windmill-parser-bash/Cargo.toml index 2915529014..0ae341ef8b 100644 --- a/backend/parsers/windmill-parser-bash/Cargo.toml +++ b/backend/parsers/windmill-parser-bash/Cargo.toml @@ -15,4 +15,5 @@ phf.workspace = true unicode-general-category.workspace = true itertools.workspace = true anyhow.workspace = true -regex.workspace = true \ No newline at end of file +regex.workspace = true +lazy_static.workspace = true \ No newline at end of file diff --git a/backend/parsers/windmill-parser-py/Cargo.toml b/backend/parsers/windmill-parser-py/Cargo.toml index 996fc4a17c..6476e27282 100644 --- a/backend/parsers/windmill-parser-py/Cargo.toml +++ b/backend/parsers/windmill-parser-py/Cargo.toml @@ -17,3 +17,4 @@ itertools.workspace = true regex.workspace = true serde_json.workspace = true anyhow.workspace = true +lazy_static.workspace = true \ No newline at end of file diff --git a/backend/parsers/windmill-parser-py/src/lib.rs b/backend/parsers/windmill-parser-py/src/lib.rs index 3b413255ee..cad7da63d5 100644 --- a/backend/parsers/windmill-parser-py/src/lib.rs +++ b/backend/parsers/windmill-parser-py/src/lib.rs @@ -9,6 +9,7 @@ use std::collections::HashMap; use itertools::Itertools; +use lazy_static::lazy_static; use phf::phf_map; use regex::Regex; @@ -189,17 +190,20 @@ fn replace_import(x: String) -> String { .to_string() } +lazy_static! { + static ref RE: Regex = Regex::new(r"^\#(\S+)$").unwrap(); +} + pub fn parse_python_imports(code: &str) -> error::Result> { let find_requirements = code .lines() .find_position(|x| x.starts_with("#requirements:")); - let re = Regex::new(r"^\#(\S+)$").unwrap(); if let Some((pos, _)) = find_requirements { let lines = code .lines() .skip(pos + 1) .map_while(|x| { - re.captures(x) + RE.captures(x) .map(|x| x.get(1).unwrap().as_str().to_string()) }) .collect(); diff --git a/backend/sqlx-data.json b/backend/sqlx-data.json index df8e6e447d..0c593db015 100644 --- a/backend/sqlx-data.json +++ b/backend/sqlx-data.json @@ -2414,6 +2414,27 @@ }, "query": "DELETE FROM token WHERE token = $1 RETURNING email" }, + "a227548b6604c56bfc15eb780bd8ee72a89dc6701a50f5048e928bd87baa7b9a": { + "describe": { + "columns": [ + { + "name": "result", + "ordinal": 0, + "type_info": "Jsonb" + } + ], + "nullable": [ + true + ], + "parameters": { + "Left": [ + "UuidArray", + "Text" + ] + } + }, + "query": "SELECT result FROM completed_job WHERE id = ANY($1) AND workspace_id = $2" + }, "a241c56415759105ccbcbf7fff77287fa4ec2cc096c0060d14db421115d63e2d": { "describe": { "columns": [ diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index 706f84bff8..981dd99d88 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -162,6 +162,7 @@ mod suspend_resume { fn flow() -> FlowValue { serde_json::from_value(serde_json::json!({ "modules": [{ + "id": "a", "input_transform": { "n": { "type": "javascript", "expr": "flow_input.n", }, "port": { "type": "javascript", "expr": "flow_input.port", }, @@ -200,8 +201,9 @@ mod suspend_resume { "required_events": 1 }, }, { + "id": "b", "input_transform": { - "n": { "type": "javascript", "expr": "previous_result", }, + "n": { "type": "javascript", "expr": "results.a", }, "resume": { "type": "javascript", "expr": "resume", }, "resumes": { "type": "javascript", "expr": "resumes", }, }, @@ -215,7 +217,7 @@ mod suspend_resume { }, }, { "input_transform": { - "last": { "type": "javascript", "expr": "previous_result", }, + "last": { "type": "javascript", "expr": "results.b", }, "resume": { "type": "javascript", "expr": "resume", }, "resumes": { "type": "javascript", "expr": "resumes", }, }, @@ -478,13 +480,14 @@ def main(last, port): fn flow_forloop_retry() -> FlowValue { serde_json::from_value(serde_json::json!({ "modules": [{ + "id": "a", "value": { "type": "forloopflow", - "iterator": { "type": "javascript", "expr": "result.items" }, + "iterator": { "type": "javascript", "expr": "flow_input.items" }, "skip_failures": false, "modules": [{ "input_transform": { - "index": { "type": "javascript", "expr": "previous_result.iter.index" }, + "index": { "type": "javascript", "expr": "flow_input.iter.index" }, "port": { "type": "javascript", "expr": "flow_input.port" }, }, "value": { @@ -497,7 +500,7 @@ def main(last, port): "retry": { "constant": { "attempts": 2, "seconds": 0 } }, }, { "input_transform": { - "last": { "type": "javascript", "expr": "previous_result" }, + "last": { "type": "javascript", "expr": "results.a" }, "port": { "type": "javascript", "expr": "flow_input.port" }, }, "value": { @@ -603,14 +606,13 @@ def main(last, port): .into_iter() .unzip::<_, _, Vec<_>, Vec<_>>(); let server = Server::start(responses).await; - let result = RunJob::from(JobPayload::RawFlow { value: flow_forloop_retry(), path: None }) + let job = RunJob::from(JobPayload::RawFlow { value: flow_forloop_retry(), path: None }) .arg("items", json!(["unused", "unused", "unused"])) .arg("port", json!(server.addr.port())) .run_until_complete(&db, server.addr.port()) - .await - .result - .unwrap(); + .await; + let result = job.result.unwrap(); assert_eq!(server.close().await, attempts); assert!(result["error"] .as_str() @@ -706,7 +708,7 @@ async fn test_iteration(db: Pool) { "input_transform": { "n": { "type": "javascript", - "expr": "previous_result.iter.value", + "expr": "flow_input.iter.value", }, }, "value": { @@ -759,7 +761,7 @@ async fn test_iteration_parallel(db: Pool) { "input_transform": { "n": { "type": "javascript", - "expr": "previous_result.iter.value", + "expr": "flow_input.iter.value", }, }, "value": { @@ -1027,7 +1029,7 @@ async fn test_deno_flow(db: Pool) { input_transforms: [( "n".to_string(), InputTransform::Javascript { - expr: "previous_result.iter.value".to_string(), + expr: "flow_input.iter.value".to_string(), }, )] .into(), @@ -1120,7 +1122,7 @@ async fn test_deno_flow_same_worker(db: Pool) { ( "i".to_string(), InputTransform::Javascript { - expr: "previous_result.iter.value".to_string(), + expr: "flow_input.iter.value".to_string(), }, ), ( @@ -1186,7 +1188,7 @@ async fn test_deno_flow_same_worker(db: Pool) { input_transforms: [ ( "loops".to_string(), - InputTransform::Javascript { expr: "previous_result".to_string() }, + InputTransform::Javascript { expr: "results.b".to_string() }, ), ( "path".to_string(), @@ -1255,7 +1257,7 @@ async fn test_flow_result_by_id(db: Pool) { "branches": [{"modules": [ { "id": "d", "value": { - "input_transforms": {"v": {"type": "javascript", "expr": "result_by_id(\"a\")"}}, + "input_transforms": {"v": {"type": "javascript", "expr": "results.a"}}, "type": "rawscript", "language": "deno", "content": "export function main(v){ return v }", @@ -1306,7 +1308,7 @@ async fn test_stop_after_if(db: Pool) { { "id": "b", "value": { - "input_transforms": { "n": { "type": "javascript", "expr": "previous_result" } }, + "input_transforms": { "n": { "type": "javascript", "expr": "results.a" } }, "type": "rawscript", "language": "python3", "content": "def main(n): return f'last step saw {n}'", @@ -1364,7 +1366,7 @@ async fn test_stop_after_if_nested(db: Pool) { { "id": "c", "value": { - "input_transforms": { "n": { "type": "javascript", "expr": "previous_result" } }, + "input_transforms": { "n": { "type": "javascript", "expr": "results.a" } }, "type": "rawscript", "language": "python3", "content": "def main(n): return f'last step saw {n}'", @@ -1425,7 +1427,7 @@ async fn test_python_flow(db: Pool) { "input_transform": { "n": { "type": "javascript", - "expr": "previous_result.iter.value", + "expr": "flow_input.iter.value", }, }, }], @@ -1435,7 +1437,7 @@ async fn test_python_flow(db: Pool) { })) .unwrap(); - for i in 0..50 { + for i in 0..10 { println!("python flow iteration: {}", i); let result = run_job_in_new_worker_until_complete( &db, @@ -1619,6 +1621,7 @@ async fn test_empty_loop(db: Pool) { let flow: FlowValue = serde_json::from_value(serde_json::json!({ "modules": [ { + "id": "a", "value": { "type": "forloopflow", "iterator": { "type": "static", "value": [] }, @@ -1628,7 +1631,7 @@ async fn test_empty_loop(db: Pool) { "input_transform": { "n": { "type": "javascript", - "expr": "previous_result.iter.value", + "expr": "flow_input.iter.value", }, }, "type": "rawscript", @@ -1644,7 +1647,7 @@ async fn test_empty_loop(db: Pool) { "input_transform": { "items": { "type": "javascript", - "expr": "previous_result", + "expr": "results.a", }, }, "type": "rawscript", @@ -1721,7 +1724,7 @@ async fn test_empty_loop_2(db: Pool) { "input_transform": { "n": { "type": "javascript", - "expr": "previous_result.iter.value", + "expr": "flow_input.iter.value", }, }, "value": { @@ -1754,6 +1757,7 @@ async fn test_step_after_loop(db: Pool) { let flow: FlowValue = serde_json::from_value(serde_json::json!({ "modules": [ { + "id": "a", "value": { "type": "forloopflow", "iterator": { "type": "static", "value": [2,3,4] }, @@ -1762,7 +1766,7 @@ async fn test_step_after_loop(db: Pool) { "input_transform": { "n": { "type": "javascript", - "expr": "previous_result.iter.value", + "expr": "flow_input.iter.value", }, }, "value": { @@ -1778,7 +1782,7 @@ async fn test_step_after_loop(db: Pool) { "input_transform": { "items": { "type": "javascript", - "expr": "previous_result", + "expr": "results.a", }, }, "value": { @@ -1800,12 +1804,13 @@ async fn test_step_after_loop(db: Pool) { assert_eq!(result, serde_json::json!(9)); } -fn module_add_item_to_list(i: i32) -> serde_json::Value { +fn module_add_item_to_list(i: i32, id: &str) -> serde_json::Value { json!({ + "id": format!("id_{}", i.to_string().replace("-", "_")), "input_transform": { "array": { "type": "javascript", - "expr": "previous_result", + "expr": format!("results.{id}"), }, "i": { "type": "static", @@ -1840,6 +1845,7 @@ async fn test_branchone_simple(db: Pool) { let flow: FlowValue = serde_json::from_value(json!({ "modules": [ { + "id": "a", "value": { "type": "rawscript", "language": "deno", @@ -1849,7 +1855,7 @@ async fn test_branchone_simple(db: Pool) { { "value": { "branches": [], - "default": [module_add_item_to_list(2)], + "default": [module_add_item_to_list(2, "a")], "type": "branchone", } }, @@ -1884,8 +1890,8 @@ async fn test_branchone_with_cond(db: Pool) { }, { "value": { - "branches": [{"expr": "previous_result[0] == 1 && result_by_id(\"a\")[0] == 1", "modules": [module_add_item_to_list(3)]}], - "default": [module_add_item_to_list(2)], + "branches": [{"expr": "results.a[0] == 1", "modules": [module_add_item_to_list(3, "a")]}], + "default": [module_add_item_to_list(2, "a")], "type": "branchone", } }, @@ -1911,6 +1917,7 @@ async fn test_branchall_sequential(db: Pool) { let flow: FlowValue = serde_json::from_value(json!({ "modules": [ { + "id": "a", "value": { "type": "rawscript", "language": "deno", @@ -1920,8 +1927,8 @@ async fn test_branchall_sequential(db: Pool) { { "value": { "branches": [ - {"modules": [module_add_item_to_list(2)]}, - {"modules": [module_add_item_to_list(3)]}], + {"modules": [module_add_item_to_list(2, "a")]}, + {"modules": [module_add_item_to_list(3, "a")]}], "type": "branchall", "parallel": true, } @@ -1948,6 +1955,7 @@ async fn test_branchall_simple(db: Pool) { let flow: FlowValue = serde_json::from_value(json!({ "modules": [ { + "id": "a", "value": { "type": "rawscript", "language": "deno", @@ -1957,8 +1965,8 @@ async fn test_branchall_simple(db: Pool) { { "value": { "branches": [ - {"modules": [module_add_item_to_list(2)]}, - {"modules": [module_add_item_to_list(3)]}], + {"modules": [module_add_item_to_list(2, "a")]}, + {"modules": [module_add_item_to_list(3, "a")]}], "type": "branchall", } }, @@ -1984,6 +1992,7 @@ async fn test_branchall_skip_failure(db: Pool) { let flow: FlowValue = serde_json::from_value(json!({ "modules": [ { + "id": "a", "value": { "type": "rawscript", "language": "deno", @@ -1994,7 +2003,7 @@ async fn test_branchall_skip_failure(db: Pool) { "value": { "branches": [ {"modules": [module_failure()], "skip_failure": false}, - {"modules": [module_add_item_to_list(3)]}], + {"modules": [module_add_item_to_list(3, "a")]}], "type": "branchall", } }, @@ -2016,6 +2025,7 @@ async fn test_branchall_skip_failure(db: Pool) { let flow: FlowValue = serde_json::from_value(json!({ "modules": [ { + "id": "a", "value": { "type": "rawscript", "language": "deno", @@ -2026,7 +2036,7 @@ async fn test_branchall_skip_failure(db: Pool) { "value": { "branches": [ {"modules": [module_failure()], "skip_failure": true}, - {"modules": [module_add_item_to_list(2)]} + {"modules": [module_add_item_to_list(2, "a")]} ], "type": "branchall", } @@ -2056,14 +2066,16 @@ async fn test_branchone_nested(db: Pool) { let flow: FlowValue = serde_json::from_value(json!({ "modules": [ { + "id": "a", "value": { "type": "rawscript", "language": "deno", "content": "export function main(){ return [] }", } }, - module_add_item_to_list(1), + module_add_item_to_list(1, "a"), { + "id": "b", "value": { "branches": [ { @@ -2079,17 +2091,17 @@ async fn test_branchone_nested(db: Pool) { "expr": "false", "modules": [] }], - "default": [module_add_item_to_list(2)], + "default": [module_add_item_to_list(2, "id_1")], "type": "branchone", } }] }, ], - "default": [module_add_item_to_list(-4)], + "default": [module_add_item_to_list(-4, "id_1")], "type": "branchone", } }, - module_add_item_to_list(3), + module_add_item_to_list(3, "b"), ], })) .unwrap(); @@ -2112,6 +2124,7 @@ async fn test_branchall_nested(db: Pool) { let flow: FlowValue = serde_json::from_value(json!({ "modules": [ { + "id": "a", "value": { "type": "rawscript", "language": "deno", @@ -2122,24 +2135,27 @@ async fn test_branchall_nested(db: Pool) { "value": { "branches": [ { - "modules": [ { + "modules": [ + { + "id": "b", "value": { "branches": [ - {"modules": [module_add_item_to_list(2)]}, - {"modules": [module_add_item_to_list(3)]}], + {"modules": [module_add_item_to_list(2, "a")]}, + {"modules": [module_add_item_to_list(3, "a")]}], "type": "branchall", } }, { "value": { "branches": [ - {"modules": [module_add_item_to_list(4)]}, - {"modules": [module_add_item_to_list(5)]}], + {"modules": [module_add_item_to_list(4, "b")]}, + {"modules": [module_add_item_to_list(5, "b")]}], "type": "branchall", } } ] }, - {"modules": [module_add_item_to_list(6)]}], + {"modules": [module_add_item_to_list(6, "a")]}], + // "parallel": false, "type": "branchall", } }, @@ -2153,6 +2169,7 @@ async fn test_branchall_nested(db: Pool) { .result .unwrap(); + println!("{:#?}", result); assert_eq!( result, serde_json::json!([[[[1, 2], [1, 3], 4], [[1, 2], [1, 3], 5]], [1, 6]]) @@ -2167,31 +2184,33 @@ async fn test_failure_module(db: Pool) { let flow: FlowValue = serde_json::from_value(serde_json::json!({ "modules": [{ - "input_transform": { - "l": { "type": "javascript", "expr": "[]", }, - "n": { "type": "javascript", "expr": "flow_input.n", }, - }, + "id": "a", "value": { + "input_transform": { + "l": { "type": "javascript", "expr": "[]", }, + "n": { "type": "javascript", "expr": "flow_input.n", }, + }, "type": "rawscript", "language": "deno", "content": "export function main(n, l) { if (n == 0) throw l; return { l: [...l, 0] } }", }, }, { - "input_transform": { - "l": { "type": "javascript", "expr": "previous_result.l", }, - "n": { "type": "javascript", "expr": "flow_input.n", }, - }, + "id": "b", "value": { + "input_transform": { + "l": { "type": "javascript", "expr": "results.a.l", }, + "n": { "type": "javascript", "expr": "flow_input.n", }, + }, "type": "rawscript", "language": "deno", "content": "export function main(n, l) { if (n == 1) throw l; return { l: [...l, 1] } }", }, }, { - "input_transform": { - "l": { "type": "javascript", "expr": "previous_result.l", }, - "n": { "type": "javascript", "expr": "flow_input.n", }, - }, "value": { + "input_transform": { + "l": { "type": "javascript", "expr": "results.b.l", }, + "n": { "type": "javascript", "expr": "flow_input.n", }, + }, "type": "rawscript", "language": "deno", "content": "export function main(n, l) { if (n == 2) throw l; return { l: [...l, 2] } }", diff --git a/backend/windmill-common/src/flow_status.rs b/backend/windmill-common/src/flow_status.rs index 507420bd5c..b20951aa45 100644 --- a/backend/windmill-common/src/flow_status.rs +++ b/backend/windmill-common/src/flow_status.rs @@ -117,6 +117,12 @@ pub enum FlowStatusModule { }, } +#[derive(Debug, Clone)] +pub enum JobResult { + SingleJob(Uuid), + ListJob(Vec), +} + impl FlowStatusModule { pub fn job(&self) -> Option { match self { @@ -129,6 +135,21 @@ impl FlowStatusModule { } } + pub fn flow_jobs(&self) -> Option> { + match self { + FlowStatusModule::InProgress { flow_jobs, .. } => flow_jobs.clone(), + FlowStatusModule::Success { flow_jobs, .. } => flow_jobs.clone(), + FlowStatusModule::Failure { flow_jobs, .. } => flow_jobs.clone(), + _ => None, + } + } + + pub fn job_result(&self) -> Option { + self.flow_jobs() + .map(JobResult::ListJob) + .or_else(|| self.job().map(JobResult::SingleJob)) + } + pub fn id(&self) -> String { match self { FlowStatusModule::WaitingForPriorSteps { id, .. } => id.clone(), diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index 9914e86746..cf716eaca4 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -16,7 +16,7 @@ use uuid::Uuid; use windmill_audit::{audit_log, ActionKind}; use windmill_common::{ error::{self, to_anyhow, Error}, - flow_status::{FlowStatus, MAX_RETRY_ATTEMPTS, MAX_RETRY_INTERVAL}, + flow_status::{FlowStatus, JobResult, MAX_RETRY_ATTEMPTS, MAX_RETRY_INTERVAL}, flows::FlowValue, scripts::{get_full_hub_script_by_path, HubScript, ScriptHash, ScriptLang}, utils::StripPath, @@ -124,7 +124,7 @@ pub async fn get_result_by_id( flow_id: String, node_id: String, ) -> error::Result { - let mut result_id: Option = None; + let mut result_id: Option = None; let mut parent_id = Uuid::from_str(&flow_id).ok(); while result_id.is_none() && parent_id.is_some() { if !skip_direct { @@ -148,7 +148,7 @@ pub async fn get_result_by_id( .modules .iter() .find(|m| m.id() == node_id) - .and_then(|m| m.job()) + .and_then(|m| m.job_result()) }); } else { parent_id = None; @@ -171,15 +171,33 @@ pub async fn get_result_by_id( "Flow result by id", format!("{}, {}", flow_id, node_id), )?; - let value = sqlx::query_scalar!( - "SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2", - result_id, - w_id, - ) - .fetch_optional(&db) - .await? - .flatten() - .unwrap_or(serde_json::Value::Null); + println!("result_id: {:#?}, {node_id}", result_id); + + let value = match result_id { + JobResult::ListJob(x) => { + let rows = sqlx::query_scalar!( + "SELECT result FROM completed_job WHERE id = ANY($1) AND workspace_id = $2", + x.as_slice(), + w_id, + ) + .fetch_all(&db) + .await? + .into_iter() + .filter_map(|x| x) + .collect::>(); + serde_json::json!(rows) + } + JobResult::SingleJob(x) => sqlx::query_scalar!( + "SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2", + x, + w_id, + ) + .fetch_optional(&db) + .await? + .flatten() + .unwrap_or(serde_json::Value::Null), + }; + Ok(value) } diff --git a/backend/windmill-worker/Cargo.toml b/backend/windmill-worker/Cargo.toml index 29d213d8ea..c883ac7e6e 100644 --- a/backend/windmill-worker/Cargo.toml +++ b/backend/windmill-worker/Cargo.toml @@ -44,4 +44,4 @@ lazy_static.workspace = true chrono.workspace = true dotenv.workspace = true rand.workspace = true # TODO: Remove. only used by token creation hack. -deno_core.workspace = true +deno_core.workspace = true \ No newline at end of file diff --git a/backend/windmill-worker/src/js_eval.rs b/backend/windmill-worker/src/js_eval.rs index c64ea5d5e2..3709ef3c22 100644 --- a/backend/windmill-worker/src/js_eval.rs +++ b/backend/windmill-worker/src/js_eval.rs @@ -10,11 +10,12 @@ use std::collections::HashMap; use deno_core::{op, serde_v8, v8, v8::IsolateHandle, Extension, JsRuntime, RuntimeOptions}; use itertools::Itertools; +use lazy_static::lazy_static; use regex::Regex; use serde_json::Value; use tokio::{sync::oneshot, time::timeout}; use uuid::Uuid; -use windmill_common::error::Error; +use windmill_common::{error::Error, flow_status::JobResult}; pub struct EvalCreds { pub workspace: String, @@ -22,13 +23,16 @@ pub struct EvalCreds { } #[derive(Debug, Clone)] -pub struct IdContext(pub Uuid, pub HashMap); +pub struct IdContext { + pub flow_job: Uuid, + pub steps_results: HashMap, + pub previous_id: String, +} pub async fn eval_timeout( expr: String, env: Vec<(String, serde_json::Value)>, creds: Option, - steps: Vec, by_id: Option, base_internal_url: String, ) -> anyhow::Result { @@ -49,12 +53,9 @@ pub async fn eval_timeout( ]) } - if !steps.is_empty() || by_id.is_some() { - ops.push(op_get_result::decl()) - } - if by_id.is_some() { - ops.push(op_get_id::decl()) + ops.push(op_get_result::decl()); + ops.push(op_get_id::decl()); } let ext = Extension::builder().ops(ops).build(); @@ -82,12 +83,13 @@ pub async fn eval_timeout( .into_iter() .fold(expr, replace_with_await); + let expr = replace_with_await_result(expr); + let r = runtime.block_on(eval( &mut js_runtime, &expr, env, creds, - steps, by_id, &base_internal_url, ))?; @@ -115,6 +117,13 @@ fn replace_with_await(expr: String, fn_name: &str) -> String { } s } +lazy_static! { + static ref RE: Regex = Regex::new("(?m)(?Presults.([a-z]|[A-Z]|_|[1-9])+)").unwrap(); +} + +fn replace_with_await_result(expr: String) -> String { + RE.replace_all(&expr, "(await $r)").to_string() +} fn add_closing_bracket(s: &str) -> String { let mut s = s.to_string(); @@ -141,7 +150,6 @@ async fn eval( expr: &str, env: Vec<(String, serde_json::Value)>, creds: Option, - steps: Vec, by_id: Option, base_internal_url: &str, ) -> anyhow::Result { @@ -153,47 +161,52 @@ async fn eval( .join("\n"), expr.split(SPLIT_PAT).last().unwrap_or_else(|| "") ); - let (steps_code, api_code, by_id_code) = if let Some(EvalCreds { workspace, token }) = creds { - let steps_code = if !steps.is_empty() { - format!( - r#" -let steps = [{}]; -async function step(n) {{ - if (n == -1) {{ - return previous_result; - }} - if (n < 0) {{ - let steps_length = steps.length; - n = n % steps.length + steps.length; - }} - let id = steps[n]; - return await Deno.core.opAsync("op_get_result", [workspace, id, token, base_url]); -}}"#, - steps.into_iter().map(|x| format!("\"{x}\"")).join(",") - ) - } else { - String::new() - }; - + let (api_code, by_id_code) = if let Some(EvalCreds { workspace, token }) = creds { let by_id_code = if let Some(by_id) = by_id { format!( r#" async function result_by_id(node_id) {{ let id_map = {{ {} }}; let id = id_map[node_id]; - if (id) {{ - return await Deno.core.opAsync("op_get_result", [workspace, id, token, base_url]); + if (node_id == "{}") {{ + return previous_result; + }} else if (id) {{ + if (Array.isArray(id)) {{ + return await Promise.all(id.map(async (id) => await get_result(id))); + }} else {{ + return await get_result(id); + }} }} else {{ let flow_job_id = "{}"; return await Deno.core.opAsync("op_get_id", [workspace, flow_job_id, token, base_url, node_id]); }} -}}"#, +}} + +async function get_result(id) {{ + return await Deno.core.opAsync("op_get_result", [workspace, id, token, base_url]); +}} +const results = new Proxy({{}}, {{ + get: function(target, name, receiver) {{ + return result_by_id(name); + }} +}}); + +"#, by_id - .1 + .steps_results .into_iter() - .map(|(k, v)| format!("\"{k}\": \"{v}\"")) + .map(|(k, v)| { + let v_str = match v { + JobResult::SingleJob(x) => x.to_string(), + JobResult::ListJob(x) => { + format!("[{}]", x.iter().map(|x| x.to_string()).join(",")) + } + }; + format!("\"{k}\": \"{v_str}\"") + }) .join(","), - by_id.0, + by_id.previous_id, + by_id.flow_job, ) } else { String::new() @@ -213,16 +226,15 @@ async function resource(path) {{ "#, base_internal_url, ); - (steps_code, api_code, by_id_code) + (api_code, by_id_code) } else { - (String::new(), String::new(), String::new()) + (String::new(), String::new()) }; let code = format!( r#" {api_code} {} -{steps_code} {by_id_code} (async () => {{ {expr} @@ -334,7 +346,7 @@ mod tests { let code = "value.test + params.test"; let mut runtime = JsRuntime::new(RuntimeOptions::default()); - let res = eval(&mut runtime, code, env, None, vec![], None, "").await?; + let res = eval(&mut runtime, code, env, None, None, "").await?; assert_eq!(res, json!(4)); Ok(()) } @@ -347,7 +359,7 @@ mod tests { multiline template`"; let mut runtime = JsRuntime::new(RuntimeOptions::default()); - let res = eval(&mut runtime, code, env, None, vec![], None, "").await?; + let res = eval(&mut runtime, code, env, None, None, "").await?; assert_eq!(res, json!("my 5\nmultiline template")); Ok(()) } @@ -360,7 +372,7 @@ multiline template`"; ]; let code = r#"params.test"#; - let res = eval_timeout(code.to_string(), env, None, vec![], None, "".to_string()).await?; + let res = eval_timeout(code.to_string(), env, None, None, "".to_string()).await?; assert_eq!(res, json!(2)); Ok(()) } diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index cd83d7dda9..08bff0e28c 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -18,7 +18,7 @@ use serde_json::{json, Map, Value}; use tokio::sync::mpsc::Sender; use tracing::instrument; use uuid::Uuid; -use windmill_common::flow_status::Iterator; +use windmill_common::flow_status::{Iterator, JobResult}; use windmill_common::{ error::{self, to_anyhow, Error}, flow_status::{ @@ -77,6 +77,7 @@ pub async fn update_flow_status_after_job_completion( })?; let module_index = usize::try_from(old_status.step).ok(); + let module_status = module_index .and_then(|i| old_status.modules.get(i)) .unwrap_or(&old_status.failure_module); @@ -578,7 +579,6 @@ async fn compute_bool_from_expr( ] .into(), creds, - vec![], by_id, base_internal_url.to_string(), ) @@ -657,7 +657,6 @@ async fn transform_input( input_transforms: &HashMap, workspace: &str, token: &str, - steps: Vec, resumes: &[Value], approvers: Vec, by_id: &IdContext, @@ -676,7 +675,7 @@ async fn transform_input( InputTransform::Static { value: _ } => (), InputTransform::Javascript { expr } => { let flow_input = flow_args.clone().unwrap_or_else(|| json!({})); - let previous_result = flatten_previous_result(last_result.clone()); + let previous_result = last_result.clone(); let context = vec![ ("params".to_string(), json!(mapped)), ("previous_result".to_string(), previous_result), @@ -693,7 +692,6 @@ async fn transform_input( expr.to_string(), context, Some(EvalCreds { workspace: workspace.to_string(), token: token.to_string() }), - steps.clone(), Some(by_id.clone()), base_internal_url.to_string(), ) @@ -712,24 +710,6 @@ async fn transform_input( Ok(mapped) } -fn flatten_previous_result(last_result: serde_json::Value) -> serde_json::Value { - if last_result.is_object() - && last_result - .as_object() - .unwrap() - .contains_key("previous_result") - { - last_result - .as_object() - .unwrap() - .get("previous_result") - .unwrap() - .clone() - } else { - last_result.clone() - } -} - #[instrument(level = "trace", skip_all)] pub async fn handle_flow( flow_job: &QueuedJob, @@ -820,6 +800,12 @@ async fn push_next_flow_job( .or_else(|| flow.failure_module.as_ref()) .with_context(|| format!("no module at index {}", status.step))?; + let previous_id = if i >= 1 { + flow.modules.get(i - 1).map(|m| m.id.clone()).unwrap() + } else { + String::new() + }; + // calculate sleep if any let mut scheduled_for_o = { let sleep_input_transform = i @@ -834,7 +820,6 @@ async fn push_next_flow_job( expr.to_string(), [("result".to_string(), last_result.clone())].into(), None, - vec![], None, "".to_string(), ) @@ -1083,9 +1068,9 @@ async fn push_next_flow_job( let mut args = match &module.value { FlowModuleValue::Script { input_transforms, .. } | FlowModuleValue::RawScript { input_transforms, .. } => { - let ctx = get_transform_context(db, &flow_job, &status, &flow.modules).await?; + let ctx = get_transform_context(db, &flow_job, previous_id.clone(), &status).await?; transform_context = Some(ctx); - let (token, steps, by_id) = transform_context.as_ref().unwrap(); + let (token, by_id) = transform_context.as_ref().unwrap(); transform_input( &flow_job.args, last_result.clone(), @@ -1096,7 +1081,6 @@ async fn push_next_flow_job( }, &flow_job.workspace_id, &token, - steps.to_vec(), resume_messages.as_slice(), approvers, by_id, @@ -1140,6 +1124,7 @@ async fn push_next_flow_job( &status_module, last_result.clone(), base_internal_url, + previous_id, ) .await?; tx.commit().await?; @@ -1177,31 +1162,12 @@ async fn push_next_flow_job( args.extend(new_args.clone()); vec![args] } - NextStatus::BranchChosen(_) => { - args.insert( - "previous_result".to_string(), - flatten_previous_result(last_result), - ); - vec![args] - } - NextStatus::NextBranchStep(NextBranch { status, .. }) => { - args.insert( - "previous_result".to_string(), - flatten_previous_result(status.previous_result.clone()), - ); - vec![args] - } + NextStatus::AllFlowJobs { branchall: Some(BranchAllStatus { len, .. }), iterator: None, .. - } => { - args.insert( - "previous_result".to_string(), - flatten_previous_result(last_result), - ); - (0..*len).map(|_| args.clone()).collect() - } + } => (0..*len).map(|_| args.clone()).collect(), NextStatus::AllFlowJobs { branchall: None, iterator: Some(Iterator { itered, .. }), @@ -1459,7 +1425,7 @@ async fn script_path_to_payload<'c>( Ok(job_payload) } -type TransformContext = (String, Vec, IdContext); +type TransformContext = (String, IdContext); async fn compute_next_flow_transform<'c>( flow_job: &QueuedJob, @@ -1472,6 +1438,7 @@ async fn compute_next_flow_transform<'c>( status_module: &FlowStatusModule, last_result: serde_json::Value, base_internal_url: &str, + previous_id: String, ) -> error::Result<(sqlx::Transaction<'c, sqlx::Postgres>, NextFlowTransform)> { match &module.value { FlowModuleValue::Identity => Ok(( @@ -1508,10 +1475,10 @@ async fn compute_next_flow_transform<'c>( let next_loop_status = match status_module { FlowStatusModule::WaitingForPriorSteps { .. } => { - let (token, steps, by_id) = if let Some(x) = transform_context { + let (token, by_id) = if let Some(x) = transform_context { x } else { - get_transform_context(db, &flow_job, &status, &flow.modules).await? + get_transform_context(db, &flow_job, previous_id, &status).await? }; let flow_input = flow_job.args.clone().unwrap_or_else(|| json!({})); /* Iterator is an InputTransform, evaluate it into an array. */ @@ -1526,7 +1493,6 @@ async fn compute_next_flow_transform<'c>( }, token, flow_job.workspace_id.clone(), - steps, Some(by_id), base_internal_url, ) @@ -1629,8 +1595,8 @@ async fn compute_next_flow_transform<'c>( let branch = match status_module { FlowStatusModule::WaitingForPriorSteps { .. } => { let mut branch_chosen = BranchChosen::Default; - let (token, _steps, idcontext) = - get_transform_context(db, &flow_job, &status, &flow.modules).await?; + let (token, idcontext) = + get_transform_context(db, &flow_job, previous_id, &status).await?; for (i, b) in branches.iter().enumerate() { let pred = compute_bool_from_expr( b.expr.to_string(), @@ -1787,8 +1753,8 @@ async fn compute_next_flow_transform<'c>( async fn get_transform_context( db: &DB, flow_job: &QueuedJob, + previous_id: String, status: &FlowStatus, - modules: &Vec, ) -> error::Result { let tx = db.begin().await?; let (tx, new_token) = crate::create_token_for_owner( @@ -1803,18 +1769,16 @@ async fn get_transform_context( //we need to commit asap otherwise the token won't be valid for auth to check outside of this transaction //which will happen with client http calls tx.commit().await?; - let new_steps: Vec = status + let steps_results: HashMap = status .modules .iter() - .map(|x| x.job().unwrap_or_default()) - .collect(); - let id_map: HashMap = modules - .iter() - .map(|x| x.id.clone()) - .zip(new_steps.clone()) + .filter_map(|x| x.job_result().map(|y| (x.id(), y))) .collect(); - Ok((new_token, new_steps, IdContext(flow_job.id, id_map))) + Ok(( + new_token, + IdContext { flow_job: flow_job.id, steps_results, previous_id }, + )) } async fn evaluate_with( @@ -1822,7 +1786,6 @@ async fn evaluate_with( vars: F, token: String, workspace: String, - steps: Vec, by_id: Option, base_internal_url: &str, ) -> anyhow::Result @@ -1836,7 +1799,6 @@ where expr, vars(), Some(EvalCreds { workspace, token }), - steps, by_id, base_internal_url.to_string(), ) diff --git a/frontend/src/lib/components/ArgInput.svelte b/frontend/src/lib/components/ArgInput.svelte index 1c13e91605..87af15d6b6 100644 --- a/frontend/src/lib/components/ArgInput.svelte +++ b/frontend/src/lib/components/ArgInput.svelte @@ -346,7 +346,6 @@ placeholder={defaultValue ?? ''} bind:value on:input={() => { - console.log(6, value) dispatch('input', { rawValue: value, isRaw: false }) }} /> diff --git a/frontend/src/lib/components/FlowPreviewContent.svelte b/frontend/src/lib/components/FlowPreviewContent.svelte index e86544dce8..bba27ef351 100644 --- a/frontend/src/lib/components/FlowPreviewContent.svelte +++ b/frontend/src/lib/components/FlowPreviewContent.svelte @@ -5,7 +5,7 @@ import { Button } from './common' import { createEventDispatcher, getContext } from 'svelte' import Icon from 'svelte-awesome' - import { dfs, flowIds, flowStore } from './flows/flowStore' + import { dfs, flowStore } from './flows/flowStore' import type { FlowEditorContext } from './flows/types' import { runFlowPreview } from './flows/utils' import SchemaForm from './SchemaForm.svelte' @@ -53,7 +53,7 @@ return $flowStore } else { const flow: Flow = JSON.parse(JSON.stringify($flowStore)) - const idOrders = dfs(flow.value.modules, true) + const idOrders = dfs(flow.value.modules) let upToIndex = idOrders.indexOf($selectedId) if (upToIndex != -1) { diff --git a/frontend/src/lib/components/FlowViewer.svelte b/frontend/src/lib/components/FlowViewer.svelte index dd38231e71..1f2ee39f8a 100644 --- a/frontend/src/lib/components/FlowViewer.svelte +++ b/frontend/src/lib/components/FlowViewer.svelte @@ -52,7 +52,7 @@ {/if}

- Flow Inputs + Flow Input

{#if flow.schema && flow.schema.properties && Object.keys(flow.schema.properties).length > 0 && flow.schema}
    diff --git a/frontend/src/lib/components/TestJobLoader.svelte b/frontend/src/lib/components/TestJobLoader.svelte index 5cb330397c..4257d5e2ad 100644 --- a/frontend/src/lib/components/TestJobLoader.svelte +++ b/frontend/src/lib/components/TestJobLoader.svelte @@ -94,9 +94,6 @@ job = await JobService.getJob({ workspace: workspace!, id }) } } else { - console.log(workspaceOverride) - console.log(workspace) - job = await JobService.getJob({ workspace: workspace!, id }) } if (job?.type === 'CompletedJob') { diff --git a/frontend/src/lib/components/flows/content/DynamicInputHelpBox.svelte b/frontend/src/lib/components/flows/content/DynamicInputHelpBox.svelte index 1fd9ad213b..0264126219 100644 --- a/frontend/src/lib/components/flows/content/DynamicInputHelpBox.svelte +++ b/frontend/src/lib/components/flows/content/DynamicInputHelpBox.svelte @@ -35,10 +35,12 @@ That snippet can be a single line:
    last_result.myarg
    or a multiline: -
    let x = 5;
     x + 2
    + >

    If it is multiline, the statement before the final expression MUST END WITH ; and a newline

    To re-enable editor assistance, import the helper functions types using:

    - {`import { previous_result, flow_input, step, variable, resource, params } from 'windmill${ + {`import { results, flow_input, variable, resource, params } from 'windmill${ importPath ? `@${importPath}` : '' }'`} diff --git a/frontend/src/lib/components/flows/content/FlowBranchOneWrapper.svelte b/frontend/src/lib/components/flows/content/FlowBranchOneWrapper.svelte index 24bdc8b9e4..b88e7d3355 100644 --- a/frontend/src/lib/components/flows/content/FlowBranchOneWrapper.svelte +++ b/frontend/src/lib/components/flows/content/FlowBranchOneWrapper.svelte @@ -16,7 +16,7 @@ expr: string modules: Array } - export let parentModule: FlowModule | undefined + export let parentModule: FlowModule export let previousModule: FlowModule | undefined const { previewArgs } = getContext('FlowEditorContext') @@ -26,8 +26,10 @@ $flowStateStore, parentModule, previousModule, + parentModule.id, $flowStore, previewArgs, + false, true ).pickableProperties @@ -44,7 +46,7 @@ Branch predicate
    { editor?.insertAtCursor(detail) diff --git a/frontend/src/lib/components/flows/content/FlowBranchesWrapper.svelte b/frontend/src/lib/components/flows/content/FlowBranchesWrapper.svelte index 2f539e687e..c086a5af17 100644 --- a/frontend/src/lib/components/flows/content/FlowBranchesWrapper.svelte +++ b/frontend/src/lib/components/flows/content/FlowBranchesWrapper.svelte @@ -127,11 +127,7 @@
    - +
    diff --git a/frontend/src/lib/components/flows/content/FlowInput.svelte b/frontend/src/lib/components/flows/content/FlowInput.svelte index 436d6a8df9..5f3d02d6c9 100644 --- a/frontend/src/lib/components/flows/content/FlowInput.svelte +++ b/frontend/src/lib/components/flows/content/FlowInput.svelte @@ -12,7 +12,7 @@ - +
    {/each} -
    +
    {/each} -
    +
    - {/if} +
    +
    + Flow Input +
    -
    -
    - -
    -{#if priorId} - Result by id
    { - const [first, ...second] = e.detail.split('.') - dispatch('select', `result_by_id('${first}')${second.length ? '.' + second.join('.') : ''}`) + dispatch('select', `flow_input.${e.detail}`) }} />
    -{/if} -{#if displayContext} - Variables -
    - {#if displayVariable} - + {#if error} + Error +
    dispatch('select', `variable('${e.detail}')`)} + pureViewer={!$propPickerConfig} + json={{ previous_result: { error: 'The error to handle' } }} + on:select /> - {:else} - +
    + {:else} + {#if previousId} + Previous Result +
    + k == previousId) + )} + on:select={(e) => { + dispatch('select', `results.${e.detail}`) + }} + /> +
    {/if} -
    - Resources -
    - {#if displayResources} - - dispatch('select', `resource('${e.detail}')`)} - /> - {:else} - + {#if Object.keys(pickableProperties.priorIds).length > 0} + All Results +
    + { + dispatch('select', `results.${e.detail}`) + }} + /> +
    {/if} -
    -{/if} + {/if} + + {#if displayContext} + Variables +
    + {#if displayVariable} + + dispatch('select', `variable('${e.detail}')`)} + /> + {:else} + + {/if} +
    + Resources +
    + {#if displayResources} + + dispatch('select', `resource('${e.detail}')`)} + /> + {:else} + + {/if} +
    + {/if} +
    diff --git a/frontend/src/lib/components/propertyPicker/PropPickerResult.svelte b/frontend/src/lib/components/propertyPicker/PropPickerResult.svelte new file mode 100644 index 0000000000..36a00b13a1 --- /dev/null +++ b/frontend/src/lib/components/propertyPicker/PropPickerResult.svelte @@ -0,0 +1,16 @@ + + +
    + Result +
    + +
    +
    diff --git a/frontend/src/lib/components/propertyPicker/utils.ts b/frontend/src/lib/components/propertyPicker/utils.ts index e60f49a84f..af0026459d 100644 --- a/frontend/src/lib/components/propertyPicker/utils.ts +++ b/frontend/src/lib/components/propertyPicker/utils.ts @@ -19,7 +19,7 @@ function diff(target: Object, source: Object): Object { const result = {} - Object.keys(target).forEach((key: string) => { + Object.keys(target ?? {}).forEach((key: string) => { if (typeof source[key] === 'object') { const difference = diff(target[key], source[key]) @@ -34,7 +34,10 @@ function diff(target: Object, source: Object): Object { return result } -export function keepByKey(json: Object, key: string): Object { +export function keepByKey(json: Object | undefined, key: string): Object { + if (!json) { + return {} + } return diff(json, filterByKey(json, key)) } diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 9cdf461a34..a9a4d6f471 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -325,44 +325,6 @@ export function mapUserToUserExt(user: User): UserExt { } } -export function buildExtraLib(flowInput: string, previousResultType?: string): string { - return ` -/** -* get variable (including secret) at path -* @param {string} path - path of the variable (e.g: g/all/pretty_secret) -*/ -export function variable(path: string): string; - -/** -* get resource at path -* @param {string} path - path of the resource (e.g: g/all/my_resource) -*/ -export function resource(path: string): any; - -/** -* get result of step n. -* If n is negative, for instance -1, it is the step just before this one. -* Step 0 is flow input. -* @param {number} n - step number. -*/ -export function step(n: number): any; - -/** -* flow input as an object -*/ -export const flow_input: ${flowInput}; - -/** -* previous result as an object -*/ -export const previous_result: ${previousResultType || 'any'}; - -/** -* static params of this same step -*/ -export const params: any;` -} - export function schemaToTsType(schema: Schema): string { if (!schema || !schema.properties) { return 'any' @@ -411,29 +373,6 @@ export function schemaToObject(schema: Schema, args: Record): Objec return object } -export function valueToTsType(value: any): string { - const typeOfValue: string = typeof value - - if (['string', 'number', 'boolean'].includes(typeOfValue)) { - return typeOfValue - } else if (Array.isArray(value)) { - const type = objectToTsType(value[0]) - return `Array<${type}>` - } else if (typeof value === 'object') { - return objectToTsType(value) - } else { - return 'any' - } -} - -export function objectToTsType(object: Object): string { - if (!object) { - return 'any' - } - const propKeys = Object.keys(object) - const types = propKeys.map((key: string) => `${key}: ${valueToTsType(object[key])}`).join(';') - return `{ ${types} }` -} export type InputCat = | 'string' diff --git a/frontend/src/routes/flows/get/[...path].svelte b/frontend/src/routes/flows/get/[...path].svelte index 765676addc..1adc45494f 100644 --- a/frontend/src/routes/flows/get/[...path].svelte +++ b/frontend/src/routes/flows/get/[...path].svelte @@ -222,7 +222,7 @@
    -

    WebhookTo trigger this script with a webhook, do a POST request to the endpoint below. Flows are not public and can only be run by users with at least view rights on them. You