From e0e78442b7661cfaab675554fb68ceefae920331 Mon Sep 17 00:00:00 2001 From: Diego Imbert <70353967+diegoimbert@users.noreply.github.com> Date: Fri, 13 Mar 2026 21:31:25 +0100 Subject: [PATCH] Support T | T[] in debounce (#8340) * Detect union types in TS * display union type arguments * Handle single values at accumulation time * nit propagate otyp * Python support * npm package update --- backend/parsers/windmill-parser-py/src/lib.rs | 87 +++++++++++++++++- backend/parsers/windmill-parser-ts/src/lib.rs | 91 ++++++++++++++++++- .../parsers/windmill-parser-ts/tests/tests.rs | 49 ++++++++++ backend/windmill-queue/src/jobs.rs | 12 ++- cli/src/utils/metadata.ts | 14 ++- frontend/package-lock.json | 72 +++++++++++++-- frontend/package.json | 6 +- .../lib/components/flows/DebounceLimit.svelte | 49 ++++++++-- frontend/src/lib/infer.ts | 25 +++-- 9 files changed, 360 insertions(+), 45 deletions(-) diff --git a/backend/parsers/windmill-parser-py/src/lib.rs b/backend/parsers/windmill-parser-py/src/lib.rs index c6852bd351..97e216acbd 100644 --- a/backend/parsers/windmill-parser-py/src/lib.rs +++ b/backend/parsers/windmill-parser-py/src/lib.rs @@ -25,6 +25,50 @@ pub mod pydantic_parser; const FUNCTION_CALL: &str = ""; +/// Get the simple type name from an expression (e.g. `str`, `int`). +fn simple_type_name(e: &Expr) -> Option<&str> { + match e { + Expr::Name(ExprName { id, .. }) => Some(id.as_ref()), + _ => None, + } +} + +/// If `e` is `list[T]` or `List[T]`, return the inner expression `T`. +fn list_elem_expr(e: &Expr) -> Option<&Expr> { + match e { + Expr::Subscript(x) => match x.value.as_ref() { + Expr::Name(ExprName { id, .. }) if id == "list" || id == "List" => { + Some(x.slice.as_ref()) + } + _ => None, + }, + _ => None, + } +} + +/// Detect `T | list[T]` or `list[T] | T` union patterns. +/// Returns the original type string (e.g. "str | list[str]") for use as `otyp`. +fn detect_py_union_array_otyp(e: &Expr) -> Option { + let Expr::BinOp(x) = e else { return None }; + // T | list[T] + if let (Some(scalar), Some(elem)) = (simple_type_name(&x.left), list_elem_expr(&x.right)) { + if let Some(elem_name) = simple_type_name(elem) { + if scalar == elem_name { + return Some(format!("{} | list[{}]", scalar, elem_name)); + } + } + } + // list[T] | T + if let (Some(elem), Some(scalar)) = (list_elem_expr(&x.left), simple_type_name(&x.right)) { + if let Some(elem_name) = simple_type_name(elem) { + if scalar == elem_name { + return Some(format!("list[{}] | {}", elem_name, scalar)); + } + } + } + None +} + /// Cheap string-based check to see if code might contain Pydantic models or dataclasses. /// Returns true if we should do full AST parsing for type detection, false otherwise. /// This avoids expensive parsing for the common case where scripts don't use these features. @@ -390,8 +434,19 @@ pub fn parse_python_signature( _ => {} } + // Detect T | list[T] union types and set otyp for + // debounce accumulation support. Falls back to docstring + // description if no union array pattern is found. + let union_otyp = params.args[i] + .as_arg() + .annotation + .as_ref() + .and_then(|ann| detect_py_union_array_otyp(ann.as_ref())); + Arg { - otyp: metadata.descriptions.get(&arg_name).map(|d| d.to_string()), + otyp: union_otyp.or_else(|| { + metadata.descriptions.get(&arg_name).map(|d| d.to_string()) + }), name: arg_name, typ, has_default: has_default || default.is_some(), @@ -441,6 +496,9 @@ fn parse_expr( Expr::Constant(ExprConstant { value: Constant::None, .. }) ) { (parse_expr(&x.left, enums, module).0, true) + } else if detect_py_union_array_otyp(e.as_ref()).is_some() { + // T | list[T] — parsed type is Unknown; otyp is set separately + (Typ::Unknown, false) } else { (Typ::Unknown, false) } @@ -1046,6 +1104,33 @@ def main(a: str, b: Optional[str], c: str | None): return Ok(()) } + #[test] + fn test_parse_python_union_array_type() -> anyhow::Result<()> { + let code = r#" +def main(items: str | list[str], numbers: list[int] | int, plain: str): + pass +"#; + let result = parse_python_signature(code, None, false)?; + assert_eq!(result.args.len(), 3); + + // str | list[str] → otyp set, typ Unknown + assert_eq!(result.args[0].name, "items"); + assert_eq!(result.args[0].otyp, Some("str | list[str]".to_string())); + assert_eq!(result.args[0].typ, Typ::Unknown); + + // list[int] | int → otyp set, typ Unknown + assert_eq!(result.args[1].name, "numbers"); + assert_eq!(result.args[1].otyp, Some("list[int] | int".to_string())); + assert_eq!(result.args[1].typ, Typ::Unknown); + + // plain str → no otyp + assert_eq!(result.args[2].name, "plain"); + assert_eq!(result.args[2].otyp, None); + assert_eq!(result.args[2].typ, Typ::Str(None)); + + Ok(()) + } + #[test] fn test_parse_python_sig_enum() -> anyhow::Result<()> { let code = r#" diff --git a/backend/parsers/windmill-parser-ts/src/lib.rs b/backend/parsers/windmill-parser-ts/src/lib.rs index 54e9d465fb..8240d52242 100644 --- a/backend/parsers/windmill-parser-ts/src/lib.rs +++ b/backend/parsers/windmill-parser-ts/src/lib.rs @@ -363,8 +363,12 @@ fn parse_param( let r = match param.pat { Pat::Ident(ident) => { let (name, typ, nullable) = binding_ident_to_arg(symbol_table, type_resolver, &ident); + let otyp = ident + .type_ann + .as_ref() + .and_then(|ta| detect_union_array_otyp(&ta.type_ann)); Ok(Arg { - otyp: None, + otyp, name, typ, default: None, @@ -374,13 +378,21 @@ fn parse_param( } // Pat::Object(ObjectPat { ... }) = todo!() Pat::Assign(AssignPat { left, right, .. }) => { - let (name, mut typ, _nullable) = match *left { - Pat::Ident(ident) => binding_ident_to_arg(symbol_table, type_resolver, &ident), + let (name, mut typ, _nullable, otyp) = match *left { + Pat::Ident(ident) => { + let otyp = ident + .type_ann + .as_ref() + .and_then(|ta| detect_union_array_otyp(&ta.type_ann)); + let (name, typ, nullable) = + binding_ident_to_arg(symbol_table, type_resolver, &ident); + (name, typ, nullable, otyp) + } Pat::Object(ObjectPat { type_ann, .. }) => { let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann); *counter += 1; let name = format!("anon{}", counter); - (name, typ, nullable) + (name, typ, nullable, None) } _ => { return Err(anyhow::anyhow!( @@ -416,7 +428,7 @@ fn parse_param( if typ == Typ::Unknown && dflt.is_some() { typ = json_to_typ(dflt.as_ref().unwrap(), false); } - Ok(Arg { otyp: None, name, typ, default: dflt, has_default: true, oidx: None }) + Ok(Arg { otyp, name, typ, default: dflt, has_default: true, oidx: None }) } Pat::Object(ObjectPat { type_ann, .. }) => { let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann); @@ -961,6 +973,75 @@ fn one_of_properties( .collect() } +fn ts_type_to_string(ts_type: &TsType) -> Option { + match ts_type { + TsType::TsKeywordType(t) => Some( + match t.kind { + TsKeywordTypeKind::TsStringKeyword => "string", + TsKeywordTypeKind::TsNumberKeyword => "number", + TsKeywordTypeKind::TsBooleanKeyword => "boolean", + TsKeywordTypeKind::TsObjectKeyword => "object", + TsKeywordTypeKind::TsBigIntKeyword => "bigint", + TsKeywordTypeKind::TsAnyKeyword => "any", + _ => return None, + } + .to_string(), + ), + TsType::TsTypeRef(TsTypeRef { type_name, .. }) => match type_name { + TsEntityName::Ident(Ident { sym, .. }) => Some(sym.to_string()), + _ => None, + }, + _ => None, + } +} + +fn get_array_elem_type(ts_type: &TsType) -> Option<&TsType> { + match ts_type { + TsType::TsArrayType(TsArrayType { elem_type, .. }) => Some(elem_type), + _ => None, + } +} + +/// Detects union types of the form `T | T[]` or `T[] | T` and returns +/// the original type string (e.g. "string | string[]"). +fn detect_union_array_otyp(ts_type: &TsType) -> Option { + let TsType::TsUnionOrIntersectionType(TsUnionOrIntersectionType::TsUnionType(TsUnionType { + types, + .. + })) = ts_type + else { + return None; + }; + + if types.len() != 2 { + return None; + } + + // Check pattern: T | T[] + if let (Some(scalar_name), Some(array_elem)) = + (ts_type_to_string(&types[0]), get_array_elem_type(&types[1])) + { + if let Some(elem_name) = ts_type_to_string(array_elem) { + if scalar_name == elem_name { + return Some(format!("{} | {}[]", scalar_name, elem_name)); + } + } + } + + // Check pattern: T[] | T + if let (Some(array_elem), Some(scalar_name)) = + (get_array_elem_type(&types[0]), ts_type_to_string(&types[1])) + { + if let Some(elem_name) = ts_type_to_string(array_elem) { + if scalar_name == elem_name { + return Some(format!("{}[] | {}", elem_name, scalar_name)); + } + } + } + + None +} + fn find_undefined(types: &Vec>) -> Option { types.into_iter().position(|x| match **x { TsType::TsKeywordType(TsKeywordType { kind, .. }) => { diff --git a/backend/parsers/windmill-parser-ts/tests/tests.rs b/backend/parsers/windmill-parser-ts/tests/tests.rs index 1d9fbda6ef..9eb1732203 100644 --- a/backend/parsers/windmill-parser-ts/tests/tests.rs +++ b/backend/parsers/windmill-parser-ts/tests/tests.rs @@ -646,6 +646,55 @@ mod tests { ); } + #[test] + fn test_parse_union_array_type() { + let code = r#" + export async function main( + items: string | string[], + numbers: number[] | number, + plain: string + ) { + return { items, numbers, plain }; + } + "#; + let sig = parse_deno_signature(code, false, false, None).unwrap(); + assert_eq!( + sig, + MainArgSignature { + star_args: false, + star_kwargs: false, + args: vec![ + Arg { + name: "items".to_string(), + otyp: Some("string | string[]".to_string()), + typ: Typ::Unknown, + default: None, + has_default: false, + oidx: None, + }, + Arg { + name: "numbers".to_string(), + otyp: Some("number[] | number".to_string()), + typ: Typ::Unknown, + default: None, + has_default: false, + oidx: None, + }, + Arg { + name: "plain".to_string(), + otyp: None, + typ: Typ::Str(None), + default: None, + has_default: false, + oidx: None, + }, + ], + no_main_func: Some(false), + has_preprocessor: Some(false), + } + ); + } + #[test] fn test_parse_invalid_typescript() { let code = r#" diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index 324ea6c0ed..e91bb281e4 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -3030,8 +3030,16 @@ impl PulledJobResult { if let Some(s) = str_o.as_ref() { match serde_json::from_str::>>(s) { Ok(ref mut vec) => accumulated_arg.append(vec), - Err(e) => { - return Err(error::Error::ArgumentErr(format!("cannot consolidate arguments of non-list type. Type provided for argument `{arg_name_to_accumulate}` is not a list\nUnwrapped Error: {e}"))); + Err(_) => { + // Value is not an array — wrap the scalar into a + // single-element array. This supports union types + // like T | T[] where the caller may pass a bare T. + match RawValue::from_string(s.to_string()) { + Ok(raw) => accumulated_arg.push(raw), + Err(e) => { + return Err(error::Error::ArgumentErr(format!("cannot consolidate argument `{arg_name_to_accumulate}`: value is neither a valid list nor a valid JSON value\nUnwrapped Error: {e}"))); + } + } } } } diff --git a/cli/src/utils/metadata.ts b/cli/src/utils/metadata.ts index 76ff33247a..871f71359f 100644 --- a/cli/src/utils/metadata.ts +++ b/cli/src/utils/metadata.ts @@ -50,26 +50,25 @@ export class LockfileGenerationError extends Error { } } -export async function generateAllMetadata() {} export async function getRawWorkspaceDependencies(): Promise> { const rawWorkspaceDeps: Record = {}; - + try { const entries = await readdir("dependencies", { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) continue; - + const filePath = `dependencies/${entry.name}`; const content = await readFile(filePath, "utf-8"); - + // Find matching language for (const lang of workspaceDependenciesLanguages) { if (entry.name.endsWith(lang.filename)) { // Check if out of sync const contentHash = await generateHash(content + filePath); const isUpToDate = await checkifMetadataUptodate(filePath, contentHash, undefined); - + if (!isUpToDate) { rawWorkspaceDeps[filePath] = content; } @@ -691,6 +690,11 @@ export async function inferSchema( argSigToJsonSchemaType(arg.typ, currentSchema.properties[arg.name]); + // For T | T[] detection for debouncing arg accumulation + if ((arg as any).otyp && (arg as any).otyp.includes('[') && (arg as any).otyp.includes('|')) { + currentSchema.properties[arg.name].originalType = (arg as any).otyp + } + currentSchema.properties[arg.name].default = arg.default; if (!arg.has_default && !currentSchema.required.includes(arg.name)) { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6de3434311..591febd797 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -81,11 +81,11 @@ "windmill-parser-wasm-java": "1.510.1", "windmill-parser-wasm-nu": "1.510.1", "windmill-parser-wasm-php": "1.647.1", - "windmill-parser-wasm-py": "1.653.0", + "windmill-parser-wasm-py": "1.655.0", "windmill-parser-wasm-regex": "1.653.0", "windmill-parser-wasm-ruby": "1.526.1", "windmill-parser-wasm-rust": "1.647.1", - "windmill-parser-wasm-ts": "1.653.0", + "windmill-parser-wasm-ts": "1.655.0", "windmill-parser-wasm-yaml": "1.593.0", "windmill-sql-datatype-parser-wasm": "1.512.0", "windmill-utils-internal": "^1.3.4", @@ -160,6 +160,14 @@ "svelte": "^5.0.0" } }, + "../backend/parsers/windmill-parser-wasm/pkg-py": { + "name": "windmill-parser-wasm-py", + "version": "1.655.0" + }, + "../backend/parsers/windmill-parser-wasm/pkg-ts": { + "name": "windmill-parser-wasm-ts", + "version": "1.655.0" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -837,6 +845,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -848,6 +857,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -858,6 +868,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1347,6 +1358,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1503,6 +1515,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1519,6 +1532,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1535,6 +1549,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1551,6 +1566,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1567,6 +1583,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1583,6 +1600,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1599,6 +1617,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1615,6 +1634,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1631,6 +1651,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1647,6 +1668,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1663,6 +1685,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1679,6 +1702,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1695,6 +1719,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1711,6 +1736,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1727,6 +1753,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2032,6 +2059,7 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -6840,7 +6868,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -7339,6 +7367,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7359,6 +7388,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7379,6 +7409,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7399,6 +7430,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7419,6 +7451,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7439,6 +7472,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7459,6 +7493,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7479,6 +7514,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7499,6 +7535,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7519,6 +7556,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7539,6 +7577,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12114,6 +12153,21 @@ } } }, + "node_modules/svelte-check/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/svelte-eslint-parser": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", @@ -12844,7 +12898,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -13625,9 +13679,8 @@ "integrity": "sha512-u2qaMkupSdhJibxvkLh3r/y36IARvnYNTLXWvOKxcQ0G/BPUB4+yF5o/yf47vv9zUV5WZv4mrdsKDt/pZDYeDg==" }, "node_modules/windmill-parser-wasm-py": { - "version": "1.653.0", - "resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.653.0.tgz", - "integrity": "sha512-vMkSL3JpELpag7nmyGA8onhYNiAG3K1mkh2k4vwVHC3W5dUd12fSS9gsBco2FqPGUPnWv1gCaHwmjBrOBVGL1w==" + "resolved": "../backend/parsers/windmill-parser-wasm/pkg-py", + "link": true }, "node_modules/windmill-parser-wasm-regex": { "version": "1.653.0", @@ -13645,9 +13698,8 @@ "integrity": "sha512-9yGLYZX2Hn9TdTqGY/5Fp50ftzgUsrfBkSK9vJkKJd5Amyg+yXLBGzd8pz6Org+4uxMenz/16wpsgijvo6uhhQ==" }, "node_modules/windmill-parser-wasm-ts": { - "version": "1.653.0", - "resolved": "https://registry.npmjs.org/windmill-parser-wasm-ts/-/windmill-parser-wasm-ts-1.653.0.tgz", - "integrity": "sha512-zwBUy7ijo58ooAKcsYflISY/+xllCw3Aq34Kj1PED6uABWVbV6A8MWHqEsjiVzC6iWfihWNtZJpck8zsRr9DCg==" + "resolved": "../backend/parsers/windmill-parser-wasm/pkg-ts", + "link": true }, "node_modules/windmill-parser-wasm-yaml": { "version": "1.593.0", diff --git a/frontend/package.json b/frontend/package.json index d34ea12a90..8c0a27b6bf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -154,11 +154,11 @@ "windmill-parser-wasm-java": "1.510.1", "windmill-parser-wasm-nu": "1.510.1", "windmill-parser-wasm-php": "1.647.1", - "windmill-parser-wasm-py": "1.653.0", + "windmill-parser-wasm-py": "1.655.0", "windmill-parser-wasm-regex": "1.653.0", "windmill-parser-wasm-ruby": "1.526.1", "windmill-parser-wasm-rust": "1.647.1", - "windmill-parser-wasm-ts": "1.653.0", + "windmill-parser-wasm-ts": "1.655.0", "windmill-parser-wasm-yaml": "1.593.0", "windmill-sql-datatype-parser-wasm": "1.512.0", "windmill-utils-internal": "^1.3.4", @@ -583,4 +583,4 @@ "@rollup/rollup-linux-x64-gnu": "^4.35.0", "fsevents": "^2.3.3" } -} +} \ No newline at end of file diff --git a/frontend/src/lib/components/flows/DebounceLimit.svelte b/frontend/src/lib/components/flows/DebounceLimit.svelte index 8c8937016d..1889d964ba 100644 --- a/frontend/src/lib/components/flows/DebounceLimit.svelte +++ b/frontend/src/lib/components/flows/DebounceLimit.svelte @@ -30,11 +30,37 @@ fontClass?: string } = $props() - // Get list of array-type arguments from schema + // Check if an originalType like "string | string[]" is a top-level + // union where at least one member is an array type (ends with "[]"). + // Splits on "|" only at the top level (not inside {}, <>, or ()). + function isUnionWithArray(originalType: string | undefined): boolean { + if (!originalType) return false + let depth = 0 + const parts: string[] = [] + let cur = '' + for (const ch of originalType) { + if (ch === '{' || ch === '<' || ch === '(') depth++ + else if (ch === '}' || ch === '>' || ch === ')') depth-- + else if (ch === '|' && depth === 0) { + parts.push(cur.trim()) + cur = '' + continue + } + cur += ch + } + parts.push(cur.trim()) + // Match TS array syntax (T[]) and Python list syntax (list[T] / List[T]) + return parts.length > 1 && parts.some((p) => p.endsWith('[]') || /^[Ll]ist\[.+\]$/.test(p)) + } + + // Get list of arguments eligible for accumulation from schema. + // Includes array-type arguments and union types like T | T[] + // whose scalar values are wrapped into single-element arrays + // at aggregation time. let arrayArgs = $derived( schema?.properties ? Object.entries(schema.properties) - .filter(([_, prop]) => prop.type === 'array') + .filter(([_, prop]) => prop.type === 'array' || isUnionWithArray(prop.originalType)) .map(([key, _]) => key) : [] ) @@ -111,8 +137,8 @@ {/if} diff --git a/frontend/src/lib/infer.ts b/frontend/src/lib/infer.ts index ab058e75d8..9861e9b016 100644 --- a/frontend/src/lib/infer.ts +++ b/frontend/src/lib/infer.ts @@ -105,17 +105,17 @@ async function initWasmAsset() { type InferAssetsResult = | { - status: 'ok' - assets: AssetWithAccessType[] - sql_queries?: InferAssetsSqlQueryDetails[] - columns?: Record - } + status: 'ok' + assets: AssetWithAccessType[] + sql_queries?: InferAssetsSqlQueryDetails[] + columns?: Record + } | { - status: 'error' - error: string - assets?: undefined - sql_queries?: undefined - } + status: 'error' + error: string + assets?: undefined + sql_queries?: undefined + } export type InferAssetsSqlQueryDetails = { query_string: string // SQL query with $1 placeholders for interpolations @@ -384,6 +384,11 @@ export async function inferArgs( argSigToJsonSchemaType(arg.typ, schema.properties[arg.name]) + // For T | T[] detection for debouncing arg accumulation + if ((arg as any).otyp && (arg as any).otyp.includes('[') && (arg as any).otyp.includes('|')) { + schema.properties[arg.name].originalType = (arg as any).otyp + } + schema.properties[arg.name].default = arg.default if (!arg.has_default && !schema.required.includes(arg.name)) {