feat(backend): array and object params support in pwsh (#6706)

* feat(backend): array and object params support in pwsh

* add bool and improve arg conversion

* fix bash

* update parser

* sqlx
This commit is contained in:
hugocasa
2025-09-30 14:25:12 +02:00
committed by GitHub
parent 5aeb3fa0b7
commit 898eb6231b
9 changed files with 172 additions and 57 deletions
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT draft_only FROM app WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "draft_only",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true
]
},
"hash": "27b0c827467cc92979f094620957bc0edfa295d6c2292e509a5536765d120bd8"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT draft_only FROM flow WHERE path = $1 AND workspace_id = $2 AND archived = false",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "draft_only",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true
]
},
"hash": "28f1ecca40c8b81cc59dffb75e2913c889b374999ece04173b2e67dc74005f60"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT draft_only FROM script WHERE path = $1 AND workspace_id = $2 AND archived = false",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "draft_only",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true
]
},
"hash": "71c945f93c0a1b561a85e8462b1687a54bd098cf6e84f57e5755eb84e1552345"
}
@@ -10,7 +10,7 @@ use regex_lite::Regex;
use serde_json::json;
use std::{collections::HashMap, str::FromStr};
use windmill_parser::{Arg, MainArgSignature, Typ};
use windmill_parser::{Arg, MainArgSignature, ObjectType, Typ};
pub fn parse_bash_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let parsed = parse_bash_file(&code)?;
@@ -48,7 +48,7 @@ lazy_static::lazy_static! {
static ref RE_BASH: Regex = Regex::new(r#"(?m)^(\w+)="\$(?:(\d+)|\{(\d+)\}|\{(\d+):-(.*)\})"(?:[\t ]*)?(?:#.*)?$"#).unwrap();
pub static ref RE_POWERSHELL_PARAM: Regex = Regex::new(r#"(?m)param[\t ]*\(([^)]*)\)"#).unwrap();
static ref RE_POWERSHELL_ARGS: Regex = Regex::new(r#"(?:\[(\w+)\])?\$(\w+)[\t ]*(?:=[\t ]*(?:(?:(?:"|')([^"\n\r\$]*)(?:"|'))|([\d.]+)))?"#).unwrap();
static ref RE_POWERSHELL_ARGS: Regex = Regex::new(r#"(?:\[([\w\[\]]+)\])?\$(\w+)[\t ]*(?:=[\t ]*(?:(?:(?:"|')([^"\n\r\$]*)(?:"|'))|([\d.]+)))?"#).unwrap();
}
fn parse_bash_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
@@ -124,6 +124,18 @@ fn split_pwsh_args(code: &str) -> Vec<&str> {
splits
}
fn parse_powershell_single_typ(typ: &str) -> Typ {
match typ.to_lowercase().as_str() {
"string" => Typ::Str(None),
"int" | "long" => Typ::Int,
"decimal" | "double" | "single" => Typ::Float,
"datetime" => Typ::Datetime,
"bool" => Typ::Bool,
"pscustomobject" => Typ::Object(ObjectType::new(None, None)),
_ => Typ::Str(None),
}
}
fn parse_powershell_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let param_wrapper = RE_POWERSHELL_PARAM.captures(code);
let mut args = vec![];
@@ -136,12 +148,12 @@ fn parse_powershell_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let name = cap.get(2).unwrap().as_str().to_string();
let mut parsed_typ = if let Some(typ) = typ {
match typ.as_str() {
"string" => Some(Typ::Str(None)),
"int" | "long" => Some(Typ::Int),
"decimal" | "double" | "single" => Some(Typ::Float),
"datetime" | "DateTime" => Some(Typ::Datetime),
_ => None,
if typ.as_str().ends_with("[]") {
Some(Typ::List(Box::new(parse_powershell_single_typ(
typ.as_str().strip_suffix("[]").unwrap(),
))))
} else {
Some(parse_powershell_single_typ(typ.as_str()))
}
} else {
None
@@ -254,7 +266,7 @@ non_required="${5:-}"
#[test]
fn test_parse_powershell_sig() -> anyhow::Result<()> {
let code = r#"param($Msg, [string]$Msg2, $Dflt = "default value, with comma", [int]$Nb = 3 , $Nb2 = 5.0, $Nb3 = 5, $Wahoo = $env:WAHOO)"#;
let code = r#"param($Msg, [string]$Msg2, $Dflt = "default value, with comma", [int]$Nb = 3 , $Nb2 = 5.0, $Nb3 = 5, $Wahoo = $env:WAHOO, [PSCustomObject]$Obj, [string[]]$Arr)"#;
assert_eq!(
parse_powershell_sig(code)?,
MainArgSignature {
@@ -316,6 +328,22 @@ non_required="${5:-}"
default: None,
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "Obj".to_string(),
typ: Typ::Object(ObjectType::new(None, None)),
default: None,
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "Arr".to_string(),
typ: Typ::List(Box::new(Typ::Str(None))),
default: None,
has_default: false,
oidx: None
}
],
no_main_func: None,
@@ -78,8 +78,8 @@ mount {
}
mount {
src: "{JOB_DIR}/wrapper.sh"
dst: "/tmp/wrapper.sh"
src: "{JOB_DIR}/wrapper.ps1"
dst: "/tmp/wrapper.ps1"
is_bind: true
mandatory: false
}
+54 -36
View File
@@ -58,6 +58,14 @@ lazy_static::lazy_static! {
pub static ref ANSI_ESCAPE_RE: Regex = Regex::new(r"\x1b\[[0-9;]*m").unwrap();
}
fn raw_to_string(x: &str) -> String {
match serde_json::from_str::<serde_json::Value>(x) {
Ok(serde_json::Value::String(x)) => x,
Ok(x) => serde_json::to_string(&x).unwrap_or_else(|_| String::new()),
_ => String::new(),
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn handle_bash_job(
mem_peak: &mut i32,
@@ -512,11 +520,37 @@ async fn container_is_alive(client: &bollard::Docker, container_id: &str) -> boo
}
}
fn raw_to_string(x: &str) -> String {
fn val_to_pwsh_param(v: serde_json::Value) -> String {
match v {
serde_json::Value::Array(x) => format!(
"@({})",
x.into_iter()
.map(|v| val_to_pwsh_param(v))
.collect::<Vec<_>>()
.join(",")
),
serde_json::Value::Object(x) => {
let str = serde_json::to_string(&x).unwrap_or_else(|_| "{}".to_string());
let escaped = str.replace("'", "''");
format!("(ConvertFrom-Json '{escaped}')")
}
serde_json::Value::Null => "$null".to_string(),
serde_json::Value::Bool(x) => format!("${x}"),
serde_json::Value::String(x) => {
let escaped = x.replace("'", "''");
format!("'{escaped}'")
}
serde_json::Value::Number(x) => x.to_string(),
}
}
fn raw_to_pwsh_param(x: &str) -> String {
match serde_json::from_str::<serde_json::Value>(x) {
Ok(serde_json::Value::String(x)) => x,
Ok(x) => serde_json::to_string(&x).unwrap_or_else(|_| String::new()),
_ => String::new(),
Ok(v) => val_to_pwsh_param(v),
Err(e) => {
tracing::error!("Error converting JSON to string: {:?}", e);
"$null".to_string()
}
}
}
@@ -622,17 +656,16 @@ pub async fn handle_powershell_job(
.map(|arg| {
(
arg.name.clone(),
job_args
.and_then(|x| x.get(&arg.name).map(|x| raw_to_string(x.get())))
.unwrap_or_else(String::new),
job_args.and_then(|x| x.get(&arg.name).map(|x| raw_to_pwsh_param(x.get()))),
)
})
.collect::<Vec<(String, String)>>();
.collect::<Vec<(String, Option<String>)>>();
args_owned
.iter()
.map(|(n, v)| vec![format!("--{n}"), format!("{v}")])
.flatten()
.into_iter()
.filter_map(|(n, v)| v.map(|v| format!("-{n} {v}")))
.collect::<Vec<_>>()
.join(" ")
};
#[cfg(windows)]
@@ -785,26 +818,16 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
write_file(job_dir, "main.ps1", content.as_str())?;
#[cfg(unix)]
write_file(
job_dir,
"wrapper.sh",
&format!("set -o pipefail\nset -e\nmkfifo bp\ncat bp | tail -1 > ./result2.out &\n{} -F ./main.ps1 \"$@\" 2>&1 | tee bp\nwait $!", POWERSHELL_PATH.as_str()),
)?;
#[cfg(windows)]
write_file(
job_dir,
"wrapper.ps1",
&format!(
"param([string[]]$args)\n\
$ErrorActionPreference = 'Stop'\n\
"$ErrorActionPreference = 'Stop'\n\
$pipe = New-TemporaryFile\n\
& \"{}\" -File ./main.ps1 @args 2>&1 | Tee-Object -FilePath $pipe\n\
./main.ps1 {pwsh_args} 2>&1 | Tee-Object -FilePath $pipe\n\
Get-Content -Path $pipe | Select-Object -Last 1 | Set-Content -Path './result2.out'\n\
Remove-Item $pipe\n\
exit $LASTEXITCODE\n",
POWERSHELL_PATH.as_str()
exit $LASTEXITCODE\n"
),
)?;
@@ -835,14 +858,13 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{CACHE_DIR}", POWERSHELL_CACHE_DIR),
)?;
let mut cmd_args = vec![
let cmd_args = vec![
"--config",
"run.config.proto",
"--",
BIN_BASH.as_str(),
"wrapper.sh",
POWERSHELL_PATH.as_str(),
"wrapper.ps1",
];
cmd_args.extend(pwsh_args.iter().map(|x| x.as_str()));
let mut cmd = Command::new(NSJAIL_PATH.as_str());
cmd.current_dir(job_dir)
.env_clear()
@@ -857,21 +879,17 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
start_child_process(cmd, NSJAIL_PATH.as_str(), false).await?
} else {
let mut cmd;
let mut cmd_args;
let mut cmd = Command::new(POWERSHELL_PATH.as_str());
let cmd_args;
#[cfg(unix)]
{
cmd_args = vec!["wrapper.sh"];
cmd_args.extend(pwsh_args.iter().map(|x| x.as_str()));
cmd = Command::new(BIN_BASH.as_str());
cmd_args = vec!["wrapper.ps1"];
}
#[cfg(windows)]
{
cmd_args = vec![r".\wrapper.ps1".to_string()];
cmd_args.extend(pwsh_args.iter().map(|x| x.replace("--", "-")));
cmd = Command::new(POWERSHELL_PATH.as_str());
cmd_args = vec![r".\wrapper.ps1"];
}
cmd.current_dir(job_dir)
+4 -4
View File
@@ -80,7 +80,7 @@
"windmill-parser-wasm-nu": "1.510.1",
"windmill-parser-wasm-php": "1.510.1",
"windmill-parser-wasm-py": "1.538.0",
"windmill-parser-wasm-regex": "1.512.0",
"windmill-parser-wasm-regex": "1.552.1",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.510.1",
"windmill-parser-wasm-ts": "1.538.0",
@@ -13512,9 +13512,9 @@
"integrity": "sha512-s+bdIgT/fA5em3zYUwF8D14uA/dZh7iu0krZYZQqZUO7txN37hwSCVfovbMkIwm4zPbsJ50mU8DRLt7UpAPZIw=="
},
"node_modules/windmill-parser-wasm-regex": {
"version": "1.512.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.512.0.tgz",
"integrity": "sha512-mOkuspfjPPhGZwmerBlFOjRKHjycrlzZUpxO7gHy5D6kwKI2bSz+VI3TsJqmK1y9PwAGgO8JnwRmNAR/YLUkzA=="
"version": "1.552.1",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.552.1.tgz",
"integrity": "sha512-TIt3+aR1hrUKuMbLAecT0c3Wdcg6w46Jm0EweMZfeqGX7lDp7SK8Ak1vwmzgBiZMJaM98RNAgyTCZHJiUoAeMA=="
},
"node_modules/windmill-parser-wasm-ruby": {
"version": "1.526.1",
+2 -2
View File
@@ -145,7 +145,7 @@
"windmill-parser-wasm-nu": "1.510.1",
"windmill-parser-wasm-php": "1.510.1",
"windmill-parser-wasm-py": "1.538.0",
"windmill-parser-wasm-regex": "1.512.0",
"windmill-parser-wasm-regex": "1.552.1",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.510.1",
"windmill-parser-wasm-ts": "1.538.0",
@@ -542,4 +542,4 @@
"@rollup/rollup-linux-x64-gnu": "^4.35.0",
"fsevents": "^2.3.3"
}
}
}
+4 -4
View File
@@ -987,7 +987,7 @@ docker pull $IMAGE
docker run --name $WM_JOB_ID -it -d $IMAGE $COMMAND
`
const POWERSHELL_INIT_CODE = `param($Msg, $Dflt = "default value", [int]$Nb = 3)
const POWERSHELL_INIT_CODE = `param($Msg, [string[]]$Names, [PSCustomObject]$Obj, $Dflt = "default value", [int]$Nb = 3)
# Import-Module MyModule
@@ -1251,8 +1251,8 @@ export const INITIAL_CODE = {
},
ruby: {
script: RUBY_INIT_CODE
},
// for related places search: ADD_NEW_LANG
}
// for related places search: ADD_NEW_LANG
}
export function isInitialCode(content: string): boolean {
@@ -1362,7 +1362,7 @@ export function initialCode(
return INITIAL_CODE.java.script
} else if (language == 'ruby') {
return INITIAL_CODE.ruby.script
// for related places search: ADD_NEW_LANG
// for related places search: ADD_NEW_LANG
} else if (language == 'bun' || language == 'bunnative') {
if (kind == 'trigger') {
return INITIAL_CODE.bun.trigger