Merge branch 'main' into dieri/triggers-queue-mode

This commit is contained in:
dieriba
2025-11-14 21:36:14 +01:00
63 changed files with 2624 additions and 1829 deletions
+14
View File
@@ -1,5 +1,19 @@
# Changelog
## [1.576.0](https://github.com/windmill-labs/windmill/compare/v1.575.4...v1.576.0) (2025-11-14)
### Features
* add support for switch and attributes in pwsh params ([#7143](https://github.com/windmill-labs/windmill/issues/7143)) ([c16bef8](https://github.com/windmill-labs/windmill/commit/c16bef8f296645ff873f9d8d28e3dcb50a65e304))
* **ai:** handle aws bedrock as provider ([#7131](https://github.com/windmill-labs/windmill/issues/7131)) ([30eb9aa](https://github.com/windmill-labs/windmill/commit/30eb9aae25eeb563ad119ef93f3ff1ab17c66d75))
* webhook by flow version ([#7062](https://github.com/windmill-labs/windmill/issues/7062)) ([09cdfb4](https://github.com/windmill-labs/windmill/commit/09cdfb4556748903dc5bbf53ef3356ac97c57d90))
### Bug Fixes
* use proper TLS connector for DuckLake instance catalog setup ([#7138](https://github.com/windmill-labs/windmill/issues/7138)) ([cf36fe3](https://github.com/windmill-labs/windmill/commit/cf36fe3bb1beec80fa84dc342a8a38cc7369bc4d))
## [1.575.4](https://github.com/windmill-labs/windmill/compare/v1.575.3...v1.575.4) (2025-11-13)
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n path \n FROM \n flow_version \n WHERE \n id = $1 AND \n workspace_id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
false
]
},
"hash": "7cab8475996f0abd7ca7888b460bd3faf3bef6e34200c258e3704981817c241e"
}
@@ -0,0 +1,66 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n flow_version.id AS version,\n flow_version.value->>'early_return' as early_return, \n flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, \n (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, \n flow.tag, \n flow.dedicated_worker, \n flow.on_behalf_of_email, \n flow.edited_by\n FROM \n flow_version\n INNER JOIN flow\n ON flow.path = flow_version.path AND\n flow.workspace_id = flow_version.workspace_id\n WHERE \n flow_version.workspace_id = $1 AND\n flow_version.path = $2 AND\n flow_version.id = $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "version",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "early_return",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "has_preprocessor",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "chat_input_enabled",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "dedicated_worker",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "on_behalf_of_email",
"type_info": "Text"
},
{
"ordinal": 7,
"name": "edited_by",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Int8"
]
},
"nullable": [
false,
null,
null,
null,
true,
true,
true,
false
]
},
"hash": "a7468e9054beed88636786c5495ac3b9d9a6086ae6212ad9237b39a0346d7d26"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n path \n FROM \n flow_version \n WHERE \n id = $1 AND \n workspace_id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
false
]
},
"hash": "b82f97f2a858e20b1fb6d9b0b3eab5bab2bafa91689eac5073c1678c338afb27"
}
@@ -1,66 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, on_behalf_of_email, edited_by, flow_version.id AS version\n FROM flow\n INNER JOIN flow_version\n ON flow_version.id = $3\n WHERE flow.path = $1 and flow.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "dedicated_worker",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "early_return",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "has_preprocessor",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "chat_input_enabled",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "on_behalf_of_email",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "version",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Int8"
]
},
"nullable": [
true,
true,
null,
null,
null,
true,
false,
false
]
},
"hash": "e1409c67b93881cf68f6ac9c8bae0856cf426c7e7860c8b5b799972baa8e6945"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n value->'preprocessor_module'->'value' as \"preprocessor_module: _\",\n schema as \"schema: _\"\n FROM flow_version\n WHERE\n path = $1\n AND workspace_id = $2\n ORDER BY created_at DESC\n LIMIT 1",
"query": "SELECT\n value->'preprocessor_module'->'value' as \"preprocessor_module: _\",\n schema as \"schema: _\"\n FROM flow_version\n WHERE\n id = $1\n AND workspace_id = $2",
"describe": {
"columns": [
{
@@ -16,7 +16,7 @@
],
"parameters": {
"Left": [
"Text",
"Int8",
"Text"
]
},
@@ -25,5 +25,5 @@
true
]
},
"hash": "e2474b7855c8b08f927f2b987421e773e537a8eb0a113477764bbab14e1f3a3d"
"hash": "f0c9670c27d9a3b8e8f316fd28f7d3ede2ecc3225ff2879ff5dfd59c4ef236d5"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path FROM flow_version WHERE id = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
false
]
},
"hash": "fc9753f501974c4b570d710c7621ff1f6787a852bb27acf49593af963a6aacca"
}
+66 -65
View File
@@ -199,22 +199,22 @@ dependencies = [
[[package]]
name = "anstyle-query"
version = "1.1.4"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.10"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -1026,7 +1026,7 @@ dependencies = [
"http 1.3.1",
"http-body 0.4.6",
"hyper 0.14.32",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-rustls 0.24.2",
"hyper-rustls 0.27.7",
"hyper-util",
@@ -1182,7 +1182,7 @@ dependencies = [
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"itoa",
"matchit",
@@ -1569,7 +1569,7 @@ dependencies = [
"hex",
"http 1.3.1",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-named-pipe",
"hyper-util",
"hyperlocal",
@@ -1799,9 +1799,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.1"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
dependencies = [
"serde",
]
@@ -1945,9 +1945,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.45"
version = "1.2.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe"
checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -3625,7 +3625,7 @@ dependencies = [
"hickory-resolver",
"http 1.3.1",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-rustls 0.27.7",
"hyper-util",
"ipnet",
@@ -3713,7 +3713,7 @@ dependencies = [
"http 1.3.1",
"httparse",
"hyper 0.14.32",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"itertools 0.10.5",
"memmem",
@@ -3906,7 +3906,7 @@ dependencies = [
"hkdf",
"http 1.3.1",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"idna",
"indexmap 2.11.1",
@@ -4177,7 +4177,7 @@ dependencies = [
"http 1.3.1",
"http-body-util",
"hyper 0.14.32",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"libc",
"log",
@@ -4231,7 +4231,7 @@ dependencies = [
"deno_error",
"deno_tls",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-rustls 0.27.7",
"hyper-util",
"log",
@@ -4361,7 +4361,7 @@ dependencies = [
"h2 0.4.12",
"http 1.3.1",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"once_cell",
"rustls-tokio-stream",
@@ -5225,7 +5225,7 @@ dependencies = [
"base64 0.21.7",
"bytes",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"pin-project",
"rand 0.8.5",
@@ -5292,9 +5292,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "fixedbitset"
@@ -6604,9 +6604,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "1.8.0"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
dependencies = [
"atomic-waker",
"bytes",
@@ -6635,7 +6635,7 @@ dependencies = [
"futures-util",
"headers",
"http 1.3.1",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-rustls 0.27.7",
"hyper-util",
"pin-project-lite",
@@ -6652,7 +6652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278"
dependencies = [
"hex",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"pin-project-lite",
"tokio",
@@ -6683,7 +6683,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
"http 1.3.1",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"log",
"rustls 0.23.29",
@@ -6701,7 +6701,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
dependencies = [
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"pin-project-lite",
"tokio",
@@ -6729,7 +6729,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"native-tls",
"tokio",
@@ -6750,7 +6750,7 @@ dependencies = [
"futures-util",
"http 1.3.1",
"http-body 1.0.1",
"hyper 1.8.0",
"hyper 1.8.1",
"ipnet",
"libc",
"percent-encoding",
@@ -6771,7 +6771,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7"
dependencies = [
"hex",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"pin-project-lite",
"tokio",
@@ -7419,7 +7419,7 @@ dependencies = [
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-http-proxy",
"hyper-rustls 0.27.7",
"hyper-timeout",
@@ -8946,7 +8946,7 @@ dependencies = [
"http-body-util",
"httparse",
"humantime",
"hyper 1.8.0",
"hyper 1.8.1",
"itertools 0.14.0",
"md-5 0.10.6",
"parking_lot 0.12.5",
@@ -10712,7 +10712,7 @@ dependencies = [
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-rustls 0.27.7",
"hyper-tls 0.6.0",
"hyper-util",
@@ -10772,7 +10772,7 @@ dependencies = [
"futures",
"getrandom 0.2.16",
"http 1.3.1",
"hyper 1.8.0",
"hyper 1.8.1",
"parking_lot 0.11.2",
"reqwest 0.12.24",
"reqwest-middleware",
@@ -13945,7 +13945,7 @@ dependencies = [
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-timeout",
"hyper-util",
"percent-encoding",
@@ -15148,7 +15148,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"aws-sdk-config",
@@ -15208,7 +15208,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"argon2",
@@ -15251,7 +15251,7 @@ dependencies = [
"hf-hub",
"hmac",
"http 1.3.1",
"hyper 1.8.0",
"hyper 1.8.1",
"indexmap 2.11.1",
"itertools 0.14.0",
"jsonwebtoken 8.3.0",
@@ -15269,6 +15269,7 @@ dependencies = [
"pg_escape",
"pin-project",
"postgres-native-tls 0.5.0",
"postgres-native-tls 0.5.1",
"prometheus",
"quick_cache",
"rand 0.9.0",
@@ -15328,7 +15329,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -15343,7 +15344,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"chrono",
"lazy_static",
@@ -15357,7 +15358,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"axum",
@@ -15376,7 +15377,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -15404,7 +15405,7 @@ dependencies = [
"globset",
"hex",
"hmac",
"hyper 1.8.0",
"hyper 1.8.1",
"indexmap 2.11.1",
"itertools 0.14.0",
"jsonwebtoken 8.3.0",
@@ -15461,7 +15462,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"regex",
"serde",
@@ -15476,7 +15477,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"bytes",
@@ -15500,7 +15501,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -15512,7 +15513,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -15521,7 +15522,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15533,7 +15534,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"serde_json",
@@ -15545,7 +15546,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"gosyn",
@@ -15557,7 +15558,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15569,7 +15570,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"serde_json",
@@ -15581,7 +15582,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -15592,7 +15593,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15603,7 +15604,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15615,7 +15616,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -15638,7 +15639,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15652,7 +15653,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -15669,7 +15670,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15683,7 +15684,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15701,7 +15702,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"serde",
@@ -15712,7 +15713,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -15747,7 +15748,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -15757,7 +15758,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.575.4"
version = "1.576.0"
dependencies = [
"anyhow",
"async-once-cell",
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.575.4"
version = "1.576.0"
authors.workspace = true
edition.workspace = true
@@ -33,7 +33,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.575.4"
version = "1.576.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
+1 -1
View File
@@ -1 +1 @@
90ef44ceb8220e7e54a2ac40756c96fe003d8695
0772f05a60809e9d9a11c25173a246724357520c
+518 -103
View File
@@ -46,8 +46,6 @@ pub fn parse_powershell_sig(code: &str) -> anyhow::Result<MainArgSignature> {
lazy_static::lazy_static! {
static ref RE_BASH: Regex = Regex::new(r#"(?m)^(\w+)="\$(?:(\d+)|\{(\d+)\}|\{(\d+):-(.*)\})"(?:[\t ]*)?(?:#.*)?\r?$"#).unwrap();
static ref RE_POWERSHELL_ARGS: Regex = Regex::new(r#"(?:\[([\w\[\]]+)\])?\$(\w+)[\t ]*(?:=[\t ]*(?:(?:(?:"|')([^"\n\r\$]*)(?:"|'))|([\d.]+)))?\r?"#).unwrap();
}
fn parse_bash_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
@@ -98,10 +96,12 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti
let lower_code = code.to_lowercase();
let param_start = lower_code.find("param")?;
// Verify that only comments and whitespace appear before "param"
// Verify that only comments, whitespace, and [CmdletBinding()] appear before "param"
let before_param = &code[..param_start];
let mut chars = before_param.chars().peekable();
let mut in_block_comment = false;
let mut in_attribute_bracket = false;
let mut bracket_depth = 0;
while let Some(ch) = chars.next() {
if in_block_comment {
@@ -110,6 +110,19 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti
chars.next(); // consume '>'
in_block_comment = false;
}
} else if in_attribute_bracket {
// Track bracket depth to handle nested brackets/parens in attributes
match ch {
'[' => bracket_depth += 1,
']' => {
bracket_depth -= 1;
if bracket_depth == 0 {
in_attribute_bracket = false;
}
}
// Allow parentheses inside attributes (e.g., [CmdletBinding()])
_ => {}
}
} else {
match ch {
// Start of block comment: <#
@@ -126,6 +139,11 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti
chars.next();
}
}
// Start of attribute bracket (e.g., [CmdletBinding()])
'[' => {
in_attribute_bracket = true;
bracket_depth = 1;
}
// Whitespace is allowed
c if c.is_whitespace() => {}
// Any other character means there's code before param
@@ -134,8 +152,8 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti
}
}
// If we're still in a block comment at the end, it's unclosed - invalid
if in_block_comment {
// If we're still in a block comment or unclosed attribute bracket, it's invalid
if in_block_comment || in_attribute_bracket {
return None;
}
@@ -214,108 +232,269 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti
None
}
enum ParserState {
Normal,
InSingleQuote,
InDoubleQuote,
}
fn split_pwsh_args(code: &str) -> Vec<&str> {
let mut chars = code.char_indices().peekable();
let mut state = ParserState::Normal;
let mut splits = vec![];
let mut last_idx = 0;
while let Some((idx, char)) = chars.next() {
match (&state, char) {
(ParserState::Normal, '\'') => {
state = ParserState::InSingleQuote;
}
(ParserState::Normal, '"') => {
state = ParserState::InDoubleQuote;
}
(ParserState::InSingleQuote, '\'') => {
state = ParserState::Normal;
}
(ParserState::InDoubleQuote, '"') => {
state = ParserState::Normal;
}
(ParserState::Normal, ',') => {
splits.push(&code[last_idx..idx]);
last_idx = idx + 1; // skip the comma
}
_ => {}
}
}
if last_idx < code.len() {
splits.push(&code[last_idx..]);
}
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,
"bool" | "switch" => 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 = extract_powershell_param_block(code, false);
let mut args = vec![];
if let Some(param_wrapper) = param_wrapper {
let params = split_pwsh_args(param_wrapper);
for param in params {
if let Some(cap) = RE_POWERSHELL_ARGS.captures(param) {
let typ = cap.get(1).map(|x| x.as_str().to_string());
let name = cap.get(2).unwrap().as_str().to_string();
/// Single-pass PowerShell parameter parser.
/// Parses the content of a param() block and extracts all parameter information.
///
/// This function processes PowerShell parameter declarations in a single pass, handling:
/// - Parameter attributes: [Parameter(Mandatory)], [Parameter(Mandatory=$true)], [ValidateSet(...)], etc.
/// - Type annotations: [string], [int[]], [PSCustomObject], etc.
/// - Variable names: $Name, $Value, etc.
/// - Default values: = 'text', = 25, = $env:VAR, etc.
/// - Mandatory detection: Parameters with Mandatory attribute are marked as required
fn parse_powershell_parameters(content: &str) -> anyhow::Result<Vec<Arg>> {
#[derive(Debug, PartialEq)]
enum State {
Normal,
InSingleQuote,
InDoubleQuote,
InBracket,
}
let mut parsed_typ = if let Some(typ) = typ {
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()))
let mut args = Vec::new();
let mut chars = content.char_indices().peekable();
let mut state = State::Normal;
let mut bracket_depth: i32 = 0;
let mut paren_depth: i32 = 0;
// Current parameter being built
let mut type_annotation: Option<String> = None;
let mut var_name: Option<String> = None;
let mut default_value: Option<String> = None;
let mut is_mandatory = false;
// Track position for extracting text
let mut last_bracket_start = None;
let mut found_dollar = false;
while let Some((idx, ch)) = chars.next() {
match state {
State::InSingleQuote => {
if ch == '\'' {
state = State::Normal;
}
}
State::InDoubleQuote => {
if ch == '"' {
// Check for escape character
if idx > 0 && content.chars().nth(idx - 1) != Some('`') {
state = State::Normal;
}
} else {
None
};
}
}
State::InBracket => {
match ch {
'[' => bracket_depth += 1,
']' => {
bracket_depth -= 1;
if bracket_depth == 0 {
// Extract the bracket content
if let Some(start) = last_bracket_start {
let bracket_content = &content[start + 1..idx];
let default = if let Some(x) = cap.get(3) {
Some(json!(x.as_str().to_string()))
} else if let Some(x) = cap.get(4) {
if parsed_typ.is_none() {
if x.as_str().parse::<i64>().is_ok() {
parsed_typ = Some(Typ::Int);
} else if x.as_str().parse::<f64>().is_ok() {
parsed_typ = Some(Typ::Float);
// Check if this is a Parameter attribute with Mandatory (case-insensitive)
let lower = bracket_content.to_lowercase();
if lower.starts_with("parameter(") || lower.starts_with("parameter ") {
// Check for Mandatory (case-insensitive)
if lower.contains("mandatory") {
// Check if it's explicitly set to false
if !lower.contains("mandatory=$false") && !lower.contains("mandatory = $false") {
is_mandatory = true;
}
}
}
// Check if this looks like a type (simple word, possibly with [])
let is_type = !bracket_content.contains('(')
&& !bracket_content.contains('=')
&& (bracket_content.chars().next().unwrap_or(' ').is_alphabetic()
|| bracket_content.starts_with('['));
if is_type && !found_dollar {
type_annotation = Some(bracket_content.to_string());
}
}
state = State::Normal;
last_bracket_start = None;
}
}
serde_json::Number::from_str(x.as_str())
.ok()
.map(serde_json::Value::Number)
} else {
None
};
'(' => paren_depth += 1,
')' => paren_depth = paren_depth.saturating_sub(1),
_ => {}
}
}
State::Normal => {
match ch {
'\'' => state = State::InSingleQuote,
'"' => state = State::InDoubleQuote,
'[' => {
state = State::InBracket;
bracket_depth = 1;
last_bracket_start = Some(idx);
}
'$' => {
found_dollar = true;
// Extract variable name
let name_start = idx + 1;
let mut name_end = name_start;
while let Some(&(_, next_ch)) = chars.peek() {
if next_ch.is_alphanumeric() || next_ch == '_' {
name_end += 1;
chars.next();
} else {
break;
}
}
var_name = Some(content[name_start..name_end].to_string());
}
'=' if found_dollar => {
// Extract default value
// Skip whitespace after =
while let Some(&(_, next_ch)) = chars.peek() {
if next_ch.is_whitespace() {
chars.next();
} else {
break;
}
}
args.push(Arg {
name: name,
typ: parsed_typ.unwrap_or(Typ::Str(None)),
default: default.clone(),
otyp: None,
has_default: default.is_some(),
oidx: None,
});
let default_start = chars.peek().map(|(i, _)| *i).unwrap_or(content.len());
let mut default_end = default_start;
let mut in_string = false;
let mut string_char = ' ';
while let Some((i, ch)) = chars.peek().copied() {
if in_string {
if ch == string_char && content.chars().nth(i.saturating_sub(1)) != Some('`') {
in_string = false;
default_end = i + 1;
chars.next();
} else {
default_end = i + 1;
chars.next();
}
} else if ch == '\'' || ch == '"' {
in_string = true;
string_char = ch;
default_end = i + 1;
chars.next();
} else if ch == ',' {
break;
} else if ch.is_whitespace() && chars.clone().skip(1).next().map(|(_, c)| c) == Some(',') {
break;
} else {
default_end = i + 1;
chars.next();
}
}
default_value = Some(content[default_start..default_end].trim().to_string());
}
',' => {
// End of parameter, finalize it
if let Some(name) = var_name.take() {
args.push(finalize_parameter(name, type_annotation.take(), default_value.take(), is_mandatory)?);
}
// Reset for next parameter
type_annotation = None;
var_name = None;
default_value = None;
is_mandatory = false;
found_dollar = false;
}
_ => {}
}
}
}
}
Ok(Some(args))
// Finalize last parameter
if let Some(name) = var_name {
args.push(finalize_parameter(name, type_annotation, default_value, is_mandatory)?);
}
Ok(args)
}
fn finalize_parameter(
name: String,
type_annotation: Option<String>,
default_value: Option<String>,
is_mandatory: bool,
) -> anyhow::Result<Arg> {
// Store the original PowerShell type for use in the executor
let otyp = type_annotation.clone();
let mut parsed_typ = if let Some(typ) = type_annotation {
if typ.ends_with("[]") {
Some(Typ::List(Box::new(parse_powershell_single_typ(
typ.strip_suffix("[]").unwrap(),
))))
} else {
Some(parse_powershell_single_typ(&typ))
}
} else {
None
};
let default = if let Some(default_str) = default_value {
// Try to parse as string (quoted)
if (default_str.starts_with('"') && default_str.ends_with('"'))
|| (default_str.starts_with('\'') && default_str.ends_with('\''))
{
Some(json!(default_str[1..default_str.len() - 1].to_string()))
} else {
// Try to parse as number
if parsed_typ.is_none() {
if default_str.parse::<i64>().is_ok() {
parsed_typ = Some(Typ::Int);
} else if default_str.parse::<f64>().is_ok() {
parsed_typ = Some(Typ::Float);
}
}
serde_json::Number::from_str(&default_str)
.ok()
.map(serde_json::Value::Number)
}
} else {
None
};
// has_default semantics:
// - true: parameter is optional (has a default value OR is not mandatory)
// - false: parameter is required (marked as Mandatory AND no default value)
// Simplified: A parameter is optional unless it's mandatory without a default
let has_default = default.is_some() || !is_mandatory;
Ok(Arg {
name,
typ: parsed_typ.unwrap_or(Typ::Str(None)),
default: default.clone(),
otyp,
has_default,
oidx: None,
})
}
fn parse_powershell_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let param_wrapper = extract_powershell_param_block(code, false);
if let Some(param_wrapper) = param_wrapper {
Ok(Some(parse_powershell_parameters(param_wrapper)?))
} else {
Ok(Some(vec![]))
}
}
#[cfg(test)]
@@ -402,23 +581,23 @@ non_required="${5:-}"
star_kwargs: false,
args: vec![
Arg {
otyp: None,
otyp: None, // No type annotation
name: "Msg".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
has_default: true, // Optional (not mandatory)
oidx: None
},
Arg {
otyp: None,
otyp: Some("string".to_string()), // [string]
name: "Msg2".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
has_default: true, // Optional (not mandatory)
oidx: None
},
Arg {
otyp: None,
otyp: None, // No type annotation
name: "Dflt".to_string(),
typ: Typ::Str(None),
default: Some(json!("default value, with comma")),
@@ -426,7 +605,7 @@ non_required="${5:-}"
oidx: None
},
Arg {
otyp: None,
otyp: Some("int".to_string()), // [int]
name: "Nb".to_string(),
typ: Typ::Int,
default: Some(json!(3)),
@@ -434,7 +613,7 @@ non_required="${5:-}"
oidx: None
},
Arg {
otyp: None,
otyp: None, // Type inferred from default value
name: "Nb2".to_string(),
typ: Typ::Float,
default: Some(json!(5.0)),
@@ -442,7 +621,7 @@ non_required="${5:-}"
oidx: None
},
Arg {
otyp: None,
otyp: None, // Type inferred from default value
name: "Nb3".to_string(),
typ: Typ::Int,
default: Some(json!(5)),
@@ -450,35 +629,35 @@ non_required="${5:-}"
oidx: None
},
Arg {
otyp: None,
otyp: None, // No type annotation
name: "Wahoo".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
has_default: true, // Optional (not mandatory)
oidx: None
},
Arg {
otyp: None,
otyp: Some("PSCustomObject".to_string()), // [PSCustomObject]
name: "Obj".to_string(),
typ: Typ::Object(ObjectType::new(None, None)),
default: None,
has_default: false,
has_default: true, // Optional (not mandatory)
oidx: None
},
Arg {
otyp: None,
otyp: Some("string[]".to_string()), // [string[]]
name: "Arr".to_string(),
typ: Typ::List(Box::new(Typ::Str(None))),
default: None,
has_default: false,
has_default: true, // Optional (not mandatory)
oidx: None
},
Arg {
otyp: None,
otyp: Some("string".to_string()), // [string] (last type bracket with Mandatory)
name: "Message".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
has_default: false, // Required (Mandatory attribute)
oidx: None
}
],
@@ -602,6 +781,242 @@ non_required="${5:-}"
extract_powershell_param_block("function test-x{ param($Name)\n}", false),
None
);
// Valid: [CmdletBinding()] before param
assert_eq!(
extract_powershell_param_block("[CmdletBinding()]\nparam($Name)", false),
Some("$Name")
);
assert_eq!(
extract_powershell_param_block("[CmdletBinding()]\nparam($Name, $Age)", true),
Some("param($Name, $Age)")
);
// Valid: [CmdletBinding()] with options before param
assert_eq!(
extract_powershell_param_block(
"[CmdletBinding(SupportsShouldProcess=$true)]\nparam($Path)",
false
),
Some("$Path")
);
// Valid: Multiple attributes before param
assert_eq!(
extract_powershell_param_block(
"[CmdletBinding()]\n[OutputType([string])]\nparam($Value)",
false
),
Some("$Value")
);
// Valid: CmdletBinding with comments
assert_eq!(
extract_powershell_param_block(
"# My function\n[CmdletBinding()]\nparam($Name)",
false
),
Some("$Name")
);
// Valid: CmdletBinding with whitespace variations
assert_eq!(
extract_powershell_param_block(
"[CmdletBinding()] \n param($Name)",
false
),
Some("$Name")
);
// Invalid: Unclosed attribute bracket
assert_eq!(
extract_powershell_param_block("[CmdletBinding(\nparam($Name)", false),
None
);
}
#[test]
fn test_parse_powershell_sig_with_parameter_attributes() -> anyhow::Result<()> {
// Test with [Parameter(Mandatory=$true)] attribute
let code = r#"[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$Name,
[Parameter(Mandatory=$false)]
[int]$Age = 25
)"#;
let result = parse_powershell_sig(code)?;
assert_eq!(result.args.len(), 2);
assert_eq!(result.args[0].name, "Name");
assert_eq!(result.args[0].typ, Typ::Str(None));
assert_eq!(result.args[0].has_default, false);
assert_eq!(result.args[1].name, "Age");
assert_eq!(result.args[1].typ, Typ::Int);
assert_eq!(result.args[1].has_default, true);
assert_eq!(result.args[1].default, Some(json!(25)));
// Test with complex attributes
let code2 = r#"param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateSet('Red', 'Green', 'Blue')]
[string]$Color,
[Parameter(ValueFromPipeline=$true)]
[string[]]$Items
)"#;
let result2 = parse_powershell_sig(code2)?;
assert_eq!(result2.args.len(), 2);
assert_eq!(result2.args[0].name, "Color");
assert_eq!(result2.args[0].typ, Typ::Str(None));
assert_eq!(result2.args[1].name, "Items");
assert_eq!(result2.args[1].typ, Typ::List(Box::new(Typ::Str(None))));
Ok(())
}
#[test]
fn test_powershell_single_pass_parser() -> anyhow::Result<()> {
// Test the single-pass parser with a complex real-world example
let code = r#"[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0, HelpMessage="Enter the server name")]
[ValidateNotNullOrEmpty()]
[string]$ServerName,
[Parameter(Mandatory=$false)]
[ValidateRange(1, 65535)]
[int]$Port = 8080,
[Parameter(ValueFromPipeline=$true)]
[string[]]$LogFiles,
[ValidateSet('Debug', 'Info', 'Warning', 'Error')]
[string]$LogLevel = 'Info',
[PSCustomObject]$Config
)"#;
let result = parse_powershell_sig(code)?;
assert_eq!(result.args.len(), 5);
// ServerName: mandatory string with no default
assert_eq!(result.args[0].name, "ServerName");
assert_eq!(result.args[0].typ, Typ::Str(None));
assert_eq!(result.args[0].has_default, false);
// Port: optional int with default
assert_eq!(result.args[1].name, "Port");
assert_eq!(result.args[1].typ, Typ::Int);
assert_eq!(result.args[1].default, Some(json!(8080)));
assert_eq!(result.args[1].has_default, true);
// LogFiles: string array (no mandatory, so optional)
assert_eq!(result.args[2].name, "LogFiles");
assert_eq!(result.args[2].typ, Typ::List(Box::new(Typ::Str(None))));
assert_eq!(result.args[2].has_default, true); // Optional (not mandatory)
// LogLevel: string with default (ValidateSet is ignored but doesn't break parsing)
assert_eq!(result.args[3].name, "LogLevel");
assert_eq!(result.args[3].typ, Typ::Str(None));
assert_eq!(result.args[3].default, Some(json!("Info")));
assert_eq!(result.args[3].has_default, true);
// Config: PSCustomObject (no mandatory, so optional)
assert_eq!(result.args[4].name, "Config");
assert_eq!(result.args[4].typ, Typ::Object(ObjectType::new(None, None)));
assert_eq!(result.args[4].has_default, true); // Optional (not mandatory)
Ok(())
}
#[test]
fn test_powershell_mandatory_attribute() -> anyhow::Result<()> {
// Test various forms of the Mandatory attribute
let code = r#"param(
[Parameter(Mandatory)]
[string]$RequiredNoEquals,
[Parameter(Mandatory=$true)]
[string]$RequiredWithTrue,
[Parameter(Mandatory = $true)]
[string]$RequiredWithSpaces,
[Parameter(Mandatory=$false)]
[string]$NotRequired,
[Parameter(Position=0)]
[string]$NoMandatory,
[string]$PlainRequired = "default",
[Parameter(Mandatory=$true)]
[int]$RequiredInt
)"#;
let result = parse_powershell_sig(code)?;
assert_eq!(result.args.len(), 7);
// RequiredNoEquals: mandatory without =$true
assert_eq!(result.args[0].name, "RequiredNoEquals");
assert_eq!(result.args[0].has_default, false); // Required (mandatory, no default)
// RequiredWithTrue: mandatory with =$true
assert_eq!(result.args[1].name, "RequiredWithTrue");
assert_eq!(result.args[1].has_default, false); // Required
// RequiredWithSpaces: mandatory with spaces
assert_eq!(result.args[2].name, "RequiredWithSpaces");
assert_eq!(result.args[2].has_default, false); // Required
// NotRequired: explicitly Mandatory=$false
assert_eq!(result.args[3].name, "NotRequired");
assert_eq!(result.args[3].has_default, true); // Optional (not mandatory)
// NoMandatory: no Mandatory attribute
assert_eq!(result.args[4].name, "NoMandatory");
assert_eq!(result.args[4].has_default, true); // Optional (not mandatory)
// PlainRequired: has default value (always optional)
assert_eq!(result.args[5].name, "PlainRequired");
assert_eq!(result.args[5].has_default, true); // Optional (has default)
assert_eq!(result.args[5].default, Some(json!("default")));
// RequiredInt: mandatory int
assert_eq!(result.args[6].name, "RequiredInt");
assert_eq!(result.args[6].typ, Typ::Int);
assert_eq!(result.args[6].has_default, false); // Required
Ok(())
}
#[test]
fn test_powershell_case_insensitive_parameter() -> anyhow::Result<()> {
// Test that [parameter(...)] is case-insensitive
let code = r#"param(
[parameter(Mandatory)]
[string]$LowerCase,
[PARAMETER(MANDATORY=$TRUE)]
[string]$UpperCase,
[Parameter(mandatory=$true)]
[string]$MixedCase
)"#;
let result = parse_powershell_sig(code)?;
assert_eq!(result.args.len(), 3);
// All should be detected as mandatory
assert_eq!(result.args[0].name, "LowerCase");
assert_eq!(result.args[0].has_default, false);
assert_eq!(result.args[1].name, "UpperCase");
assert_eq!(result.args[1].has_default, false);
assert_eq!(result.args[2].name, "MixedCase");
assert_eq!(result.args[2].has_default, false);
Ok(())
}
#[test]
+1 -1
View File
@@ -3,7 +3,7 @@
pub use crate::ee::*;
#[cfg(not(feature = "private"))]
pub async fn set_license_key(_license_key: String) -> () {
pub async fn set_license_key(_license_key: String, _db: Option<&windmill_common::db::DB>) -> () {
// Implementation is not open source
}
+46 -48
View File
@@ -79,7 +79,7 @@ use windmill_common::{
OTEL_METRICS_ENABLED, OTEL_TRACING_ENABLED, SERVICE_LOG_RETENTION_SECS,
};
use windmill_common::{client::AuthedClient, global_settings::APP_WORKSPACED_ROUTE_SETTING};
use windmill_queue::{SameWorkerPayload, cancel_job, get_queued_job_v2};
use windmill_queue::{cancel_job, get_queued_job_v2, SameWorkerPayload};
use windmill_worker::{
handle_job_error, JobCompletedSender, SameWorkerSender, BUNFIG_INSTALL_SCOPES,
INSTANCE_PYTHON_VERSION, JOB_DEFAULT_TIMEOUT, KEEP_JOB_DIR, MAVEN_REPOS, NO_DEFAULT_MAVEN,
@@ -2219,7 +2219,6 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, node_n
);
}
timeouts
};
@@ -2253,7 +2252,6 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, node_n
}
}
let timeouts = non_restartable_jobs
.into_iter()
.map(|x| (x, ErrorMessage::RestartDisabled))
@@ -2289,59 +2287,59 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, node_n
continue;
}
if let Some(job) = job.unwrap() {
let label = if job.permissioned_as != format!("u/{}", job.created_by)
&& job.permissioned_as != job.created_by
{
format!("ephemeral-script-end-user-{}", job.created_by)
} else {
"ephemeral-script".to_string()
};
let token = create_token_for_owner(
&db,
&job.workspace_id,
&job.permissioned_as,
&label,
*SCRIPT_TOKEN_EXPIRY,
&job.permissioned_as_email,
&job.id,
None,
Some(format!("handle_zombie_jobs")),
)
.await
.expect("could not create job token");
let label = if job.permissioned_as != format!("u/{}", job.created_by)
&& job.permissioned_as != job.created_by
{
format!("ephemeral-script-end-user-{}", job.created_by)
} else {
"ephemeral-script".to_string()
};
let token = create_token_for_owner(
&db,
&job.workspace_id,
&job.permissioned_as,
&label,
*SCRIPT_TOKEN_EXPIRY,
&job.permissioned_as_email,
&job.id,
None,
Some(format!("handle_zombie_jobs")),
)
.await
.expect("could not create job token");
let client = AuthedClient::new(
base_internal_url.to_string(),
job.workspace_id.to_string(),
token,
None,
);
let client = AuthedClient::new(
base_internal_url.to_string(),
job.workspace_id.to_string(),
token,
None,
);
let error_message = format!(
let error_message = format!(
"Job timed out after no ping from job since {} (ZOMBIE_JOB_TIMEOUT: {}, reason: {:?}).\nThis likely means that the job died on worker {}, OOM are a common reason for worker crashes.\nCheck the workers around the time of the last ping and the exit code if any.",
job.last_ping.unwrap_or_default(),
*ZOMBIE_JOB_TIMEOUT,
error_kind.to_string(),
job.worker.clone().unwrap_or_default(),
);
let memory_peak = job.memory_peak.unwrap_or(0);
let _ = handle_job_error(
db,
&client,
&windmill_queue::MiniCompletedJob::from(job),
memory_peak,
None,
error::Error::ExecutionErr(error_message),
true,
Some(&same_worker_tx_never_used),
"",
node_name,
send_result_never_used,
#[cfg(feature = "benchmark")]
&mut windmill_common::bench::BenchmarkIter::new(),
)
.await;
}
let memory_peak = job.memory_peak.unwrap_or(0);
let _ = handle_job_error(
db,
&client,
&windmill_queue::MiniCompletedJob::from(job),
memory_peak,
None,
error::Error::ExecutionErr(error_message),
true,
Some(&same_worker_tx_never_used),
"",
node_name,
send_result_never_used,
#[cfg(feature = "benchmark")]
&mut windmill_common::bench::BenchmarkIter::new(),
)
.await;
}
}
}
+1 -1
View File
@@ -2769,7 +2769,7 @@ async fn test_result_format(db: Pool<Postgres>) -> anyhow::Result<()> {
let response = windmill_api::jobs::run_wait_result(
&db,
Uuid::parse_str(ordered_result_job_id).unwrap(),
"test-workspace".to_string(),
"test-workspace",
None,
"test-user",
)
+1
View File
@@ -94,6 +94,7 @@ tempfile.workspace = true
tokio-util.workspace = true
tokio-tar.workspace = true
tokio-postgres.workspace = true
postgres-native-tls.workspace = true
hmac.workspace = true
cookie.workspace = true
sha2.workspace = true
+223 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.575.4
version: 1.576.0
title: Windmill API
contact:
@@ -5956,6 +5956,80 @@ paths:
application/json:
schema: {}
/w/{workspace}/jobs/run_wait_result/fv/{version}:
post:
summary: run flow by version and wait until completion
operationId: runWaitResultFlowByVersion
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: version
description: flow version ID
in: path
required: true
schema:
type: integer
format: int64
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
requestBody:
description: script args
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ScriptArgs"
responses:
"200":
description: job result
content:
application/json:
schema: {}
get:
summary: run flow by version with GET and wait until completion
operationId: runWaitResultFlowByVersionGet
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: version
description: flow version ID
in: path
required: true
schema:
type: integer
format: int64
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/Payload"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
responses:
"200":
description: job result
content:
application/json:
schema: {}
/w/{workspace}/jobs/run_and_stream/f/{path}:
post:
summary: run flow by path and stream updates via SSE
@@ -6032,6 +6106,94 @@ paths:
schema:
type: string
/w/{workspace}/jobs/run_and_stream/fv/{version}:
post:
summary: run flow by version and stream updates via SSE
operationId: runAndStreamFlowByVersion
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: version
description: flow version ID
in: path
required: true
schema:
type: integer
format: int64
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
- name: poll_delay_ms
description: delay between polling for job updates in milliseconds
in: query
schema:
type: integer
format: int64
requestBody:
description: flow args
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ScriptArgs"
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
get:
summary: run flow by version with GET and stream updates via SSE
operationId: runAndStreamFlowByVersionGet
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: version
description: flow version ID
in: path
required: true
schema:
type: integer
format: int64
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/Payload"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
- name: poll_delay_ms
description: delay between polling for job updates in milliseconds
in: query
schema:
type: integer
format: int64
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
/w/{workspace}/jobs/run_and_stream/p/{path}:
post:
summary: run script by path and stream updates via SSE
@@ -7680,6 +7842,64 @@ paths:
type: string
format: uuid
/w/{workspace}/jobs/run/fv/{version}:
post:
summary: run flow by version
operationId: runFlowByVersion
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: version
description: flow version ID
in: path
required: true
schema:
type: integer
format: int64
- name: scheduled_for
description: when to schedule this job (leave empty for immediate run)
in: query
schema:
type: string
format: date-time
- name: scheduled_in_secs
description: schedule the script to execute in the number of seconds starting now
in: query
schema:
type: integer
- $ref: "#/components/parameters/SkipPreprocessor"
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
description: make the run invisible to the the flow owner (default false)
in: query
schema:
type: boolean
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
requestBody:
description: flow args
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ScriptArgs"
responses:
"201":
description: job created
content:
text/plain:
schema:
type: string
format: uuid
/w/{workspace}/jobs/run/batch_rerun_jobs:
post:
summary: re-run multiple jobs
@@ -18835,6 +19055,8 @@ components:
properties:
lock_error_logs:
type: string
version_id:
type: number
ExtraPerms:
type: object
+4 -1
View File
@@ -13,7 +13,10 @@ use anyhow::anyhow;
#[cfg(all(feature = "enterprise", not(feature = "private")))]
use {std::sync::Arc, tokio::sync::RwLock};
#[cfg(not(feature = "private"))]
pub async fn validate_license_key(_license_key: String) -> anyhow::Result<(String, bool)> {
pub async fn validate_license_key(
_license_key: String,
_db: Option<&crate::db::DB>,
) -> anyhow::Result<(String, bool)> {
// Implementation is not open source
Err(anyhow!("License can't be validated in Windmill CE"))
}
+3 -1
View File
@@ -1135,6 +1135,7 @@ async fn get_flow_by_path(
flow.timeout,
flow.visible_to_runner_only,
flow.on_behalf_of_email,
flow_version.id AS version_id,
flow_version.schema,
flow_version.value,
flow_version.created_at AS edited_at,
@@ -1174,8 +1175,9 @@ async fn get_flow_by_path(
flow.timeout,
flow.visible_to_runner_only,
flow.on_behalf_of_email,
flow_version.id AS version_id,
flow_version.schema,
flow_version.value,
flow_version.value,
flow_version.created_at AS edited_at,
flow_version.created_by AS edited_by,
NULL AS starred
File diff suppressed because it is too large Load Diff
@@ -71,10 +71,12 @@ fn is_public_route_whitelisted(path: &str) -> bool {
"/user/login_callback/*",
"/api/workspaces/users",
"/api/users/whoami",
"/api/apps_u/*",
"/api/oauth/list_connects",
"/api/oauth/list_logins",
"/public/*",
"/a/*",
"/api/oauth/get_connect/*",
"/Inter-Variable.woff2",
];
+38 -12
View File
@@ -701,23 +701,49 @@ async fn setup_ducklake_catalog_db_inner(
sslmode = ssl_mode
);
let (client, connection) = tokio::time::timeout(
std::time::Duration::from_secs(20),
tokio_postgres::connect(&conn_str, tokio_postgres::NoTls),
)
.await
.map_err(|e| error::Error::ExecutionErr(format!("timeout: {}", e.to_string())))?
.map_err(|e| error::Error::ExecutionErr(format!("error: {}", e.to_string())))?;
let join_handle = tokio::spawn(async move { connection.await });
let (client, join_handle) = if ssl_mode == "require" {
use native_tls::TlsConnector;
use postgres_native_tls::MakeTlsConnector;
let mut connector = TlsConnector::builder();
connector.danger_accept_invalid_certs(true);
connector.danger_accept_invalid_hostnames(true);
let (client, connection) = tokio::time::timeout(
std::time::Duration::from_secs(20),
tokio_postgres::connect(
&conn_str,
MakeTlsConnector::new(connector.build().map_err(to_anyhow)?),
),
)
.await
.map_err(|e| error::Error::ExecutionErr(format!("timeout: {}", e.to_string())))?
.map_err(|e| error::Error::ExecutionErr(format!("error: {}", e.to_string())))?;
let join_handle = tokio::spawn(async move { connection.await });
(client, join_handle)
} else {
let (client, connection) = tokio::time::timeout(
std::time::Duration::from_secs(20),
tokio_postgres::connect(&conn_str, tokio_postgres::NoTls),
)
.await
.map_err(|e| error::Error::ExecutionErr(format!("timeout: {}", e.to_string())))?
.map_err(|e| error::Error::ExecutionErr(format!("error: {}", e.to_string())))?;
let join_handle = tokio::spawn(async move { connection.await });
(client, join_handle)
};
logs.db_connect = "OK".to_string();
client
.batch_execute(&format!(
"GRANT CONNECT ON DATABASE \"{dbname}\" TO ducklake_user;
GRANT USAGE ON SCHEMA public TO ducklake_user;
GRANT CREATE ON SCHEMA public TO ducklake_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ducklake_user;"
GRANT USAGE ON SCHEMA public TO ducklake_user;
GRANT CREATE ON SCHEMA public TO ducklake_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ducklake_user;"
))
.await
.map_err(|e| {
@@ -9,9 +9,19 @@ use std::collections::HashMap;
use std::future::Future;
use uuid::Uuid;
use windmill_common::{
FlowVersionInfo, db::{UserDB, UserDbWithAuthed}, error::Result, flows::{FlowModuleValue, Retry}, get_latest_deployed_hash_for_path, get_latest_flow_version_info_for_path, jobs::{JobPayload, get_has_preprocessor_from_content_and_lang, script_path_to_payload}, scripts::{ScriptHash, ScriptLang, get_full_hub_script_by_path}, triggers::{
HubOrWorkspaceId, RUNNABLE_FORMAT_VERSION_CACHE, RunnableFormat, RunnableFormatVersion, TriggerInfo, TriggerKind
}, users::username_to_permissioned_as, utils::StripPath, worker::to_raw_value
db::{UserDB, UserDbWithAuthed},
error::Result,
flows::{FlowModuleValue, Retry},
get_latest_deployed_hash_for_path, get_latest_flow_version_info_for_path,
jobs::{get_has_preprocessor_from_content_and_lang, script_path_to_payload, JobPayload},
scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang},
triggers::{
HubOrWorkspaceId, RunnableFormat, RunnableFormatVersion, TriggerInfo, TriggerKind,
RUNNABLE_FORMAT_VERSION_CACHE,
},
users::username_to_permissioned_as,
utils::StripPath,
worker::to_raw_value,
};
use windmill_queue::{push, PushArgs, PushArgsOwned, PushIsolationLevel};
@@ -48,7 +58,7 @@ struct PartialSchema {
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum RunnableId {
FlowPath(String),
FlowId(FlowId),
ScriptId(ScriptId),
HubScript(String),
}
@@ -67,7 +77,33 @@ impl RunnableId {
}
pub fn from_flow_path(path: &str) -> Self {
Self::FlowPath(path.to_string())
Self::FlowId(FlowId::FlowPath(path.to_string()))
}
pub fn from_flow_version(version: i64) -> Self {
Self::FlowId(FlowId::FlowVersion(version))
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum FlowId {
FlowPath(String),
FlowVersion(i64),
}
impl FlowId {
async fn get_flow_version_id(self, workspace_id: &str, db: &DB) -> Result<i64> {
let version_id = match self {
FlowId::FlowPath(path) => {
let info =
get_latest_flow_version_info_for_path(None, db, workspace_id, &path, true)
.await?;
info.version
}
FlowId::FlowVersion(version) => version,
};
Ok(version_id)
}
}
@@ -247,9 +283,8 @@ pub async fn get_runnable_format(
},
)
}
RunnableId::FlowPath(path) => {
let FlowVersionInfo { version, .. } =
get_latest_flow_version_info_for_path(None, &db, workspace_id, &path, true).await?;
RunnableId::FlowId(flow_id) => {
let version = flow_id.get_flow_version_id(workspace_id, db).await?;
let key = (
HubOrWorkspaceId::WorkspaceId(workspace_id.to_string()),
@@ -260,7 +295,7 @@ pub async fn get_runnable_format(
let runnable_format = RUNNABLE_FORMAT_VERSION_CACHE.get(&key);
if let Some(runnable_format) = runnable_format {
tracing::debug!("Using cached runnable format for flow {path}");
tracing::debug!("Using cached runnable format for flow version {version}");
return Ok(runnable_format);
}
@@ -271,11 +306,9 @@ pub async fn get_runnable_format(
schema as \"schema: _\"
FROM flow_version
WHERE
path = $1
AND workspace_id = $2
ORDER BY created_at DESC
LIMIT 1",
path,
id = $1
AND workspace_id = $2",
version,
workspace_id,
)
.fetch_one(db)
@@ -612,8 +645,7 @@ pub async fn trigger_runnable_and_wait_for_result(
)
.await?;
let (result, success) =
run_wait_result_internal(db, uuid, workspace_id.to_string(), early_return, &username)
.await?;
run_wait_result_internal(db, uuid, &workspace_id, early_return, &username).await?;
if delete_after_use.unwrap_or(false) {
delete_job_metadata_after_use(&db, uuid).await?;
@@ -657,7 +689,7 @@ pub async fn trigger_runnable_and_wait_for_raw_result(
.await?;
let (result, success) =
run_wait_result_internal(db, uuid, workspace_id.to_string(), early_return, &username)
run_wait_result_internal(db, uuid, &workspace_id, early_return, &username)
.await
.with_context(|| {
format!(
+1
View File
@@ -66,6 +66,7 @@ pub struct FlowWithStarred {
pub starred: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lock_error_logs: Option<String>,
pub version_id: i64,
}
fn is_none_or_false(b: &Option<bool>) -> bool {
+32 -17
View File
@@ -769,7 +769,7 @@ where
}
}
pub fn get_latest_flow_version_info_for_path_from_version<
pub fn get_flow_version_info_from_version<
'a,
'e,
A: sqlx::Acquire<'e, Database = Postgres> + Send + 'a,
@@ -782,7 +782,6 @@ pub fn get_latest_flow_version_info_for_path_from_version<
async move {
// as instructed in the docstring of sqlx::Acquire
let key = (w_id.to_string(), version);
match FLOW_INFO_CACHE.get(&key) {
Some(info) => {
tracing::debug!("Using cached flow version info for {version} ({path})");
@@ -791,21 +790,37 @@ pub fn get_latest_flow_version_info_for_path_from_version<
_ => {
tracing::debug!("Fetching flow version info for {version} ({path})");
let mut conn = db.acquire().await?;
let info = sqlx::query_as!(
FlowVersionInfo,
"SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, on_behalf_of_email, edited_by, flow_version.id AS version
FROM flow
INNER JOIN flow_version
ON flow_version.id = $3
WHERE flow.path = $1 and flow.workspace_id = $2",
path,
w_id,
version
)
.fetch_optional(&mut *conn)
.await?;
let flow_info =
sqlx::query_as!(
FlowVersionInfo,
r#"
SELECT
flow_version.id AS version,
flow_version.value->>'early_return' as early_return,
flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor,
(flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled,
flow.tag,
flow.dedicated_worker,
flow.on_behalf_of_email,
flow.edited_by
FROM
flow_version
INNER JOIN flow
ON flow.path = flow_version.path AND
flow.workspace_id = flow_version.workspace_id
WHERE
flow_version.workspace_id = $1 AND
flow_version.path = $2 AND
flow_version.id = $3
"#,
w_id,
path,
version,
)
.fetch_optional(&mut *conn)
.await?;
let info = utils::not_found_if_none(info, "flow", path)?;
let info = utils::not_found_if_none(flow_info, "flow", path)?;
FLOW_INFO_CACHE.insert(key, info.clone());
@@ -825,7 +840,7 @@ pub async fn get_latest_flow_version_info_for_path<'e>(
// as instructed in the docstring of sqlx::Acquire
let version =
get_latest_flow_version_id_for_path(db_authed, &db.clone(), w_id, path, use_cache).await?;
get_latest_flow_version_info_for_path_from_version(db, version, w_id, path).await
get_flow_version_info_from_version(db, version, w_id, path).await
}
async fn get_latest_flow_version_for_path<'e, E: sqlx::PgExecutor<'e>>(
+4 -7
View File
@@ -17,8 +17,8 @@ use std::str::FromStr;
use windmill_common::db::Authed;
use windmill_common::ee_oss::LICENSE_KEY_VALID;
use windmill_common::flows::Retry;
use windmill_common::get_flow_version_info_from_version;
use windmill_common::get_latest_flow_version_id_for_path;
use windmill_common::get_latest_flow_version_info_for_path_from_version;
use windmill_common::jobs::check_tag_available_for_workspace_internal;
use windmill_common::jobs::JobPayload;
use windmill_common::jobs::JobTriggerKind;
@@ -65,7 +65,7 @@ async fn get_schedule_metadata<'c>(
.await?;
let FlowVersionInfo { tag, on_behalf_of_email, edited_by, .. } =
get_latest_flow_version_info_for_path_from_version(
get_flow_version_info_from_version(
&mut **tx,
version,
&schedule.workspace_id,
@@ -293,16 +293,13 @@ pub async fn push_scheduled_job<'c>(
let FlowVersionInfo {
version, tag, dedicated_worker, on_behalf_of_email, edited_by, ..
} = get_latest_flow_version_info_for_path_from_version(
} = get_flow_version_info_from_version(
&mut *tx,
version,
&schedule.workspace_id,
&schedule.script_path,
)
.warn_after_seconds_with_sql(
1,
"get_latest_flow_version_info_for_path_from_version".to_string(),
)
.warn_after_seconds_with_sql(1, "get_flow_version_info_from_version".to_string())
.await?;
(
+33 -11
View File
@@ -255,20 +255,42 @@ pub async fn handle_powershell_job(
job.args.as_ref()
};
let args_owned = windmill_parser_bash::parse_powershell_sig(&content)?
let parsed_sig = windmill_parser_bash::parse_powershell_sig(&content)?;
parsed_sig
.args
.iter()
.map(|arg| {
(
arg.name.clone(),
job_args.and_then(|x| x.get(&arg.name).map(|x| raw_to_pwsh_param(x.get()))),
)
})
.collect::<Vec<(String, Option<String>)>>();
.filter_map(|arg| {
let value_opt = job_args.and_then(|x| x.get(&arg.name));
args_owned
.into_iter()
.filter_map(|(n, v)| v.map(|v| format!("-{n} {v}")))
// Check if this is a switch parameter (only [switch], not [bool])
let is_switch = arg.otyp.as_ref().map(|t| {
t.to_lowercase() == "switch"
}).unwrap_or(false);
if is_switch {
// Handle switch parameters: -SwitchName or omit
if let Some(value) = value_opt {
match serde_json::from_str::<serde_json::Value>(value.get()) {
Ok(serde_json::Value::Bool(true)) => {
// Switch is enabled: just pass -SwitchName
Some(format!("-{}", arg.name))
}
Ok(serde_json::Value::Bool(false)) | _ => {
// Switch is disabled or invalid: omit the parameter
None
}
}
} else {
// No value provided, omit the switch (defaults to false)
None
}
} else {
// Regular parameter (including [bool]): format as -ParamName Value
// For [bool] parameters, this will be -ParamName $true or -ParamName $false
value_opt.map(|v| format!("-{} {}", arg.name, raw_to_pwsh_param(v.get())))
}
})
.collect::<Vec<_>>()
.join(" ")
};
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.575.4";
export const VERSION = "v1.576.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+1 -1
View File
@@ -68,7 +68,7 @@ export {
// }
// });
export const VERSION = "1.575.4";
export const VERSION = "1.576.0";
export const WM_FORK_PREFIX = "wm-fork";
+8 -8
View File
@@ -1,14 +1,14 @@
/* tslint:disable */
/* eslint-disable */
export function parse_assets_sql(code: string): string;
export function parse_db_resource(code: string): string | undefined;
export function parse_bash(code: string): string;
export function parse_mssql(code: string): string;
export function parse_oracledb(code: string): string;
export function parse_powershell(code: string): string;
export function parse_bigquery(code: string): string;
export function parse_oracledb(code: string): string;
export function parse_graphql(code: string): string;
export function parse_snowflake(code: string): string;
export function parse_duckdb(code: string): string;
export function parse_sql(code: string): string;
export function parse_assets_sql(code: string): string;
export function parse_mysql(code: string): string;
export function parse_bash(code: string): string;
export function parse_duckdb(code: string): string;
export function parse_db_resource(code: string): string | undefined;
export function parse_bigquery(code: string): string;
export function parse_snowflake(code: string): string;
export function parse_sql(code: string): string;
+81 -81
View File
@@ -68,60 +68,6 @@ function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return decodeText(ptr, len);
}
/**
* @param {string} code
* @returns {string}
*/
export function parse_assets_sql(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_assets_sql(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} code
* @returns {string | undefined}
*/
export function parse_db_resource(code) {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_db_resource(ptr0, len0);
let v2;
if (ret[0] !== 0) {
v2 = getStringFromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
}
return v2;
}
/**
* @param {string} code
* @returns {string}
*/
export function parse_bash(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_bash(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} code
* @returns {string}
@@ -141,25 +87,6 @@ export function parse_mssql(code) {
}
}
/**
* @param {string} code
* @returns {string}
*/
export function parse_oracledb(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_oracledb(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} code
* @returns {string}
@@ -183,13 +110,13 @@ export function parse_powershell(code) {
* @param {string} code
* @returns {string}
*/
export function parse_bigquery(code) {
export function parse_oracledb(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_bigquery(ptr0, len0);
const ret = wasm.parse_oracledb(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
@@ -221,13 +148,51 @@ export function parse_graphql(code) {
* @param {string} code
* @returns {string}
*/
export function parse_snowflake(code) {
export function parse_assets_sql(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_snowflake(ptr0, len0);
const ret = wasm.parse_assets_sql(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} code
* @returns {string}
*/
export function parse_mysql(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_mysql(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} code
* @returns {string}
*/
export function parse_bash(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_bash(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
@@ -255,17 +220,33 @@ export function parse_duckdb(code) {
}
}
/**
* @param {string} code
* @returns {string | undefined}
*/
export function parse_db_resource(code) {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_db_resource(ptr0, len0);
let v2;
if (ret[0] !== 0) {
v2 = getStringFromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
}
return v2;
}
/**
* @param {string} code
* @returns {string}
*/
export function parse_sql(code) {
export function parse_bigquery(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_sql(ptr0, len0);
const ret = wasm.parse_bigquery(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
@@ -278,13 +259,32 @@ export function parse_sql(code) {
* @param {string} code
* @returns {string}
*/
export function parse_mysql(code) {
export function parse_snowflake(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_mysql(ptr0, len0);
const ret = wasm.parse_snowflake(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} code
* @returns {string}
*/
export function parse_sql(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_sql(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
Binary file not shown.
+6 -6
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.575.4",
"version": "1.576.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.575.4",
"version": "1.576.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@@ -79,7 +79,7 @@
"windmill-parser-wasm-nu": "1.510.1",
"windmill-parser-wasm-php": "1.574.1",
"windmill-parser-wasm-py": "1.538.0",
"windmill-parser-wasm-regex": "1.574.1",
"windmill-parser-wasm-regex": "1.575.4",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.558.1",
"windmill-parser-wasm-ts": "1.565.0",
@@ -13759,9 +13759,9 @@
"integrity": "sha512-s+bdIgT/fA5em3zYUwF8D14uA/dZh7iu0krZYZQqZUO7txN37hwSCVfovbMkIwm4zPbsJ50mU8DRLt7UpAPZIw=="
},
"node_modules/windmill-parser-wasm-regex": {
"version": "1.574.1",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.574.1.tgz",
"integrity": "sha512-KnNBnpTGcnBPSzTQHdsjTPJbRQ81iem98eUnMGO8Zt1qgBFJ/eFZPwmUg6TZMbMITzTi0QqRCJj08eQID1FcPg=="
"version": "1.575.4",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.575.4.tgz",
"integrity": "sha512-G7Ni8XxdSKLNew50B1KRmwn/z9qq3obpEDaaDFS7Z74MeUdaBB/TD69SY6RxDL8SZrh0Tl7SCbS/GGLnt1AzkQ=="
},
"node_modules/windmill-parser-wasm-ruby": {
"version": "1.526.1",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.575.4",
"version": "1.576.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -144,7 +144,7 @@
"windmill-parser-wasm-nu": "1.510.1",
"windmill-parser-wasm-php": "1.574.1",
"windmill-parser-wasm-py": "1.538.0",
"windmill-parser-wasm-regex": "1.574.1",
"windmill-parser-wasm-regex": "1.575.4",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.558.1",
"windmill-parser-wasm-ts": "1.565.0",
@@ -33,6 +33,7 @@
import TextInput from './text_input/TextInput.svelte'
import { usePromise } from '$lib/svelte5Utils.svelte'
import { pollJobResult } from './jobs/utils'
import { sameTopDomainOrigin } from '$lib/cookies'
interface Props {
step?: number
@@ -239,7 +240,7 @@
function popupListener(event) {
console.log('Received oauth popup message', event)
let data = event.data
if (event.origin == null || event.origin !== window.location.origin) {
if (!sameTopDomainOrigin(event.origin, window.location.origin)) {
console.log(
'Received oauth popup message from different origin',
event.origin,
@@ -397,6 +398,7 @@
}
window.addEventListener('message', popupListener)
window.addEventListener('storage', handleStorageEvent)
console.log('opening popup', url.toString())
window.open(url.toString(), '_blank', 'popup=true')
step += 1
}
@@ -42,7 +42,7 @@
<Button variant="default" on:click={appConnect?.back ?? (() => {})}>Back</Button>
{/if}
<Button {disabled} on:click={appConnect?.next ?? (() => {})}>
<Button variant="accent" {disabled} on:click={appConnect?.next ?? (() => {})}>
{#if step == 2 && !manual}
Connect
{:else if step == 1}
+7 -3
View File
@@ -19,6 +19,7 @@
import { onDestroy, onMount } from 'svelte'
import Skeleton from './common/skeleton/Skeleton.svelte'
import Button from './common/button/Button.svelte'
import { sameTopDomainOrigin } from '$lib/cookies'
interface Props {
rd?: string | undefined
@@ -212,8 +213,9 @@
function popupListener(event) {
let data = event.data
console.log('popupListener', data, event.origin, window.location.origin)
if (event.origin !== window.location.origin) {
// console.log('popupListener', data, event.origin, window.location.origin)
if (!sameTopDomainOrigin(event.origin, window.location.origin)) {
console.log('popupListener from different origin', event.origin, window.location.origin)
return
}
@@ -261,7 +263,9 @@
console.error('Could not persist redirection to local storage', e)
}
}
let url = base + '/api/oauth/login/' + provider
let url = base + '/api/oauth/login/' + provider + (popup ? '?close=true' : '')
console.log('storeRedirect', popup, url)
if (popup) {
localStorage.setItem('closeUponLogin', 'true')
window.addEventListener('message', popupListener)
@@ -1668,7 +1668,7 @@
args={hasPreprocessor && selectedInputTab !== 'preprocessor' ? {} : args}
isDeployed={savedScript && !savedScript?.draft_only}
schema={script.schema}
hash={script.parent_hash}
runnableVersion={script.parent_hash}
onDeployTrigger={handleDeployTrigger}
/>
@@ -0,0 +1,145 @@
<script lang="ts">
import { User, UserRoundX } from 'lucide-svelte'
import { enterpriseLicense, userStore } from '$lib/stores'
import { base } from '$app/paths'
import { page } from '$app/state'
import Login from '$lib/components/Login.svelte'
import { isCloudHosted } from '$lib/cloud'
import { Alert, Skeleton } from '$lib/components/common'
import { WindmillIcon } from '$lib/components/icons'
import { onMount, setContext } from 'svelte'
import { IS_APP_PUBLIC_CONTEXT_KEY, type EditorBreakpoint } from '../types'
import { UserService, type AppWithLastVersion, type GlobalWhoamiResponse } from '$lib/gen'
import { urlParamsToObject } from '$lib/utils'
import { goto } from '$app/navigation'
import AppPreview from './AppPreview.svelte'
import { twMerge } from 'tailwind-merge'
import { writable } from 'svelte/store'
let {
notExists,
noPermission,
jwtError,
onLoginSuccess,
app,
workspace
}: {
notExists: boolean
noPermission: boolean
jwtError: boolean
onLoginSuccess: () => void
app: (AppWithLastVersion & { value: any }) | undefined
workspace: string | undefined
} = $props()
setContext(IS_APP_PUBLIC_CONTEXT_KEY, true)
const breakpoint = writable<EditorBreakpoint>('lg')
const darkMode =
window.localStorage.getItem('dark-mode') ??
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
if (darkMode === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
let globalUser = $state<GlobalWhoamiResponse | undefined>(undefined)
async function loadGlobalUser() {
try {
globalUser = await UserService.globalWhoami()
} catch (error) {
console.error(error)
}
// const user = await fetch('/api/global/user')
// console.log(user)
}
onMount(() => {
// this is to avoid loading global user if the userStore is set at loading
setTimeout(() => {
if ($userStore) return
loadGlobalUser()
}, 2000)
})
</script>
<div
class="z-50 text-xs fixed bottom-1 right-2 {$enterpriseLicense && !isCloudHosted()
? 'transition-opacity delay-1000 duration-1000 opacity-20 hover:delay-0 hover:opacity-100'
: ''}"
>
<a href="https://windmill.dev" class="whitespace-nowrap text-primary inline-flex items-center"
>Powered by &nbsp;<WindmillIcon />&nbsp;Windmill</a
>
</div>
{#snippet userInfo(child)}
<div class="flex gap-1 items-center"><User size={14} />{child}</div>
{/snippet}
<div class="z-50 text-2xs text-primary absolute top-3 left-2"
>{#if $userStore}
{@render userInfo($userStore.username)}
{:else if globalUser}
{@render userInfo(globalUser.email)}
{:else}<UserRoundX size={14} />{/if}
</div>
{#if notExists}
<div class="px-4 mt-20"
><Alert type="error" title="Not found"
>There was an error loading the app, is the url correct? <a href={base}>Go to Windmill</a>
</Alert></div
>
{:else if noPermission}
<div class="px-4 mt-20 w-full text-center font-bold text-xl"> This app requires read access </div>
<div class="text-center mt-8 text-sm text-primary">
{#if $userStore}You are logged in but have no read access to this app{:else if globalUser && workspace}
You are logged in but are not a member of the workspace <span class="text-xl font-bold"
>{workspace}</span
> this app is part of
{:else}You must be logged in and have read access to this app{/if}</div
>
<div class="px-2 mx-auto mt-20 max-w-xl w-full">
{#if !jwtError}
<Login {onLoginSuccess} popup rd={page.url.toString()} />
{/if}
</div>
{:else if app}
{#key app}
<div
class={twMerge(
'min-h-screen h-full w-full flex',
app?.value?.['css']?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={app?.value?.['css']?.['app']?.['viewer']?.style}
>
<AppPreview
noBackend={false}
context={{
email: $userStore?.email,
name: $userStore?.name,
groups: $userStore?.groups,
username: $userStore?.username,
query: urlParamsToObject(page.url.searchParams),
hash: page.url.hash.substring(1)
}}
{workspace}
summary={app.summary}
app={app.value}
appPath={app.path}
{breakpoint}
policy={app.policy}
isEditor={false}
replaceStateFn={(path) => goto(path)}
gotoFn={(path, opt) => goto(path, opt)}
/>
</div>
{/key}
{:else}
<Skeleton layout={[[4], 0.5, [50]]} />
{/if}
@@ -41,7 +41,7 @@
newItem?: boolean
currentPath: string
fakeInitialPath?: string
hash?: string | undefined
runnableVersion?: string | undefined
args?: Record<string, any>
initialPath: string
isFlow: boolean
@@ -59,7 +59,7 @@
newItem = false,
currentPath,
fakeInitialPath = '',
hash = undefined,
runnableVersion = undefined,
args = {},
initialPath,
isFlow,
@@ -363,7 +363,7 @@
{initialPath}
{fakeInitialPath}
{currentPath}
{hash}
{runnableVersion}
{isDeployed}
small={useVerticalTriggerBar}
{args}
@@ -416,7 +416,7 @@
{hasPreprocessor}
{canHavePreprocessor}
args={config}
data={{ args, hash, emailDomain }}
data={{ args, hash: !isFlow ? runnableVersion : undefined, emailDomain }}
{isValid}
triggerDeployed={!triggersState.selectedTrigger.isDraft}
on:applyArgs
@@ -24,7 +24,7 @@
initialPath: string
fakeInitialPath: string
currentPath: string
hash?: string
runnableVersion?: string
isDeployed: boolean
small: boolean
args: Record<string, any>
@@ -45,7 +45,7 @@
initialPath,
fakeInitialPath,
currentPath,
hash,
runnableVersion,
small,
args,
newItem,
@@ -68,7 +68,7 @@
<WebhooksPanel
{isFlow}
path={initialPath || fakeInitialPath}
{hash}
{runnableVersion}
token=""
{args}
scopes={isFlow ? [`jobs:run:flows:${currentPath}`] : [`jobs:run:scripts:${currentPath}`]}
@@ -80,7 +80,7 @@
scopes={isFlow ? [`jobs:run:flows:${currentPath}`] : [`jobs:run:scripts:${currentPath}`]}
path={initialPath || fakeInitialPath}
{isFlow}
{hash}
runnableVersion={!isFlow ? runnableVersion : undefined}
{onEmailDomain}
/>
{:else if selectedTrigger.type === 'schedule'}
@@ -14,12 +14,12 @@
import UserSettings from '$lib/components/UserSettings.svelte'
import TextInput from '$lib/components/text_input/TextInput.svelte'
let requestType: 'hash' | 'path' = $state('path')
let requestType: 'runnableVersion' | 'path' = $state('path')
function emailAddress() {
const pathOrHash = requestType === 'hash' ? hash : path.replaceAll('/', '.')
const pathOrHash = requestType === 'runnableVersion' ? runnableVersion : path.replaceAll('/', '.')
const plainPrefix = `${$workspaceStore}+${
(requestType === 'hash' ? 'hash.' : isFlow ? 'flow.' : '') + pathOrHash
(requestType === 'runnableVersion' ? 'hash.' : isFlow ? 'flow.' : '') + pathOrHash
}+${token}`
const encodedPrefix = base32
.stringify(new TextEncoder().encode(plainPrefix), {
@@ -32,7 +32,7 @@
interface Props {
token?: string
isFlow?: boolean
hash?: string | undefined
runnableVersion?: string | undefined
path: string
userSettings: UserSettings
emailDomain?: string | null
@@ -42,7 +42,7 @@
let {
token = $bindable(''),
isFlow = false,
hash = undefined,
runnableVersion = undefined,
path,
userSettings,
emailDomain = null,
@@ -90,7 +90,7 @@
<ToggleButtonGroup class="w-auto" bind:selected={requestType}>
{#snippet children({ item })}
<ToggleButton label="By path" value="path" {item} />
<ToggleButton label="By hash" value="hash" {item} />
<ToggleButton label="By hash" value="runnableVersion" {item} />
{/snippet}
</ToggleButtonGroup>
</div>
@@ -16,7 +16,7 @@
token: string
scopes?: string[]
isFlow?: boolean
hash?: string | undefined
runnableVersion?: string | undefined
path: string
onEmailDomain: (domain: string) => void
}
@@ -25,7 +25,7 @@
token = $bindable(),
scopes = [],
isFlow = false,
hash = undefined,
runnableVersion = undefined,
path,
onEmailDomain
}: Props = $props()
@@ -70,7 +70,7 @@
<Skeleton layout={[[18]]} />
{:else}
{#if emailDomain}
<DefaultEmailConfigSection {hash} {token} {path} {isFlow} {userSettings} {emailDomain} />
<DefaultEmailConfigSection {runnableVersion} {token} {path} {isFlow} {userSettings} {emailDomain} />
{:else}
<div>
<Alert title="Email triggers are disabled" size="xs" type="warning">
@@ -26,7 +26,7 @@
interface Props {
isFlow?: boolean
path?: string
hash?: string | undefined
runnableVersion?: string | undefined
token?: string
runnableArgs: any
triggerTokens?: TriggerTokens | undefined
@@ -36,46 +36,38 @@
let {
isFlow = false,
path = '',
hash = undefined,
runnableVersion = undefined,
token = $bindable(''),
runnableArgs,
triggerTokens = $bindable(undefined),
scopes = []
}: Props = $props()
let webhooks: {
async: {
get: {}
post: {
hash?: string
path: string
}
const WEBHOOK_BASE_URL = `${location.origin}${base}/api/w/${$workspaceStore}/jobs`
let baseWebhookUrl = $derived.by(() => {
let webhookUrlPath: string
if (isFlow) {
webhookUrlPath = runnableId == 'path' ? `f/${path}` : `fv/${runnableVersion}`
} else {
webhookUrlPath = runnableId == 'path' ? `p/${path}` : `h/${runnableVersion}`
}
sync: {
get: {
path: string
}
post: {
hash?: string
path: string
}
if (requestType == 'async') {
return `${WEBHOOK_BASE_URL}/run/${webhookUrlPath}`
} else if (requestType == 'sync') {
return `${WEBHOOK_BASE_URL}/run_wait_result/${webhookUrlPath}`
} else {
return `${WEBHOOK_BASE_URL}/run_and_stream/${webhookUrlPath}`
}
sync_sse: {
get: {
hash?: string
path: string
}
post: {
hash?: string
path: string
}
}
} = $derived(isFlow ? computeFlowWebhooks(path) : computeScriptWebhooks(hash, path))
})
let selectedTab: string = $state('rest')
let userSettings: UserSettings | undefined = $state()
let requestType = $state(DEFAULT_WEBHOOK_TYPE) as 'async' | 'sync' | 'sync_sse'
let callMethod = $state('post') as 'get' | 'post'
let runnableId = $state('path') as 'hash' | 'path'
let runnableId = $state('path') as 'runnableVersion' | 'path'
let tokenType = $state('headers') as 'query' | 'headers'
$effect(() => {
@@ -91,7 +83,7 @@
: runnableArgs
})
let url: string = $derived(
webhooks[requestType][callMethod][runnableId] +
baseWebhookUrl +
(tokenType === 'query'
? `?token=${token}${
callMethod === 'get' || requestType === 'sync_sse'
@@ -105,70 +97,6 @@
}`)
)
function computeScriptWebhooks(hash: string | undefined, path: string) {
let webhookBase = `${location.origin}${base}/api/w/${$workspaceStore}/jobs`
return {
async: {
get: {},
post: {
hash: `${webhookBase}/run/h/${hash}`,
path: `${webhookBase}/run/p/${path}`
}
},
sync: {
get: {
path: `${webhookBase}/run_wait_result/p/${path}`
},
post: {
hash: `${webhookBase}/run_wait_result/h/${hash}`,
path: `${webhookBase}/run_wait_result/p/${path}`
}
},
sync_sse: {
get: {
path: `${webhookBase}/run_and_stream/p/${path}`,
hash: `${webhookBase}/run_and_stream/h/${hash}`
},
post: {
hash: `${webhookBase}/run_and_stream/h/${hash}`,
path: `${webhookBase}/run_and_stream/p/${path}`
}
}
}
}
function computeFlowWebhooks(path: string) {
let webhooksBase = `${location.origin}${base}/api/w/${$workspaceStore}/jobs`
let urlAsync = `${webhooksBase}/run/f/${path}`
let urlSync = `${webhooksBase}/run_wait_result/f/${path}`
let urlStream = `${webhooksBase}/run_and_stream/f/${path}`
return {
async: {
get: {},
post: {
path: urlAsync
}
},
sync: {
get: {
path: urlSync
},
post: {
path: urlSync
}
},
sync_sse: {
get: {
path: urlStream
},
post: {
path: urlStream
}
}
}
}
function headers() {
const headers = {}
if (callMethod === 'post') {
@@ -433,16 +361,19 @@ done`
{/snippet}
</ToggleButtonGroup>
</Label>
{#if !isFlow}
<Label label="Reference type">
<ToggleButtonGroup bind:selected={runnableId}>
{#snippet children({ item })}
<ToggleButton label="Path" value="path" {item} />
<ToggleButton label="Hash" value="hash" disabled={!hash} {item} />
{/snippet}
</ToggleButtonGroup>
</Label>
{/if}
<Label label="Reference type">
<ToggleButtonGroup bind:selected={runnableId}>
{#snippet children({ item })}
<ToggleButton label="Path" value="path" {item} />
<ToggleButton
label={isFlow ? 'Flow version' : 'Hash'}
value="runnableVersion"
disabled={!runnableVersion}
{item}
/>
{/snippet}
</ToggleButtonGroup>
</Label>
<Label label="Token configuration">
<ToggleButtonGroup bind:selected={tokenType}>
{#snippet children({ item })}
@@ -10,7 +10,7 @@
args?: Record<string, any>
scopes?: string[]
isFlow?: boolean
hash?: string | undefined
runnableVersion?: string | undefined
path: string
newItem?: boolean
}
@@ -20,7 +20,7 @@
args = {},
scopes = [],
isFlow = false,
hash = undefined,
runnableVersion = undefined,
path,
newItem = false
}: Props = $props()
@@ -39,5 +39,5 @@
{isFlow ? 'flow' : 'script'}.
</Alert>
{/if}
<WebhooksConfigSection {isFlow} {path} {hash} {token} runnableArgs={args} {scopes} />
<WebhooksConfigSection {isFlow} {path} {runnableVersion} {token} runnableArgs={args} {scopes} />
</Section>
+29
View File
@@ -0,0 +1,29 @@
/**
* Reads the value of a cookie by name.
* @param {string} name - The name of the cookie to retrieve.
* @returns {string | undefined} The cookie value, or undefined if not found.
*/
export function getCookie(name: string): string | undefined {
const match = document.cookie.match(
new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()\[\]\\\/\+^])/g, '\\$1') + '=([^;]*)')
)
return match ? decodeURIComponent(match[1]) : undefined
}
// this only check the last 2 segments to work for popup on shared top-domain
export function sameTopDomainOrigin(origin: string | null, desktopOrigin: string): boolean {
if (origin == null) {
return false
}
const getLastTwoSegments = (url: string) => {
const parts = url.split('.');
return parts.length >= 2 ? parts.slice(-2).join('.') : url;
};
if (origin.includes('.') && desktopOrigin.includes('.')) {
return getLastTwoSegments(origin) === getLastTwoSegments(desktopOrigin);
} else {
return origin === desktopOrigin;
}
}
@@ -675,6 +675,7 @@
{#if flow}
<TriggersEditor
{args}
runnableVersion={flow.version_id?.toString()}
initialPath={flow.path}
currentPath={flow.path}
noEditor={true}
@@ -791,7 +791,7 @@
{#if script}
<TriggersEditor
{args}
hash={script.hash}
runnableVersion={script.hash}
initialPath={script.path}
currentPath={script.path}
noEditor={true}
+17 -105
View File
@@ -1,33 +1,21 @@
<script lang="ts">
import { BROWSER } from 'esm-env'
import { base } from '$lib/base'
import AppPreview from '$lib/components/apps/editor/AppPreview.svelte'
import { IS_APP_PUBLIC_CONTEXT_KEY, type EditorBreakpoint } from '$lib/components/apps/types'
import { Alert, Skeleton } from '$lib/components/common'
import { WindmillIcon } from '$lib/components/icons'
import { AppService, OpenAPI, type AppWithLastVersion } from '$lib/gen'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { twMerge } from 'tailwind-merge'
import { userStore, workspaceStore } from '$lib/stores'
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
import { setLicense } from '$lib/enterpriseUtils'
import { isCloudHosted } from '$lib/cloud'
import Login from '$lib/components/Login.svelte'
import { getUserExt } from '$lib/user'
import { User, UserRoundX } from 'lucide-svelte'
import { goto } from '$app/navigation'
import { sendUserToast } from '$lib/toast'
import { page } from '$app/state'
import { urlParamsToObject } from '$lib/utils'
import PublicApp from '$lib/components/apps/editor/PublicApp.svelte'
let app: (AppWithLastVersion & { value: any }) | undefined = $state(undefined)
let notExists = $state(false)
let noPermission = $state(false)
let jwtError = $state(false)
setContext(IS_APP_PUBLIC_CONTEXT_KEY, true)
function isJwt(t: string) {
// simply check that the first part is a valid base64 encoded json
@@ -56,9 +44,10 @@
}
}
const parsedCustomPath = parseCustomPath(page.params.path ?? '')
let workspace: string | undefined = $state(undefined)
async function loadApp() {
const parsedCustomPath = parseCustomPath(page.params.path ?? '')
if (parsedCustomPath.jwt) {
const token = 'jwt_ext_' + parsedCustomPath.jwt
OpenAPI.TOKEN = token
@@ -69,6 +58,7 @@
app = await AppService.getPublicAppByCustomPath({
customPath: parsedCustomPath.path
})
workspace = app.workspace_id
workspaceStore.set(app.workspace_id)
noPermission = false
notExists = false
@@ -95,93 +85,15 @@
setLicense()
loadApp()
}
const breakpoint = writable<EditorBreakpoint>('lg')
const darkMode =
window.localStorage.getItem('dark-mode') ??
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
if (darkMode === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
</script>
<div
class="z-50 text-xs fixed bottom-1 right-2 {$enterpriseLicense && !isCloudHosted()
? 'transition-opacity delay-1000 duration-1000 opacity-20 hover:delay-0 hover:opacity-100'
: ''}"
>
<a href="https://windmill.dev" class="whitespace-nowrap text-primary inline-flex items-center"
>Powered by &nbsp;<WindmillIcon />&nbsp;Windmill</a
>
</div>
<div class="z-50 text-2xs text-primary absolute top-3 left-2"
>{#if $userStore}
<div class="flex gap-1 items-center"><User size={14} />{$userStore.username}</div>
{:else}<UserRoundX size={14} />{/if}
</div>
{#if notExists}
<div class="px-4 mt-20"
><Alert type="error" title="Not found"
>There was an error loading the app, is the url correct? <a href={base}>Go to Windmill</a>
</Alert></div
>
{:else if noPermission}
<div class="px-4 mt-20 w-full text-center font-bold text-xl"
>{#if $userStore}You are logged in but have no read access to this app{:else}You must be logged
in and have read access to this app{/if}</div
>
<div class="px-2 mx-auto mt-20 max-w-xl w-full">
{#if !jwtError}
<Login
onLoginSuccess={() => {
// window.location.reload()
loadApp()
app = app
}}
popup
rd={page.url.toString()}
/>
{/if}
</div>
{:else if app}
{#key app}
<div
class={twMerge(
'min-h-screen h-full w-full flex',
app?.value?.['css']?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={app?.value?.['css']?.['app']?.['viewer']?.style}
>
<AppPreview
noBackend={false}
context={{
email: $userStore?.email,
name: $userStore?.name,
groups: $userStore?.groups,
username: $userStore?.username,
query: urlParamsToObject(page.url.searchParams),
hash: page.url.hash.substring(1)
}}
workspace={page.params.workspace}
summary={app.summary}
app={app.value}
appPath={app.path}
{breakpoint}
policy={app.policy}
isEditor={false}
replaceStateFn={(path) => goto(path)}
gotoFn={(path, opt) => goto(path, opt)}
/>
</div>
{/key}
{:else}
<Skeleton layout={[[4], 0.5, [50]]} />
{/if}
<PublicApp
{workspace}
{notExists}
{noPermission}
{jwtError}
{app}
onLoginSuccess={() => {
loadApp()
}}
></PublicApp>
@@ -1,34 +1,22 @@
<script lang="ts">
import { BROWSER } from 'esm-env'
import { base } from '$lib/base'
import AppPreview from '$lib/components/apps/editor/AppPreview.svelte'
import { IS_APP_PUBLIC_CONTEXT_KEY, type EditorBreakpoint } from '$lib/components/apps/types'
import { WindmillIcon } from '$lib/components/icons'
import { AppService, OpenAPI, type AppWithLastVersion } from '$lib/gen'
import { enterpriseLicense, userStore } from '$lib/stores'
import { twMerge } from 'tailwind-merge'
import { userStore } from '$lib/stores'
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
import { setLicense } from '$lib/enterpriseUtils'
import { isCloudHosted } from '$lib/cloud'
import Login from '$lib/components/Login.svelte'
import { getUserExt } from '$lib/user'
import { User, UserRoundX } from 'lucide-svelte'
import { goto } from '$app/navigation'
import { sendUserToast } from '$lib/toast'
import Alert from '$lib/components/common/alert/Alert.svelte'
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
import { page } from '$app/state'
import { urlParamsToObject } from '$lib/utils'
import PublicApp from '$lib/components/apps/editor/PublicApp.svelte'
let app: (AppWithLastVersion & { value: any }) | undefined = $state(undefined)
let notExists = $state(false)
let noPermission = $state(false)
let jwtError = $state(false)
setContext(IS_APP_PUBLIC_CONTEXT_KEY, true)
function parseSecret(secret: string): { secret: string; jwt: string } {
const parts = secret.split('/')
@@ -59,6 +47,11 @@
if (BROWSER) {
setLicense()
loadAll()
}
function loadAll() {
console.log('loadAll')
loadUser().then(() => {
loadApp()
})
@@ -81,95 +74,15 @@
console.warn('Anonymous user')
}
}
const breakpoint = writable<EditorBreakpoint>('lg')
const darkMode =
window.localStorage.getItem('dark-mode') ??
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
if (darkMode === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
</script>
<div
class="z-50 text-xs fixed bottom-1 right-2 {$enterpriseLicense && !isCloudHosted()
? 'transition-opacity delay-1000 duration-1000 opacity-20 hover:delay-0 hover:opacity-100'
: ''}"
>
<a href="https://windmill.dev" class="whitespace-nowrap text-primary inline-flex items-center"
>Powered by &nbsp;<WindmillIcon />&nbsp;Windmill</a
>
</div>
<div class="z-50 text-2xs text-primary absolute top-3 left-2"
>{#if $userStore}
<div class="flex gap-1 items-center"><User size={14} />{$userStore.username}</div>
{:else}<UserRoundX size={14} />{/if}
</div>
{#if notExists}
<div class="px-4 mt-20"
><Alert type="error" title="Not found"
>There was an error loading the app, is the url correct? <a href={base}>Go to Windmill</a>
</Alert></div
>
{:else if noPermission}
<div class="px-4 mt-20 w-full text-center font-bold text-xl"
>{#if $userStore}You are logged in but have no read access to this app{:else}You must be logged
in and have read access to this app{/if}</div
>
<div class="px-2 mx-auto mt-20 max-w-xl w-full">
{#if !jwtError}
<Login
onLoginSuccess={() => {
console.log('login')
// window.location.reload()
loadUser().then(() => {
loadApp()
})
app = app
}}
popup
rd={page.url.toString()}
/>
{/if}
</div>
{:else if app}
{#key app}
<div
class={twMerge(
'min-h-screen h-full w-full flex',
app?.value?.['css']?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={app?.value?.['css']?.['app']?.['viewer']?.style}
>
<AppPreview
noBackend={false}
context={{
email: $userStore?.email,
name: $userStore?.name,
groups: $userStore?.groups,
username: $userStore?.username,
query: urlParamsToObject(page.url.searchParams),
hash: page.url.hash.substring(1)
}}
workspace={page.params.workspace}
summary={app.summary}
app={app.value}
appPath={app.path}
{breakpoint}
policy={app.policy}
isEditor={false}
replaceStateFn={(path) => goto(path)}
gotoFn={(path, opt) => goto(path, opt)}
/>
</div>
{/key}
{:else}
<Skeleton layout={[[4], 0.5, [50]]} />
{/if}
<PublicApp
{app}
workspace={page.params.workspace}
{notExists}
{noPermission}
{jwtError}
onLoginSuccess={() => {
loadAll()
}}
></PublicApp>
@@ -12,6 +12,8 @@
import { parseQueryParams } from '$lib/utils'
import { page } from '$app/state'
import { isCloudHosted } from '$lib/cloud'
import { getCookie } from '$lib/cookies'
// import { getAndDeleteCookie } from '$lib/cookies'
let error = page.url.searchParams.get('error')
let clientName = page.params.client_name ?? ''
@@ -19,11 +21,14 @@
let state = page.url.searchParams.get('state') ?? undefined
onMount(async () => {
// const closeCookie = getAndDeleteCookie('close')
// console.log('closeCookie', closeCookie)
const rd = localStorage.getItem('rd')
if (rd) {
localStorage.removeItem('rd')
}
const closeUponLogin = localStorage.getItem('closeUponLogin') == 'true'
const cookieCloseUponLogin = getCookie('close') == 'true'
const closeUponLogin = cookieCloseUponLogin ?? localStorage.getItem('closeUponLogin') == 'true'
if (error) {
sendUserToast(`Error trying to login with ${clientName} ${error}`, true)
if (closeUponLogin) {
+10 -2
View File
@@ -11,7 +11,15 @@ const version = JSON.parse(json)
const config = {
server: {
https: process.env.HTTPS === 'true',
allowedHosts: ['localhost', '127.0.0.1', '0.0.0.0', 'rubendev.wimill.xyz', 'windmill.xyz'],
allowedHosts: [
'localhost',
'127.0.0.1',
'0.0.0.0',
'rubendev.wimill.xyz',
'windmill.xyz',
'app.windmill.xyz',
'public.windmill.xyz'
],
port: 3000,
proxy: {
'^/api/w/[^/]+/s3_proxy/.*': {
@@ -64,7 +72,7 @@ const config = {
exclude: [
'@codingame/monaco-vscode-standalone-typescript-language-features',
'@codingame/monaco-vscode-standalone-languages'
],
]
},
worker: {
format: 'es'
+2 -2
View File
@@ -4,8 +4,8 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.575.4"
wmill_pg = ">=1.575.4"
wmill = ">=1.576.0"
wmill_pg = ">=1.576.0"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.575.4
version: 1.576.0
title: OpenFlow Spec
contact:
name: Ruben Fiszel
@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.575.4'
ModuleVersion = '1.576.0'
# Supported PSEditions
# CompatiblePSEditions = @()
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.575.4"
version = "1.576.0"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill-pg"
version = "1.575.4"
version = "1.576.0"
description = "An extension client for the wmill client library focused on pg"
license = "Apache-2.0"
homepage = "https://windmill.dev"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.575.4",
"version": "1.576.0",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./client.ts"]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.575.4",
"version": "1.576.0",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"devDependencies": {
+1 -1
View File
@@ -1 +1 @@
1.575.4
1.576.0