mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-20 08:02:29 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81ab777cbb | ||
|
|
61a867f086 | ||
|
|
8581a3300d | ||
|
|
ff5fa9f64f | ||
|
|
55e8a5cff1 | ||
|
|
1a39bd538d | ||
|
|
1049c1026b | ||
|
|
619ebb65ce | ||
|
|
d2d6810db9 | ||
|
|
39af1b75af | ||
|
|
d569e9e29c | ||
|
|
381011a4a8 | ||
|
|
f0437eba19 | ||
|
|
d8edaad99c | ||
|
|
7fd0bf974d | ||
|
|
6a5cfbc159 | ||
|
|
8c3c97f7a6 |
@@ -1,5 +1,32 @@
|
||||
# Changelog
|
||||
|
||||
## [1.673.0](https://github.com/windmill-labs/windmill/compare/v1.672.0...v1.673.0) (2026-04-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add endpoint to restart workers in a worker group ([#8659](https://github.com/windmill-labs/windmill/issues/8659)) ([f0437eb](https://github.com/windmill-labs/windmill/commit/f0437eba1925a9aa4c430008027d637a0c89ee39))
|
||||
* add Entra ID (Azure Workload Identity) database auth ([#8526](https://github.com/windmill-labs/windmill/issues/8526)) ([6a5cfbc](https://github.com/windmill-labs/windmill/commit/6a5cfbc159a0ad7925fd7ce5eefc8eaa21bbb70b))
|
||||
* add LIMIT_WINDOWS_TO_1CU env var for Windows worker memory limits ([#8681](https://github.com/windmill-labs/windmill/issues/8681)) ([d2d6810](https://github.com/windmill-labs/windmill/commit/d2d6810db954114f3333853bd3476cb8fc735f92))
|
||||
* restore bun for dedicated workers, fix dispatch & serialization, cross-workspace deps ([#8645](https://github.com/windmill-labs/windmill/issues/8645)) ([619ebb6](https://github.com/windmill-labs/windmill/commit/619ebb65ce8dce8264add31c3147919802a8286a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add HMAC signature verification to Slack interactive callback endpoint ([#8611](https://github.com/windmill-labs/windmill/issues/8611)) ([55e8a5c](https://github.com/windmill-labs/windmill/commit/55e8a5cff1f185b1dbd332d37b877972efa1ed7d))
|
||||
* correct raw app flow inputs ([#8667](https://github.com/windmill-labs/windmill/issues/8667)) ([28c0730](https://github.com/windmill-labs/windmill/commit/28c073056c65d4ed1600e39679497e5af964347f))
|
||||
* pass selected language to AI agent when generating flow scripts ([#8680](https://github.com/windmill-labs/windmill/issues/8680)) ([381011a](https://github.com/windmill-labs/windmill/commit/381011a4a8e48454e9c146c64db502293e646b99))
|
||||
* poll for preview results to avoid undici headers timeout ([#8682](https://github.com/windmill-labs/windmill/issues/8682)) ([ff5fa9f](https://github.com/windmill-labs/windmill/commit/ff5fa9f64fe4aaf33e06b20f02373894b5df0f95))
|
||||
* pre-fix trigger edited_by for superadmins not in workspace ([#8669](https://github.com/windmill-labs/windmill/issues/8669)) ([350ffdc](https://github.com/windmill-labs/windmill/commit/350ffdce297ba5b84f9dd247eede6da0c6b0956c))
|
||||
* resolve race condition where flow sync push reverts to stale version ([#8673](https://github.com/windmill-labs/windmill/issues/8673)) ([d569e9e](https://github.com/windmill-labs/windmill/commit/d569e9e29c588243a90b1cd25f866efb0d178640))
|
||||
* respect disabled fields in JSON input mode ([#8663](https://github.com/windmill-labs/windmill/issues/8663)) ([7fd0bf9](https://github.com/windmill-labs/windmill/commit/7fd0bf974d2ba2644bb01dd5e9ddc84749e166f5))
|
||||
* Run typed pg queries in a single protocol conversation ([#8679](https://github.com/windmill-labs/windmill/issues/8679)) ([8581a33](https://github.com/windmill-labs/windmill/commit/8581a3300d056040b7e3ab77d629c74f034c9c97))
|
||||
* sanitize MCP tool schemas for JSON Schema draft 2020-12 compliance ([#8666](https://github.com/windmill-labs/windmill/issues/8666)) ([8c3c97f](https://github.com/windmill-labs/windmill/commit/8c3c97f7a670d47019cc666219f8187f48499672))
|
||||
* skip generate-metadata confirmation prompt in non-interactive CI ([#8678](https://github.com/windmill-labs/windmill/issues/8678)) ([39af1b7](https://github.com/windmill-labs/windmill/commit/39af1b75afc8458f85dec4fe51dfaed3d0cb000d))
|
||||
* strip f/ prefix from folder paths when deploying from workspace forks ([#8662](https://github.com/windmill-labs/windmill/issues/8662)) ([7ab0ea5](https://github.com/windmill-labs/windmill/commit/7ab0ea581d349fbfdb56d22cf9903a90efa045bb))
|
||||
* support branch-specific folder.meta.yaml in missing-meta check ([#8661](https://github.com/windmill-labs/windmill/issues/8661)) ([c87a6a0](https://github.com/windmill-labs/windmill/commit/c87a6a0f2c1346bf5e21f128d32d89bdca039243))
|
||||
* validate rd redirect on login with same rules as logout ([#8655](https://github.com/windmill-labs/windmill/issues/8655)) ([bcce627](https://github.com/windmill-labs/windmill/commit/bcce62738791a4e9b9f4dbc64731eef163230172))
|
||||
|
||||
## [1.672.0](https://github.com/windmill-labs/windmill/compare/v1.671.0...v1.672.0) (2026-04-01)
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -34,7 +34,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -76,7 +76,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -33,7 +33,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -43,7 +43,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE flow SET versions = array_append(versions, $1) WHERE path = $2 AND workspace_id = $3 AND (versions[array_upper(versions, 1)] IS DISTINCT FROM $1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "32f26d69b15a74fc96dd585f9eb3c998447ca8b97894bb1a2b238f984c1dc588"
|
||||
}
|
||||
+2
-1
@@ -33,7 +33,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -44,7 +44,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -70,7 +70,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -34,7 +34,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -40,7 +40,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
+2
-1
@@ -148,7 +148,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -43,7 +43,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO notify_event (channel, payload) VALUES ('restart_worker_group', $1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "78f7c4efd7be2aad03f0ec67ab882e1f9aaa7141ab75046a71ac789ab659bdc6"
|
||||
}
|
||||
+2
-1
@@ -76,7 +76,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -41,7 +41,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -153,7 +153,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -118,7 +118,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -83,7 +83,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -133,7 +133,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -38,7 +38,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -153,7 +153,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -123,7 +123,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -43,7 +43,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -40,7 +40,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -77,7 +77,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value AS \"value!: Json<Box<RawValue>>\" FROM flow WHERE path = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value!: Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c569535d4aded82ffef5e5066e24948c8fcdb8d36913775449b982782c8583e9"
|
||||
}
|
||||
+2
-1
@@ -68,7 +68,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -77,7 +77,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -133,7 +133,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -40,7 +40,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -38,7 +38,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -84,7 +85,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -33,7 +33,8 @@
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+170
-126
@@ -2108,7 +2108,7 @@ dependencies = [
|
||||
"rayon",
|
||||
"safetensors",
|
||||
"thiserror 2.0.18",
|
||||
"yoke 0.8.1",
|
||||
"yoke 0.8.2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@@ -2373,9 +2373,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cmov"
|
||||
version = "0.5.2"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de0758edba32d61d1fd9f4d69491b47604b91ee2f7e6b33de7e54ca4ebe55dc3"
|
||||
checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746"
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
@@ -2805,9 +2805,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctutils"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1005a6d4446f5120ef475ad3d2af2b30c49c2c9c6904258e3bb30219bebed5e4"
|
||||
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
|
||||
dependencies = [
|
||||
"cmov",
|
||||
]
|
||||
@@ -7224,22 +7224,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
|
||||
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"yoke 0.8.1",
|
||||
"utf8_iter",
|
||||
"yoke 0.8.2",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
|
||||
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
@@ -7250,9 +7251,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
|
||||
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
@@ -7264,15 +7265,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
|
||||
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.1.2"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
|
||||
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
@@ -7284,20 +7285,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.1.2"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
|
||||
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
|
||||
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"writeable",
|
||||
"yoke 0.8.1",
|
||||
"yoke 0.8.2",
|
||||
"zerofrom",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
@@ -8046,9 +8047,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
version = "0.2.184"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||
|
||||
[[package]]
|
||||
name = "libffi"
|
||||
@@ -8204,9 +8205,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
@@ -10427,9 +10428,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
|
||||
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
@@ -12117,6 +12118,15 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71"
|
||||
|
||||
[[package]]
|
||||
name = "scc"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc"
|
||||
dependencies = [
|
||||
"sdd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.29"
|
||||
@@ -12222,6 +12232,12 @@ dependencies = [
|
||||
"untrusted 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sdd"
|
||||
version = "3.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
|
||||
|
||||
[[package]]
|
||||
name = "seahash"
|
||||
version = "4.1.0"
|
||||
@@ -12558,6 +12574,32 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial_test"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f"
|
||||
dependencies = [
|
||||
"futures-executor",
|
||||
"futures-util",
|
||||
"log",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"scc",
|
||||
"serial_test_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial_test_derive"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
@@ -14235,9 +14277,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
||||
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
@@ -14625,9 +14667,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39ca317ebc49f06bd748bfba29533eac9485569dc9bf80b849024b025e814fb9"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
dependencies = [
|
||||
"winnow 1.0.1",
|
||||
]
|
||||
@@ -15941,7 +15983,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -15970,6 +16012,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_yml",
|
||||
"serial_test",
|
||||
"sha1",
|
||||
"sha2 0.10.9",
|
||||
"sql-builder",
|
||||
@@ -16019,7 +16062,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-alerting"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16032,7 +16075,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -16173,7 +16216,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-agent-workers"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16196,7 +16239,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-assets"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16209,7 +16252,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-auth"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16235,7 +16278,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
@@ -16245,7 +16288,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-configs"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16262,7 +16305,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-debug"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"base64 0.22.1",
|
||||
@@ -16285,7 +16328,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-embeddings"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16308,7 +16351,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flow-conversations"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16324,7 +16367,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flows"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16344,7 +16387,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-groups"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16364,7 +16407,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-inputs"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16378,7 +16421,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-integration-tests"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -16409,7 +16452,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-jobs"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16434,7 +16477,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-npm-proxy"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"flate2",
|
||||
@@ -16452,7 +16495,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-openapi"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16474,7 +16517,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-schedule"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16494,7 +16537,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-scripts"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16524,7 +16567,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-settings"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16551,7 +16594,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-sse"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"serde",
|
||||
@@ -16563,7 +16606,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-users"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"axum 0.8.4",
|
||||
@@ -16588,7 +16631,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workers"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16602,7 +16645,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workspaces"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16634,7 +16677,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -16648,7 +16691,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16667,7 +16710,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"aho-corasick",
|
||||
@@ -16769,7 +16812,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-dep-map"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"itertools 0.14.0",
|
||||
@@ -16788,7 +16831,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -16803,7 +16846,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -16827,7 +16870,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-jseval"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -16844,7 +16887,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -16860,7 +16903,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-mcp"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16881,7 +16924,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-native-triggers"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16912,7 +16955,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-oauth"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-oauth2",
|
||||
@@ -16936,7 +16979,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-object-store"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -16970,7 +17013,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-operator"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -16988,7 +17031,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -16997,7 +17040,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -17009,7 +17052,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -17021,7 +17064,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -17033,7 +17076,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -17045,7 +17088,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -17057,7 +17100,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -17068,7 +17111,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -17079,7 +17122,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -17091,7 +17134,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -17102,7 +17145,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -17124,7 +17167,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-r"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -17136,7 +17179,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -17150,7 +17193,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -17167,7 +17210,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -17180,7 +17223,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -17192,7 +17235,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -17210,7 +17253,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -17226,7 +17269,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -17242,7 +17285,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -17253,7 +17296,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -17290,7 +17333,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-runtime-nativets"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"const_format",
|
||||
@@ -17328,7 +17371,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"wasm-bindgen",
|
||||
@@ -17339,7 +17382,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-store"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -17368,7 +17411,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-test-utils"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17392,7 +17435,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17425,7 +17468,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-email"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17445,7 +17488,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-gcp"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17479,7 +17522,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-http"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17515,7 +17558,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-kafka"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17538,7 +17581,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-mqtt"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17562,7 +17605,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-nats"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -17586,7 +17629,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-postgres"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17621,7 +17664,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-sqs"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17649,7 +17692,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-websocket"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17672,7 +17715,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.4",
|
||||
@@ -17691,7 +17734,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
@@ -17795,13 +17838,14 @@ dependencies = [
|
||||
"windmill-runtime-nativets",
|
||||
"windmill-types",
|
||||
"windmill-worker-volumes",
|
||||
"windows 0.61.3",
|
||||
"x509-parser 0.16.0",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker-volumes"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
@@ -18501,9 +18545,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||
|
||||
[[package]]
|
||||
name = "wtf8"
|
||||
@@ -18636,12 +18680,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
|
||||
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
"yoke-derive 0.8.1",
|
||||
"yoke-derive 0.8.2",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
@@ -18659,9 +18703,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
||||
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -18691,18 +18735,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.6"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
|
||||
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.6"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -18732,31 +18776,31 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
|
||||
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke 0.8.1",
|
||||
"yoke 0.8.2",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.5"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
||||
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
||||
dependencies = [
|
||||
"yoke 0.8.1",
|
||||
"yoke 0.8.2",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.2"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
||||
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -85,7 +85,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
e08a87450627bef9013498e40ee93a47bedda7ee
|
||||
57dd88faa3b0b354f813385cf3f6a34eca54a4a1
|
||||
+41
-22
@@ -41,50 +41,69 @@ pub async fn connect_db(
|
||||
};
|
||||
|
||||
let pool = connect(database_url.clone(), max_connections, worker_mode).await?;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
let pool2 = pool.clone();
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
if let DatabaseUrl::IamRds(database_url) = database_url {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = killpill_rx.recv() => {
|
||||
break;
|
||||
}
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {
|
||||
let needs_refresh = {
|
||||
let read_guard = database_url.read().await;
|
||||
read_guard.needs_refresh()
|
||||
};
|
||||
if needs_refresh {
|
||||
let new_url = tokio::time::timeout(std::time::Duration::from_secs(10), get_database_url()).await;
|
||||
{
|
||||
let needs_token_refresh = matches!(
|
||||
database_url,
|
||||
DatabaseUrl::IamRds(_) | DatabaseUrl::EntraId(_)
|
||||
);
|
||||
let label = match &database_url {
|
||||
DatabaseUrl::IamRds(_) => "IAM RDS",
|
||||
DatabaseUrl::EntraId(_) => "Entra ID",
|
||||
DatabaseUrl::Static(_) => "",
|
||||
};
|
||||
if needs_token_refresh {
|
||||
let pool2 = pool.clone();
|
||||
let database_url2 = database_url.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = killpill_rx.recv() => {
|
||||
break;
|
||||
}
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {
|
||||
if !database_url2.needs_refresh().await {
|
||||
continue;
|
||||
}
|
||||
let new_url = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
get_database_url(),
|
||||
)
|
||||
.await;
|
||||
match new_url {
|
||||
Ok(Ok(new_url)) => {
|
||||
match new_url.connect_options().await {
|
||||
Ok(connect_options) => {
|
||||
pool2.set_connect_options(connect_options);
|
||||
tracing::info!("Refreshed IAM RDS URL successfully");
|
||||
tracing::info!("Refreshed {label} URL successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error getting IAM RDS connect options, retrying in 10s: {}", e);
|
||||
tracing::error!(
|
||||
"Error getting {label} connect options, retrying in 10s: {e}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
tracing::error!("Error refreshing IAM RDS URL, trying again in 10s: {}", e);
|
||||
tracing::error!(
|
||||
"Error refreshing {label} URL, trying again in 10s: {e}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Timeout after 10s refreshing IAM RDS URL, trying again in 10 seconds: {}", e);
|
||||
tracing::error!(
|
||||
"Timeout after 10s refreshing {label} URL, trying again in 10s: {e}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(pool)
|
||||
|
||||
@@ -1597,6 +1597,12 @@ async fn process_notify_event(
|
||||
tracing::debug!("config changed but did not target this server/worker");
|
||||
}
|
||||
}
|
||||
"restart_worker_group" => {
|
||||
if worker_mode && payload == *WORKER_GROUP {
|
||||
tracing::info!("Restart requested for worker group '{payload}'");
|
||||
spawn_graceful_killpill(tx, db, 10, "worker group restart requested").await;
|
||||
}
|
||||
}
|
||||
"notify_webhook_change" => {
|
||||
tracing::info!(
|
||||
"Webhook change detected, invalidating webhook cache: {}",
|
||||
|
||||
@@ -429,6 +429,59 @@ export function main() {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_bun_job_syntax_error_unclosed_bracket(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
// Reproduces the "Unexpected end of file at main.ts:0" error reported
|
||||
// when a TS file has a missing closing bracket — Bun's bundler gives no
|
||||
// useful location info.
|
||||
let content = r#"
|
||||
export async function main() {
|
||||
if (true) {
|
||||
return "hello";
|
||||
// missing closing bracket for the function
|
||||
"#
|
||||
.to_owned();
|
||||
|
||||
let job = JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: None,
|
||||
language: ScriptLang::Bun,
|
||||
lock: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
modules: None,
|
||||
});
|
||||
|
||||
let completed = run_job_in_new_worker_until_complete(&db, false, job, port).await;
|
||||
|
||||
assert!(!completed.success);
|
||||
let result = completed
|
||||
.result
|
||||
.as_ref()
|
||||
.and_then(|v| v.get("error"))
|
||||
.and_then(|v| v.get("message"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
assert!(
|
||||
result.contains("Unexpected end of file"),
|
||||
"should contain bun parser error, got: {result}"
|
||||
);
|
||||
assert!(
|
||||
result.contains("syntax error"),
|
||||
"should contain helpful hint about syntax errors, got: {result}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Annotation Mode Tests
|
||||
// ============================================================================
|
||||
|
||||
@@ -1107,6 +1107,7 @@ main <- function(msg) {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
modules: None,
|
||||
}))
|
||||
.arg("msg", json!("world"))
|
||||
.run_until_complete(&db, false, port)
|
||||
@@ -1144,6 +1145,7 @@ main <- function() {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
modules: None,
|
||||
}))
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
@@ -1180,6 +1182,7 @@ main <- function() {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
modules: None,
|
||||
}))
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
|
||||
@@ -12,9 +12,7 @@ use std::{collections::HashMap, time::Duration};
|
||||
mod ee;
|
||||
pub mod ee_oss;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_api_auth::require_devops_role;
|
||||
use windmill_api_auth::{require_super_admin, ApiAuthed};
|
||||
use windmill_api_auth::{require_devops_role, require_super_admin, ApiAuthed};
|
||||
use windmill_common::utils::HTTP_CLIENT_PERMISSIVE as HTTP_CLIENT;
|
||||
use windmill_common::DB;
|
||||
|
||||
@@ -102,6 +100,10 @@ pub fn global_service() -> Router {
|
||||
.route(
|
||||
"/sync_cached_resource_types",
|
||||
post(sync_cached_resource_types),
|
||||
)
|
||||
.route(
|
||||
"/restart_worker_group/{worker_group}",
|
||||
post(restart_worker_group),
|
||||
);
|
||||
|
||||
// Vault integration routes (EE only - requires both private and enterprise features)
|
||||
@@ -662,6 +664,25 @@ pub async fn send_stats(Extension(db): Extension<DB>, authed: ApiAuthed) -> Resu
|
||||
Ok("Sent stats".to_string())
|
||||
}
|
||||
|
||||
async fn restart_worker_group(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
Path(worker_group): Path<String>,
|
||||
) -> error::Result<String> {
|
||||
require_devops_role(&db, &authed.email).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO notify_event (channel, payload) VALUES ('restart_worker_group', $1)",
|
||||
worker_group
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
Ok(format!(
|
||||
"Restart signal sent to worker group '{worker_group}'"
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct StatsDownload {
|
||||
pub signature: String,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.672.0
|
||||
version: 1.673.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -1370,6 +1370,28 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/settings/restart_worker_group/{worker_group}:
|
||||
post:
|
||||
summary: restart worker group
|
||||
description: Send a restart signal to all workers in the specified worker group. Workers will gracefully shut down and are expected to be restarted by their supervisor. Requires devops role.
|
||||
operationId: restartWorkerGroup
|
||||
tags:
|
||||
- setting
|
||||
parameters:
|
||||
- name: worker_group
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: the name of the worker group to restart
|
||||
responses:
|
||||
"200":
|
||||
description: restart signal sent
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/settings/get_stats:
|
||||
get:
|
||||
summary: get telemetry stats with HMAC signature (EE only)
|
||||
|
||||
@@ -159,13 +159,24 @@ pub async fn check_nb_of_user(db: &DB) -> error::Result<()> {
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub struct SlackVerifier {
|
||||
_mac: HmacSha256,
|
||||
mac: HmacSha256,
|
||||
}
|
||||
#[cfg(not(feature = "private"))]
|
||||
impl SlackVerifier {
|
||||
pub fn new<S: AsRef<[u8]>>(secret: S) -> anyhow::Result<SlackVerifier> {
|
||||
HmacSha256::new_from_slice(secret.as_ref())
|
||||
.map(|mac| SlackVerifier { _mac: mac })
|
||||
.map(|mac| SlackVerifier { mac })
|
||||
.map_err(|_| anyhow::anyhow!("invalid secret"))
|
||||
}
|
||||
|
||||
pub fn verify(&self, ts: &str, body: &str, exp_sig: &str) -> anyhow::Result<()> {
|
||||
let basestring = format!("v0:{}:{}", ts, body);
|
||||
let mut mac = self.mac.clone();
|
||||
mac.update(basestring.as_bytes());
|
||||
let sig = format!("v0={}", hex::encode(mac.finalize().into_bytes()));
|
||||
if sig != exp_sig {
|
||||
Err(anyhow::anyhow!("signature mismatch"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use axum::{
|
||||
extract::{Form, Path, Query},
|
||||
extract::{Path, Query},
|
||||
Extension,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use http::HeaderMap;
|
||||
use hyper::StatusCode;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -119,12 +121,41 @@ struct PrivateMetadata {
|
||||
hide_cancel: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "oauth2")]
|
||||
fn verify_slack_callback_signature(headers: &HeaderMap, body: &str) -> Result<(), Error> {
|
||||
if let Some(sv) = crate::SLACK_SIGNING_SECRET.as_ref() {
|
||||
let sig = headers
|
||||
.get("X-Slack-Signature")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
let ts = headers
|
||||
.get("X-Slack-Request-Timestamp")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
sv.verify(ts, body, sig)
|
||||
.map_err(|_| Error::BadRequest("Slack signature verification failed".to_string()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "oauth2"))]
|
||||
fn verify_slack_callback_signature(_headers: &HeaderMap, _body: &str) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn slack_app_callback_handler(
|
||||
authed: Option<ApiAuthed>,
|
||||
opt_tokened: OptTokened,
|
||||
Extension(db): Extension<DB>,
|
||||
Form(form_data): Form<SlackFormData>,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Result<StatusCode, Error> {
|
||||
let body_str = String::from_utf8_lossy(&body);
|
||||
verify_slack_callback_signature(&headers, &body_str)?;
|
||||
|
||||
let form_data: SlackFormData = serde_urlencoded::from_bytes(&body)
|
||||
.map_err(|e| Error::BadRequest(format!("invalid form data: {}", e)))?;
|
||||
|
||||
tracing::debug!("Form data: {:#?}", form_data);
|
||||
let payload: Payload = serde_json::from_str(&form_data.payload)?;
|
||||
tracing::debug!("Payload: {:#?}", payload);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
use anyhow::Result;
|
||||
|
||||
/// Parsed database connection parameters, shared across DB auth providers (IAM RDS, Entra ID, etc.)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DatabaseParams {
|
||||
pub hostname: String,
|
||||
pub port: u64,
|
||||
pub username: String,
|
||||
pub database: String,
|
||||
}
|
||||
|
||||
/// Extract database connection parameters from a PostgreSQL URL
|
||||
pub fn extract_database_params(database_url: &str) -> Result<DatabaseParams> {
|
||||
let url = url::Url::parse(database_url)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse database URL: {}", e))?;
|
||||
|
||||
let hostname = url
|
||||
.host_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("Database URL missing hostname"))?
|
||||
.to_string();
|
||||
|
||||
let port = url.port().unwrap_or(5432) as u64;
|
||||
|
||||
let username = if url.username().is_empty() {
|
||||
return Err(anyhow::anyhow!("Database URL missing username"));
|
||||
} else {
|
||||
urlencoding::decode(url.username())?.to_string()
|
||||
};
|
||||
|
||||
let database = url
|
||||
.path()
|
||||
.trim_start_matches('/')
|
||||
.split('/')
|
||||
.next()
|
||||
.filter(|s| !s.is_empty())
|
||||
.ok_or_else(|| anyhow::anyhow!("Database URL missing database name"))?
|
||||
.to_string();
|
||||
|
||||
Ok(DatabaseParams {
|
||||
hostname,
|
||||
port,
|
||||
username,
|
||||
database: urlencoding::decode(&database)?.to_string(),
|
||||
})
|
||||
}
|
||||
@@ -43,7 +43,10 @@ pub mod cache;
|
||||
pub mod client;
|
||||
pub mod db;
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
mod db_entra_ee;
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
mod db_iam_ee;
|
||||
pub mod db_params;
|
||||
#[cfg(feature = "private")]
|
||||
pub mod ee;
|
||||
pub mod ee_oss;
|
||||
@@ -633,12 +636,14 @@ impl PgDatabase {
|
||||
pub enum DatabaseUrl {
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
IamRds(std::sync::Arc<tokio::sync::RwLock<db_iam_ee::IamRdsUrl>>),
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
EntraId(std::sync::Arc<tokio::sync::RwLock<db_entra_ee::EntraIdUrl>>),
|
||||
Static(String),
|
||||
}
|
||||
|
||||
impl DatabaseUrl {
|
||||
/// Get the database URL as a string.
|
||||
/// Note: For IAM RDS, this returns the original URL (for metadata extraction).
|
||||
/// For token-based auth, this returns the original URL (for metadata extraction).
|
||||
/// For actual database connections, use connect_options() instead.
|
||||
pub async fn as_str(&self) -> String {
|
||||
match self {
|
||||
@@ -647,13 +652,18 @@ impl DatabaseUrl {
|
||||
let guard = rds_url.read().await;
|
||||
guard.as_str().to_string()
|
||||
}
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
DatabaseUrl::EntraId(entra_url) => {
|
||||
let guard = entra_url.read().await;
|
||||
guard.as_str().to_string()
|
||||
}
|
||||
DatabaseUrl::Static(url) => url.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get PgConnectOptions for this database URL.
|
||||
/// For IAM RDS, this returns options built directly from the token to avoid double-encoding
|
||||
/// issues with temporary credentials (IRSA/Pod Identity).
|
||||
/// For token-based auth (IAM RDS, Entra ID), this returns options built directly from the
|
||||
/// token to avoid double-encoding issues with temporary credentials.
|
||||
/// For static URLs, this parses the URL string.
|
||||
pub async fn connect_options(&self) -> Result<sqlx::postgres::PgConnectOptions, Error> {
|
||||
match self {
|
||||
@@ -662,6 +672,11 @@ impl DatabaseUrl {
|
||||
let guard = rds_url.read().await;
|
||||
Ok(guard.connect_options())
|
||||
}
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
DatabaseUrl::EntraId(entra_url) => {
|
||||
let guard = entra_url.read().await;
|
||||
Ok(guard.connect_options())
|
||||
}
|
||||
DatabaseUrl::Static(url) => sqlx::postgres::PgConnectOptions::from_str(url)
|
||||
.map_err(|e| Error::InternalErr(format!("Failed to parse database URL: {}", e))),
|
||||
}
|
||||
@@ -671,9 +686,31 @@ impl DatabaseUrl {
|
||||
match self {
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
DatabaseUrl::IamRds(rds_url) => rds_url.write().await.refresh().await,
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
DatabaseUrl::EntraId(entra_url) => entra_url.write().await.refresh().await,
|
||||
DatabaseUrl::Static(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn needs_refresh(&self) -> bool {
|
||||
match self {
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
DatabaseUrl::IamRds(rds_url) => rds_url.read().await.needs_refresh(),
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
DatabaseUrl::EntraId(entra_url) => entra_url.read().await.needs_refresh(),
|
||||
DatabaseUrl::Static(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Double-checked refresh: read-lock to check, then write-lock to refresh if still needed.
|
||||
pub async fn refresh_if_needed(&self) -> Result<(), Error> {
|
||||
if self.needs_refresh().await {
|
||||
self.refresh().await.map_err(|e| {
|
||||
Error::InternalErr(format!("Failed to refresh database token: {}", e))
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
static DATABASE_URL_CACHE: tokio::sync::OnceCell<DatabaseUrl> = tokio::sync::OnceCell::const_new();
|
||||
@@ -701,7 +738,9 @@ pub async fn get_database_url() -> Result<DatabaseUrl, Error> {
|
||||
|
||||
let parsed_url = url::Url::parse(&url)?;
|
||||
|
||||
if parsed_url.password().is_some_and(|x| x == "iamrds") {
|
||||
let password = parsed_url.password().unwrap_or_default();
|
||||
|
||||
if password == "iamrds" {
|
||||
let region = var("AWS_REGION").map_err(|_| {
|
||||
Error::BadConfig(
|
||||
"AWS_REGION env var is required for IAM RDS authentication".to_string(),
|
||||
@@ -731,34 +770,68 @@ pub async fn get_database_url() -> Result<DatabaseUrl, Error> {
|
||||
"IAM RDS authentication is not enabled in OSS mode".to_string(),
|
||||
));
|
||||
}
|
||||
} else if password == "entraid" {
|
||||
let tenant_id = var("AZURE_TENANT_ID").map_err(|_| {
|
||||
Error::BadConfig(
|
||||
"AZURE_TENANT_ID env var is required for Entra ID authentication"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
tracing::info!(
|
||||
"entraid mode detected, generating Entra ID URL for tenant: {tenant_id}"
|
||||
);
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
{
|
||||
let client_id = var("AZURE_CLIENT_ID").map_err(|_| {
|
||||
Error::BadConfig(
|
||||
"AZURE_CLIENT_ID env var is required for Entra ID authentication"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
let federated_token_file =
|
||||
var("AZURE_FEDERATED_TOKEN_FILE").map_err(|_| {
|
||||
Error::BadConfig(
|
||||
"AZURE_FEDERATED_TOKEN_FILE env var is required for Entra ID authentication".to_string(),
|
||||
)
|
||||
})?;
|
||||
let authority_host = var("AZURE_AUTHORITY_HOST")
|
||||
.unwrap_or_else(|_| "login.microsoftonline.com".to_string());
|
||||
|
||||
let entra_url = db_entra_ee::generate_database_url(
|
||||
&url,
|
||||
&tenant_id,
|
||||
&client_id,
|
||||
&federated_token_file,
|
||||
&authority_host,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"Failed to generate Entra ID database URL: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
tracing::info!("Entra ID URL generated successfully");
|
||||
Ok::<DatabaseUrl, Error>(DatabaseUrl::EntraId(std::sync::Arc::new(
|
||||
tokio::sync::RwLock::new(entra_url),
|
||||
)))
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "enterprise", feature = "private")))]
|
||||
{
|
||||
return Err(Error::BadConfig(
|
||||
"Entra ID authentication is not enabled in OSS mode".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
Ok::<DatabaseUrl, Error>(DatabaseUrl::Static(url.to_string()))
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Check if we need to refresh and do so if necessary
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
if let DatabaseUrl::IamRds(ref rds_url_lock) = database_url {
|
||||
// Check if refresh is needed
|
||||
let needs_refresh = {
|
||||
let read_guard = rds_url_lock.read().await;
|
||||
read_guard.needs_refresh()
|
||||
};
|
||||
database_url.refresh_if_needed().await?;
|
||||
|
||||
// If refresh is needed, acquire write lock and refresh
|
||||
if needs_refresh {
|
||||
let mut write_guard = rds_url_lock.write().await;
|
||||
// Double-check after acquiring write lock (another task might have refreshed)
|
||||
if write_guard.needs_refresh() {
|
||||
write_guard.refresh().await.map_err(|e| {
|
||||
Error::InternalErr(format!("Failed to refresh IAM token: {}", e))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the URL string
|
||||
Ok(database_url.clone())
|
||||
}
|
||||
|
||||
|
||||
@@ -401,7 +401,7 @@ pub fn build_parameters(columns: &[SimpleColumn], db_type: DbType) -> String {
|
||||
.map(|(i, col)| {
|
||||
let base_type = col.datatype.split('(').next().unwrap_or(&col.datatype);
|
||||
match db_type {
|
||||
DbType::Postgresql => format!("-- ${} {}", i + 1, col.field),
|
||||
DbType::Postgresql => format!("-- ${} {} ({})", i + 1, col.field, base_type),
|
||||
DbType::Mysql => format!("-- :{} ({})", col.field, base_type),
|
||||
DbType::MsSqlServer => {
|
||||
format!("-- @p{} {} ({})", i + 1, col.field, base_type)
|
||||
@@ -765,7 +765,7 @@ pub fn make_select_query(
|
||||
quicksearch
|
||||
));
|
||||
query.push_str(&format!(" ORDER BY {}\n", order_by));
|
||||
query.push_str(" LIMIT $1::INT OFFSET $2::INT");
|
||||
query.push_str(" LIMIT $1 OFFSET $2");
|
||||
Ok(query)
|
||||
}
|
||||
DbType::MsSqlServer => {
|
||||
@@ -1158,13 +1158,11 @@ pub fn make_delete_query(table: &str, columns: &[ColumnDef], db_type: DbType) ->
|
||||
.map(|(i, c)| {
|
||||
let qf = qi(&c.field, db_type);
|
||||
format!(
|
||||
"(${}::text::{} IS NULL AND {} IS NULL OR {} = ${}::text::{})",
|
||||
"(${} IS NULL AND {} IS NULL OR {} = ${})",
|
||||
i + 1,
|
||||
c.datatype,
|
||||
qf,
|
||||
qf,
|
||||
i + 1,
|
||||
c.datatype
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -1280,7 +1278,7 @@ fn format_insert_values(columns: &[ColumnDef], db_type: DbType, start_index: usi
|
||||
.enumerate()
|
||||
.map(|(i, c)| match db_type {
|
||||
DbType::Mysql => format!(":{}", c.field),
|
||||
DbType::Postgresql => format!("${}::{}", start_index + i, c.datatype),
|
||||
DbType::Postgresql => format!("${}", start_index + i),
|
||||
DbType::MsSqlServer => format!("@p{}", start_index + i),
|
||||
DbType::Snowflake => "?".to_string(),
|
||||
DbType::Bigquery => format!("@{}", c.field),
|
||||
@@ -1458,21 +1456,19 @@ pub fn make_update_query(
|
||||
.map(|(i, c)| {
|
||||
let qf = qi(&c.field, db_type);
|
||||
format!(
|
||||
"(${}::text::{} IS NULL AND {} IS NULL OR {} = ${}::text::{})",
|
||||
"(${} IS NULL AND {} IS NULL OR {} = ${})",
|
||||
i + 2,
|
||||
c.datatype,
|
||||
qf,
|
||||
qf,
|
||||
i + 2,
|
||||
c.datatype
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n AND ");
|
||||
|
||||
query.push_str(&format!(
|
||||
"\nUPDATE {} SET {} = $1::text::{} \nWHERE {}\tRETURNING 1",
|
||||
qt, qcol, column.datatype, conditions
|
||||
"\nUPDATE {} SET {} = $1 \nWHERE {}\tRETURNING 1",
|
||||
qt, qcol, conditions
|
||||
));
|
||||
}
|
||||
DbType::Mysql => {
|
||||
@@ -2706,7 +2702,7 @@ mod tests {
|
||||
fn test_build_parameters_postgresql() {
|
||||
let cols = vec![simple_col("limit", "int"), simple_col("offset", "int")];
|
||||
let result = build_parameters(&cols, DbType::Postgresql);
|
||||
assert_eq!(result, "-- $1 limit\n-- $2 offset");
|
||||
assert_eq!(result, "-- $1 limit (int)\n-- $2 offset (int)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2815,11 +2811,11 @@ mod tests {
|
||||
make_select_query("my_table", &cols, None, DbType::Postgresql, None, None).unwrap();
|
||||
|
||||
assert!(result.starts_with(
|
||||
"-- $1 limit\n-- $2 offset\n-- $3 quicksearch\n-- $4 order_by\n-- $5 is_desc\n"
|
||||
"-- $1 limit (int)\n-- $2 offset (int)\n-- $3 quicksearch (text)\n-- $4 order_by (text)\n-- $5 is_desc (boolean)\n"
|
||||
));
|
||||
assert!(result.contains("SELECT \"id\"::text, \"name\"::text FROM \"my_table\"\n"));
|
||||
assert!(result.contains("($3 = '' OR CONCAT(\"id\", \"name\") ILIKE '%' || $3 || '%')"));
|
||||
assert!(result.contains("LIMIT $1::INT OFFSET $2::INT"));
|
||||
assert!(result.contains("LIMIT $1 OFFSET $2"));
|
||||
assert!(result.contains("$4 = 'id' AND $5 IS false THEN \"id\"::text"));
|
||||
assert!(result.contains("$4 = 'name' AND $5 IS true THEN \"name\"::text END) DESC"));
|
||||
}
|
||||
@@ -3011,7 +3007,7 @@ mod tests {
|
||||
let cols = vec![col("id", "int4"), col("name", "text")];
|
||||
let result = make_count_query(DbType::Postgresql, "my_table", None, &cols).unwrap();
|
||||
|
||||
assert!(result.contains("-- $1 quicksearch"));
|
||||
assert!(result.contains("-- $1 quicksearch (text)"));
|
||||
assert!(result.contains("SELECT COUNT(*) as count FROM \"my_table\""));
|
||||
assert!(result.contains("($1 = '' OR CONCAT(\"id\", \"name\") ILIKE '%' || $1 || '%')"));
|
||||
// Should use WHERE not AND
|
||||
@@ -3148,13 +3144,10 @@ mod tests {
|
||||
let cols = vec![col("id", "int4"), col("name", "text")];
|
||||
let result = make_delete_query("my_table", &cols, DbType::Postgresql);
|
||||
|
||||
assert!(result.contains("-- $1 id\n-- $2 name"));
|
||||
assert!(result.contains("-- $1 id (int4)\n-- $2 name (text)"));
|
||||
assert!(result.contains("DELETE FROM \"my_table\""));
|
||||
assert!(result
|
||||
.contains("($1::text::int4 IS NULL AND \"id\" IS NULL OR \"id\" = $1::text::int4)"));
|
||||
assert!(result.contains(
|
||||
"($2::text::text IS NULL AND \"name\" IS NULL OR \"name\" = $2::text::text)"
|
||||
));
|
||||
assert!(result.contains("($1 IS NULL AND \"id\" IS NULL OR \"id\" = $1)"));
|
||||
assert!(result.contains("($2 IS NULL AND \"name\" IS NULL OR \"name\" = $2)"));
|
||||
assert!(result.contains("RETURNING 1;"));
|
||||
}
|
||||
|
||||
@@ -3221,9 +3214,8 @@ mod tests {
|
||||
let cols = vec![col("id", "int4"), col("name", "text")];
|
||||
let result = make_insert_query("my_table", &cols, DbType::Postgresql).unwrap();
|
||||
|
||||
assert!(result.contains("-- $1 id\n-- $2 name"));
|
||||
assert!(result
|
||||
.contains("INSERT INTO \"my_table\" (\"id\", \"name\") VALUES ($1::int4, $2::text)"));
|
||||
assert!(result.contains("-- $1 id (int4)\n-- $2 name (text)"));
|
||||
assert!(result.contains("INSERT INTO \"my_table\" (\"id\", \"name\") VALUES ($1, $2)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3279,7 +3271,7 @@ mod tests {
|
||||
make_insert_query("my_table", &[id_col, name_col], DbType::Postgresql).unwrap();
|
||||
|
||||
// id should be skipped from insert columns (has nextval default in pg)
|
||||
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1::text)"));
|
||||
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3293,9 +3285,7 @@ mod tests {
|
||||
make_insert_query("my_table", &[id_col, name_col], DbType::Postgresql).unwrap();
|
||||
|
||||
// name should be in insert params, id should be in defaults
|
||||
assert!(
|
||||
result.contains("INSERT INTO \"my_table\" (\"name\", \"id\") VALUES ($1::text, '42')")
|
||||
);
|
||||
assert!(result.contains("INSERT INTO \"my_table\" (\"name\", \"id\") VALUES ($1, '42')"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3308,7 +3298,7 @@ mod tests {
|
||||
let result =
|
||||
make_insert_query("my_table", &[id_col, name_col], DbType::Postgresql).unwrap();
|
||||
|
||||
assert!(result.contains("VALUES ($1::text, NULL)"));
|
||||
assert!(result.contains("VALUES ($1, NULL)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3323,7 +3313,7 @@ mod tests {
|
||||
make_insert_query("my_table", &[id_col, name_col], DbType::Postgresql).unwrap();
|
||||
|
||||
// Column is hidden, not nullable, has db default, no user default -> omit (use db default)
|
||||
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1::text)"));
|
||||
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3337,7 +3327,7 @@ mod tests {
|
||||
make_insert_query("my_table", &[id_col, name_col], DbType::Postgresql).unwrap();
|
||||
|
||||
// Always identity should be omitted
|
||||
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1::text)"));
|
||||
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3375,13 +3365,12 @@ mod tests {
|
||||
let where_cols = vec![simple_col("id", "int4"), simple_col("email", "text")];
|
||||
let result = make_update_query("my_table", &update_col, &where_cols, DbType::Postgresql);
|
||||
|
||||
assert!(result.contains("-- $1 value_to_update\n-- $2 id\n-- $3 email"));
|
||||
assert!(result.contains("UPDATE \"my_table\" SET \"name\" = $1::text::text"));
|
||||
assert!(result
|
||||
.contains("($2::text::int4 IS NULL AND \"id\" IS NULL OR \"id\" = $2::text::int4)"));
|
||||
assert!(result.contains(
|
||||
"($3::text::text IS NULL AND \"email\" IS NULL OR \"email\" = $3::text::text)"
|
||||
));
|
||||
assert!(
|
||||
result.contains("-- $1 value_to_update (text)\n-- $2 id (int4)\n-- $3 email (text)")
|
||||
);
|
||||
assert!(result.contains("UPDATE \"my_table\" SET \"name\" = $1"));
|
||||
assert!(result.contains("($2 IS NULL AND \"id\" IS NULL OR \"id\" = $2)"));
|
||||
assert!(result.contains("($3 IS NULL AND \"email\" IS NULL OR \"email\" = $3)"));
|
||||
assert!(result.contains("RETURNING 1"));
|
||||
}
|
||||
|
||||
@@ -3463,10 +3452,8 @@ mod tests {
|
||||
let cols = vec![col("a", "int4"), col("b", "text"), col("c", "bool")];
|
||||
let result = make_delete_query("t", &cols, DbType::Postgresql);
|
||||
// Check all three conditions are present and joined
|
||||
assert!(result
|
||||
.contains("AND ($2::text::text IS NULL AND \"b\" IS NULL OR \"b\" = $2::text::text)"));
|
||||
assert!(result
|
||||
.contains("AND ($3::text::bool IS NULL AND \"c\" IS NULL OR \"c\" = $3::text::bool)"));
|
||||
assert!(result.contains("AND ($2 IS NULL AND \"b\" IS NULL OR \"b\" = $2)"));
|
||||
assert!(result.contains("AND ($3 IS NULL AND \"c\" IS NULL OR \"c\" = $3)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3518,7 +3505,7 @@ mod tests {
|
||||
let result = make_insert_query("my_table", &[name_col, col1], DbType::Postgresql).unwrap();
|
||||
|
||||
// Numeric value should not be quoted
|
||||
assert!(result.contains("VALUES ($1::text, 5)"));
|
||||
assert!(result.contains("VALUES ($1, 5)"));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -3540,7 +3527,7 @@ mod tests {
|
||||
assert!(result.is_some());
|
||||
let sql = result.unwrap().unwrap().code;
|
||||
assert!(sql.contains("SELECT \"id\"::text, \"name\"::text FROM \"my_table\""));
|
||||
assert!(sql.contains("LIMIT $1::INT OFFSET $2::INT"));
|
||||
assert!(sql.contains("LIMIT $1 OFFSET $2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3571,16 +3558,14 @@ mod tests {
|
||||
fn test_expand_insert_marker() {
|
||||
let marker = r#"-- WM_INTERNAL_DB_INSERT {"table":"my_table","columns":[{"field":"id","datatype":"int4"},{"field":"name","datatype":"text"}]}"#;
|
||||
let sql = expand_code(marker, &ScriptLang::Postgresql);
|
||||
assert!(
|
||||
sql.contains("INSERT INTO \"my_table\" (\"id\", \"name\") VALUES ($1::int4, $2::text)")
|
||||
);
|
||||
assert!(sql.contains("INSERT INTO \"my_table\" (\"id\", \"name\") VALUES ($1, $2)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expand_update_marker() {
|
||||
let marker = r#"-- WM_INTERNAL_DB_UPDATE {"table":"my_table","column":{"field":"name","datatype":"text"},"columns":[{"field":"id","datatype":"int4"}]}"#;
|
||||
let sql = expand_code(marker, &ScriptLang::Postgresql);
|
||||
assert!(sql.contains("UPDATE \"my_table\" SET \"name\" = $1::text::text"));
|
||||
assert!(sql.contains("UPDATE \"my_table\" SET \"name\" = $1"));
|
||||
assert!(sql.contains("RETURNING 1"));
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ pub struct Smtp {
|
||||
pub from: String,
|
||||
pub tls_implicit: Option<bool>,
|
||||
pub disable_tls: Option<bool>,
|
||||
pub clicktracking_off: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq)]
|
||||
@@ -22,6 +23,7 @@ pub struct SmtpConfigOpt {
|
||||
pub smtp_from: Option<String>,
|
||||
pub smtp_tls_implicit: Option<bool>,
|
||||
pub smtp_disable_tls: Option<bool>,
|
||||
pub smtp_clicktracking_off: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn load_smtp_config(db: &DB) -> error::Result<Option<Smtp>> {
|
||||
@@ -46,6 +48,7 @@ pub async fn load_smtp_config(db: &DB) -> error::Result<Option<Smtp>> {
|
||||
from: config
|
||||
.smtp_from
|
||||
.unwrap_or_else(|| "noreply@getwindmill.com".to_string()),
|
||||
clicktracking_off: config.smtp_clicktracking_off,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -72,6 +75,9 @@ pub async fn load_smtp_config(db: &DB) -> error::Result<Option<Smtp>> {
|
||||
.unwrap_or(587),
|
||||
from: std::env::var("SMTP_FROM")
|
||||
.unwrap_or_else(|_| "noreply@getwindmill.com".to_string()),
|
||||
clicktracking_off: std::env::var("SMTP_CLICKTRACKING_OFF")
|
||||
.ok()
|
||||
.and_then(|p| p.parse().ok()),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -94,6 +100,7 @@ impl Default for SmtpConfigOpt {
|
||||
smtp_tls_implicit: None,
|
||||
smtp_username: None,
|
||||
smtp_disable_tls: None,
|
||||
smtp_clicktracking_off: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +157,8 @@ lazy_static::lazy_static! {
|
||||
|
||||
pub static ref NATIVE_MODE: bool = std::env::var("NATIVE_MODE").ok().is_some_and(|x| x == "1" || x == "true");
|
||||
|
||||
pub static ref LIMIT_WINDOWS_TO_1CU: bool = std::env::var("LIMIT_WINDOWS_TO_1CU").ok().is_some_and(|x| x == "1" || x == "true");
|
||||
|
||||
pub static ref CGROUP_V2_PATH_RE: Regex = Regex::new(r#"(?m)^0::(/.*)$"#).unwrap();
|
||||
pub static ref CGROUP_V2_CPU_RE: Regex = Regex::new(r#"(?m)^(\d+) \S+$"#).unwrap();
|
||||
pub static ref CGROUP_V1_INACTIVE_FILE_RE: Regex = Regex::new(r#"(?m)^total_inactive_file (\d+)$"#).unwrap();
|
||||
@@ -1052,6 +1054,9 @@ pub fn get_vcpus() -> Option<i64> {
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn get_vcpus() -> Option<i64> {
|
||||
if *LIMIT_WINDOWS_TO_1CU {
|
||||
return Some(100000); // 1 vCPU
|
||||
}
|
||||
let mut sys = System::new();
|
||||
sys.refresh_cpu_all();
|
||||
(sys.cpus().len() * 100000).try_into().ok()
|
||||
@@ -1101,6 +1106,9 @@ pub fn get_memory() -> Option<i64> {
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn get_memory() -> Option<i64> {
|
||||
if *LIMIT_WINDOWS_TO_1CU {
|
||||
return Some(2 * 1024 * 1024 * 1024); // 2 GB
|
||||
}
|
||||
let mut sys = System::new();
|
||||
sys.refresh_memory();
|
||||
Some(sys.total_memory() as i64)
|
||||
|
||||
@@ -42,13 +42,39 @@ pub fn extract_resource_types_from_schema(schema: &SchemaType) -> HashSet<String
|
||||
|
||||
/// Transform a JSON schema for maximum MCP client compatibility.
|
||||
///
|
||||
/// Some MCP clients (e.g., n8n) have limited JSON Schema support:
|
||||
/// - `integer` type is not supported (convert to `number`)
|
||||
/// - invalid non-array `enum` values are removed
|
||||
/// Ensures schemas conform to JSON Schema draft 2020-12 by:
|
||||
/// - Converting `integer` type to `number` (some clients don't support integer)
|
||||
/// - Removing invalid non-array `enum` values
|
||||
/// - Stripping non-standard keywords (`originalType`, `format` with `resource-*` prefix)
|
||||
/// - Fixing contradictory schemas (`type: "string"` with `properties` → `type: "object"`)
|
||||
/// - Removing `default: null` when the type doesn't include `null`
|
||||
/// - Adding `type: "object"` to empty schemas that have no type
|
||||
pub fn make_schema_compatible(schema: &mut Value) {
|
||||
let Value::Object(obj) = schema else { return };
|
||||
|
||||
// 1. Convert integer to number
|
||||
// 1. Strip non-standard keywords that aren't part of JSON Schema
|
||||
obj.remove("originalType");
|
||||
|
||||
// 2. Strip non-standard format values (resource-* is Windmill-internal)
|
||||
if obj
|
||||
.get("format")
|
||||
.and_then(|v| v.as_str())
|
||||
.is_some_and(|s| s.starts_with("resource-"))
|
||||
{
|
||||
obj.remove("format");
|
||||
}
|
||||
|
||||
// 3. Fix contradictory type: if `properties` is present, type must be "object"
|
||||
if obj.contains_key("properties") {
|
||||
match obj.get("type").and_then(|v| v.as_str()) {
|
||||
Some("object") => {}
|
||||
_ => {
|
||||
obj.insert("type".to_string(), Value::String("object".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Convert integer to number
|
||||
if let Some(type_val) = obj.get_mut("type") {
|
||||
match type_val {
|
||||
Value::String(s) if s == "integer" => *s = "number".to_string(),
|
||||
@@ -65,11 +91,28 @@ pub fn make_schema_compatible(schema: &mut Value) {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Invalid enum values like `enum: null` are not valid draft 2020-12.
|
||||
// 5. Remove `default: null` when type doesn't include "null"
|
||||
if obj.get("default").is_some_and(|v| v.is_null()) {
|
||||
let type_includes_null = match obj.get("type") {
|
||||
Some(Value::String(s)) => s == "null",
|
||||
Some(Value::Array(arr)) => arr.iter().any(|v| v.as_str() == Some("null")),
|
||||
_ => false,
|
||||
};
|
||||
if !type_includes_null {
|
||||
obj.remove("default");
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Invalid enum values like `enum: null` are not valid draft 2020-12.
|
||||
if obj.get("enum").is_some_and(|enum_val| !enum_val.is_array()) {
|
||||
obj.remove("enum");
|
||||
}
|
||||
|
||||
// 7. Ensure schemas with no type but with properties get type: "object"
|
||||
if !obj.contains_key("type") && !obj.is_empty() {
|
||||
obj.insert("type".to_string(), Value::String("object".to_string()));
|
||||
}
|
||||
|
||||
// Recursively process nested schemas
|
||||
if let Some(Value::Object(props)) = obj.get_mut("properties") {
|
||||
for value in props.values_mut() {
|
||||
@@ -174,4 +217,155 @@ mod tests {
|
||||
json!(["open", "closed"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_original_type() {
|
||||
let mut schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"originalType": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string",
|
||||
"originalType": "bytes"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
make_schema_compatible(&mut schema);
|
||||
|
||||
assert!(schema["properties"]["name"].get("originalType").is_none());
|
||||
assert!(schema["properties"]["data"].get("originalType").is_none());
|
||||
assert_eq!(schema["properties"]["name"]["type"], json!("string"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_resource_format() {
|
||||
let mut schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"db": {
|
||||
"type": "string",
|
||||
"format": "resource-postgresql"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
make_schema_compatible(&mut schema);
|
||||
|
||||
assert!(schema["properties"]["db"].get("format").is_none());
|
||||
assert_eq!(schema["properties"]["db"]["type"], json!("string"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_standard_format() {
|
||||
let mut schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
make_schema_compatible(&mut schema);
|
||||
|
||||
assert_eq!(schema["properties"]["email"]["format"], json!("email"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_string_type_with_properties() {
|
||||
let mut schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config": {
|
||||
"type": "string",
|
||||
"format": "resource-record",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
make_schema_compatible(&mut schema);
|
||||
|
||||
assert_eq!(schema["properties"]["config"]["type"], json!("object"));
|
||||
assert!(schema["properties"]["config"].get("format").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removes_null_default_on_string_type() {
|
||||
let mut schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
make_schema_compatible(&mut schema);
|
||||
|
||||
assert!(schema["properties"]["name"].get("default").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_null_default_when_type_includes_null() {
|
||||
let mut schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": ["string", "null"],
|
||||
"default": null
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
make_schema_compatible(&mut schema);
|
||||
|
||||
assert_eq!(schema["properties"]["name"]["default"], json!(null));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_non_null_default() {
|
||||
let mut schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"default": "hello"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
make_schema_compatible(&mut schema);
|
||||
|
||||
assert_eq!(schema["properties"]["name"]["default"], json!("hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_type_object_to_empty_schema() {
|
||||
let mut schema = json!({});
|
||||
|
||||
make_schema_compatible(&mut schema);
|
||||
|
||||
// Empty schema stays empty (no keys = truly empty)
|
||||
assert_eq!(schema, json!({}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_type_to_schema_with_properties_but_no_type() {
|
||||
let mut schema = json!({
|
||||
"properties": {
|
||||
"value": {}
|
||||
}
|
||||
});
|
||||
|
||||
make_schema_compatible(&mut schema);
|
||||
|
||||
assert_eq!(schema["type"], json!("object"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,9 @@ hyper-tls = { workspace = true, optional = true }
|
||||
hyper-util = { workspace = true, optional = true }
|
||||
rcgen = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.61", features = ["Win32_System_JobObjects", "Win32_System_Threading"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
x509-parser.workspace = true
|
||||
|
||||
@@ -6,8 +6,15 @@ try {
|
||||
external: ["*"],
|
||||
});
|
||||
} catch (err) {
|
||||
const msgs = (err?.errors ?? []).map((e) => e?.message ?? String(e));
|
||||
msgs.push(err?.message ?? String(err));
|
||||
const full = msgs.join(" ");
|
||||
if (full.includes("Unexpected end of file") || full.includes("Unterminated")) {
|
||||
console.log("Failed to build bundle: syntax error in the script (e.g. unclosed bracket, string, or comment)");
|
||||
} else {
|
||||
console.log("Failed to build bundle");
|
||||
}
|
||||
console.log(err);
|
||||
console.log("Failed to build bundle");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -702,6 +702,120 @@ lazy_static! {
|
||||
static ref DISABLE_PROCESS_GROUP: bool = std::env::var("DISABLE_PROCESS_GROUP").is_ok();
|
||||
}
|
||||
|
||||
/// 2 GB memory limit in bytes for LIMIT_WINDOWS_TO_1CU
|
||||
#[cfg(windows)]
|
||||
const MEMORY_LIMIT_1CU: usize = 2 * 1024 * 1024 * 1024;
|
||||
|
||||
/// Wrapper that holds a Windows Job Object handle alongside the child process.
|
||||
/// The job object enforces memory limits and is closed when the child is dropped.
|
||||
#[cfg(windows)]
|
||||
struct MemoryLimitedChild {
|
||||
inner: Box<dyn TokioChildWrapper>,
|
||||
_job_handle: Win32JobHandle,
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
impl std::fmt::Debug for MemoryLimitedChild {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("MemoryLimitedChild").finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// RAII wrapper for a raw Win32 HANDLE that closes it on drop.
|
||||
#[cfg(windows)]
|
||||
struct Win32JobHandle(windows::Win32::Foundation::HANDLE);
|
||||
|
||||
// SAFETY: Win32 HANDLEs are plain pointer-sized values with no thread affinity;
|
||||
// the kernel ref-counts the underlying object, so sending/sharing the handle is safe.
|
||||
#[cfg(windows)]
|
||||
unsafe impl Send for Win32JobHandle {}
|
||||
#[cfg(windows)]
|
||||
unsafe impl Sync for Win32JobHandle {}
|
||||
|
||||
#[cfg(windows)]
|
||||
impl Drop for Win32JobHandle {
|
||||
fn drop(&mut self) {
|
||||
let _ = unsafe { windows::Win32::Foundation::CloseHandle(self.0) };
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
impl process_wrap::tokio::TokioChildWrapper for MemoryLimitedChild {
|
||||
fn inner(&self) -> &tokio::process::Child {
|
||||
self.inner.inner()
|
||||
}
|
||||
fn inner_mut(&mut self) -> &mut tokio::process::Child {
|
||||
self.inner.inner_mut()
|
||||
}
|
||||
fn into_inner(self: Box<Self>) -> tokio::process::Child {
|
||||
self.inner.into_inner()
|
||||
}
|
||||
fn start_kill(&mut self) -> std::io::Result<()> {
|
||||
self.inner.start_kill()
|
||||
}
|
||||
fn wait(
|
||||
&mut self,
|
||||
) -> Box<dyn std::future::Future<Output = std::io::Result<std::process::ExitStatus>> + Send + '_>
|
||||
{
|
||||
self.inner.wait()
|
||||
}
|
||||
fn try_wait(&mut self) -> std::io::Result<Option<std::process::ExitStatus>> {
|
||||
self.inner.try_wait()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a Windows Job Object with a memory limit and assign the process to it.
|
||||
#[cfg(windows)]
|
||||
fn apply_job_memory_limit(pid: u32, memory_limit: usize) -> Result<Win32JobHandle, std::io::Error> {
|
||||
use windows::Win32::System::JobObjects::*;
|
||||
use windows::Win32::System::Threading::{OpenProcess, PROCESS_SET_QUOTA, PROCESS_TERMINATE};
|
||||
|
||||
unsafe {
|
||||
let job = CreateJobObjectW(None, None).map_err(|e| {
|
||||
std::io::Error::new(std::io::ErrorKind::Other, format!("CreateJobObjectW: {e}"))
|
||||
})?;
|
||||
|
||||
let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
|
||||
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_JOB_MEMORY;
|
||||
info.JobMemoryLimit = memory_limit;
|
||||
|
||||
SetInformationJobObject(
|
||||
job,
|
||||
JobObjectExtendedLimitInformation,
|
||||
&info as *const _ as _,
|
||||
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
|
||||
)
|
||||
.map_err(|e| {
|
||||
let _ = windows::Win32::Foundation::CloseHandle(job);
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("SetInformationJobObject: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let process_handle = OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, false, pid)
|
||||
.map_err(|e| {
|
||||
let _ = windows::Win32::Foundation::CloseHandle(job);
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("OpenProcess({pid}): {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let assign_result = AssignProcessToJobObject(job, process_handle);
|
||||
let _ = windows::Win32::Foundation::CloseHandle(process_handle);
|
||||
assign_result.map_err(|e| {
|
||||
let _ = windows::Win32::Foundation::CloseHandle(job);
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("AssignProcessToJobObject: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Win32JobHandle(job))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_command_with_isolation(program: &str, args: &[&str]) -> Command {
|
||||
use tokio::process::Command;
|
||||
|
||||
@@ -767,9 +881,31 @@ pub async fn start_child_process(
|
||||
}
|
||||
}
|
||||
|
||||
return cmd
|
||||
let child: Box<dyn TokioChildWrapper> = cmd
|
||||
.spawn()
|
||||
.map_err(|err| tentatively_improve_error(err.into(), executable));
|
||||
.map_err(|err| tentatively_improve_error(err.into(), executable))?;
|
||||
|
||||
#[cfg(windows)]
|
||||
if *windmill_common::worker::LIMIT_WINDOWS_TO_1CU {
|
||||
if let Some(pid) = child.inner().id() {
|
||||
match apply_job_memory_limit(pid, MEMORY_LIMIT_1CU) {
|
||||
Ok(job_handle) => {
|
||||
tracing::info!(
|
||||
"Applied 2GB memory limit (LIMIT_WINDOWS_TO_1CU) to child process {pid}"
|
||||
);
|
||||
return Ok(Box::new(MemoryLimitedChild {
|
||||
inner: child,
|
||||
_job_handle: job_handle,
|
||||
}));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to apply memory limit to child process {pid}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
pub async fn resolve_job_timeout(
|
||||
|
||||
@@ -8,7 +8,7 @@ use anyhow::Context;
|
||||
use base64::{engine, Engine as _};
|
||||
use chrono::Utc;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt};
|
||||
use futures::{FutureExt, StreamExt, TryStreamExt};
|
||||
use itertools::Itertools;
|
||||
use rust_decimal::{prelude::FromPrimitive, Decimal};
|
||||
use serde_json::value::RawValue;
|
||||
@@ -134,45 +134,45 @@ fn do_postgresql_inner<'a>(
|
||||
let result_f = async move {
|
||||
let mut res: Vec<Box<serde_json::value::RawValue>> = vec![];
|
||||
|
||||
let query_params = query_params
|
||||
.iter()
|
||||
.map(|p| &**p as &(dyn ToSql + Sync))
|
||||
.collect_vec();
|
||||
|
||||
let statement = if typed_schema {
|
||||
// Use query_typed_raw (unnamed prepared statement) when all param types are
|
||||
// resolved. This avoids named prepared statements ("s0", "s1", ...) which break
|
||||
// with transaction-mode connection poolers (e.g. PgBouncer/Supabase) since the
|
||||
// prepare and query can land on different backend connections.
|
||||
// Fall back to prepare + query_raw for custom/unsupported types.
|
||||
let rows = if typed_schema {
|
||||
let typed_params = query_params
|
||||
.iter()
|
||||
.zip(param_types.iter())
|
||||
.map(|(p, t)| (&**p as &(dyn ToSql + Sync), t.clone()));
|
||||
client
|
||||
.prepare_typed(&query, ¶m_types)
|
||||
.query_typed_raw(&query, typed_params)
|
||||
.await
|
||||
.map_err(to_anyhow)?
|
||||
} else {
|
||||
client.prepare(&query).await.map_err(to_anyhow)?
|
||||
let query_params = query_params
|
||||
.iter()
|
||||
.map(|p| &**p as &(dyn ToSql + Sync))
|
||||
.collect_vec();
|
||||
let statement = client.prepare(&query).await.map_err(to_anyhow)?;
|
||||
client
|
||||
.query_raw(&statement, query_params)
|
||||
.await
|
||||
.map_err(to_anyhow)?
|
||||
};
|
||||
|
||||
if skip_collect {
|
||||
client
|
||||
.execute_raw(&statement, query_params)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
futures::pin_mut!(rows);
|
||||
while rows.try_next().await.map_err(to_anyhow)?.is_some() {}
|
||||
} else if let Some(ref s3) = s3 {
|
||||
let rows_stream = client
|
||||
.query_raw(&statement, query_params)
|
||||
.map_err(to_anyhow)
|
||||
.await?
|
||||
.map_err(to_anyhow)
|
||||
.map(|row_result| {
|
||||
row_result.and_then(|row| postgres_row_to_json_value(row).map_err(to_anyhow))
|
||||
});
|
||||
let rows_stream = rows.map_err(to_anyhow).map(|row_result| {
|
||||
row_result.and_then(|row| postgres_row_to_json_value(row).map_err(to_anyhow))
|
||||
});
|
||||
|
||||
let stream = convert_json_line_stream(rows_stream.boxed(), s3.format).await?;
|
||||
s3.upload(stream.boxed()).await?;
|
||||
|
||||
return Ok(vec![to_raw_value(&s3.to_return_s3_obj())]);
|
||||
} else {
|
||||
let rows = client
|
||||
.query_raw(&statement, query_params)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
|
||||
let rows = if first_row_only {
|
||||
rows.take(1).boxed()
|
||||
} else {
|
||||
|
||||
@@ -429,7 +429,7 @@ pub async fn handle_flow_dependency_job(
|
||||
// `JobKind::FlowDependencies` job store either:
|
||||
// - A saved flow version `id` in the `script_hash` column.
|
||||
// - Preview raw flow in the `queue` or `job` table.
|
||||
let (mut flow, extras) = match job.runnable_id {
|
||||
let (mut flow, mut extras) = match job.runnable_id {
|
||||
Some(ScriptHash(id)) => {
|
||||
let flow = cache::flow::fetch_version(db, id).await?;
|
||||
(flow.value().clone(), flow.extras())
|
||||
@@ -440,6 +440,51 @@ pub async fn handle_flow_dependency_job(
|
||||
},
|
||||
};
|
||||
|
||||
// When triggered by a relative import (e.g. a dependent script was updated),
|
||||
// the version captured at job creation time may be stale if the flow was
|
||||
// updated between job creation and execution. Re-query the latest version
|
||||
// and read the current flow value from the flow table to avoid overwriting
|
||||
// a newer flow definition with a stale one.
|
||||
let version = if triggered_by_relative_import && !skip_flow_update {
|
||||
let latest_version = sqlx::query_scalar!(
|
||||
"SELECT id FROM flow_version WHERE path = $1 AND workspace_id = $2 ORDER BY created_at DESC LIMIT 1",
|
||||
job_path,
|
||||
job.workspace_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
if let Some(latest_version) = latest_version {
|
||||
if version != Some(latest_version) {
|
||||
tracing::info!(
|
||||
"Flow version changed since dependency job was queued ({:?} -> {}), using latest",
|
||||
version,
|
||||
latest_version
|
||||
);
|
||||
}
|
||||
// Read the current flow value from the flow table (not version cache).
|
||||
// This ensures we have the latest committed state, including any locks
|
||||
// computed by a concurrent FlowDependencies job from a direct flow update.
|
||||
let raw_flow_value = sqlx::query_scalar!(
|
||||
"SELECT value AS \"value!: Json<Box<RawValue>>\" FROM flow WHERE path = $1 AND workspace_id = $2",
|
||||
job_path,
|
||||
job.workspace_id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
|
||||
let flow_data = cache::FlowData::from_raw(raw_flow_value.0)?;
|
||||
flow = flow_data.value().clone();
|
||||
extras = flow_data.extras();
|
||||
|
||||
Some(latest_version)
|
||||
} else {
|
||||
version
|
||||
}
|
||||
} else {
|
||||
version
|
||||
};
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
let mut dependency_map = ScopedDependencyMap::fetch_maybe_rearranged(
|
||||
@@ -575,6 +620,33 @@ pub async fn handle_flow_dependency_job(
|
||||
|
||||
tx = dependency_map.dissolve(tx).await;
|
||||
|
||||
// When triggered by a relative import, re-check that our version is still
|
||||
// the latest before writing. Between reading the flow value and now (module
|
||||
// locking can take significant time), another job may have created a newer
|
||||
// version. If so, skip the update — the newer version's dep job will handle it.
|
||||
if triggered_by_relative_import {
|
||||
let current_latest = sqlx::query_scalar!(
|
||||
"SELECT id FROM flow_version WHERE path = $1 AND workspace_id = $2 ORDER BY created_at DESC LIMIT 1",
|
||||
job_path,
|
||||
job.workspace_id
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if current_latest != Some(version) {
|
||||
tracing::info!(
|
||||
"Flow version changed during dependency locking ({} -> {:?}), skipping update to avoid overwriting newer version",
|
||||
version,
|
||||
current_latest
|
||||
);
|
||||
tx.commit().await?;
|
||||
return Ok(to_raw_value_owned(json!({
|
||||
"status": "Skipped: newer flow version exists",
|
||||
"modified_ids": modified_ids,
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE flow SET value = $1 WHERE path = $2 AND workspace_id = $3",
|
||||
&new_flow_value as &Json<Box<RawValue>>,
|
||||
@@ -627,7 +699,9 @@ pub async fn handle_flow_dependency_job(
|
||||
// Making new version viewable as the current one.
|
||||
// This will also trigger `flow_versions_append_trigger` (check _flow_versions_update_notify.up.sql)
|
||||
// which will invalidate cache for the latest flow versions for all workers.
|
||||
sqlx::query!("UPDATE flow SET versions = array_append(versions, $1) WHERE path = $2 AND workspace_id = $3",
|
||||
// Only append if this version isn't already the last element in the array.
|
||||
// This prevents duplicates when update_flow already appended this version.
|
||||
sqlx::query!("UPDATE flow SET versions = array_append(versions, $1) WHERE path = $2 AND workspace_id = $3 AND (versions[array_upper(versions, 1)] IS DISTINCT FROM $1)",
|
||||
version,
|
||||
&job_path,
|
||||
&job.workspace_id,
|
||||
|
||||
+1
-1
@@ -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.672.0";
|
||||
export const VERSION = "v1.673.0";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -15,7 +15,7 @@ import { buildFolderPath, getMetadataFileName, loadNonDottedPathsSetting } from
|
||||
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
import { resolveWorkspace, validatePath } from "../../core/context.ts";
|
||||
import { resolve, track_job } from "../script/script.ts";
|
||||
import { resolve, track_job, pollForJobResult } from "../script/script.ts";
|
||||
import { defaultFlowDefinition } from "../../../bootstrap/flow_bootstrap.ts";
|
||||
import { SyncOptions, mergeConfigWithConfigFile } from "../../core/conf.ts";
|
||||
import { FSFSElement, elementsToMap, ignoreF } from "../sync/sync.ts";
|
||||
@@ -574,32 +574,27 @@ async function preview(
|
||||
|
||||
log.debug(`Flow value: ${JSON.stringify(localFlow.value, null, 2)}`);
|
||||
|
||||
// Run the flow preview
|
||||
let result;
|
||||
try {
|
||||
result = await wmill.runFlowPreviewAndWaitResult({
|
||||
workspace: workspace.workspaceId,
|
||||
requestBody: {
|
||||
value: localFlow.value,
|
||||
path: flowPath.substring(0, flowPath.indexOf(".flow")).replaceAll(SEP, "/"),
|
||||
args: input,
|
||||
},
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (e.body) {
|
||||
// If a failure_module ran, the body contains its result — not an error
|
||||
if (e.body.result !== undefined) {
|
||||
if (opts.silent) {
|
||||
console.log(JSON.stringify(e.body.result));
|
||||
} else {
|
||||
log.info(colors.yellow.bold("Flow failed, error handler result:"));
|
||||
log.info(JSON.stringify(e.body.result, null, 2));
|
||||
}
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
// Run the flow preview — start the job, then poll for completion
|
||||
const jobId = await wmill.runFlowPreview({
|
||||
workspace: workspace.workspaceId,
|
||||
requestBody: {
|
||||
value: localFlow.value,
|
||||
path: flowPath.substring(0, flowPath.indexOf(".flow")).replaceAll(SEP, "/"),
|
||||
args: input,
|
||||
},
|
||||
});
|
||||
|
||||
const { result, success } = await pollForJobResult(workspace.workspaceId, jobId);
|
||||
|
||||
if (!success) {
|
||||
if (opts.silent) {
|
||||
console.log(JSON.stringify(result));
|
||||
} else {
|
||||
log.info(colors.yellow.bold("Flow failed, error handler result:"));
|
||||
log.info(JSON.stringify(result, null, 2));
|
||||
}
|
||||
throw e;
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.silent) {
|
||||
|
||||
@@ -333,8 +333,9 @@ async function generateMetadata(
|
||||
|
||||
log.info("");
|
||||
|
||||
const isInteractive = process.stdin.isTTY ?? false;
|
||||
if (
|
||||
!opts.yes &&
|
||||
!opts.yes && isInteractive &&
|
||||
!(await Confirm.prompt({
|
||||
message: "Update metadata?",
|
||||
default: true,
|
||||
|
||||
@@ -1118,6 +1118,27 @@ export async function track_job(workspace: string, id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const POLL_INTERVAL_MS = 2000;
|
||||
|
||||
export async function pollForJobResult(
|
||||
workspace: string,
|
||||
jobId: string,
|
||||
): Promise<{ result: unknown; success: boolean }> {
|
||||
while (true) {
|
||||
const maybeResult = await wmill.getCompletedJobResultMaybe({
|
||||
workspace,
|
||||
id: jobId,
|
||||
getStarted: false,
|
||||
});
|
||||
|
||||
if (maybeResult.completed) {
|
||||
return { result: maybeResult.result, success: maybeResult.success ?? false };
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
||||
}
|
||||
}
|
||||
|
||||
async function show(opts: GlobalOptions, path: string) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
@@ -1532,8 +1553,8 @@ async function preview(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For regular scripts, use the standard preview API
|
||||
const result = await wmill.runScriptPreviewAndWaitResult({
|
||||
// For regular scripts, start the preview job then poll for completion
|
||||
const jobId = await wmill.runScriptPreview({
|
||||
workspace: workspace.workspaceId,
|
||||
requestBody: {
|
||||
content,
|
||||
@@ -1544,8 +1565,21 @@ async function preview(
|
||||
},
|
||||
});
|
||||
|
||||
const { result, success } = await pollForJobResult(workspace.workspaceId, jobId);
|
||||
|
||||
if (!success) {
|
||||
if (opts.silent) {
|
||||
console.log(JSON.stringify(result));
|
||||
} else {
|
||||
log.info(colors.red.bold("Preview failed"));
|
||||
log.info(JSON.stringify(result, null, 2));
|
||||
}
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.silent) {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
console.log(JSON.stringify(result));
|
||||
} else {
|
||||
log.info(colors.bold.underline.green("Preview completed"));
|
||||
log.info(JSON.stringify(result, null, 2));
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ export {
|
||||
token,
|
||||
};
|
||||
|
||||
export const VERSION = "1.672.0";
|
||||
export const VERSION = "1.673.0";
|
||||
|
||||
// Re-exported from constants.ts to maintain backwards compatibility
|
||||
export { WM_FORK_PREFIX } from "./core/constants.ts";
|
||||
|
||||
@@ -1053,12 +1053,12 @@ export async function parseMetadataFile(
|
||||
}
|
||||
}
|
||||
// no metadata file at all. Create it
|
||||
metadataFilePath = scriptPath + ".script.yaml";
|
||||
log.info(
|
||||
(await blueColor())(
|
||||
`Creating script metadata file for ${metadataFilePath}`
|
||||
)
|
||||
);
|
||||
metadataFilePath = scriptPath + ".script.yaml";
|
||||
let scriptInitialMetadata = defaultScriptMetadata();
|
||||
const lockPath = scriptPath + ".script.lock";
|
||||
scriptInitialMetadata.lock = "!inline " + lockPath;
|
||||
|
||||
@@ -385,7 +385,145 @@ lock: ""
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Test 4: Apps - Create app via API and test filterWorkspaceDependenciesForApp
|
||||
// Test 4: Hash consistency - generate-metadata (tree) then deprecated check (legacy)
|
||||
// =============================================================================
|
||||
|
||||
test("Workspace deps: hash consistency between tree-based and legacy staleness checks", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "workspace_deps_hash_consistency_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
excludes: []`, "utf-8");
|
||||
|
||||
// Setup workspace deps
|
||||
await mkdir(`${tempDir}/dependencies`, { recursive: true });
|
||||
const bunDep = `{"dependencies": {"lodash": "4.17.21"}}`;
|
||||
await writeFile(`${tempDir}/dependencies/package.json`, bunDep, "utf-8");
|
||||
|
||||
// Setup script
|
||||
await mkdir(`${tempDir}/f/test`, { recursive: true });
|
||||
const scriptContent = `export async function main() {
|
||||
return "uses default deps";
|
||||
}
|
||||
`;
|
||||
const scriptMetadata = `summary: "test"
|
||||
schema:
|
||||
type: object
|
||||
properties: {}
|
||||
lock: ""
|
||||
`;
|
||||
await writeFile(`${tempDir}/f/test/my_script.ts`, scriptContent, "utf-8");
|
||||
await writeFile(`${tempDir}/f/test/my_script.script.yaml`, scriptMetadata, "utf-8");
|
||||
|
||||
// Step 1: Run the NEW generate-metadata command (tree-based, stores hash with {} for deps)
|
||||
const genResult = await backend.runCLICommand(
|
||||
["generate-metadata", "--yes", "--schema-only"],
|
||||
tempDir,
|
||||
"workspace_deps_hash_consistency_test"
|
||||
);
|
||||
expect(genResult.code).toEqual(0);
|
||||
expect(genResult.stdout).toContain("Done. Updated");
|
||||
|
||||
// Step 2: Run the DEPRECATED script generate-metadata --dry-run (legacy, checks hash with deps)
|
||||
// This simulates what sync push does internally — the legacy check path.
|
||||
// If hashes are inconsistent, this would falsely report the script as stale.
|
||||
const legacyResult = await backend.runCLICommand(
|
||||
["script", "generate-metadata", "-i", "f/test/my_script*", "--yes", "--dry-run"],
|
||||
tempDir,
|
||||
"workspace_deps_hash_consistency_test"
|
||||
);
|
||||
expect(legacyResult.code).toEqual(0);
|
||||
// The legacy check should NOT find it stale — generate-metadata persisted
|
||||
// the dep hashes, so getRawWorkspaceDependencies(true) returns {} for the
|
||||
// legacy path, making the hash formula equivalent.
|
||||
expect(legacyResult.stdout).toContain("No metadata to update");
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Test 5: Dep change after generate-metadata — legacy check detects staleness
|
||||
// =============================================================================
|
||||
|
||||
test("Workspace deps: dep change after generate-metadata is detected by legacy check", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "workspace_deps_dep_change_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
excludes: []`, "utf-8");
|
||||
|
||||
// Setup workspace deps
|
||||
await mkdir(`${tempDir}/dependencies`, { recursive: true });
|
||||
const bunDep = `{"dependencies": {"lodash": "4.17.21"}}`;
|
||||
await writeFile(`${tempDir}/dependencies/package.json`, bunDep, "utf-8");
|
||||
|
||||
// Setup script (no annotation = uses default dep)
|
||||
await mkdir(`${tempDir}/f/test`, { recursive: true });
|
||||
const scriptContent = `export async function main() {
|
||||
return "uses default deps";
|
||||
}
|
||||
`;
|
||||
const scriptMetadata = `summary: "test"
|
||||
schema:
|
||||
type: object
|
||||
properties: {}
|
||||
lock: ""
|
||||
`;
|
||||
await writeFile(`${tempDir}/f/test/my_script.ts`, scriptContent, "utf-8");
|
||||
await writeFile(`${tempDir}/f/test/my_script.script.yaml`, scriptMetadata, "utf-8");
|
||||
|
||||
// Step 1: Run generate-metadata to establish baseline hashes
|
||||
const genResult = await backend.runCLICommand(
|
||||
["generate-metadata", "--yes", "--schema-only"],
|
||||
tempDir,
|
||||
"workspace_deps_dep_change_test"
|
||||
);
|
||||
expect(genResult.code).toEqual(0);
|
||||
|
||||
// Step 2: Verify baseline is up-to-date via legacy check
|
||||
const checkResult = await backend.runCLICommand(
|
||||
["script", "generate-metadata", "-i", "f/test/my_script*", "--yes", "--dry-run"],
|
||||
tempDir,
|
||||
"workspace_deps_dep_change_test"
|
||||
);
|
||||
expect(checkResult.code).toEqual(0);
|
||||
expect(checkResult.stdout).toContain("No metadata to update");
|
||||
|
||||
// Step 3: Change the workspace dep
|
||||
const newBunDep = `{"dependencies": {"lodash": "4.17.22"}}`;
|
||||
await writeFile(`${tempDir}/dependencies/package.json`, newBunDep, "utf-8");
|
||||
|
||||
// Step 4: Legacy check should detect the dep change as stale
|
||||
// This works because getRawWorkspaceDependencies(true) returns the changed dep
|
||||
// (its content hash no longer matches the lockfile), and the legacy path
|
||||
// includes deps in the hash formula.
|
||||
const afterDepChangeResult = await backend.runCLICommand(
|
||||
["script", "generate-metadata", "-i", "f/test/my_script*", "--yes", "--dry-run"],
|
||||
tempDir,
|
||||
"workspace_deps_dep_change_test"
|
||||
);
|
||||
expect(afterDepChangeResult.code).toEqual(0);
|
||||
expect(afterDepChangeResult.stdout).toContain("my_script");
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Test 6: Apps - Create app via API and test filterWorkspaceDependenciesForApp
|
||||
// =============================================================================
|
||||
|
||||
test("Workspace deps: Apps - filterWorkspaceDependenciesForApp with real app via API", async () => {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.672.0",
|
||||
"version": "1.673.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.672.0",
|
||||
"version": "1.673.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.672.0",
|
||||
"version": "1.673.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -38,7 +38,10 @@
|
||||
export async function run(overrideScheduledForStr?: string | undefined | null) {
|
||||
let processedArgs: Record<string, any>
|
||||
try {
|
||||
processedArgs = await processSecretArgs(args ?? {}, runnable?.schema)
|
||||
processedArgs = await processSecretArgs(
|
||||
enforceDisabledDefaults(args ?? {}, true),
|
||||
runnable?.schema
|
||||
)
|
||||
} catch (e) {
|
||||
sendUserToast('Failed to process sensitive args: ' + e, true)
|
||||
return
|
||||
@@ -132,6 +135,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
function enforceDisabledDefaults(
|
||||
args: Record<string, any>,
|
||||
notify: boolean = false
|
||||
): Record<string, any> {
|
||||
const schema = runnable?.schema
|
||||
if (!schema?.properties) return args
|
||||
const result = { ...args }
|
||||
const resetKeys: string[] = []
|
||||
for (const [key, prop] of Object.entries(schema.properties) as [string, any][]) {
|
||||
if (prop?.disabled && 'default' in prop) {
|
||||
if (notify && result[key] !== prop.default) {
|
||||
resetKeys.push(key)
|
||||
}
|
||||
result[key] = prop.default
|
||||
}
|
||||
}
|
||||
if (resetKeys.length > 0) {
|
||||
sendUserToast(
|
||||
`Disabled field${resetKeys.length > 1 ? 's' : ''} ${resetKeys.map((k) => `'${k}'`).join(', ')} reset to default value${resetKeys.length > 1 ? 's' : ''}`
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function setCode(code: string) {
|
||||
jsonEditor?.setCode(code)
|
||||
}
|
||||
@@ -249,7 +276,7 @@
|
||||
bind:this={jsonEditor}
|
||||
on:select={(e) => {
|
||||
if (e.detail) {
|
||||
args = e.detail
|
||||
args = enforceDisabledDefaults(e.detail)
|
||||
}
|
||||
}}
|
||||
updateOnBlur={false}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
RotateCcw,
|
||||
Settings,
|
||||
Trash,
|
||||
Power,
|
||||
X,
|
||||
ExternalLink,
|
||||
FileCode
|
||||
@@ -14,7 +15,13 @@
|
||||
import Badge from './common/badge/Badge.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import { ConfigService, WorkspaceService, type WorkerPing, type Workspace } from '$lib/gen'
|
||||
import {
|
||||
ConfigService,
|
||||
SettingService,
|
||||
WorkspaceService,
|
||||
type WorkerPing,
|
||||
type Workspace
|
||||
} from '$lib/gen'
|
||||
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
@@ -264,6 +271,7 @@
|
||||
})
|
||||
let openDelete = $state(false)
|
||||
let openClean = $state(false)
|
||||
let openRestart = $state(false)
|
||||
|
||||
// Compute hashed tags for display (actual tags used by the worker)
|
||||
let hashedDedicatedTags: Map<string, string> = $state(new Map())
|
||||
@@ -365,6 +373,32 @@
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<ConfirmationModal
|
||||
open={openRestart}
|
||||
title="Restart workers"
|
||||
confirmationText="Restart"
|
||||
on:canceled={() => {
|
||||
openRestart = false
|
||||
}}
|
||||
on:confirmed={async () => {
|
||||
try {
|
||||
await SettingService.restartWorkerGroup({ workerGroup: name })
|
||||
sendUserToast(`Restart signal sent to worker group '${name}'`)
|
||||
dispatch('reload')
|
||||
} catch (e) {
|
||||
sendUserToast(`Failed to restart worker group: ${e}`, true)
|
||||
}
|
||||
openRestart = false
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col w-full space-y-4">
|
||||
<span
|
||||
>Are you sure you want to restart all workers in worker group '{name}'? Workers will be
|
||||
gracefully shut down and are expected to be restarted by their supervisor.</span
|
||||
>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<Drawer bind:this={drawer} size="800px">
|
||||
<DrawerContent
|
||||
on:close={() => drawer?.closeDrawer()}
|
||||
@@ -1236,6 +1270,17 @@
|
||||
>
|
||||
Clean cache
|
||||
</Button>
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="subtle"
|
||||
on:click={() => {
|
||||
openRestart = true
|
||||
}}
|
||||
startIcon={{ icon: Power }}
|
||||
destructive
|
||||
>
|
||||
Restart workers
|
||||
</Button>
|
||||
{:else}
|
||||
<Dropdown
|
||||
items={[
|
||||
@@ -1248,6 +1293,13 @@
|
||||
disabled: !config,
|
||||
type: 'delete'
|
||||
},
|
||||
{
|
||||
displayName: 'Restart workers',
|
||||
action: () => {
|
||||
openRestart = true
|
||||
},
|
||||
type: 'delete'
|
||||
},
|
||||
{
|
||||
displayName: 'Delete config',
|
||||
action: () => {
|
||||
|
||||
@@ -227,6 +227,7 @@ class AIChatManager {
|
||||
pendingPrompt?: string,
|
||||
options?: {
|
||||
closeScriptSettings?: boolean
|
||||
lang?: ScriptLang | 'bunnative'
|
||||
}
|
||||
) {
|
||||
if (mode === AIMode.SCRIPT && !tryGetCurrentModel()) return
|
||||
@@ -235,7 +236,7 @@ class AIChatManager {
|
||||
if (mode === AIMode.SCRIPT) {
|
||||
const currentModel = getCurrentModel()
|
||||
const customPrompt = getCombinedCustomPrompt(mode)
|
||||
const lang = this.scriptEditorOptions?.lang ?? 'bun'
|
||||
const lang = options?.lang ?? this.scriptEditorOptions?.lang ?? 'bun'
|
||||
const context = this.contextManager.getSelectedContext()
|
||||
this.systemMessage = prepareScriptSystemMessage(currentModel, lang, {}, customPrompt)
|
||||
this.systemMessage.content = this.systemMessage.content
|
||||
@@ -445,10 +446,7 @@ class AIChatManager {
|
||||
if (!pendingPrompt) return undefined
|
||||
this.pendingPrompt = ''
|
||||
if (this.mode === AIMode.SCRIPT) {
|
||||
return prepareScriptUserMessage(
|
||||
pendingPrompt,
|
||||
this.contextManager.getSelectedContext()
|
||||
)
|
||||
return prepareScriptUserMessage(pendingPrompt, this.contextManager.getSelectedContext())
|
||||
} else if (this.mode === AIMode.FLOW) {
|
||||
return prepareFlowUserMessage(
|
||||
pendingPrompt,
|
||||
@@ -574,9 +572,9 @@ class AIChatManager {
|
||||
} = {}
|
||||
) => {
|
||||
if (options.mode) {
|
||||
this.changeMode(options.mode)
|
||||
this.changeMode(options.mode, undefined, { lang: options.lang })
|
||||
} else {
|
||||
this.changeMode(this.mode)
|
||||
this.changeMode(this.mode, undefined, { lang: options.lang })
|
||||
}
|
||||
if (options.instructions) {
|
||||
this.instructions = options.instructions
|
||||
|
||||
@@ -145,6 +145,14 @@
|
||||
}}
|
||||
options={{ right: 'Disable TLS' }}
|
||||
/>
|
||||
|
||||
<Toggle
|
||||
id="smtp_clicktracking_off"
|
||||
{disabled}
|
||||
bind:checked={$values['smtp_settings'].smtp_clicktracking_off}
|
||||
size="xs"
|
||||
options={{ right: 'Disable click tracking on links' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Test Email -->
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
wmill = ">=1.672.0"
|
||||
wmill = ">=1.673.0"
|
||||
sendgrid = "*"
|
||||
mysql-connector-python = "*"
|
||||
pymongo = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: '3.0.3'
|
||||
|
||||
info:
|
||||
version: 1.672.0
|
||||
version: 1.673.0
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.672.0'
|
||||
ModuleVersion = '1.673.0'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill"
|
||||
version = "1.672.0"
|
||||
version = "1.673.0"
|
||||
description = "A client library for accessing Windmill server wrapping the Windmill client API"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill/windmill",
|
||||
"version": "1.672.0",
|
||||
"version": "1.673.0",
|
||||
"exports": "./src/index.ts",
|
||||
"publish": {
|
||||
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "windmill-client",
|
||||
"description": "Windmill SDK client for browsers and Node.js",
|
||||
"version": "1.672.0",
|
||||
"version": "1.673.0",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache 2.0",
|
||||
"sideEffects": false,
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.672.0
|
||||
1.673.0
|
||||
|
||||
Reference in New Issue
Block a user