diff --git a/.github/workflows/backend-test.yml b/.github/workflows/backend-test.yml index 5fa4a289f2..366e6bd27d 100644 --- a/.github/workflows/backend-test.yml +++ b/.github/workflows/backend-test.yml @@ -67,13 +67,21 @@ jobs: - name: Substitute EE code (EE logic is behind feature flag) run: | ./substitute_ee_code.sh --copy --dir ./windmill-ee-private + - name: Cache DuckDB FFI module build + uses: actions/cache@v3 + with: + path: ./backend/windmill-duckdb-ffi-internal/target + key: ${{ runner.os }}-duckdb-ffi-${{ hashFiles('./backend/windmill-duckdb-ffi-internal/src/**/*.rs', './backend/windmill-duckdb-ffi-internal/Cargo.toml', './backend/windmill-duckdb-ffi-internal/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-duckdb-ffi- - name: cargo test timeout-minutes: 16 run: deno --version && bun -v && go version && python3 --version && + cd windmill-duckdb-ffi-internal && ./build_dev.sh && cd .. && SQLX_OFFLINE=true DATABASE_URL=postgres://postgres:changeme@localhost:5432/windmill DISABLE_EMBEDDING=true RUST_LOG=info RUST_LOG_STYLE=never DENO_PATH=$(which deno) BUN_PATH=$(which bun) GO_PATH=$(which go) UV_PATH=$(which uv) cargo test --features - enterprise,deno_core,license,python,rust,scoped_cache,private --all -- + enterprise,deno_core,license,python,duckdb,rust,scoped_cache,private --all -- --nocapture diff --git a/CHANGELOG.md b/CHANGELOG.md index d1b0a05c89..f32cb9c377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,70 @@ # Changelog +## [1.576.3](https://github.com/windmill-labs/windmill/compare/v1.576.2...v1.576.3) (2025-11-15) + + +### Bug Fixes + +* handle better alias types in duckdb ([2c04e04](https://github.com/windmill-labs/windmill/commit/2c04e04bf0e3272c89f321392158888d02a1191b)) + +## [1.576.2](https://github.com/windmill-labs/windmill/compare/v1.576.1...v1.576.2) (2025-11-15) + + +### Bug Fixes + +* temporary fix for duckdb type_aliases causing issues ([#7148](https://github.com/windmill-labs/windmill/issues/7148)) ([6426ebf](https://github.com/windmill-labs/windmill/commit/6426ebf8cb713443904065064b6a07eb1db0761a)) + +## [1.576.1](https://github.com/windmill-labs/windmill/compare/v1.576.0...v1.576.1) (2025-11-14) + + +### Bug Fixes + +* DuckDB FFI crash fix ([#7145](https://github.com/windmill-labs/windmill/issues/7145)) ([d3fc459](https://github.com/windmill-labs/windmill/commit/d3fc459b407682bf588236236916363d94f3e1ff)) + +## [1.576.0](https://github.com/windmill-labs/windmill/compare/v1.575.4...v1.576.0) (2025-11-14) + + +### Features + +* add support for switch and attributes in pwsh params ([#7143](https://github.com/windmill-labs/windmill/issues/7143)) ([c16bef8](https://github.com/windmill-labs/windmill/commit/c16bef8f296645ff873f9d8d28e3dcb50a65e304)) +* **ai:** handle aws bedrock as provider ([#7131](https://github.com/windmill-labs/windmill/issues/7131)) ([30eb9aa](https://github.com/windmill-labs/windmill/commit/30eb9aae25eeb563ad119ef93f3ff1ab17c66d75)) +* webhook by flow version ([#7062](https://github.com/windmill-labs/windmill/issues/7062)) ([09cdfb4](https://github.com/windmill-labs/windmill/commit/09cdfb4556748903dc5bbf53ef3356ac97c57d90)) + + +### Bug Fixes + +* use proper TLS connector for DuckLake instance catalog setup ([#7138](https://github.com/windmill-labs/windmill/issues/7138)) ([cf36fe3](https://github.com/windmill-labs/windmill/commit/cf36fe3bb1beec80fa84dc342a8a38cc7369bc4d)) + +## [1.575.4](https://github.com/windmill-labs/windmill/compare/v1.575.3...v1.575.4) (2025-11-13) + + +### Bug Fixes + +* fix cancel job selection ([d4340b8](https://github.com/windmill-labs/windmill/commit/d4340b83ac94a9dd9febdcfa11288f35c0a57799)) + +## [1.575.3](https://github.com/windmill-labs/windmill/compare/v1.575.2...v1.575.3) (2025-11-13) + + +### Bug Fixes + +* fix cancel job selection ([fbab86c](https://github.com/windmill-labs/windmill/commit/fbab86cc43dae17727a3aa99abc43a57c6b59b57)) + +## [1.575.2](https://github.com/windmill-labs/windmill/compare/v1.575.1...v1.575.2) (2025-11-13) + + +### Bug Fixes + +* fix cancel job selection ([1c0f80d](https://github.com/windmill-labs/windmill/commit/1c0f80dc5594146b0ec18caf3b49c036695ce759)) + +## [1.575.1](https://github.com/windmill-labs/windmill/compare/v1.575.0...v1.575.1) (2025-11-13) + + +### Bug Fixes + +* Fix flow buttons clipped in Safari ([#7126](https://github.com/windmill-labs/windmill/issues/7126)) ([77316cb](https://github.com/windmill-labs/windmill/commit/77316cbd0e3ee81fccee9680541f9b2346c8a209)) +* handle string in input transform strings ([2ee17ae](https://github.com/windmill-labs/windmill/commit/2ee17aea8b29d99c5d2bb609b2becad354835de7)) +* send alternatives on timeout toast ([#6920](https://github.com/windmill-labs/windmill/issues/6920)) ([6fac896](https://github.com/windmill-labs/windmill/commit/6fac8963909fb40726d85f5e897398d2a691edb0)) + ## [1.575.0](https://github.com/windmill-labs/windmill/compare/v1.574.3...v1.575.0) (2025-11-12) diff --git a/backend/.sqlx/query-24d302b8215d49a289bedd14a5791e9366d1f6d3d2aa485e0f50c6f2d85693dd.json b/backend/.sqlx/query-24d302b8215d49a289bedd14a5791e9366d1f6d3d2aa485e0f50c6f2d85693dd.json deleted file mode 100644 index 7acbf9deec..0000000000 --- a/backend/.sqlx/query-24d302b8215d49a289bedd14a5791e9366d1f6d3d2aa485e0f50c6f2d85693dd.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT j.id AS \"id!\" FROM v2_job j WHERE j.id = ANY($1) AND j.trigger_kind != 'schedule'::job_trigger_kind AND ($2::text[] IS NULL OR j.tag = ANY($2))", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id!", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "UuidArray", - "TextArray" - ] - }, - "nullable": [ - false - ] - }, - "hash": "24d302b8215d49a289bedd14a5791e9366d1f6d3d2aa485e0f50c6f2d85693dd" -} diff --git a/backend/.sqlx/query-78f5684086a8c28a602c583389e974fffd7f74b10618b1467426623973c10131.json b/backend/.sqlx/query-78f5684086a8c28a602c583389e974fffd7f74b10618b1467426623973c10131.json new file mode 100644 index 0000000000..94c81eb5b5 --- /dev/null +++ b/backend/.sqlx/query-78f5684086a8c28a602c583389e974fffd7f74b10618b1467426623973c10131.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT j.id AS \"id!\" FROM v2_job j LEFT JOIN v2_job_queue q USING (id) WHERE j.id = ANY($1) AND j.trigger_kind IS DISTINCT FROM 'schedule'::job_trigger_kind AND ($2::text[] IS NULL OR j.tag = ANY($2))", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id!", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "UuidArray", + "TextArray" + ] + }, + "nullable": [ + false + ] + }, + "hash": "78f5684086a8c28a602c583389e974fffd7f74b10618b1467426623973c10131" +} diff --git a/backend/.sqlx/query-7cab8475996f0abd7ca7888b460bd3faf3bef6e34200c258e3704981817c241e.json b/backend/.sqlx/query-7cab8475996f0abd7ca7888b460bd3faf3bef6e34200c258e3704981817c241e.json new file mode 100644 index 0000000000..4311abf5ff --- /dev/null +++ b/backend/.sqlx/query-7cab8475996f0abd7ca7888b460bd3faf3bef6e34200c258e3704981817c241e.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT \n path \n FROM \n flow_version \n WHERE \n id = $1 AND \n workspace_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "path", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "7cab8475996f0abd7ca7888b460bd3faf3bef6e34200c258e3704981817c241e" +} diff --git a/backend/.sqlx/query-a7468e9054beed88636786c5495ac3b9d9a6086ae6212ad9237b39a0346d7d26.json b/backend/.sqlx/query-a7468e9054beed88636786c5495ac3b9d9a6086ae6212ad9237b39a0346d7d26.json new file mode 100644 index 0000000000..cead5d7019 --- /dev/null +++ b/backend/.sqlx/query-a7468e9054beed88636786c5495ac3b9d9a6086ae6212ad9237b39a0346d7d26.json @@ -0,0 +1,66 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n flow_version.id AS version,\n flow_version.value->>'early_return' as early_return, \n flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, \n (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, \n flow.tag, \n flow.dedicated_worker, \n flow.on_behalf_of_email, \n flow.edited_by\n FROM \n flow_version\n INNER JOIN flow\n ON flow.path = flow_version.path AND\n flow.workspace_id = flow_version.workspace_id\n WHERE \n flow_version.workspace_id = $1 AND\n flow_version.path = $2 AND\n flow_version.id = $3\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "early_return", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "has_preprocessor", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "chat_input_enabled", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "tag", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "dedicated_worker", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "on_behalf_of_email", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "edited_by", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Int8" + ] + }, + "nullable": [ + false, + null, + null, + null, + true, + true, + true, + false + ] + }, + "hash": "a7468e9054beed88636786c5495ac3b9d9a6086ae6212ad9237b39a0346d7d26" +} diff --git a/backend/.sqlx/query-b82f97f2a858e20b1fb6d9b0b3eab5bab2bafa91689eac5073c1678c338afb27.json b/backend/.sqlx/query-b82f97f2a858e20b1fb6d9b0b3eab5bab2bafa91689eac5073c1678c338afb27.json new file mode 100644 index 0000000000..64b774cbd9 --- /dev/null +++ b/backend/.sqlx/query-b82f97f2a858e20b1fb6d9b0b3eab5bab2bafa91689eac5073c1678c338afb27.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT \n path \n FROM \n flow_version \n WHERE \n id = $1 AND \n workspace_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "path", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "b82f97f2a858e20b1fb6d9b0b3eab5bab2bafa91689eac5073c1678c338afb27" +} diff --git a/backend/.sqlx/query-cccf9e216d84dfbb801b92d697496f44d93b344190a26c8393867b3a268b9aba.json b/backend/.sqlx/query-cccf9e216d84dfbb801b92d697496f44d93b344190a26c8393867b3a268b9aba.json deleted file mode 100644 index 4a34830b0e..0000000000 --- a/backend/.sqlx/query-cccf9e216d84dfbb801b92d697496f44d93b344190a26c8393867b3a268b9aba.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO resource (workspace_id, path, value, description, resource_type, created_by, edited_at) VALUES ($1, 'f/app_themes/theme_0', '{\"name\": \"Default Theme\", \"value\": \"\"}', 'The default app theme', 'app_theme', $2, now()) ON CONFLICT DO NOTHING", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar" - ] - }, - "nullable": [] - }, - "hash": "cccf9e216d84dfbb801b92d697496f44d93b344190a26c8393867b3a268b9aba" -} diff --git a/backend/.sqlx/query-e1409c67b93881cf68f6ac9c8bae0856cf426c7e7860c8b5b799972baa8e6945.json b/backend/.sqlx/query-e1409c67b93881cf68f6ac9c8bae0856cf426c7e7860c8b5b799972baa8e6945.json deleted file mode 100644 index da6ca296b1..0000000000 --- a/backend/.sqlx/query-e1409c67b93881cf68f6ac9c8bae0856cf426c7e7860c8b5b799972baa8e6945.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, on_behalf_of_email, edited_by, flow_version.id AS version\n FROM flow\n INNER JOIN flow_version\n ON flow_version.id = $3\n WHERE flow.path = $1 and flow.workspace_id = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "tag", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "dedicated_worker", - "type_info": "Bool" - }, - { - "ordinal": 2, - "name": "early_return", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "has_preprocessor", - "type_info": "Bool" - }, - { - "ordinal": 4, - "name": "chat_input_enabled", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "on_behalf_of_email", - "type_info": "Text" - }, - { - "ordinal": 6, - "name": "edited_by", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "version", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Text", - "Int8" - ] - }, - "nullable": [ - true, - true, - null, - null, - null, - true, - false, - false - ] - }, - "hash": "e1409c67b93881cf68f6ac9c8bae0856cf426c7e7860c8b5b799972baa8e6945" -} diff --git a/backend/.sqlx/query-e2474b7855c8b08f927f2b987421e773e537a8eb0a113477764bbab14e1f3a3d.json b/backend/.sqlx/query-f0c9670c27d9a3b8e8f316fd28f7d3ede2ecc3225ff2879ff5dfd59c4ef236d5.json similarity index 69% rename from backend/.sqlx/query-e2474b7855c8b08f927f2b987421e773e537a8eb0a113477764bbab14e1f3a3d.json rename to backend/.sqlx/query-f0c9670c27d9a3b8e8f316fd28f7d3ede2ecc3225ff2879ff5dfd59c4ef236d5.json index c7c7214a87..19842dda7d 100644 --- a/backend/.sqlx/query-e2474b7855c8b08f927f2b987421e773e537a8eb0a113477764bbab14e1f3a3d.json +++ b/backend/.sqlx/query-f0c9670c27d9a3b8e8f316fd28f7d3ede2ecc3225ff2879ff5dfd59c4ef236d5.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n value->'preprocessor_module'->'value' as \"preprocessor_module: _\",\n schema as \"schema: _\"\n FROM flow_version\n WHERE\n path = $1\n AND workspace_id = $2\n ORDER BY created_at DESC\n LIMIT 1", + "query": "SELECT\n value->'preprocessor_module'->'value' as \"preprocessor_module: _\",\n schema as \"schema: _\"\n FROM flow_version\n WHERE\n id = $1\n AND workspace_id = $2", "describe": { "columns": [ { @@ -16,7 +16,7 @@ ], "parameters": { "Left": [ - "Text", + "Int8", "Text" ] }, @@ -25,5 +25,5 @@ true ] }, - "hash": "e2474b7855c8b08f927f2b987421e773e537a8eb0a113477764bbab14e1f3a3d" + "hash": "f0c9670c27d9a3b8e8f316fd28f7d3ede2ecc3225ff2879ff5dfd59c4ef236d5" } diff --git a/backend/.sqlx/query-fc9753f501974c4b570d710c7621ff1f6787a852bb27acf49593af963a6aacca.json b/backend/.sqlx/query-fc9753f501974c4b570d710c7621ff1f6787a852bb27acf49593af963a6aacca.json new file mode 100644 index 0000000000..b699cc582a --- /dev/null +++ b/backend/.sqlx/query-fc9753f501974c4b570d710c7621ff1f6787a852bb27acf49593af963a6aacca.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT path FROM flow_version WHERE id = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "path", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "fc9753f501974c4b570d710c7621ff1f6787a852bb27acf49593af963a6aacca" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index d224f52b22..836da4c13c 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -199,22 +199,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -800,9 +800,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" +checksum = "5932a7d9d28b0d2ea34c6b3779d35e3dd6f6345317c34e73438c4f1f29144151" dependencies = [ "aws-lc-sys", "zeroize", @@ -810,9 +810,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.32.3" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c" +checksum = "1826f2e4cfc2cd19ee53c42fbf68e2f81ec21108e0b7ecf6a71cf062137360fc" dependencies = [ "bindgen 0.72.1", "cc", @@ -1026,7 +1026,7 @@ dependencies = [ "http 1.3.1", "http-body 0.4.6", "hyper 0.14.32", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-rustls 0.24.2", "hyper-rustls 0.27.7", "hyper-util", @@ -1182,7 +1182,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-util", "itoa", "matchit", @@ -1569,7 +1569,7 @@ dependencies = [ "hex", "http 1.3.1", "http-body-util", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-named-pipe", "hyper-util", "hyperlocal", @@ -1799,9 +1799,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" dependencies = [ "serde", ] @@ -1945,9 +1945,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.45" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" dependencies = [ "find-msvc-tools", "jobserver", @@ -3625,7 +3625,7 @@ dependencies = [ "hickory-resolver", "http 1.3.1", "http-body-util", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-rustls 0.27.7", "hyper-util", "ipnet", @@ -3713,7 +3713,7 @@ dependencies = [ "http 1.3.1", "httparse", "hyper 0.14.32", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-util", "itertools 0.10.5", "memmem", @@ -3906,7 +3906,7 @@ dependencies = [ "hkdf", "http 1.3.1", "http-body-util", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-util", "idna", "indexmap 2.11.1", @@ -4177,7 +4177,7 @@ dependencies = [ "http 1.3.1", "http-body-util", "hyper 0.14.32", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-util", "libc", "log", @@ -4231,7 +4231,7 @@ dependencies = [ "deno_error", "deno_tls", "http-body-util", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-rustls 0.27.7", "hyper-util", "log", @@ -4361,7 +4361,7 @@ dependencies = [ "h2 0.4.12", "http 1.3.1", "http-body-util", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-util", "once_cell", "rustls-tokio-stream", @@ -5225,7 +5225,7 @@ dependencies = [ "base64 0.21.7", "bytes", "http-body-util", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-util", "pin-project", "rand 0.8.5", @@ -5292,9 +5292,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "fixedbitset" @@ -6604,9 +6604,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -6635,7 +6635,7 @@ dependencies = [ "futures-util", "headers", "http 1.3.1", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-rustls 0.27.7", "hyper-util", "pin-project-lite", @@ -6652,7 +6652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" dependencies = [ "hex", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-util", "pin-project-lite", "tokio", @@ -6683,7 +6683,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.3.1", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-util", "log", "rustls 0.23.29", @@ -6701,7 +6701,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.8.0", + "hyper 1.8.1", "hyper-util", "pin-project-lite", "tokio", @@ -6729,7 +6729,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-util", "native-tls", "tokio", @@ -6739,9 +6739,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "base64 0.22.1", "bytes", @@ -6750,7 +6750,7 @@ dependencies = [ "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.8.0", + "hyper 1.8.1", "ipnet", "libc", "percent-encoding", @@ -6771,7 +6771,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" dependencies = [ "hex", "http-body-util", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-util", "pin-project-lite", "tokio", @@ -7419,7 +7419,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-http-proxy", "hyper-rustls 0.27.7", "hyper-timeout", @@ -8946,7 +8946,7 @@ dependencies = [ "http-body-util", "httparse", "humantime", - "hyper 1.8.0", + "hyper 1.8.1", "itertools 0.14.0", "md-5 0.10.6", "parking_lot 0.12.5", @@ -10712,7 +10712,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-rustls 0.27.7", "hyper-tls 0.6.0", "hyper-util", @@ -10772,7 +10772,7 @@ dependencies = [ "futures", "getrandom 0.2.16", "http 1.3.1", - "hyper 1.8.0", + "hyper 1.8.1", "parking_lot 0.11.2", "reqwest 0.12.24", "reqwest-middleware", @@ -10933,9 +10933,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" dependencies = [ "const-oid", "digest 0.10.7", @@ -13945,7 +13945,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.8.0", + "hyper 1.8.1", "hyper-timeout", "hyper-util", "percent-encoding", @@ -15148,7 +15148,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "aws-sdk-config", @@ -15208,7 +15208,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "argon2", @@ -15251,7 +15251,7 @@ dependencies = [ "hf-hub", "hmac", "http 1.3.1", - "hyper 1.8.0", + "hyper 1.8.1", "indexmap 2.11.1", "itertools 0.14.0", "jsonwebtoken 8.3.0", @@ -15269,6 +15269,7 @@ dependencies = [ "pg_escape", "pin-project", "postgres-native-tls 0.5.0", + "postgres-native-tls 0.5.1", "prometheus", "quick_cache", "rand 0.9.0", @@ -15328,7 +15329,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.575.0" +version = "1.576.3" dependencies = [ "base64 0.22.1", "chrono", @@ -15343,7 +15344,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.575.0" +version = "1.576.3" dependencies = [ "chrono", "lazy_static", @@ -15357,7 +15358,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "axum", @@ -15376,7 +15377,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "async-recursion", @@ -15404,7 +15405,7 @@ dependencies = [ "globset", "hex", "hmac", - "hyper 1.8.0", + "hyper 1.8.1", "indexmap 2.11.1", "itertools 0.14.0", "jsonwebtoken 8.3.0", @@ -15461,7 +15462,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.575.0" +version = "1.576.3" dependencies = [ "regex", "serde", @@ -15476,7 +15477,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "bytes", @@ -15500,7 +15501,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.575.0" +version = "1.576.3" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -15512,7 +15513,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.575.0" +version = "1.576.3" dependencies = [ "convert_case 0.6.0", "serde", @@ -15521,7 +15522,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "lazy_static", @@ -15533,7 +15534,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "serde_json", @@ -15545,7 +15546,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "gosyn", @@ -15557,7 +15558,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "lazy_static", @@ -15569,7 +15570,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "serde_json", @@ -15581,7 +15582,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "nu-parser", @@ -15592,7 +15593,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15603,7 +15604,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15615,7 +15616,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "async-recursion", @@ -15638,7 +15639,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "lazy_static", @@ -15652,7 +15653,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -15669,7 +15670,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "lazy_static", @@ -15683,7 +15684,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "lazy_static", @@ -15701,7 +15702,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "serde", @@ -15712,7 +15713,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "async-recursion", @@ -15747,7 +15748,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.575.0" +version = "1.576.3" dependencies = [ "wasm-bindgen", "wasm-bindgen-test", @@ -15757,7 +15758,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.575.0" +version = "1.576.3" dependencies = [ "anyhow", "async-once-cell", @@ -16051,13 +16052,13 @@ dependencies = [ [[package]] name = "windows-registry" -version = "0.5.3" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 5702c3a8f5..18bfd888d2 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.575.0" +version = "1.576.3" authors.workspace = true edition.workspace = true @@ -33,7 +33,7 @@ members = [ exclude = ["./windmill-duckdb-ffi-internal"] [workspace.package] -version = "1.575.0" +version = "1.576.3" authors = ["Ruben Fiszel "] edition = "2021" @@ -259,7 +259,7 @@ tokio-tar = "^0" tempfile = "^3" tokio-util = { version = "^0", features = ["io"] } json-pointer = "^0" -itertools = "^0" +itertools = "^0.14.0" regex = "^1" semver = "^1" aws-sigv4 = "^1.3.4" @@ -300,7 +300,7 @@ swc_ecma_visit = "=0.104.8" async-recursion = "^1" -base64 = "^0" +base64 = "^0.22.1" base32 = "^0" hmac = "0.12.1" sha2 = "0.10.6" diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 0da534da09..7d5311398e 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -7cdb62d968feef22046b12a1217b711af457cced \ No newline at end of file +423c3c2d175b15b7c0f010e08f1848a7710e31be \ No newline at end of file diff --git a/backend/parsers/windmill-parser-bash/src/lib.rs b/backend/parsers/windmill-parser-bash/src/lib.rs index 5bfe6b20c5..f1652ed5d6 100644 --- a/backend/parsers/windmill-parser-bash/src/lib.rs +++ b/backend/parsers/windmill-parser-bash/src/lib.rs @@ -46,8 +46,6 @@ pub fn parse_powershell_sig(code: &str) -> anyhow::Result { lazy_static::lazy_static! { static ref RE_BASH: Regex = Regex::new(r#"(?m)^(\w+)="\$(?:(\d+)|\{(\d+)\}|\{(\d+):-(.*)\})"(?:[\t ]*)?(?:#.*)?\r?$"#).unwrap(); - - static ref RE_POWERSHELL_ARGS: Regex = Regex::new(r#"(?:\[([\w\[\]]+)\])?\$(\w+)[\t ]*(?:=[\t ]*(?:(?:(?:"|')([^"\n\r\$]*)(?:"|'))|([\d.]+)))?\r?"#).unwrap(); } fn parse_bash_file(code: &str) -> anyhow::Result>> { @@ -98,10 +96,12 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti let lower_code = code.to_lowercase(); let param_start = lower_code.find("param")?; - // Verify that only comments and whitespace appear before "param" + // Verify that only comments, whitespace, and [CmdletBinding()] appear before "param" let before_param = &code[..param_start]; let mut chars = before_param.chars().peekable(); let mut in_block_comment = false; + let mut in_attribute_bracket = false; + let mut bracket_depth = 0; while let Some(ch) = chars.next() { if in_block_comment { @@ -110,6 +110,19 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti chars.next(); // consume '>' in_block_comment = false; } + } else if in_attribute_bracket { + // Track bracket depth to handle nested brackets/parens in attributes + match ch { + '[' => bracket_depth += 1, + ']' => { + bracket_depth -= 1; + if bracket_depth == 0 { + in_attribute_bracket = false; + } + } + // Allow parentheses inside attributes (e.g., [CmdletBinding()]) + _ => {} + } } else { match ch { // Start of block comment: <# @@ -126,6 +139,11 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti chars.next(); } } + // Start of attribute bracket (e.g., [CmdletBinding()]) + '[' => { + in_attribute_bracket = true; + bracket_depth = 1; + } // Whitespace is allowed c if c.is_whitespace() => {} // Any other character means there's code before param @@ -134,8 +152,8 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti } } - // If we're still in a block comment at the end, it's unclosed - invalid - if in_block_comment { + // If we're still in a block comment or unclosed attribute bracket, it's invalid + if in_block_comment || in_attribute_bracket { return None; } @@ -214,108 +232,269 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti None } -enum ParserState { - Normal, - InSingleQuote, - InDoubleQuote, -} -fn split_pwsh_args(code: &str) -> Vec<&str> { - let mut chars = code.char_indices().peekable(); - let mut state = ParserState::Normal; - let mut splits = vec![]; - let mut last_idx = 0; - while let Some((idx, char)) = chars.next() { - match (&state, char) { - (ParserState::Normal, '\'') => { - state = ParserState::InSingleQuote; - } - (ParserState::Normal, '"') => { - state = ParserState::InDoubleQuote; - } - (ParserState::InSingleQuote, '\'') => { - state = ParserState::Normal; - } - (ParserState::InDoubleQuote, '"') => { - state = ParserState::Normal; - } - (ParserState::Normal, ',') => { - splits.push(&code[last_idx..idx]); - last_idx = idx + 1; // skip the comma - } - _ => {} - } - } - - if last_idx < code.len() { - splits.push(&code[last_idx..]); - } - - splits -} - fn parse_powershell_single_typ(typ: &str) -> Typ { match typ.to_lowercase().as_str() { "string" => Typ::Str(None), "int" | "long" => Typ::Int, "decimal" | "double" | "single" => Typ::Float, "datetime" => Typ::Datetime, - "bool" => Typ::Bool, + "bool" | "switch" => Typ::Bool, "pscustomobject" => Typ::Object(ObjectType::new(None, None)), _ => Typ::Str(None), } } -fn parse_powershell_file(code: &str) -> anyhow::Result>> { - let param_wrapper = extract_powershell_param_block(code, false); - let mut args = vec![]; - if let Some(param_wrapper) = param_wrapper { - let params = split_pwsh_args(param_wrapper); - for param in params { - if let Some(cap) = RE_POWERSHELL_ARGS.captures(param) { - let typ = cap.get(1).map(|x| x.as_str().to_string()); - let name = cap.get(2).unwrap().as_str().to_string(); +/// Single-pass PowerShell parameter parser. +/// Parses the content of a param() block and extracts all parameter information. +/// +/// This function processes PowerShell parameter declarations in a single pass, handling: +/// - Parameter attributes: [Parameter(Mandatory)], [Parameter(Mandatory=$true)], [ValidateSet(...)], etc. +/// - Type annotations: [string], [int[]], [PSCustomObject], etc. +/// - Variable names: $Name, $Value, etc. +/// - Default values: = 'text', = 25, = $env:VAR, etc. +/// - Mandatory detection: Parameters with Mandatory attribute are marked as required +fn parse_powershell_parameters(content: &str) -> anyhow::Result> { + #[derive(Debug, PartialEq)] + enum State { + Normal, + InSingleQuote, + InDoubleQuote, + InBracket, + } - let mut parsed_typ = if let Some(typ) = typ { - if typ.as_str().ends_with("[]") { - Some(Typ::List(Box::new(parse_powershell_single_typ( - typ.as_str().strip_suffix("[]").unwrap(), - )))) - } else { - Some(parse_powershell_single_typ(typ.as_str())) + let mut args = Vec::new(); + let mut chars = content.char_indices().peekable(); + let mut state = State::Normal; + let mut bracket_depth: i32 = 0; + let mut paren_depth: i32 = 0; + + // Current parameter being built + let mut type_annotation: Option = None; + let mut var_name: Option = None; + let mut default_value: Option = None; + let mut is_mandatory = false; + + // Track position for extracting text + let mut last_bracket_start = None; + let mut found_dollar = false; + + while let Some((idx, ch)) = chars.next() { + match state { + State::InSingleQuote => { + if ch == '\'' { + state = State::Normal; + } + } + State::InDoubleQuote => { + if ch == '"' { + // Check for escape character + if idx > 0 && content.chars().nth(idx - 1) != Some('`') { + state = State::Normal; } - } else { - None - }; + } + } + State::InBracket => { + match ch { + '[' => bracket_depth += 1, + ']' => { + bracket_depth -= 1; + if bracket_depth == 0 { + // Extract the bracket content + if let Some(start) = last_bracket_start { + let bracket_content = &content[start + 1..idx]; - let default = if let Some(x) = cap.get(3) { - Some(json!(x.as_str().to_string())) - } else if let Some(x) = cap.get(4) { - if parsed_typ.is_none() { - if x.as_str().parse::().is_ok() { - parsed_typ = Some(Typ::Int); - } else if x.as_str().parse::().is_ok() { - parsed_typ = Some(Typ::Float); + // Check if this is a Parameter attribute with Mandatory (case-insensitive) + let lower = bracket_content.to_lowercase(); + if lower.starts_with("parameter(") || lower.starts_with("parameter ") { + // Check for Mandatory (case-insensitive) + if lower.contains("mandatory") { + // Check if it's explicitly set to false + if !lower.contains("mandatory=$false") && !lower.contains("mandatory = $false") { + is_mandatory = true; + } + } + } + + // Check if this looks like a type (simple word, possibly with []) + let is_type = !bracket_content.contains('(') + && !bracket_content.contains('=') + && (bracket_content.chars().next().unwrap_or(' ').is_alphabetic() + || bracket_content.starts_with('[')); + + if is_type && !found_dollar { + type_annotation = Some(bracket_content.to_string()); + } + } + state = State::Normal; + last_bracket_start = None; } } - serde_json::Number::from_str(x.as_str()) - .ok() - .map(serde_json::Value::Number) - } else { - None - }; + '(' => paren_depth += 1, + ')' => paren_depth = paren_depth.saturating_sub(1), + _ => {} + } + } + State::Normal => { + match ch { + '\'' => state = State::InSingleQuote, + '"' => state = State::InDoubleQuote, + '[' => { + state = State::InBracket; + bracket_depth = 1; + last_bracket_start = Some(idx); + } + '$' => { + found_dollar = true; + // Extract variable name + let name_start = idx + 1; + let mut name_end = name_start; + while let Some(&(_, next_ch)) = chars.peek() { + if next_ch.is_alphanumeric() || next_ch == '_' { + name_end += 1; + chars.next(); + } else { + break; + } + } + var_name = Some(content[name_start..name_end].to_string()); + } + '=' if found_dollar => { + // Extract default value + // Skip whitespace after = + while let Some(&(_, next_ch)) = chars.peek() { + if next_ch.is_whitespace() { + chars.next(); + } else { + break; + } + } - args.push(Arg { - name: name, - typ: parsed_typ.unwrap_or(Typ::Str(None)), - default: default.clone(), - otyp: None, - has_default: default.is_some(), - oidx: None, - }); + let default_start = chars.peek().map(|(i, _)| *i).unwrap_or(content.len()); + let mut default_end = default_start; + let mut in_string = false; + let mut string_char = ' '; + + while let Some((i, ch)) = chars.peek().copied() { + if in_string { + if ch == string_char && content.chars().nth(i.saturating_sub(1)) != Some('`') { + in_string = false; + default_end = i + 1; + chars.next(); + } else { + default_end = i + 1; + chars.next(); + } + } else if ch == '\'' || ch == '"' { + in_string = true; + string_char = ch; + default_end = i + 1; + chars.next(); + } else if ch == ',' { + break; + } else if ch.is_whitespace() && chars.clone().skip(1).next().map(|(_, c)| c) == Some(',') { + break; + } else { + default_end = i + 1; + chars.next(); + } + } + + default_value = Some(content[default_start..default_end].trim().to_string()); + } + ',' => { + // End of parameter, finalize it + if let Some(name) = var_name.take() { + args.push(finalize_parameter(name, type_annotation.take(), default_value.take(), is_mandatory)?); + } + + // Reset for next parameter + type_annotation = None; + var_name = None; + default_value = None; + is_mandatory = false; + found_dollar = false; + } + _ => {} + } } } } - Ok(Some(args)) + + // Finalize last parameter + if let Some(name) = var_name { + args.push(finalize_parameter(name, type_annotation, default_value, is_mandatory)?); + } + + Ok(args) +} + +fn finalize_parameter( + name: String, + type_annotation: Option, + default_value: Option, + is_mandatory: bool, +) -> anyhow::Result { + // Store the original PowerShell type for use in the executor + let otyp = type_annotation.clone(); + + let mut parsed_typ = if let Some(typ) = type_annotation { + if typ.ends_with("[]") { + Some(Typ::List(Box::new(parse_powershell_single_typ( + typ.strip_suffix("[]").unwrap(), + )))) + } else { + Some(parse_powershell_single_typ(&typ)) + } + } else { + None + }; + + let default = if let Some(default_str) = default_value { + // Try to parse as string (quoted) + if (default_str.starts_with('"') && default_str.ends_with('"')) + || (default_str.starts_with('\'') && default_str.ends_with('\'')) + { + Some(json!(default_str[1..default_str.len() - 1].to_string())) + } else { + // Try to parse as number + if parsed_typ.is_none() { + if default_str.parse::().is_ok() { + parsed_typ = Some(Typ::Int); + } else if default_str.parse::().is_ok() { + parsed_typ = Some(Typ::Float); + } + } + serde_json::Number::from_str(&default_str) + .ok() + .map(serde_json::Value::Number) + } + } else { + None + }; + + // has_default semantics: + // - true: parameter is optional (has a default value OR is not mandatory) + // - false: parameter is required (marked as Mandatory AND no default value) + // Simplified: A parameter is optional unless it's mandatory without a default + let has_default = default.is_some() || !is_mandatory; + + Ok(Arg { + name, + typ: parsed_typ.unwrap_or(Typ::Str(None)), + default: default.clone(), + otyp, + has_default, + oidx: None, + }) +} + +fn parse_powershell_file(code: &str) -> anyhow::Result>> { + let param_wrapper = extract_powershell_param_block(code, false); + if let Some(param_wrapper) = param_wrapper { + Ok(Some(parse_powershell_parameters(param_wrapper)?)) + } else { + Ok(Some(vec![])) + } } #[cfg(test)] @@ -402,23 +581,23 @@ non_required="${5:-}" star_kwargs: false, args: vec![ Arg { - otyp: None, + otyp: None, // No type annotation name: "Msg".to_string(), typ: Typ::Str(None), default: None, - has_default: false, + has_default: true, // Optional (not mandatory) oidx: None }, Arg { - otyp: None, + otyp: Some("string".to_string()), // [string] name: "Msg2".to_string(), typ: Typ::Str(None), default: None, - has_default: false, + has_default: true, // Optional (not mandatory) oidx: None }, Arg { - otyp: None, + otyp: None, // No type annotation name: "Dflt".to_string(), typ: Typ::Str(None), default: Some(json!("default value, with comma")), @@ -426,7 +605,7 @@ non_required="${5:-}" oidx: None }, Arg { - otyp: None, + otyp: Some("int".to_string()), // [int] name: "Nb".to_string(), typ: Typ::Int, default: Some(json!(3)), @@ -434,7 +613,7 @@ non_required="${5:-}" oidx: None }, Arg { - otyp: None, + otyp: None, // Type inferred from default value name: "Nb2".to_string(), typ: Typ::Float, default: Some(json!(5.0)), @@ -442,7 +621,7 @@ non_required="${5:-}" oidx: None }, Arg { - otyp: None, + otyp: None, // Type inferred from default value name: "Nb3".to_string(), typ: Typ::Int, default: Some(json!(5)), @@ -450,35 +629,35 @@ non_required="${5:-}" oidx: None }, Arg { - otyp: None, + otyp: None, // No type annotation name: "Wahoo".to_string(), typ: Typ::Str(None), default: None, - has_default: false, + has_default: true, // Optional (not mandatory) oidx: None }, Arg { - otyp: None, + otyp: Some("PSCustomObject".to_string()), // [PSCustomObject] name: "Obj".to_string(), typ: Typ::Object(ObjectType::new(None, None)), default: None, - has_default: false, + has_default: true, // Optional (not mandatory) oidx: None }, Arg { - otyp: None, + otyp: Some("string[]".to_string()), // [string[]] name: "Arr".to_string(), typ: Typ::List(Box::new(Typ::Str(None))), default: None, - has_default: false, + has_default: true, // Optional (not mandatory) oidx: None }, Arg { - otyp: None, + otyp: Some("string".to_string()), // [string] (last type bracket with Mandatory) name: "Message".to_string(), typ: Typ::Str(None), default: None, - has_default: false, + has_default: false, // Required (Mandatory attribute) oidx: None } ], @@ -602,6 +781,242 @@ non_required="${5:-}" extract_powershell_param_block("function test-x{ param($Name)\n}", false), None ); + + // Valid: [CmdletBinding()] before param + assert_eq!( + extract_powershell_param_block("[CmdletBinding()]\nparam($Name)", false), + Some("$Name") + ); + assert_eq!( + extract_powershell_param_block("[CmdletBinding()]\nparam($Name, $Age)", true), + Some("param($Name, $Age)") + ); + + // Valid: [CmdletBinding()] with options before param + assert_eq!( + extract_powershell_param_block( + "[CmdletBinding(SupportsShouldProcess=$true)]\nparam($Path)", + false + ), + Some("$Path") + ); + + // Valid: Multiple attributes before param + assert_eq!( + extract_powershell_param_block( + "[CmdletBinding()]\n[OutputType([string])]\nparam($Value)", + false + ), + Some("$Value") + ); + + // Valid: CmdletBinding with comments + assert_eq!( + extract_powershell_param_block( + "# My function\n[CmdletBinding()]\nparam($Name)", + false + ), + Some("$Name") + ); + + // Valid: CmdletBinding with whitespace variations + assert_eq!( + extract_powershell_param_block( + "[CmdletBinding()] \n param($Name)", + false + ), + Some("$Name") + ); + + // Invalid: Unclosed attribute bracket + assert_eq!( + extract_powershell_param_block("[CmdletBinding(\nparam($Name)", false), + None + ); + } + + #[test] + fn test_parse_powershell_sig_with_parameter_attributes() -> anyhow::Result<()> { + // Test with [Parameter(Mandatory=$true)] attribute + let code = r#"[CmdletBinding()] +param( + [Parameter(Mandatory=$true)] + [string]$Name, + [Parameter(Mandatory=$false)] + [int]$Age = 25 +)"#; + let result = parse_powershell_sig(code)?; + assert_eq!(result.args.len(), 2); + assert_eq!(result.args[0].name, "Name"); + assert_eq!(result.args[0].typ, Typ::Str(None)); + assert_eq!(result.args[0].has_default, false); + assert_eq!(result.args[1].name, "Age"); + assert_eq!(result.args[1].typ, Typ::Int); + assert_eq!(result.args[1].has_default, true); + assert_eq!(result.args[1].default, Some(json!(25))); + + // Test with complex attributes + let code2 = r#"param( + [Parameter(Mandatory=$true, Position=0)] + [ValidateSet('Red', 'Green', 'Blue')] + [string]$Color, + [Parameter(ValueFromPipeline=$true)] + [string[]]$Items +)"#; + let result2 = parse_powershell_sig(code2)?; + assert_eq!(result2.args.len(), 2); + assert_eq!(result2.args[0].name, "Color"); + assert_eq!(result2.args[0].typ, Typ::Str(None)); + assert_eq!(result2.args[1].name, "Items"); + assert_eq!(result2.args[1].typ, Typ::List(Box::new(Typ::Str(None)))); + + Ok(()) + } + + #[test] + fn test_powershell_single_pass_parser() -> anyhow::Result<()> { + // Test the single-pass parser with a complex real-world example + let code = r#"[CmdletBinding()] +param( + [Parameter(Mandatory=$true, Position=0, HelpMessage="Enter the server name")] + [ValidateNotNullOrEmpty()] + [string]$ServerName, + + [Parameter(Mandatory=$false)] + [ValidateRange(1, 65535)] + [int]$Port = 8080, + + [Parameter(ValueFromPipeline=$true)] + [string[]]$LogFiles, + + [ValidateSet('Debug', 'Info', 'Warning', 'Error')] + [string]$LogLevel = 'Info', + + [PSCustomObject]$Config +)"#; + let result = parse_powershell_sig(code)?; + + assert_eq!(result.args.len(), 5); + + // ServerName: mandatory string with no default + assert_eq!(result.args[0].name, "ServerName"); + assert_eq!(result.args[0].typ, Typ::Str(None)); + assert_eq!(result.args[0].has_default, false); + + // Port: optional int with default + assert_eq!(result.args[1].name, "Port"); + assert_eq!(result.args[1].typ, Typ::Int); + assert_eq!(result.args[1].default, Some(json!(8080))); + assert_eq!(result.args[1].has_default, true); + + // LogFiles: string array (no mandatory, so optional) + assert_eq!(result.args[2].name, "LogFiles"); + assert_eq!(result.args[2].typ, Typ::List(Box::new(Typ::Str(None)))); + assert_eq!(result.args[2].has_default, true); // Optional (not mandatory) + + // LogLevel: string with default (ValidateSet is ignored but doesn't break parsing) + assert_eq!(result.args[3].name, "LogLevel"); + assert_eq!(result.args[3].typ, Typ::Str(None)); + assert_eq!(result.args[3].default, Some(json!("Info"))); + assert_eq!(result.args[3].has_default, true); + + // Config: PSCustomObject (no mandatory, so optional) + assert_eq!(result.args[4].name, "Config"); + assert_eq!(result.args[4].typ, Typ::Object(ObjectType::new(None, None))); + assert_eq!(result.args[4].has_default, true); // Optional (not mandatory) + + Ok(()) + } + + #[test] + fn test_powershell_mandatory_attribute() -> anyhow::Result<()> { + // Test various forms of the Mandatory attribute + let code = r#"param( + [Parameter(Mandatory)] + [string]$RequiredNoEquals, + + [Parameter(Mandatory=$true)] + [string]$RequiredWithTrue, + + [Parameter(Mandatory = $true)] + [string]$RequiredWithSpaces, + + [Parameter(Mandatory=$false)] + [string]$NotRequired, + + [Parameter(Position=0)] + [string]$NoMandatory, + + [string]$PlainRequired = "default", + + [Parameter(Mandatory=$true)] + [int]$RequiredInt +)"#; + let result = parse_powershell_sig(code)?; + + assert_eq!(result.args.len(), 7); + + // RequiredNoEquals: mandatory without =$true + assert_eq!(result.args[0].name, "RequiredNoEquals"); + assert_eq!(result.args[0].has_default, false); // Required (mandatory, no default) + + // RequiredWithTrue: mandatory with =$true + assert_eq!(result.args[1].name, "RequiredWithTrue"); + assert_eq!(result.args[1].has_default, false); // Required + + // RequiredWithSpaces: mandatory with spaces + assert_eq!(result.args[2].name, "RequiredWithSpaces"); + assert_eq!(result.args[2].has_default, false); // Required + + // NotRequired: explicitly Mandatory=$false + assert_eq!(result.args[3].name, "NotRequired"); + assert_eq!(result.args[3].has_default, true); // Optional (not mandatory) + + // NoMandatory: no Mandatory attribute + assert_eq!(result.args[4].name, "NoMandatory"); + assert_eq!(result.args[4].has_default, true); // Optional (not mandatory) + + // PlainRequired: has default value (always optional) + assert_eq!(result.args[5].name, "PlainRequired"); + assert_eq!(result.args[5].has_default, true); // Optional (has default) + assert_eq!(result.args[5].default, Some(json!("default"))); + + // RequiredInt: mandatory int + assert_eq!(result.args[6].name, "RequiredInt"); + assert_eq!(result.args[6].typ, Typ::Int); + assert_eq!(result.args[6].has_default, false); // Required + + Ok(()) + } + + #[test] + fn test_powershell_case_insensitive_parameter() -> anyhow::Result<()> { + // Test that [parameter(...)] is case-insensitive + let code = r#"param( + [parameter(Mandatory)] + [string]$LowerCase, + + [PARAMETER(MANDATORY=$TRUE)] + [string]$UpperCase, + + [Parameter(mandatory=$true)] + [string]$MixedCase +)"#; + let result = parse_powershell_sig(code)?; + + assert_eq!(result.args.len(), 3); + + // All should be detected as mandatory + assert_eq!(result.args[0].name, "LowerCase"); + assert_eq!(result.args[0].has_default, false); + + assert_eq!(result.args[1].name, "UpperCase"); + assert_eq!(result.args[1].has_default, false); + + assert_eq!(result.args[2].name, "MixedCase"); + assert_eq!(result.args[2].has_default, false); + + Ok(()) } #[test] diff --git a/backend/run_until_fail.sh b/backend/run_until_fail.sh index d8c461504b..935bacc88c 100755 --- a/backend/run_until_fail.sh +++ b/backend/run_until_fail.sh @@ -1,6 +1,7 @@ #!/bin/bash # Run the command repeatedly until it fails (exits with non-zero code) +cd windmill-duckdb-ffi-internal && ./build_dev.sh && cd .. while true; do DISABLE_EMBEDDING=true \ RUST_LOG=info \ @@ -9,7 +10,7 @@ while true; do GO_PATH=$(which go) \ UV_PATH=$(which uv) \ CARGO_PATH=$(which cargo) \ - cargo test --features enterprise,deno_core,license,python,rust,scoped_cache \ + cargo test --features enterprise,deno_core,license,python,duckdb,rust,scoped_cache \ -- --nocapture --test-threads=8 | tee /tmp/test.log # Capture the exit code of the cargo test command (not tee) diff --git a/backend/src/ee_oss.rs b/backend/src/ee_oss.rs index 4791a243a3..2aefaf7431 100644 --- a/backend/src/ee_oss.rs +++ b/backend/src/ee_oss.rs @@ -3,7 +3,7 @@ pub use crate::ee::*; #[cfg(not(feature = "private"))] -pub async fn set_license_key(_license_key: String) -> () { +pub async fn set_license_key(_license_key: String, _db: Option<&windmill_common::db::DB>) -> () { // Implementation is not open source } diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index b0818c96cd..eaf83c2e4c 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -79,7 +79,7 @@ use windmill_common::{ OTEL_METRICS_ENABLED, OTEL_TRACING_ENABLED, SERVICE_LOG_RETENTION_SECS, }; use windmill_common::{client::AuthedClient, global_settings::APP_WORKSPACED_ROUTE_SETTING}; -use windmill_queue::{SameWorkerPayload, cancel_job, get_queued_job_v2}; +use windmill_queue::{cancel_job, get_queued_job_v2, SameWorkerPayload}; use windmill_worker::{ handle_job_error, JobCompletedSender, SameWorkerSender, BUNFIG_INSTALL_SCOPES, INSTANCE_PYTHON_VERSION, JOB_DEFAULT_TIMEOUT, KEEP_JOB_DIR, MAVEN_REPOS, NO_DEFAULT_MAVEN, @@ -1271,7 +1271,7 @@ pub async fn reload_license_key(conn: &Connection) -> anyhow::Result<()> { tracing::error!("Could not parse LICENSE_KEY found: {:#?}", &q); } }; - set_license_key(value).await; + set_license_key(value, conn.as_sql()).await; Ok(()) } @@ -2219,7 +2219,6 @@ async fn handle_zombie_jobs(db: &Pool, base_internal_url: &str, node_n ); } - timeouts }; @@ -2253,7 +2252,6 @@ async fn handle_zombie_jobs(db: &Pool, base_internal_url: &str, node_n } } - let timeouts = non_restartable_jobs .into_iter() .map(|x| (x, ErrorMessage::RestartDisabled)) @@ -2289,59 +2287,59 @@ async fn handle_zombie_jobs(db: &Pool, base_internal_url: &str, node_n continue; } if let Some(job) = job.unwrap() { - let label = if job.permissioned_as != format!("u/{}", job.created_by) - && job.permissioned_as != job.created_by - { - format!("ephemeral-script-end-user-{}", job.created_by) - } else { - "ephemeral-script".to_string() - }; - let token = create_token_for_owner( - &db, - &job.workspace_id, - &job.permissioned_as, - &label, - *SCRIPT_TOKEN_EXPIRY, - &job.permissioned_as_email, - &job.id, - None, - Some(format!("handle_zombie_jobs")), - ) - .await - .expect("could not create job token"); + let label = if job.permissioned_as != format!("u/{}", job.created_by) + && job.permissioned_as != job.created_by + { + format!("ephemeral-script-end-user-{}", job.created_by) + } else { + "ephemeral-script".to_string() + }; + let token = create_token_for_owner( + &db, + &job.workspace_id, + &job.permissioned_as, + &label, + *SCRIPT_TOKEN_EXPIRY, + &job.permissioned_as_email, + &job.id, + None, + Some(format!("handle_zombie_jobs")), + ) + .await + .expect("could not create job token"); - let client = AuthedClient::new( - base_internal_url.to_string(), - job.workspace_id.to_string(), - token, - None, - ); + let client = AuthedClient::new( + base_internal_url.to_string(), + job.workspace_id.to_string(), + token, + None, + ); - let error_message = format!( + let error_message = format!( "Job timed out after no ping from job since {} (ZOMBIE_JOB_TIMEOUT: {}, reason: {:?}).\nThis likely means that the job died on worker {}, OOM are a common reason for worker crashes.\nCheck the workers around the time of the last ping and the exit code if any.", job.last_ping.unwrap_or_default(), *ZOMBIE_JOB_TIMEOUT, error_kind.to_string(), job.worker.clone().unwrap_or_default(), ); - let memory_peak = job.memory_peak.unwrap_or(0); - let _ = handle_job_error( - db, - &client, - &windmill_queue::MiniCompletedJob::from(job), - memory_peak, - None, - error::Error::ExecutionErr(error_message), - true, - Some(&same_worker_tx_never_used), - "", - node_name, - send_result_never_used, - #[cfg(feature = "benchmark")] - &mut windmill_common::bench::BenchmarkIter::new(), - ) - .await; - } + let memory_peak = job.memory_peak.unwrap_or(0); + let _ = handle_job_error( + db, + &client, + &windmill_queue::MiniCompletedJob::from(job), + memory_peak, + None, + error::Error::ExecutionErr(error_message), + true, + Some(&same_worker_tx_never_used), + "", + node_name, + send_result_never_used, + #[cfg(feature = "benchmark")] + &mut windmill_common::bench::BenchmarkIter::new(), + ) + .await; + } } } diff --git a/backend/tests/common/mod.rs b/backend/tests/common/mod.rs index 40a9980a96..129fd86d63 100644 --- a/backend/tests/common/mod.rs +++ b/backend/tests/common/mod.rs @@ -208,6 +208,7 @@ impl RunJob { false, None, debounce_job_id_o, + None, ) .await .expect("push has to succeed"); diff --git a/backend/tests/relative_imports.rs b/backend/tests/relative_imports.rs index 2de18732b4..052f5a3743 100644 --- a/backend/tests/relative_imports.rs +++ b/backend/tests/relative_imports.rs @@ -421,14 +421,16 @@ def main(): path: "f/rel/root_flow_renamed".into(), summary: "".into(), description: None, - value: to_raw_value(&serde_json::from_str::( - &serde_json::to_string(flow.value()) - .unwrap() - .replace("nstep1", "Foxes") - .replace("nstep2_2", "like") - .replace("nstep_4_1", "Emeralds"), - ) - .unwrap()), + value: to_raw_value( + &serde_json::from_str::( + &serde_json::to_string(flow.value()) + .unwrap() + .replace("nstep1", "Foxes") + .replace("nstep2_2", "like") + .replace("nstep_4_1", "Emeralds"), + ) + .unwrap(), + ), schema: None, draft_only: None, tag: None, @@ -437,7 +439,7 @@ def main(): deployment_message: None, visible_to_runner_only: None, on_behalf_of_email: None, - ws_error_handler_muted: None + ws_error_handler_muted: None, }) .send() .await @@ -862,6 +864,7 @@ def main(): false, None, None, + None, ) .await .unwrap(); @@ -1021,6 +1024,7 @@ def main(): false, None, debounce_job_id_o, + None, ) .await .unwrap(); @@ -1198,6 +1202,7 @@ def main(): false, None, debounce_job_id_o, + None, ) .await .unwrap(); @@ -1705,6 +1710,7 @@ WHERE false, None, None, + None, ) .await .unwrap(); @@ -1844,6 +1850,7 @@ WHERE false, None, None, + None, ) .await .unwrap(); @@ -2274,6 +2281,7 @@ WHERE false, None, None, + None, ) .await .unwrap(); @@ -2399,6 +2407,7 @@ WHERE // false, // None, // None, + // None, // ) // .await // .unwrap(); diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index 72b6aeea52..0a92dfc74d 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -2769,7 +2769,7 @@ async fn test_result_format(db: Pool) -> anyhow::Result<()> { let response = windmill_api::jobs::run_wait_result( &db, Uuid::parse_str(ordered_result_job_id).unwrap(), - "test-workspace".to_string(), + "test-workspace", None, "test-user", ) @@ -2944,3 +2944,33 @@ async fn test_workflow_as_code(db: Pool) -> anyhow::Result<()> { .await; Ok(()) } + +#[cfg(feature = "duckdb")] +#[sqlx::test(fixtures("base"))] +async fn test_duckdb_ffi(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + + let server = ApiServer::start(db.clone()).await?; + + let content = "-- result_collection=last_statement_first_row_scalar\nSELECT 'Hello world!';"; + + let flow: FlowValue = serde_json::from_value(serde_json::json!({ + "modules": [{ + "value": { + "type": "rawscript", + "language": "duckdb", + "content": content, + }, + }], + })) + .unwrap(); + + let result = + RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None, restarted_from: None }) + .run_until_complete(&db, false, server.addr.port()) + .await + .json_result() + .unwrap(); + assert_eq!(result, serde_json::json!("Hello world!")); + Ok(()) +} diff --git a/backend/windmill-api/Cargo.toml b/backend/windmill-api/Cargo.toml index 34cb7adfe9..7b8888b38e 100644 --- a/backend/windmill-api/Cargo.toml +++ b/backend/windmill-api/Cargo.toml @@ -94,6 +94,7 @@ tempfile.workspace = true tokio-util.workspace = true tokio-tar.workspace = true tokio-postgres.workspace = true +postgres-native-tls.workspace = true hmac.workspace = true cookie.workspace = true sha2.workspace = true diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index eecf52efbf..e6c4ce9ea4 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.575.0 + version: 1.576.3 title: Windmill API contact: @@ -5956,6 +5956,80 @@ paths: application/json: schema: {} + /w/{workspace}/jobs/run_wait_result/fv/{version}: + post: + summary: run flow by version and wait until completion + operationId: runWaitResultFlowByVersion + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: version + description: flow version ID + in: path + required: true + schema: + type: integer + format: int64 + - $ref: "#/components/parameters/IncludeHeader" + - $ref: "#/components/parameters/QueueLimit" + - $ref: "#/components/parameters/NewJobId" + - $ref: "#/components/parameters/SkipPreprocessor" + - name: memory_id + description: memory ID for chat-enabled flows + in: query + schema: + type: string + format: uuid + + requestBody: + description: script args + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ScriptArgs" + + responses: + "200": + description: job result + content: + application/json: + schema: {} + + get: + summary: run flow by version with GET and wait until completion + operationId: runWaitResultFlowByVersionGet + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: version + description: flow version ID + in: path + required: true + schema: + type: integer + format: int64 + - $ref: "#/components/parameters/IncludeHeader" + - $ref: "#/components/parameters/QueueLimit" + - $ref: "#/components/parameters/Payload" + - $ref: "#/components/parameters/NewJobId" + - $ref: "#/components/parameters/SkipPreprocessor" + - name: memory_id + description: memory ID for chat-enabled flows + in: query + schema: + type: string + format: uuid + + responses: + "200": + description: job result + content: + application/json: + schema: {} + /w/{workspace}/jobs/run_and_stream/f/{path}: post: summary: run flow by path and stream updates via SSE @@ -6032,6 +6106,94 @@ paths: schema: type: string + /w/{workspace}/jobs/run_and_stream/fv/{version}: + post: + summary: run flow by version and stream updates via SSE + operationId: runAndStreamFlowByVersion + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: version + description: flow version ID + in: path + required: true + schema: + type: integer + format: int64 + - $ref: "#/components/parameters/IncludeHeader" + - $ref: "#/components/parameters/QueueLimit" + - $ref: "#/components/parameters/NewJobId" + - $ref: "#/components/parameters/SkipPreprocessor" + - name: memory_id + description: memory ID for chat-enabled flows + in: query + schema: + type: string + format: uuid + - name: poll_delay_ms + description: delay between polling for job updates in milliseconds + in: query + schema: + type: integer + format: int64 + + requestBody: + description: flow args + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ScriptArgs" + + responses: + "200": + description: server-sent events stream of job updates + content: + text/event-stream: + schema: + type: string + + get: + summary: run flow by version with GET and stream updates via SSE + operationId: runAndStreamFlowByVersionGet + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: version + description: flow version ID + in: path + required: true + schema: + type: integer + format: int64 + - $ref: "#/components/parameters/IncludeHeader" + - $ref: "#/components/parameters/QueueLimit" + - $ref: "#/components/parameters/Payload" + - $ref: "#/components/parameters/NewJobId" + - $ref: "#/components/parameters/SkipPreprocessor" + - name: memory_id + description: memory ID for chat-enabled flows + in: query + schema: + type: string + format: uuid + - name: poll_delay_ms + description: delay between polling for job updates in milliseconds + in: query + schema: + type: integer + format: int64 + + responses: + "200": + description: server-sent events stream of job updates + content: + text/event-stream: + schema: + type: string + /w/{workspace}/jobs/run_and_stream/p/{path}: post: summary: run script by path and stream updates via SSE @@ -7680,6 +7842,64 @@ paths: type: string format: uuid + /w/{workspace}/jobs/run/fv/{version}: + post: + summary: run flow by version + operationId: runFlowByVersion + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: version + description: flow version ID + in: path + required: true + schema: + type: integer + format: int64 + - name: scheduled_for + description: when to schedule this job (leave empty for immediate run) + in: query + schema: + type: string + format: date-time + - name: scheduled_in_secs + description: schedule the script to execute in the number of seconds starting now + in: query + schema: + type: integer + - $ref: "#/components/parameters/SkipPreprocessor" + - $ref: "#/components/parameters/ParentJob" + - $ref: "#/components/parameters/WorkerTag" + - $ref: "#/components/parameters/NewJobId" + - $ref: "#/components/parameters/IncludeHeader" + - name: invisible_to_owner + description: make the run invisible to the the flow owner (default false) + in: query + schema: + type: boolean + - name: memory_id + description: memory ID for chat-enabled flows + in: query + schema: + type: string + format: uuid + requestBody: + description: flow args + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ScriptArgs" + responses: + "201": + description: job created + content: + text/plain: + schema: + type: string + format: uuid + /w/{workspace}/jobs/run/batch_rerun_jobs: post: summary: re-run multiple jobs @@ -8451,6 +8671,7 @@ paths: - $ref: "#/components/parameters/ResultFilter" - $ref: "#/components/parameters/AllowWildcards" - $ref: "#/components/parameters/PerPage" + - $ref: "#/components/parameters/JobTriggerKind" - name: is_skipped description: is the job skipped in: query @@ -14424,6 +14645,7 @@ paths: - $ref: "#/components/parameters/AllowWildcards" - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PerPage" + - $ref: "#/components/parameters/JobTriggerKind" - name: is_skipped description: is the job skipped in: query @@ -14862,6 +15084,27 @@ components: in: query schema: type: integer + JobTriggerKind: + name: trigger_kind + description: trigger kind (schedule, http, websocket...) + in: query + schema: + type: string + enum: + - webhook + - default_email + - email + - schedule + - http + - websocket + - postgres + - kafka + - nats + - mqtt + - sqs + - gcp + - poll + - cli OrderDesc: name: order_desc description: order by desc order (default true) @@ -18698,6 +18941,8 @@ components: properties: lock_error_logs: type: string + version_id: + type: number ExtraPerms: type: object diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index b669f5e9a9..c7b1bd18bc 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -1241,6 +1241,7 @@ async fn create_app_internal<'a>( false, None, None, + None, ) .await?; tracing::info!("Pushed app dependency job {}", dependency_job_uuid); @@ -1630,6 +1631,7 @@ async fn update_app_internal<'a>( false, None, None, + None, ) .await?; tracing::info!("Pushed app dependency job {}", dependency_job_uuid); @@ -1949,6 +1951,7 @@ async fn execute_component( false, end_user_email, None, + None, ) .await?; tx.commit().await?; diff --git a/backend/windmill-api/src/concurrency_groups.rs b/backend/windmill-api/src/concurrency_groups.rs index bfd0b8182f..4a96a16e0b 100644 --- a/backend/windmill-api/src/concurrency_groups.rs +++ b/backend/windmill-api/src/concurrency_groups.rs @@ -222,6 +222,7 @@ async fn get_concurrent_intervals( all_workspaces: _, concurrency_key: Some(_), allow_wildcards: None, + trigger_kind: _, } => true, _ => false, }; diff --git a/backend/windmill-api/src/ee_oss.rs b/backend/windmill-api/src/ee_oss.rs index a69c59c573..30756af8ab 100644 --- a/backend/windmill-api/src/ee_oss.rs +++ b/backend/windmill-api/src/ee_oss.rs @@ -13,7 +13,10 @@ use anyhow::anyhow; #[cfg(all(feature = "enterprise", not(feature = "private")))] use {std::sync::Arc, tokio::sync::RwLock}; #[cfg(not(feature = "private"))] -pub async fn validate_license_key(_license_key: String) -> anyhow::Result<(String, bool)> { +pub async fn validate_license_key( + _license_key: String, + _db: Option<&crate::db::DB>, +) -> anyhow::Result<(String, bool)> { // Implementation is not open source Err(anyhow!("License can't be validated in Windmill CE")) } diff --git a/backend/windmill-api/src/flows.rs b/backend/windmill-api/src/flows.rs index 1459a22a0e..7adb568b46 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -562,6 +562,7 @@ async fn create_flow( false, None, None, + None, ) .await?; @@ -1025,6 +1026,7 @@ async fn update_flow( false, None, None, + None, ) .await?; @@ -1118,24 +1120,25 @@ async fn get_flow_by_path( let flow_o = if query.with_starred_info.unwrap_or(false) { sqlx::query_as::<_, FlowWithStarred>( r#" - SELECT - flow.workspace_id, - flow.path, - flow.lock_error_logs, - flow.summary, - flow.description, - flow.archived, - flow.extra_perms, - flow.draft_only, - flow.dedicated_worker, - flow.tag, - flow.ws_error_handler_muted, - flow.timeout, - flow.visible_to_runner_only, - flow.on_behalf_of_email, - flow_version.schema, - flow_version.value, - flow_version.created_at AS edited_at, + SELECT + flow.workspace_id, + flow.path, + flow.lock_error_logs, + flow.summary, + flow.description, + flow.archived, + flow.extra_perms, + flow.draft_only, + flow.dedicated_worker, + flow.tag, + flow.ws_error_handler_muted, + flow.timeout, + flow.visible_to_runner_only, + flow.on_behalf_of_email, + flow_version.id AS version_id, + flow_version.schema, + flow_version.value, + flow_version.created_at AS edited_at, flow_version.created_by AS edited_by, favorite.path IS NOT NULL AS starred FROM flow @@ -1157,25 +1160,26 @@ async fn get_flow_by_path( } else { sqlx::query_as::<_, FlowWithStarred>( r#" - SELECT - flow.workspace_id, - flow.path, - flow.lock_error_logs, - flow.summary, - flow.description, - flow.archived, - flow.extra_perms, - flow.draft_only, - flow.dedicated_worker, - flow.tag, - flow.ws_error_handler_muted, - flow.timeout, - flow.visible_to_runner_only, - flow.on_behalf_of_email, - flow_version.schema, + SELECT + flow.workspace_id, + flow.path, + flow.lock_error_logs, + flow.summary, + flow.description, + flow.archived, + flow.extra_perms, + flow.draft_only, + flow.dedicated_worker, + flow.tag, + flow.ws_error_handler_muted, + flow.timeout, + flow.visible_to_runner_only, + flow.on_behalf_of_email, + flow_version.id AS version_id, + flow_version.schema, flow_version.value, - flow_version.created_at AS edited_at, - flow_version.created_by AS edited_by, + flow_version.created_at AS edited_at, + flow_version.created_by AS edited_by, NULL AS starred FROM flow LEFT JOIN flow_version diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index d695a6091d..dcb2b6cd08 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -34,7 +34,7 @@ use windmill_common::flow_conversations::add_message_to_conversation_tx; use windmill_common::flow_status::{JobResult, RestartedFrom}; use windmill_common::jobs::{ check_tag_available_for_workspace_internal, format_completed_job_result, format_result, - DynamicInput, ENTRYPOINT_OVERRIDE, + DynamicInput, JobTriggerKind, ENTRYPOINT_OVERRIDE, }; use windmill_common::s3_helpers::{upload_artifact_to_store, BundleFormat}; use windmill_common::utils::{RunnableKind, WarnAfterExt}; @@ -45,7 +45,7 @@ use windmill_common::{email_oss::send_email_html, server::load_smtp_config}; use windmill_common::variables::get_workspace_key; -use crate::triggers::trigger_helpers::ScriptId; +use crate::triggers::trigger_helpers::{FlowId, ScriptId}; use crate::{ add_webhook_allowed_origin, args::{self, RawWebhookArgs}, @@ -95,8 +95,9 @@ use windmill_common::{ }; use windmill_common::{ - get_latest_deployed_hash_for_path, get_latest_flow_version_info_for_path, - get_script_info_for_hash, utils::empty_as_none, FlowVersionInfo, ScriptHashInfo, BASE_URL, + get_flow_version_info_from_version, get_latest_deployed_hash_for_path, + get_latest_flow_version_info_for_path, get_script_info_for_hash, utils::empty_as_none, + FlowVersionInfo, ScriptHashInfo, BASE_URL, }; use windmill_queue::{ cancel_job, get_result_and_success_by_id_from_flow, job_is_complete, push, PushArgs, @@ -124,6 +125,13 @@ pub fn workspaced_service() -> Router { .layer(cors.clone()) .layer(ce_headers.clone()), ) + .route( + "/run/fv/:version", + post(run_flow_by_version) + .head(|| async { "" }) + .layer(cors.clone()) + .layer(ce_headers.clone()), + ) .route( "/run/batch_rerun_jobs", post(batch_rerun_jobs) @@ -176,6 +184,14 @@ pub fn workspaced_service() -> Router { .layer(cors.clone()) .layer(ce_headers.clone()), ) + .route( + "/run_wait_result/fv/:version", + post(run_wait_result_flow_by_version) + .get(run_wait_result_flow_by_version_get) + .head(|| async { "" }) + .layer(cors.clone()) + .layer(ce_headers.clone()), + ) .route( "/run_and_stream/f/*script_path", get(stream_flow_by_path) @@ -184,6 +200,14 @@ pub fn workspaced_service() -> Router { .layer(cors.clone()) .layer(ce_headers.clone()), ) + .route( + "/run_and_stream/fv/:version", + get(stream_flow_by_version) + .post(stream_flow_by_version) + .head(|| async { "" }) + .layer(cors.clone()) + .layer(ce_headers.clone()), + ) .route( "/run_and_stream/p/*script_path", get(stream_script_by_path) @@ -1750,6 +1774,22 @@ impl RunJobQuery { Ok(None) } } + + fn payload_as_args(&self) -> error::Result>> { + let payload_r = self.payload.clone().map(decode_payload).map(|x| { + x.map_err(|e| { + error::Error::internal_err(format!("Impossible to decode query payload: {e:#?}")) + }) + }); + + let payload_as_args = if let Some(payload) = payload_r { + payload? + } else { + HashMap::new() + }; + + Ok(payload_as_args) + } } #[derive(Deserialize, Clone)] @@ -1781,6 +1821,7 @@ pub struct ListQueueQuery { pub is_not_schedule: Option, pub concurrency_key: Option, pub allow_wildcards: Option, + pub trigger_kind: Option, } impl From for ListQueueQuery { @@ -1812,6 +1853,7 @@ impl From for ListQueueQuery { is_not_schedule: lcq.is_not_schedule, concurrency_key: lcq.concurrency_key, allow_wildcards: lcq.allow_wildcards, + trigger_kind: lcq.trigger_kind, } } } @@ -1935,6 +1977,10 @@ pub fn filter_list_queue_query( sqlb.and_where("trigger_kind IS DISTINCT FROM 'schedule'"); } + if let Some(tk) = &lq.trigger_kind { + sqlb.and_where_eq("trigger_kind", "?".bind(&format!("{}", tk))); + } + sqlb } @@ -2142,7 +2188,7 @@ async fn cancel_selection( let mut tx = user_db.begin(&authed).await?; let tags = get_scope_tags(&authed).map(|v| v.iter().map(|s| s.to_string()).collect_vec()); let jobs_to_cancel = sqlx::query_scalar!( - "SELECT j.id AS \"id!\" FROM v2_job j WHERE j.id = ANY($1) AND j.trigger_kind != 'schedule'::job_trigger_kind AND ($2::text[] IS NULL OR j.tag = ANY($2))", + "SELECT j.id AS \"id!\" FROM v2_job j LEFT JOIN v2_job_queue q USING (id) WHERE j.id = ANY($1) AND j.trigger_kind IS DISTINCT FROM 'schedule'::job_trigger_kind AND ($2::text[] IS NULL OR j.tag = ANY($2))", &jobs, tags.as_ref().map(|v| v.as_slice()) ) @@ -3851,6 +3897,7 @@ async fn batch_rerun_handle_job( StripPath(job.script_path.clone()), RunJobQuery { skip_preprocessor: Some(true), ..Default::default() }, PushArgsOwned { extra: None, args }, + None, ) .await; if let Ok((uuid, _)) = result { @@ -3867,6 +3914,7 @@ async fn batch_rerun_handle_job( StripPath(job.script_path.clone()), RunJobQuery { skip_preprocessor: Some(true), ..Default::default() }, PushArgsOwned { extra: None, args }, + None, ) .await } else { @@ -3987,28 +4035,22 @@ pub async fn run_flow_by_path( .await?; let (uuid, _) = - run_flow_by_path_inner(authed, db, user_db, w_id, flow_path, run_query, args).await?; + run_flow_by_path_inner(authed, db, user_db, w_id, flow_path, run_query, args, None).await?; Ok((StatusCode::CREATED, uuid.to_string())) } -pub async fn run_flow_by_path_inner( - authed: ApiAuthed, - db: DB, +pub async fn run_flow( + authed: &ApiAuthed, + db: &DB, user_db: UserDB, - w_id: String, - flow_path: StripPath, + w_id: &str, + flow_path: &str, + flow_version_info: FlowVersionInfo, run_query: RunJobQuery, args: PushArgsOwned, + trigger_kind: Option, ) -> error::Result<(Uuid, Option)> { - #[cfg(feature = "enterprise")] - check_license_key_valid().await?; - - let flow_path = flow_path.to_path(); - check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?; - - let userdb_authed = UserDbWithAuthed { db: user_db.clone(), authed: &authed.to_authed_ref() }; - let FlowVersionInfo { version, tag, @@ -4019,8 +4061,7 @@ pub async fn run_flow_by_path_inner( edited_by, early_return, .. - } = get_latest_flow_version_info_for_path(Some(userdb_authed), &db, &w_id, &flow_path, true) - .await?; + } = flow_version_info; let tag = run_query.tag.clone().or(tag); @@ -4078,6 +4119,7 @@ pub async fn run_flow_by_path_inner( false, None, None, + trigger_kind, ) .await?; @@ -4100,9 +4142,146 @@ pub async fn run_flow_by_path_inner( } tx.commit().await?; + Ok((uuid, early_return)) } +pub async fn run_flow_and_wait_result( + authed: &ApiAuthed, + db: &DB, + user_db: UserDB, + w_id: &str, + flow_path: &str, + flow_version_info: FlowVersionInfo, + run_query: RunJobQuery, + args: PushArgsOwned, + trigger_kind: Option, +) -> error::Result { + let (uuid, early_return) = run_flow( + authed, + db, + user_db, + w_id, + flow_path, + flow_version_info, + run_query, + args, + trigger_kind, + ) + .await?; + + run_wait_result(&db, uuid, w_id, early_return, &authed.username).await +} + +pub async fn run_flow_by_path_inner( + authed: ApiAuthed, + db: DB, + user_db: UserDB, + w_id: String, + flow_path: StripPath, + run_query: RunJobQuery, + args: PushArgsOwned, + trigger_kind: Option, +) -> error::Result<(Uuid, Option)> { + #[cfg(feature = "enterprise")] + check_license_key_valid().await?; + + let flow_path = flow_path.to_path(); + check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?; + + let userdb_authed = UserDbWithAuthed { db: user_db.clone(), authed: &authed.to_authed_ref() }; + + let flow_version_info = + get_latest_flow_version_info_for_path(Some(userdb_authed), &db, &w_id, &flow_path, true) + .await?; + + run_flow( + &authed, + &db, + user_db, + &w_id, + flow_path, + flow_version_info, + run_query, + args, + trigger_kind, + ) + .await +} + +pub async fn run_flow_by_version( + authed: ApiAuthed, + Extension(db): Extension, + Extension(user_db): Extension, + Path((w_id, version)): Path<(String, i64)>, + Query(run_query): Query, + args: RawWebhookArgs, +) -> error::Result<(StatusCode, String)> { + let args = args + .to_args_from_runnable( + &authed, + &db, + &w_id, + RunnableId::from_flow_version(version), + run_query.skip_preprocessor, + ) + .await?; + + let (uuid, _) = + run_flow_by_version_inner(authed, db, user_db, w_id, version, run_query, args, None) + .await?; + + Ok((StatusCode::CREATED, uuid.to_string())) +} + +pub async fn run_flow_by_version_inner( + authed: ApiAuthed, + db: DB, + user_db: UserDB, + w_id: String, + version: i64, + run_query: RunJobQuery, + args: PushArgsOwned, + trigger_kind: Option, +) -> error::Result<(Uuid, Option)> { + #[cfg(feature = "enterprise")] + check_license_key_valid().await?; + + let flow_path = sqlx::query_scalar!( + r#" + SELECT + path + FROM + flow_version + WHERE + id = $1 AND + workspace_id = $2 + "#, + version, + &w_id + ) + .fetch_one(&db) + .await?; + + check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?; + + let flow_version_info = + get_flow_version_info_from_version(&db, version, &w_id, &flow_path).await?; + + run_flow( + &authed, + &db, + user_db, + &w_id, + &flow_path, + flow_version_info, + run_query, + args, + trigger_kind, + ) + .await +} + #[cfg(not(feature = "enterprise"))] pub async fn restart_flow( _authed: ApiAuthed, @@ -4195,6 +4374,7 @@ pub async fn restart_flow( false, None, None, + None, ) .await?; tx.commit().await?; @@ -4219,8 +4399,17 @@ pub async fn run_script_by_path( ) .await?; - let (uuid, _) = - run_script_by_path_inner(authed, db, user_db, w_id, script_path, run_query, args).await?; + let (uuid, _) = run_script_by_path_inner( + authed, + db, + user_db, + w_id, + script_path, + run_query, + args, + None, + ) + .await?; Ok((StatusCode::CREATED, uuid.to_string())) } @@ -4233,6 +4422,7 @@ pub async fn run_script_by_path_inner( script_path: StripPath, run_query: RunJobQuery, args: PushArgsOwned, + trigger_kind: Option, ) -> error::Result<(Uuid, Option)> { #[cfg(feature = "enterprise")] check_license_key_valid().await?; @@ -4304,6 +4494,7 @@ pub async fn run_script_by_path_inner( false, None, None, + trigger_kind, ) .await?; tx.commit().await?; @@ -4461,6 +4652,7 @@ pub async fn run_workflow_as_code( false, None, None, + None, ) .await?; @@ -4577,7 +4769,7 @@ pub struct WindmillCompositeResult { pub async fn run_wait_result_internal( db: &DB, uuid: Uuid, - w_id: String, + w_id: &str, node_id_for_empty_return: Option, username: &str, ) -> error::Result<(Box, bool)> { @@ -4593,7 +4785,7 @@ pub async fn run_wait_result_internal( let mut g = Guard { done: false, id: uuid, - w_id: w_id.clone(), + w_id: w_id.to_string(), db: db.clone(), username: username.to_string(), }; @@ -4605,7 +4797,7 @@ pub async fn run_wait_result_internal( if let Some(node_id_for_empty_return) = node_id_for_empty_return.as_ref() { let result_and_success = get_result_and_success_by_id_from_flow( &db, - &w_id, + w_id, &uuid, node_id_for_empty_return, None, @@ -4762,7 +4954,7 @@ pub fn result_to_response(result: Box, success: bool) -> error::Result pub async fn run_wait_result( db: &DB, uuid: Uuid, - w_id: String, + w_id: &str, node_id_for_empty_return: Option, username: &str, ) -> error::Result { @@ -4922,18 +5114,11 @@ pub async fn run_wait_result_job_by_path_get( if method == http::Method::HEAD { return Ok(Json(serde_json::json!("")).into_response()); } - let payload_r = run_query.payload.map(decode_payload).map(|x| { - x.map_err(|e| Error::internal_err(format!("Impossible to decode query payload: {e:#?}"))) - }); - let payload_args = if let Some(payload) = payload_r { - payload? - } else { - HashMap::new() - }; + let payload_as_args = run_query.payload_as_args()?; let mut args = args.process_args(&authed, &db, &w_id, None).await?; - args.body = args::Body::HashMap(payload_args); + args.body = args::Body::HashMap(payload_as_args); let args = args .to_args_from_runnable( @@ -5005,11 +5190,12 @@ pub async fn run_wait_result_job_by_path_get( false, None, None, + None, ) .await?; tx.commit().await?; - let wait_result = run_wait_result(&db, uuid, w_id, None, &authed.username).await; + let wait_result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await; if delete_after_use.unwrap_or(false) { delete_job_metadata_after_use(&db, uuid).await?; } @@ -5034,20 +5220,10 @@ pub async fn run_wait_result_flow_by_path_get( if method == http::Method::HEAD { return Ok(Json(serde_json::json!("")).into_response()); } - let payload_r = run_query.payload.clone().map(decode_payload).map(|x| { - x.map_err(|e| { - error::Error::internal_err(format!("Impossible to decode query payload: {e:#?}")) - }) - }); - - let payload_args = if let Some(payload) = payload_r { - payload? - } else { - HashMap::new() - }; + let payload_as_args = run_query.payload_as_args()?; let mut args = args.process_args(&authed, &db, &w_id, None).await?; - args.body = args::Body::HashMap(payload_args); + args.body = args::Body::HashMap(payload_as_args); let args = args .to_args_from_runnable( @@ -5159,11 +5335,12 @@ pub async fn run_wait_result_script_by_path_internal( false, None, None, + None, ) .await?; tx.commit().await?; - let wait_result = run_wait_result(&db, uuid, w_id, None, &authed.username).await; + let wait_result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await; if delete_after_use.unwrap_or(false) { delete_job_metadata_after_use(&db, uuid).await?; } @@ -5281,11 +5458,12 @@ pub async fn run_wait_result_script_by_hash( false, None, None, + None, ) .await?; tx.commit().await?; - let wait_result = run_wait_result(&db, uuid, w_id, None, &authed.username).await; + let wait_result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await; if delete_after_use.unwrap_or(false) { delete_job_metadata_after_use(&db, uuid).await?; } @@ -5342,6 +5520,28 @@ pub async fn stream_flow_by_path( .await } +pub async fn stream_flow_by_version( + authed: ApiAuthed, + Extension(db): Extension, + Extension(user_db): Extension, + Path((w_id, version)): Path<(String, i64)>, + Query(run_query): Query, + method: hyper::http::Method, + args: RawWebhookArgs, +) -> error::Result { + stream_job( + authed, + db, + user_db, + w_id, + RunnableId::from_flow_version(version), + args, + run_query, + method == http::Method::GET, + ) + .await +} + pub async fn stream_script_by_path( authed: ApiAuthed, Extension(db): Extension, @@ -5397,20 +5597,10 @@ pub async fn stream_job( is_get: bool, ) -> error::Result { let args = if is_get { - let payload_r = run_query.payload.clone().map(decode_payload).map(|x| { - x.map_err(|e| { - Error::internal_err(format!("Impossible to decode query payload: {e:#?}")) - }) - }); - - let payload_args = if let Some(payload) = payload_r { - payload? - } else { - HashMap::new() - }; + let payload_as_args = run_query.payload_as_args()?; let mut args = args.process_args(&authed, &db, &w_id, None).await?; - args.body = args::Body::HashMap(payload_args); + args.body = args::Body::HashMap(payload_as_args); let args = args .to_args_from_runnable(&db, &w_id, runnable_id.clone(), run_query.skip_preprocessor) @@ -5439,6 +5629,7 @@ pub async fn stream_job( StripPath(script_path), run_query, args, + None, ) .await? .0 @@ -5456,7 +5647,7 @@ pub async fn stream_job( .await? .0 } - RunnableId::FlowPath(flow_path) => { + RunnableId::FlowId(FlowId::FlowPath(flow_path)) => { run_flow_by_path_inner( authed.clone(), db.clone(), @@ -5465,6 +5656,21 @@ pub async fn stream_job( StripPath(flow_path), run_query, args, + None, + ) + .await? + .0 + } + RunnableId::FlowId(FlowId::FlowVersion(version)) => { + run_flow_by_version_inner( + authed.clone(), + db.clone(), + user_db, + w_id.clone(), + version, + run_query, + args, + None ) .await? .0 @@ -5524,100 +5730,137 @@ pub async fn run_wait_result_flow_by_path_internal( let flow_path = flow_path.to_path(); - let scheduled_for = run_query.get_scheduled_for(&db).await?; - let userdb_authed = UserDbWithAuthed { db: user_db.clone(), authed: &authed.to_authed_ref() }; - let FlowVersionInfo { - tag, - dedicated_worker, - early_return, - has_preprocessor, - chat_input_enabled, - on_behalf_of_email, - edited_by, - version, - } = get_latest_flow_version_info_for_path(Some(userdb_authed), &db, &w_id, &flow_path, true) - .await?; + let flow_version_info = + get_latest_flow_version_info_for_path(Some(userdb_authed), &db, &w_id, &flow_path, true) + .await?; - let tag = run_query.tag.clone().or(tag); - check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?; - - let (email, permissioned_as, push_authed, tx) = - if let Some(on_behalf_of_email) = on_behalf_of_email.as_ref() { - ( - on_behalf_of_email, - username_to_permissioned_as(&edited_by), - None, - PushIsolationLevel::IsolatedRoot(db.clone()), - ) - } else { - ( - &authed.email, - username_to_permissioned_as(&authed.username), - Some(authed.clone().into()), - PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into()), - ) - }; - - let (uuid, mut tx) = push( + run_flow_and_wait_result( + &authed, &db, - tx, + user_db, &w_id, - JobPayload::Flow { - path: flow_path.to_string(), - dedicated_worker, - version, - apply_preprocessor: !run_query.skip_preprocessor.unwrap_or(false) - && has_preprocessor.unwrap_or(false), - }, - PushArgs { args: &args.args, extra: args.extra }, - authed.display_username(), - email, - permissioned_as, - authed.token_prefix.as_deref(), - scheduled_for, - None, - run_query.parent_job, - None, - run_query.root_job, - run_query.job_id, - false, - false, - None, - !run_query.invisible_to_owner.unwrap_or(false), - tag, - None, - None, - None, - push_authed.as_ref(), - false, - None, + flow_path, + flow_version_info, + run_query, + args, None, ) + .await +} + +pub async fn run_wait_result_flow_by_version_get( + method: hyper::http::Method, + authed: ApiAuthed, + Extension(user_db): Extension, + Extension(db): Extension, + Path((w_id, version)): Path<(String, i64)>, + Query(run_query): Query, + args: RawWebhookArgs, +) -> error::Result { + #[cfg(feature = "enterprise")] + check_license_key_valid().await?; + + let flow_path = sqlx::query_scalar!( + "SELECT path FROM flow_version WHERE id = $1 AND workspace_id = $2", + version, + &w_id + ) + .fetch_one(&db) .await?; - // Set conversation_id if provided (for agent memory) - if let Some(memory_id) = run_query.memory_id { - set_flow_memory_id(&mut tx, uuid, memory_id).await?; + check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?; + + if method == http::Method::HEAD { + return Ok(Json(serde_json::json!("")).into_response()); } - // Handle conversation messages for chat-enabled flows - if chat_input_enabled.unwrap_or(false) { - handle_chat_conversation_messages( - &mut tx, - &authed, + let payload_as_args = run_query.payload_as_args()?; + + let mut args = args.process_args(&authed, &db, &w_id, None).await?; + args.body = args::Body::HashMap(payload_as_args); + + let args = args + .to_args_from_runnable( + &db, &w_id, - &flow_path.to_string(), - &run_query, - args.args.get("user_message"), + RunnableId::from_flow_version(version), + run_query.skip_preprocessor, ) .await?; - } - tx.commit().await?; + let flow_version_info = + get_flow_version_info_from_version(&db, version, &w_id, &flow_path).await?; - run_wait_result(&db, uuid, w_id, early_return, &authed.username).await + run_flow_and_wait_result( + &authed, + &db, + user_db, + &w_id, + &flow_path, + flow_version_info, + run_query, + args, + None, + ) + .await +} + +pub async fn run_wait_result_flow_by_version( + authed: ApiAuthed, + Extension(user_db): Extension, + Extension(db): Extension, + Path((w_id, version)): Path<(String, i64)>, + Query(run_query): Query, + args: RawWebhookArgs, +) -> error::Result { + #[cfg(feature = "enterprise")] + check_license_key_valid().await?; + + let flow_path = sqlx::query_scalar!( + r#" + SELECT + path + FROM + flow_version + WHERE + id = $1 AND + workspace_id = $2 + "#, + version, + &w_id + ) + .fetch_one(&db) + .await?; + + check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?; + + let args = args + .to_args_from_runnable( + &authed, + &db, + &w_id, + RunnableId::from_flow_version(version), + run_query.skip_preprocessor, + ) + .await?; + + let flow_version_info = + get_flow_version_info_from_version(&db, version, &w_id, &flow_path).await?; + + run_flow_and_wait_result( + &authed, + &db, + user_db, + &w_id, + &flow_path, + flow_version_info, + run_query, + args, + None, + ) + .await } async fn run_preview_script( @@ -5688,6 +5931,7 @@ async fn run_preview_script( false, None, None, + None, ) .await?; tx.commit().await?; @@ -5715,7 +5959,7 @@ async fn run_wait_result_preview_script( let uuid = uuid .parse::() .map_err(|_| Error::BadRequest("Invalid UUID".to_string()))?; - let result = run_wait_result(&db, uuid, w_id, None, &authed.username).await; + let result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await; return result; } @@ -5808,6 +6052,7 @@ async fn run_bundle_preview_script( false, None, None, + None, ) .await?; job_id = Some(uuid); @@ -5947,11 +6192,12 @@ async fn run_dependencies_job( false, None, None, + None, ) .await?; tx.commit().await?; - let wait_result = run_wait_result(&db, uuid, w_id, None, &authed.username).await; + let wait_result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await; wait_result } @@ -6016,11 +6262,12 @@ async fn run_flow_dependencies_job( false, None, None, + None, ) .await?; tx.commit().await?; - let wait_result = run_wait_result(&db, uuid, w_id, None, &authed.username).await; + let wait_result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await; wait_result } @@ -6369,6 +6616,7 @@ async fn run_preview_flow_job( false, None, None, + None, ) .await?; @@ -6415,7 +6663,7 @@ async fn run_wait_result_preview_flow( let uuid = uuid .parse::() .map_err(|_| Error::BadRequest("Invalid UUID".to_string()))?; - let result = run_wait_result(&db, uuid, w_id, None, &authed.username).await; + let result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await; return result; } @@ -6461,6 +6709,7 @@ async fn run_dynamic_select( StripPath(path), run_query.clone(), push_args.clone(), + None, ) .await?; @@ -6565,6 +6814,7 @@ async fn run_dynamic_select( false, None, None, + None, ) .await?; tx.commit().await?; @@ -6698,6 +6948,7 @@ pub async fn run_job_by_hash_inner( false, None, None, + None, ) .await?; tx.commit().await?; @@ -7666,6 +7917,10 @@ pub fn filter_list_completed_query( sqlb.and_where("trigger_kind IS DISTINCT FROM 'schedule'"); } + if let Some(tk) = &lq.trigger_kind { + sqlb.and_where_eq("trigger_kind", "?".bind(&format!("{}", tk))); + } + sqlb } @@ -7742,6 +7997,7 @@ pub struct ListCompletedQuery { pub concurrency_key: Option, pub worker: Option, pub allow_wildcards: Option, + pub trigger_kind: Option, } async fn list_completed_jobs( diff --git a/backend/windmill-api/src/public_app_layer.rs b/backend/windmill-api/src/public_app_layer.rs index 7d5e5a6e3b..cce8af1b35 100644 --- a/backend/windmill-api/src/public_app_layer.rs +++ b/backend/windmill-api/src/public_app_layer.rs @@ -71,10 +71,12 @@ fn is_public_route_whitelisted(path: &str) -> bool { "/user/login_callback/*", "/api/workspaces/users", "/api/users/whoami", + "/api/apps_u/*", "/api/oauth/list_connects", "/api/oauth/list_logins", "/public/*", "/a/*", + "/api/oauth/get_connect/*", "/Inter-Variable.woff2", ]; diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index ec1eafe08a..4aafcb0207 100644 --- a/backend/windmill-api/src/resources.rs +++ b/backend/windmill-api/src/resources.rs @@ -738,6 +738,31 @@ async fn create_resource( let res_value = resource.value.unwrap_or_default(); let raw_json = sqlx::types::Json(res_value.as_ref()); + if resource.path.starts_with("f/app_themes/") { + sqlx::query!( + "INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_themes', 'App Themes', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING", + w_id, + authed.username, + ) + .execute(&db) + .await?; + } else if resource.path.starts_with("f/app_custom/") { + sqlx::query!( + "INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_custom', 'App Custom Components', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING", + w_id, + authed.username, + ) + .execute(&db) + .await?; + } else if resource.path.starts_with("f/app_groups/") { + sqlx::query!( + "INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_groups', 'App Groups', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING", + w_id, + authed.username, + ) + .execute(&db) + .await?; + } sqlx::query!( "INSERT INTO resource (workspace_id, path, value, description, resource_type, created_by, edited_at) diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index a639c58f6e..82c8864fe3 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -1022,6 +1022,7 @@ async fn create_script_internal<'c>( false, None, None, + None, ) .await?; Ok((hash, new_tx, None)) diff --git a/backend/windmill-api/src/settings.rs b/backend/windmill-api/src/settings.rs index 4851130286..62cb87d391 100644 --- a/backend/windmill-api/src/settings.rs +++ b/backend/windmill-api/src/settings.rs @@ -188,7 +188,7 @@ pub async fn test_license_key( Json(TestKey { license_key }): Json, ) -> error::Result { require_super_admin(&db, &authed.email).await?; - let (_, expired) = validate_license_key(license_key).await?; + let (_, expired) = validate_license_key(license_key, Some(&db)).await?; if expired { Err(error::Error::BadRequest("Expired license key".to_string())) @@ -701,23 +701,49 @@ async fn setup_ducklake_catalog_db_inner( sslmode = ssl_mode ); - let (client, connection) = tokio::time::timeout( - std::time::Duration::from_secs(20), - tokio_postgres::connect(&conn_str, tokio_postgres::NoTls), - ) - .await - .map_err(|e| error::Error::ExecutionErr(format!("timeout: {}", e.to_string())))? - .map_err(|e| error::Error::ExecutionErr(format!("error: {}", e.to_string())))?; - let join_handle = tokio::spawn(async move { connection.await }); + let (client, join_handle) = if ssl_mode == "require" { + use native_tls::TlsConnector; + use postgres_native_tls::MakeTlsConnector; + + let mut connector = TlsConnector::builder(); + connector.danger_accept_invalid_certs(true); + connector.danger_accept_invalid_hostnames(true); + + let (client, connection) = tokio::time::timeout( + std::time::Duration::from_secs(20), + tokio_postgres::connect( + &conn_str, + MakeTlsConnector::new(connector.build().map_err(to_anyhow)?), + ), + ) + .await + .map_err(|e| error::Error::ExecutionErr(format!("timeout: {}", e.to_string())))? + .map_err(|e| error::Error::ExecutionErr(format!("error: {}", e.to_string())))?; + + let join_handle = tokio::spawn(async move { connection.await }); + (client, join_handle) + } else { + let (client, connection) = tokio::time::timeout( + std::time::Duration::from_secs(20), + tokio_postgres::connect(&conn_str, tokio_postgres::NoTls), + ) + .await + .map_err(|e| error::Error::ExecutionErr(format!("timeout: {}", e.to_string())))? + .map_err(|e| error::Error::ExecutionErr(format!("error: {}", e.to_string())))?; + + let join_handle = tokio::spawn(async move { connection.await }); + (client, join_handle) + }; + logs.db_connect = "OK".to_string(); client .batch_execute(&format!( "GRANT CONNECT ON DATABASE \"{dbname}\" TO ducklake_user; - GRANT USAGE ON SCHEMA public TO ducklake_user; - GRANT CREATE ON SCHEMA public TO ducklake_user; - ALTER DEFAULT PRIVILEGES IN SCHEMA public - GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ducklake_user;" + GRANT USAGE ON SCHEMA public TO ducklake_user; + GRANT CREATE ON SCHEMA public TO ducklake_user; + ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ducklake_user;" )) .await .map_err(|e| { diff --git a/backend/windmill-api/src/triggers/http/handler.rs b/backend/windmill-api/src/triggers/http/handler.rs index 5d8898f121..fd051e05be 100644 --- a/backend/windmill-api/src/triggers/http/handler.rs +++ b/backend/windmill-api/src/triggers/http/handler.rs @@ -1050,6 +1050,7 @@ async fn route_job( trigger.error_handler_args.as_ref(), format!("http_trigger/{}", trigger.path), None, + Some(windmill_common::jobs::JobTriggerKind::Http), ) .await .map_err(|e| e.into_response())?; @@ -1108,6 +1109,7 @@ async fn route_job( trigger.error_handler_args.as_ref(), format!("http_trigger/{}", trigger.path), None, + Some(windmill_common::jobs::JobTriggerKind::Http), ) .await .map_err(|e| e.into_response()), @@ -1123,6 +1125,7 @@ async fn route_job( trigger.error_handler_path.as_deref(), trigger.error_handler_args.as_ref(), format!("http_trigger/{}", trigger.path), + Some(windmill_common::jobs::JobTriggerKind::Http), ) .await .map_err(|e| e.into_response()), diff --git a/backend/windmill-api/src/triggers/listener.rs b/backend/windmill-api/src/triggers/listener.rs index 431c4313f9..af9d0e32ce 100644 --- a/backend/windmill-api/src/triggers/listener.rs +++ b/backend/windmill-api/src/triggers/listener.rs @@ -515,6 +515,7 @@ pub trait Listener: TriggerCrud + TriggerJobArgs { error_handler_args, format!("{}_trigger/{}", Self::TRIGGER_KIND, listening_trigger.path), None, + Some(Self::JOB_TRIGGER_KIND), ) .await?; diff --git a/backend/windmill-api/src/triggers/trigger_helpers.rs b/backend/windmill-api/src/triggers/trigger_helpers.rs index 7deaf6dc1d..e592c43bb3 100644 --- a/backend/windmill-api/src/triggers/trigger_helpers.rs +++ b/backend/windmill-api/src/triggers/trigger_helpers.rs @@ -12,7 +12,10 @@ use windmill_common::{ error::Result, flows::{FlowModuleValue, Retry}, get_latest_deployed_hash_for_path, get_latest_flow_version_info_for_path, - jobs::{get_has_preprocessor_from_content_and_lang, script_path_to_payload, JobPayload}, + jobs::{ + get_has_preprocessor_from_content_and_lang, script_path_to_payload, JobPayload, + JobTriggerKind, + }, scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang}, triggers::{ HubOrWorkspaceId, RunnableFormat, RunnableFormatVersion, TriggerKind, @@ -21,7 +24,6 @@ use windmill_common::{ users::username_to_permissioned_as, utils::StripPath, worker::to_raw_value, - FlowVersionInfo, }; use windmill_queue::{push, PushArgs, PushArgsOwned, PushIsolationLevel}; @@ -56,7 +58,7 @@ struct PartialSchema { #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum RunnableId { - FlowPath(String), + FlowId(FlowId), ScriptId(ScriptId), HubScript(String), } @@ -75,7 +77,33 @@ impl RunnableId { } pub fn from_flow_path(path: &str) -> Self { - Self::FlowPath(path.to_string()) + Self::FlowId(FlowId::FlowPath(path.to_string())) + } + + pub fn from_flow_version(version: i64) -> Self { + Self::FlowId(FlowId::FlowVersion(version)) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum FlowId { + FlowPath(String), + FlowVersion(i64), +} + +impl FlowId { + async fn get_flow_version_id(self, workspace_id: &str, db: &DB) -> Result { + let version_id = match self { + FlowId::FlowPath(path) => { + let info = + get_latest_flow_version_info_for_path(None, db, workspace_id, &path, true) + .await?; + info.version + } + FlowId::FlowVersion(version) => version, + }; + + Ok(version_id) } } @@ -255,9 +283,8 @@ pub async fn get_runnable_format( }, ) } - RunnableId::FlowPath(path) => { - let FlowVersionInfo { version, .. } = - get_latest_flow_version_info_for_path(None, &db, workspace_id, &path, true).await?; + RunnableId::FlowId(flow_id) => { + let version = flow_id.get_flow_version_id(workspace_id, db).await?; let key = ( HubOrWorkspaceId::WorkspaceId(workspace_id.to_string()), @@ -268,7 +295,7 @@ pub async fn get_runnable_format( let runnable_format = RUNNABLE_FORMAT_VERSION_CACHE.get(&key); if let Some(runnable_format) = runnable_format { - tracing::debug!("Using cached runnable format for flow {path}"); + tracing::debug!("Using cached runnable format for flow version {version}"); return Ok(runnable_format); } @@ -279,11 +306,9 @@ pub async fn get_runnable_format( schema as \"schema: _\" FROM flow_version WHERE - path = $1 - AND workspace_id = $2 - ORDER BY created_at DESC - LIMIT 1", - path, + id = $1 + AND workspace_id = $2", + version, workspace_id, ) .fetch_one(db) @@ -494,6 +519,7 @@ pub async fn trigger_runnable_inner( error_handler_args: Option<&sqlx::types::Json>>, trigger_path: String, job_id: Option, + trigger_kind: Option, ) -> Result<(Uuid, Option, Option)> { let error_handler_args = error_handler_args.map(|args| { let args = args @@ -516,6 +542,7 @@ pub async fn trigger_runnable_inner( path, run_query, args, + trigger_kind, ) .await?; (uuid, None, early_return) @@ -532,6 +559,7 @@ pub async fn trigger_runnable_inner( error_handler_args.as_ref(), trigger_path, job_id, + trigger_kind, ) .await?; (uuid, delete_after_use, None) @@ -554,6 +582,7 @@ pub async fn trigger_runnable( error_handler_args: Option<&sqlx::types::Json>>, trigger_path: String, job_id: Option, + trigger_kind: Option, ) -> Result { let (uuid, _, _) = trigger_runnable_inner( db, @@ -568,6 +597,7 @@ pub async fn trigger_runnable( error_handler_args, trigger_path, job_id, + trigger_kind, ) .await?; Ok((StatusCode::CREATED, uuid.to_string()).into_response()) @@ -586,6 +616,7 @@ pub async fn trigger_runnable_and_wait_for_result( error_handler_path: Option<&str>, error_handler_args: Option<&sqlx::types::Json>>, trigger_path: String, + trigger_kind: Option, ) -> Result { let username = authed.username.clone(); let (uuid, delete_after_use, early_return) = trigger_runnable_inner( @@ -601,11 +632,11 @@ pub async fn trigger_runnable_and_wait_for_result( error_handler_args, trigger_path, None, + trigger_kind, ) .await?; let (result, success) = - run_wait_result_internal(db, uuid, workspace_id.to_string(), early_return, &username) - .await?; + run_wait_result_internal(db, uuid, &workspace_id, early_return, &username).await?; if delete_after_use.unwrap_or(false) { delete_job_metadata_after_use(&db, uuid).await?; @@ -627,6 +658,7 @@ pub async fn trigger_runnable_and_wait_for_raw_result( error_handler_path: Option<&str>, error_handler_args: Option<&sqlx::types::Json>>, trigger_path: String, + trigger_kind: Option, ) -> Result<(Box, bool)> { let username = authed.username.clone(); let (uuid, delete_after_use, early_return) = trigger_runnable_inner( @@ -642,11 +674,12 @@ pub async fn trigger_runnable_and_wait_for_raw_result( error_handler_args, trigger_path, None, + trigger_kind, ) .await?; let (result, success) = - run_wait_result_internal(db, uuid, workspace_id.to_string(), early_return, &username) + run_wait_result_internal(db, uuid, &workspace_id, early_return, &username) .await .with_context(|| { format!( @@ -675,6 +708,7 @@ pub async fn trigger_runnable_and_wait_for_raw_result_with_error_ctx( error_handler_path: Option<&str>, error_handler_args: Option<&sqlx::types::Json>>, trigger_path: String, + trigger_kind: Option, ) -> Result> { let (result, success) = trigger_runnable_and_wait_for_raw_result( db, @@ -688,6 +722,7 @@ pub async fn trigger_runnable_and_wait_for_raw_result_with_error_ctx( error_handler_path, error_handler_args, trigger_path, + trigger_kind, ) .await?; @@ -714,6 +749,7 @@ async fn trigger_script_internal( error_handler_args: Option<&sqlx::types::Json>>>, trigger_path: String, job_id: Option, + trigger_kind: Option, ) -> Result<(Uuid, Option)> { if retry.is_none() && error_handler_path.is_none() { let run_query = RunJobQuery { job_id, ..Default::default() }; @@ -726,6 +762,7 @@ async fn trigger_script_internal( path, run_query, args, + trigger_kind, ) .await } else { @@ -741,6 +778,7 @@ async fn trigger_script_internal( error_handler_args, trigger_path, job_id, + trigger_kind, ) .await } @@ -758,6 +796,7 @@ async fn trigger_script_with_retry_and_error_handler( error_handler_args: Option<&sqlx::types::Json>>>, trigger_path: String, job_id: Option, + trigger_kind: Option, ) -> Result<(Uuid, Option)> { #[cfg(feature = "enterprise")] check_license_key_valid().await?; @@ -870,6 +909,7 @@ async fn trigger_script_with_retry_and_error_handler( false, None, None, + trigger_kind, ) .await?; tx.commit().await?; diff --git a/backend/windmill-api/src/triggers/websocket/listener.rs b/backend/windmill-api/src/triggers/websocket/listener.rs index bfecaf479d..a89532566a 100644 --- a/backend/windmill-api/src/triggers/websocket/listener.rs +++ b/backend/windmill-api/src/triggers/websocket/listener.rs @@ -94,6 +94,7 @@ impl ListeningTrigger { None, None, "".to_string(), // doesn't matter as no retry/error handler + Some(windmill_common::jobs::JobTriggerKind::Websocket), ) .await .map(|r| r.get().to_owned())?; @@ -392,6 +393,7 @@ impl Listener for WebsocketTrigger { error_handler_path.as_deref(), error_handler_args.as_ref(), format!("websocket_trigger/{}", trigger_path), + Some(windmill_common::jobs::JobTriggerKind::Websocket), ) => { if let Ok((result, success)) = result { if !success && !can_return_error_result { @@ -428,6 +430,7 @@ impl Listener for WebsocketTrigger { error_handler_args, format!("websocket_trigger/{}", listening_trigger.path), None, + Some(windmill_common::jobs::JobTriggerKind::Websocket), ) .await?; } diff --git a/backend/windmill-api/src/triggers/websocket/mod.rs b/backend/windmill-api/src/triggers/websocket/mod.rs index 7a4d98bcc1..e19c958423 100644 --- a/backend/windmill-api/src/triggers/websocket/mod.rs +++ b/backend/windmill-api/src/triggers/websocket/mod.rs @@ -2,7 +2,9 @@ use std::collections::HashMap; use crate::{ db::ApiAuthed, - triggers::trigger_helpers::{trigger_runnable_and_wait_for_raw_result_with_error_ctx, TriggerJobArgs}, + triggers::trigger_helpers::{ + trigger_runnable_and_wait_for_raw_result_with_error_ctx, TriggerJobArgs, + }, }; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; @@ -64,9 +66,8 @@ pub fn value_to_args_hashmap( args: Option<&Box>, ) -> Result>> { let args = if let Some(args) = args { - let args_map: Option> = - serde_json::from_str(args.get()) - .map_err(|e| Error::BadRequest(format!("invalid json: {}", e)))?; + let args_map: Option> = serde_json::from_str(args.get()) + .map_err(|e| Error::BadRequest(format!("invalid json: {}", e)))?; args_map .unwrap_or_else(HashMap::new) @@ -112,6 +113,7 @@ pub async fn get_url_from_runnable_value( None, None, "".to_string(), // doesn't matter as no retry/error handler + Some(windmill_common::jobs::JobTriggerKind::Websocket), ) .await?; diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index c7053209da..cea6c2ba5c 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -2455,38 +2455,6 @@ async fn create_workspace( .execute(&mut *tx) .await?; - sqlx::query!( - "INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_themes', 'App Themes', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING", - nw.id, - username, - ) - .execute(&mut *tx) - .await?; - - sqlx::query!( - "INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_custom', 'App Custom Components', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING", - nw.id, - username, - ) - .execute(&mut *tx) - .await?; - - sqlx::query!( - "INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_groups', 'App Groups', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING", - nw.id, - username, - ) - .execute(&mut *tx) - .await?; - - sqlx::query!( - "INSERT INTO resource (workspace_id, path, value, description, resource_type, created_by, edited_at) VALUES ($1, 'f/app_themes/theme_0', '{\"name\": \"Default Theme\", \"value\": \"\"}', 'The default app theme', 'app_theme', $2, now()) ON CONFLICT DO NOTHING", - nw.id, - username, - ) - .execute(&mut *tx) - .await?; - audit_log( &mut *tx, &authed, diff --git a/backend/windmill-common/src/flows.rs b/backend/windmill-common/src/flows.rs index cf230bfddc..ef6e70b09a 100644 --- a/backend/windmill-common/src/flows.rs +++ b/backend/windmill-common/src/flows.rs @@ -66,6 +66,7 @@ pub struct FlowWithStarred { pub starred: Option, #[serde(skip_serializing_if = "Option::is_none")] pub lock_error_logs: Option, + pub version_id: i64, } fn is_none_or_false(b: &Option) -> bool { diff --git a/backend/windmill-common/src/lib.rs b/backend/windmill-common/src/lib.rs index b0a3f5e4ea..b91dcfaf95 100644 --- a/backend/windmill-common/src/lib.rs +++ b/backend/windmill-common/src/lib.rs @@ -769,7 +769,7 @@ where } } -pub fn get_latest_flow_version_info_for_path_from_version< +pub fn get_flow_version_info_from_version< 'a, 'e, A: sqlx::Acquire<'e, Database = Postgres> + Send + 'a, @@ -782,7 +782,6 @@ pub fn get_latest_flow_version_info_for_path_from_version< async move { // as instructed in the docstring of sqlx::Acquire let key = (w_id.to_string(), version); - match FLOW_INFO_CACHE.get(&key) { Some(info) => { tracing::debug!("Using cached flow version info for {version} ({path})"); @@ -791,21 +790,37 @@ pub fn get_latest_flow_version_info_for_path_from_version< _ => { tracing::debug!("Fetching flow version info for {version} ({path})"); let mut conn = db.acquire().await?; - let info = sqlx::query_as!( - FlowVersionInfo, - "SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, on_behalf_of_email, edited_by, flow_version.id AS version - FROM flow - INNER JOIN flow_version - ON flow_version.id = $3 - WHERE flow.path = $1 and flow.workspace_id = $2", - path, - w_id, - version - ) - .fetch_optional(&mut *conn) - .await?; + let flow_info = + sqlx::query_as!( + FlowVersionInfo, + r#" + SELECT + flow_version.id AS version, + flow_version.value->>'early_return' as early_return, + flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, + (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, + flow.tag, + flow.dedicated_worker, + flow.on_behalf_of_email, + flow.edited_by + FROM + flow_version + INNER JOIN flow + ON flow.path = flow_version.path AND + flow.workspace_id = flow_version.workspace_id + WHERE + flow_version.workspace_id = $1 AND + flow_version.path = $2 AND + flow_version.id = $3 + "#, + w_id, + path, + version, + ) + .fetch_optional(&mut *conn) + .await?; - let info = utils::not_found_if_none(info, "flow", path)?; + let info = utils::not_found_if_none(flow_info, "flow", path)?; FLOW_INFO_CACHE.insert(key, info.clone()); @@ -825,7 +840,7 @@ pub async fn get_latest_flow_version_info_for_path<'e>( // as instructed in the docstring of sqlx::Acquire let version = get_latest_flow_version_id_for_path(db_authed, &db.clone(), w_id, path, use_cache).await?; - get_latest_flow_version_info_for_path_from_version(db, version, w_id, path).await + get_flow_version_info_from_version(db, version, w_id, path).await } async fn get_latest_flow_version_for_path<'e, E: sqlx::PgExecutor<'e>>( diff --git a/backend/windmill-common/src/s3_helpers.rs b/backend/windmill-common/src/s3_helpers.rs index 899e3a2fd5..69fbaa4b29 100644 --- a/backend/windmill-common/src/s3_helpers.rs +++ b/backend/windmill-common/src/s3_helpers.rs @@ -396,6 +396,21 @@ pub struct S3Resource { pub port: Option, } +impl S3Resource { + pub fn endpoint_with_region_fallback(&self, region_fallback: Option) -> String { + if self.endpoint.is_empty() { + let final_region = if self.region.is_empty() { + region_fallback.unwrap_or_else(|| "us-east-1".to_string()) + } else { + self.region.clone() + }; + format!("s3.{}.amazonaws.com", final_region) + } else { + self.endpoint.clone() + } + } +} + #[derive(Debug, Deserialize, Serialize, Clone)] pub struct AzureBlobResource { pub endpoint: Option, @@ -642,7 +657,7 @@ pub async fn build_s3_client(s3_resource_ref: &S3Resource) -> error::Result { type_aliases = Some( (0..stmt.column_count()) - .map(|i| stmt.column_logical_type(i).get_alias()) + .map(|i| { + let logical_type = stmt.column_logical_type(i); + if logical_type.is_invalid() { + None + } else { + logical_type.get_alias() + } + }) .collect::>(), ); type_aliases.as_ref().unwrap() } }; + // let type_aliases = (0..stmt.column_count()).map(|_| None).collect::>(); + let row = row_to_value(row, &column_names.as_slice(), &type_aliases.as_slice()) .map_err(|e| e.to_string())?; rows_vec.push(row); diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index 9b941ce911..6dffa5b540 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -470,6 +470,7 @@ pub async fn push_init_job<'c>( false, None, None, + None, ) .await?; inner_tx.commit().await?; @@ -528,6 +529,7 @@ pub async fn push_periodic_bash_job<'c>( false, None, None, + None, ) .await?; inner_tx.commit().await?; @@ -1373,6 +1375,7 @@ async fn restart_job_if_perpetual_inner( false, None, None, + None, ) .await?; tx.commit().await?; @@ -1923,6 +1926,7 @@ pub async fn push_error_handler<'a, 'c, T: Serialize + Send + Sync>( false, None, None, + None, ) .await?; tx.commit().await?; @@ -3882,6 +3886,7 @@ pub async fn push<'c, 'd>( // If we know there is already a debounce job, we can use this for debouncing. // NOTE: Only works with dependency jobs triggered by relative imports debounce_job_id_o: Option, + trigger_kind: Option, ) -> Result<(Uuid, Transaction<'c, Postgres>), Error> { #[cfg(feature = "cloud")] if *CLOUD_HOSTED { @@ -5278,11 +5283,13 @@ pub async fn push<'c, 'd>( // tracing::error!("Could not insert job_perms for job {job_id}: {err:#}"); // } - let trigger_kind = if schedule_path.is_some() { - Some(JobTriggerKind::Schedule) - } else { - None - }; + let trigger_kind = trigger_kind.or_else(|| { + if schedule_path.is_some() { + Some(JobTriggerKind::Schedule) + } else { + None + } + }); let root_job = if root_job.is_some() && (root_job == flow_innermost_root_job.or(parent_job).or(Some(job_id))) diff --git a/backend/windmill-queue/src/schedule.rs b/backend/windmill-queue/src/schedule.rs index aed22e3f28..52a397ce33 100644 --- a/backend/windmill-queue/src/schedule.rs +++ b/backend/windmill-queue/src/schedule.rs @@ -17,8 +17,8 @@ use std::str::FromStr; use windmill_common::db::Authed; use windmill_common::ee_oss::LICENSE_KEY_VALID; use windmill_common::flows::Retry; +use windmill_common::get_flow_version_info_from_version; use windmill_common::get_latest_flow_version_id_for_path; -use windmill_common::get_latest_flow_version_info_for_path_from_version; use windmill_common::jobs::check_tag_available_for_workspace_internal; use windmill_common::jobs::JobPayload; use windmill_common::schedule::schedule_to_user; @@ -63,7 +63,7 @@ async fn get_schedule_metadata<'c>( .await?; let FlowVersionInfo { tag, on_behalf_of_email, edited_by, .. } = - get_latest_flow_version_info_for_path_from_version( + get_flow_version_info_from_version( &mut **tx, version, &schedule.workspace_id, @@ -291,16 +291,13 @@ pub async fn push_scheduled_job<'c>( let FlowVersionInfo { version, tag, dedicated_worker, on_behalf_of_email, edited_by, .. - } = get_latest_flow_version_info_for_path_from_version( + } = get_flow_version_info_from_version( &mut *tx, version, &schedule.workspace_id, &schedule.script_path, ) - .warn_after_seconds_with_sql( - 1, - "get_latest_flow_version_info_for_path_from_version".to_string(), - ) + .warn_after_seconds_with_sql(1, "get_flow_version_info_from_version".to_string()) .await?; ( @@ -503,6 +500,7 @@ pub async fn push_scheduled_job<'c>( false, None, None, + Some(windmill_common::jobs::JobTriggerKind::Schedule), ) .warn_after_seconds_with_sql(1, "push in push_scheduled_job".to_string()) .await?; diff --git a/backend/windmill-worker/src/ai/tools.rs b/backend/windmill-worker/src/ai/tools.rs index 74f46c2bf4..478eca5e54 100644 --- a/backend/windmill-worker/src/ai/tools.rs +++ b/backend/windmill-worker/src/ai/tools.rs @@ -471,6 +471,7 @@ async fn execute_windmill_tool( true, None, None, + None, ) .await?; diff --git a/backend/windmill-worker/src/js_eval.rs b/backend/windmill-worker/src/js_eval.rs index 5827c8ba54..5feeab8003 100644 --- a/backend/windmill-worker/src/js_eval.rs +++ b/backend/windmill-worker/src/js_eval.rs @@ -171,6 +171,76 @@ const DOT_PATTERN: &'static str = "."; const START_BRACKET_PATTERN: &'static str = "[\""; const END_BRACKET_PATTERN: &'static str = "\"]"; +/// Determines if we should prepend "return" to the expression +#[cfg(feature = "deno_core")] +fn should_add_return(expr: &str) -> bool { + // Trim whitespace + let trimmed = expr.trim(); + + // If it's empty, add return + if trimmed.is_empty() { + return true; + } + + // Check if it already starts with 'return' keyword (as a statement) + // Use word boundary to avoid matching "return" in variable names + if trimmed.starts_with("return ") || trimmed.starts_with("return;") || trimmed == "return" { + return false; + } + + // Check for common statement patterns that shouldn't have return prepended + let statement_prefixes = [ + "const ", "let ", "var ", "if ", "if(", "for ", "for(", + "while ", "while(", "switch ", "switch(", "try ", "try{", + "throw ", "function ", "class ", "async ", "await " + ]; + + for prefix in &statement_prefixes { + if trimmed.starts_with(prefix) { + return false; + } + } + + // Check for multiple statements (contains semicolon not in a string) + // This is still not perfect but better than current logic + if contains_semicolon_outside_strings(trimmed) { + return false; + } + + // Default: assume it's an expression that needs return + true +} + +/// Checks if the expression contains a semicolon outside of strings +#[cfg(feature = "deno_core")] +fn contains_semicolon_outside_strings(expr: &str) -> bool { + let mut in_single_quote = false; + let mut in_double_quote = false; + let mut in_template = false; + let mut prev_char = '\0'; + + for ch in expr.chars() { + match ch { + '\'' if prev_char != '\\' && !in_double_quote && !in_template => { + in_single_quote = !in_single_quote; + } + '"' if prev_char != '\\' && !in_single_quote && !in_template => { + in_double_quote = !in_double_quote; + } + '`' if prev_char != '\\' && !in_single_quote && !in_double_quote => { + in_template = !in_template; + } + ';' if !in_single_quote && !in_double_quote && !in_template => { + return true; + } + _ => {} + } + prev_char = ch; + } + + false +} + fn try_exact_property_access( expr: &str, flow_input: Option<&mappable_rc::Marc>>>, @@ -574,10 +644,10 @@ async function resource(path) {{ (String::new(), String::new()) }; - let f = if expr.contains("return ") { - expr.to_string() - } else { + let f = if should_add_return(expr) { format!("return {expr}") + } else { + expr.to_string() }; let ctx_str = ctx @@ -1492,4 +1562,74 @@ multiline template`"; // assert_eq!(res.0.get(), "\"\""); // Ok(()) // } + + #[test] + fn test_should_add_return() { + // Simple expressions should get return added + assert_eq!(should_add_return("5"), true); + assert_eq!(should_add_return("x + y"), true); + assert_eq!(should_add_return("foo()"), true); + assert_eq!(should_add_return("obj.property"), true); + + // Object literals should get return added + assert_eq!(should_add_return("{ foo: 'bar' }"), true); + assert_eq!(should_add_return("{ a: 1, b: 2 }"), true); + assert_eq!(should_add_return("{}"), true); + + // Already has return + assert_eq!(should_add_return("return 5"), false); + assert_eq!(should_add_return("return x + y"), false); + assert_eq!(should_add_return("return;"), false); + assert_eq!(should_add_return("return"), false); + + // Should NOT add return for statements + assert_eq!(should_add_return("const x = 5"), false); + assert_eq!(should_add_return("let y = 10"), false); + assert_eq!(should_add_return("var z = 15"), false); + assert_eq!(should_add_return("if (x > 5) { return x; }"), false); + assert_eq!(should_add_return("for (let i = 0; i < 10; i++) {}"), false); + assert_eq!(should_add_return("while (true) {}"), false); + assert_eq!(should_add_return("function foo() {}"), false); + assert_eq!(should_add_return("throw new Error('test')"), false); + + // Multiple statements with semicolons (including block statements) + assert_eq!(should_add_return("let x = 5; x + 1"), false); + assert_eq!(should_add_return("{ const x = 5; return x; }"), false); + + // Edge case: "return" in a string should still get return prepended + assert_eq!(should_add_return("\"return this string\""), true); + assert_eq!(should_add_return("'return in single quotes'"), true); + assert_eq!(should_add_return("`return in template literal`"), true); + + // Semicolons in strings should not trigger multi-statement detection + assert_eq!(should_add_return("\"hello; world\""), true); + assert_eq!(should_add_return("'test; string'"), true); + assert_eq!(should_add_return("`template; literal`"), true); + } + + #[test] + fn test_contains_semicolon_outside_strings() { + // Semicolons outside strings + assert_eq!(contains_semicolon_outside_strings("let x = 5; x + 1"), true); + assert_eq!(contains_semicolon_outside_strings("x; y"), true); + + // Semicolons inside strings (should NOT be detected) + assert_eq!(contains_semicolon_outside_strings("\"hello; world\""), false); + assert_eq!(contains_semicolon_outside_strings("'test; string'"), false); + assert_eq!(contains_semicolon_outside_strings("`template; literal`"), false); + + // Mixed cases + assert_eq!( + contains_semicolon_outside_strings("let x = 'hello; world'; x"), + true + ); + assert_eq!( + contains_semicolon_outside_strings("console.log(\"test; string\")"), + false + ); + + // No semicolons + assert_eq!(contains_semicolon_outside_strings("x + y"), false); + assert_eq!(contains_semicolon_outside_strings("foo()"), false); + } } diff --git a/backend/windmill-worker/src/pwsh_executor.rs b/backend/windmill-worker/src/pwsh_executor.rs index 4a4b85f8ac..cda448dc31 100644 --- a/backend/windmill-worker/src/pwsh_executor.rs +++ b/backend/windmill-worker/src/pwsh_executor.rs @@ -255,20 +255,42 @@ pub async fn handle_powershell_job( job.args.as_ref() }; - let args_owned = windmill_parser_bash::parse_powershell_sig(&content)? + let parsed_sig = windmill_parser_bash::parse_powershell_sig(&content)?; + + parsed_sig .args .iter() - .map(|arg| { - ( - arg.name.clone(), - job_args.and_then(|x| x.get(&arg.name).map(|x| raw_to_pwsh_param(x.get()))), - ) - }) - .collect::)>>(); + .filter_map(|arg| { + let value_opt = job_args.and_then(|x| x.get(&arg.name)); - args_owned - .into_iter() - .filter_map(|(n, v)| v.map(|v| format!("-{n} {v}"))) + // Check if this is a switch parameter (only [switch], not [bool]) + let is_switch = arg.otyp.as_ref().map(|t| { + t.to_lowercase() == "switch" + }).unwrap_or(false); + + if is_switch { + // Handle switch parameters: -SwitchName or omit + if let Some(value) = value_opt { + match serde_json::from_str::(value.get()) { + Ok(serde_json::Value::Bool(true)) => { + // Switch is enabled: just pass -SwitchName + Some(format!("-{}", arg.name)) + } + Ok(serde_json::Value::Bool(false)) | _ => { + // Switch is disabled or invalid: omit the parameter + None + } + } + } else { + // No value provided, omit the switch (defaults to false) + None + } + } else { + // Regular parameter (including [bool]): format as -ParamName Value + // For [bool] parameters, this will be -ParamName $true or -ParamName $false + value_opt.map(|v| format!("-{} {}", arg.name, raw_to_pwsh_param(v.get()))) + } + }) .collect::>() .join(" ") }; diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index f550bcd09d..f0bc4cfc1e 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -3303,6 +3303,7 @@ async fn push_next_flow_job( false, None, None, + None, ) .warn_after_seconds(2) .await?; diff --git a/backend/windmill-worker/src/worker_lockfiles.rs b/backend/windmill-worker/src/worker_lockfiles.rs index 44b68948a7..0b840884f5 100644 --- a/backend/windmill-worker/src/worker_lockfiles.rs +++ b/backend/windmill-worker/src/worker_lockfiles.rs @@ -670,6 +670,7 @@ pub async fn trigger_dependents_to_recompute_dependencies( false, None, debounce_job_id_o, + None, ) .await?; diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 4038cd90b3..fe33a51841 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts"; import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts"; import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts"; -export const VERSION = "v1.575.0"; +export const VERSION = "v1.576.3"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/install_dev.sh b/cli/install_dev.sh new file mode 100755 index 0000000000..e172ac1288 --- /dev/null +++ b/cli/install_dev.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -e + +if [ -z "$1" ]; then + name="wmill" +else + name="$1" +fi + +./gen_wm_client.sh +./windmill-utils-internal/gen_wm_client.sh + +echo "Installing dev cli as $name (pass arg to override)" +deno install -f -A -g src/main.ts --name $name --unstable \ No newline at end of file diff --git a/cli/src/main.ts b/cli/src/main.ts index 42d17a4231..6d362eee47 100644 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -68,7 +68,7 @@ export { // } // }); -export const VERSION = "1.575.0"; +export const VERSION = "1.576.3"; export const WM_FORK_PREFIX = "wm-fork"; diff --git a/cli/wasm/regex/windmill_parser_wasm.d.ts b/cli/wasm/regex/windmill_parser_wasm.d.ts index 59f34f2b18..7886800b20 100644 --- a/cli/wasm/regex/windmill_parser_wasm.d.ts +++ b/cli/wasm/regex/windmill_parser_wasm.d.ts @@ -1,14 +1,14 @@ /* tslint:disable */ /* eslint-disable */ -export function parse_assets_sql(code: string): string; -export function parse_db_resource(code: string): string | undefined; -export function parse_bash(code: string): string; export function parse_mssql(code: string): string; -export function parse_oracledb(code: string): string; export function parse_powershell(code: string): string; -export function parse_bigquery(code: string): string; +export function parse_oracledb(code: string): string; export function parse_graphql(code: string): string; -export function parse_snowflake(code: string): string; -export function parse_duckdb(code: string): string; -export function parse_sql(code: string): string; +export function parse_assets_sql(code: string): string; export function parse_mysql(code: string): string; +export function parse_bash(code: string): string; +export function parse_duckdb(code: string): string; +export function parse_db_resource(code: string): string | undefined; +export function parse_bigquery(code: string): string; +export function parse_snowflake(code: string): string; +export function parse_sql(code: string): string; diff --git a/cli/wasm/regex/windmill_parser_wasm.js b/cli/wasm/regex/windmill_parser_wasm.js index 25a60a9b8e..b3b62ebd32 100644 --- a/cli/wasm/regex/windmill_parser_wasm.js +++ b/cli/wasm/regex/windmill_parser_wasm.js @@ -68,60 +68,6 @@ function getStringFromWasm0(ptr, len) { ptr = ptr >>> 0; return decodeText(ptr, len); } -/** - * @param {string} code - * @returns {string} - */ -export function parse_assets_sql(code) { - let deferred2_0; - let deferred2_1; - try { - const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ret = wasm.parse_assets_sql(ptr0, len0); - deferred2_0 = ret[0]; - deferred2_1 = ret[1]; - return getStringFromWasm0(ret[0], ret[1]); - } finally { - wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); - } -} - -/** - * @param {string} code - * @returns {string | undefined} - */ -export function parse_db_resource(code) { - const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ret = wasm.parse_db_resource(ptr0, len0); - let v2; - if (ret[0] !== 0) { - v2 = getStringFromWasm0(ret[0], ret[1]).slice(); - wasm.__wbindgen_free(ret[0], ret[1] * 1, 1); - } - return v2; -} - -/** - * @param {string} code - * @returns {string} - */ -export function parse_bash(code) { - let deferred2_0; - let deferred2_1; - try { - const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ret = wasm.parse_bash(ptr0, len0); - deferred2_0 = ret[0]; - deferred2_1 = ret[1]; - return getStringFromWasm0(ret[0], ret[1]); - } finally { - wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); - } -} - /** * @param {string} code * @returns {string} @@ -141,25 +87,6 @@ export function parse_mssql(code) { } } -/** - * @param {string} code - * @returns {string} - */ -export function parse_oracledb(code) { - let deferred2_0; - let deferred2_1; - try { - const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ret = wasm.parse_oracledb(ptr0, len0); - deferred2_0 = ret[0]; - deferred2_1 = ret[1]; - return getStringFromWasm0(ret[0], ret[1]); - } finally { - wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); - } -} - /** * @param {string} code * @returns {string} @@ -183,13 +110,13 @@ export function parse_powershell(code) { * @param {string} code * @returns {string} */ -export function parse_bigquery(code) { +export function parse_oracledb(code) { let deferred2_0; let deferred2_1; try { const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; - const ret = wasm.parse_bigquery(ptr0, len0); + const ret = wasm.parse_oracledb(ptr0, len0); deferred2_0 = ret[0]; deferred2_1 = ret[1]; return getStringFromWasm0(ret[0], ret[1]); @@ -221,13 +148,51 @@ export function parse_graphql(code) { * @param {string} code * @returns {string} */ -export function parse_snowflake(code) { +export function parse_assets_sql(code) { let deferred2_0; let deferred2_1; try { const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; - const ret = wasm.parse_snowflake(ptr0, len0); + const ret = wasm.parse_assets_sql(ptr0, len0); + deferred2_0 = ret[0]; + deferred2_1 = ret[1]; + return getStringFromWasm0(ret[0], ret[1]); + } finally { + wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); + } +} + +/** + * @param {string} code + * @returns {string} + */ +export function parse_mysql(code) { + let deferred2_0; + let deferred2_1; + try { + const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.parse_mysql(ptr0, len0); + deferred2_0 = ret[0]; + deferred2_1 = ret[1]; + return getStringFromWasm0(ret[0], ret[1]); + } finally { + wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); + } +} + +/** + * @param {string} code + * @returns {string} + */ +export function parse_bash(code) { + let deferred2_0; + let deferred2_1; + try { + const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.parse_bash(ptr0, len0); deferred2_0 = ret[0]; deferred2_1 = ret[1]; return getStringFromWasm0(ret[0], ret[1]); @@ -255,17 +220,33 @@ export function parse_duckdb(code) { } } +/** + * @param {string} code + * @returns {string | undefined} + */ +export function parse_db_resource(code) { + const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.parse_db_resource(ptr0, len0); + let v2; + if (ret[0] !== 0) { + v2 = getStringFromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 1, 1); + } + return v2; +} + /** * @param {string} code * @returns {string} */ -export function parse_sql(code) { +export function parse_bigquery(code) { let deferred2_0; let deferred2_1; try { const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; - const ret = wasm.parse_sql(ptr0, len0); + const ret = wasm.parse_bigquery(ptr0, len0); deferred2_0 = ret[0]; deferred2_1 = ret[1]; return getStringFromWasm0(ret[0], ret[1]); @@ -278,13 +259,32 @@ export function parse_sql(code) { * @param {string} code * @returns {string} */ -export function parse_mysql(code) { +export function parse_snowflake(code) { let deferred2_0; let deferred2_1; try { const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; - const ret = wasm.parse_mysql(ptr0, len0); + const ret = wasm.parse_snowflake(ptr0, len0); + deferred2_0 = ret[0]; + deferred2_1 = ret[1]; + return getStringFromWasm0(ret[0], ret[1]); + } finally { + wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); + } +} + +/** + * @param {string} code + * @returns {string} + */ +export function parse_sql(code) { + let deferred2_0; + let deferred2_1; + try { + const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.parse_sql(ptr0, len0); deferred2_0 = ret[0]; deferred2_1 = ret[1]; return getStringFromWasm0(ret[0], ret[1]); diff --git a/cli/wasm/regex/windmill_parser_wasm_bg.wasm b/cli/wasm/regex/windmill_parser_wasm_bg.wasm index 45a144132d..34d6284a98 100644 Binary files a/cli/wasm/regex/windmill_parser_wasm_bg.wasm and b/cli/wasm/regex/windmill_parser_wasm_bg.wasm differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d651c99a6f..30ae14a391 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-components", - "version": "1.575.0", + "version": "1.576.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-components", - "version": "1.575.0", + "version": "1.576.3", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -24,7 +24,7 @@ "@redocly/json-to-json-schema": "^0.0.1", "@scalar/openapi-parser": "^0.15.0", "@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1", - "@tutorlatin/svelte-tiny-virtual-list": "^3.0.2", + "@tutorlatin/svelte-tiny-virtual-list": "^3.0.16", "@windmill-labs/svelte-dnd-action": "^0.9.44", "@xterm/addon-fit": "^0.10.0", "@xyflow/svelte": "^1.0.0", @@ -79,7 +79,7 @@ "windmill-parser-wasm-nu": "1.510.1", "windmill-parser-wasm-php": "1.574.1", "windmill-parser-wasm-py": "1.538.0", - "windmill-parser-wasm-regex": "1.574.1", + "windmill-parser-wasm-regex": "1.575.4", "windmill-parser-wasm-ruby": "1.526.1", "windmill-parser-wasm-rust": "1.558.1", "windmill-parser-wasm-ts": "1.565.0", @@ -3020,9 +3020,9 @@ } }, "node_modules/@tutorlatin/svelte-tiny-virtual-list": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@tutorlatin/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-3.0.15.tgz", - "integrity": "sha512-ew61aZNXGf0b5X+UjbOAhiNwzI21vijhB/mtBs8bpNOVYQ50TG6Qx00t+fR5C72eGnmdzguewZ2WP6QPNOTQJg==", + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/@tutorlatin/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-3.0.16.tgz", + "integrity": "sha512-JQSmhRDAFZbq2rTlzn+kFXJayi5VPLxeGjD01ZVyV2ti7PlQE/ov6rQFR1c8s7Y3B1OiTcv3oEWGi3ib69V8eQ==", "license": "MIT", "engines": { "node": ">=20.17.0" @@ -13759,9 +13759,9 @@ "integrity": "sha512-s+bdIgT/fA5em3zYUwF8D14uA/dZh7iu0krZYZQqZUO7txN37hwSCVfovbMkIwm4zPbsJ50mU8DRLt7UpAPZIw==" }, "node_modules/windmill-parser-wasm-regex": { - "version": "1.574.1", - "resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.574.1.tgz", - "integrity": "sha512-KnNBnpTGcnBPSzTQHdsjTPJbRQ81iem98eUnMGO8Zt1qgBFJ/eFZPwmUg6TZMbMITzTi0QqRCJj08eQID1FcPg==" + "version": "1.575.4", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.575.4.tgz", + "integrity": "sha512-G7Ni8XxdSKLNew50B1KRmwn/z9qq3obpEDaaDFS7Z74MeUdaBB/TD69SY6RxDL8SZrh0Tl7SCbS/GGLnt1AzkQ==" }, "node_modules/windmill-parser-wasm-ruby": { "version": "1.526.1", @@ -13936,29 +13936,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/xml-utils": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index dfb0fda59a..da4f163db7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "windmill-components", - "version": "1.575.0", + "version": "1.576.3", "scripts": { "dev": "vite dev", "build": "vite build", @@ -89,7 +89,7 @@ "@redocly/json-to-json-schema": "^0.0.1", "@scalar/openapi-parser": "^0.15.0", "@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1", - "@tutorlatin/svelte-tiny-virtual-list": "^3.0.2", + "@tutorlatin/svelte-tiny-virtual-list": "^3.0.16", "@windmill-labs/svelte-dnd-action": "^0.9.44", "@xterm/addon-fit": "^0.10.0", "@xyflow/svelte": "^1.0.0", @@ -144,7 +144,7 @@ "windmill-parser-wasm-nu": "1.510.1", "windmill-parser-wasm-php": "1.574.1", "windmill-parser-wasm-py": "1.538.0", - "windmill-parser-wasm-regex": "1.574.1", + "windmill-parser-wasm-regex": "1.575.4", "windmill-parser-wasm-ruby": "1.526.1", "windmill-parser-wasm-rust": "1.558.1", "windmill-parser-wasm-ts": "1.565.0", diff --git a/frontend/src/lib/cancelable-promise-utils.ts b/frontend/src/lib/cancelable-promise-utils.ts new file mode 100644 index 0000000000..17e8be5a93 --- /dev/null +++ b/frontend/src/lib/cancelable-promise-utils.ts @@ -0,0 +1,84 @@ +import { CancelablePromise } from './gen' + +export namespace CancelablePromiseUtils { + export function then( + promise: CancelablePromise, + f: (value: T) => CancelablePromise + ): CancelablePromise { + let promiseToBeCanceled: CancelablePromise = promise + let p = new CancelablePromise((resolve, reject) => { + promise + .then((value1) => { + let promise2 = f(value1) + promiseToBeCanceled = promise2 + promise2.then((value2) => resolve(value2)).catch((err) => reject(err)) + }) + .catch((err) => reject(err)) + }) + p.cancel = () => promiseToBeCanceled.cancel() + return p + } + + export function pure(value: T): CancelablePromise { + return new CancelablePromise((resolve) => resolve(value)) + } + + export function err(error: any): CancelablePromise { + return new CancelablePromise((_, reject) => reject(error)) + } + + export function map( + promise: CancelablePromise, + f: (value: T) => U + ): CancelablePromise { + return then(promise, (value) => pure(f(value))) + } + + export function pipe( + promise: CancelablePromise, + f: (value: T) => void + ): CancelablePromise { + promise.then((value) => { + f(value) + }) + return promise + } + + export function catchErr( + promise: CancelablePromise, + f: (error: any) => CancelablePromise + ): CancelablePromise { + let promiseToBeCanceled: CancelablePromise = promise + let p = new CancelablePromise((resolve, reject) => { + promise + .then((value) => resolve(value)) + .catch((err) => { + let promise2 = f(err) + promiseToBeCanceled = promise2 + return promise2.then((value2) => resolve(value2)).catch((err2) => reject(err2)) + }) + .catch((err) => reject(err)) + }) + p.cancel = () => promiseToBeCanceled.cancel() + return p + } + + export function finallyDo(promise: CancelablePromise, f: () => void): CancelablePromise { + promise = map(promise, (value) => (f(), value)) + promise = catchErr(promise, (e) => (f(), err(e))) + return promise + } + + // Calls onTimeout if the promise does not settle within timeoutMs milliseconds + export function onTimeout( + promise: CancelablePromise, + timeoutMs: number, + onTimeout: () => void + ): CancelablePromise { + let timeoutId: number | undefined = setTimeout(onTimeout, timeoutMs) + promise = finallyDo(promise, () => { + if (timeoutId !== undefined) clearTimeout(timeoutId) + }) + return promise + } +} diff --git a/frontend/src/lib/components/AppConnectInner.svelte b/frontend/src/lib/components/AppConnectInner.svelte index 817a6f4f16..6e4854e550 100644 --- a/frontend/src/lib/components/AppConnectInner.svelte +++ b/frontend/src/lib/components/AppConnectInner.svelte @@ -33,6 +33,7 @@ import TextInput from './text_input/TextInput.svelte' import { usePromise } from '$lib/svelte5Utils.svelte' import { pollJobResult } from './jobs/utils' + import { sameTopDomainOrigin } from '$lib/cookies' interface Props { step?: number @@ -239,7 +240,7 @@ function popupListener(event) { console.log('Received oauth popup message', event) let data = event.data - if (event.origin == null || event.origin !== window.location.origin) { + if (!sameTopDomainOrigin(event.origin, window.location.origin)) { console.log( 'Received oauth popup message from different origin', event.origin, @@ -397,6 +398,7 @@ } window.addEventListener('message', popupListener) window.addEventListener('storage', handleStorageEvent) + console.log('opening popup', url.toString()) window.open(url.toString(), '_blank', 'popup=true') step += 1 } diff --git a/frontend/src/lib/components/AppConnectLightweightResourcePicker.svelte b/frontend/src/lib/components/AppConnectLightweightResourcePicker.svelte index 9e3fe4b55a..81dd26c04b 100644 --- a/frontend/src/lib/components/AppConnectLightweightResourcePicker.svelte +++ b/frontend/src/lib/components/AppConnectLightweightResourcePicker.svelte @@ -42,7 +42,7 @@ {/if} - +
+ +
{/if} diff --git a/frontend/src/lib/components/RunsPage.svelte b/frontend/src/lib/components/RunsPage.svelte new file mode 100644 index 0000000000..86bcac405d --- /dev/null +++ b/frontend/src/lib/components/RunsPage.svelte @@ -0,0 +1,1389 @@ + + + + + + + { + const func = askingForConfirmation?.onConfirm + await func?.(forceCancelInPopup) + askingForConfirmation = undefined + }} + type={askingForConfirmation?.type} + loading={askingForConfirmation?.loading} + on:canceled={() => { + askingForConfirmation = undefined + }} +> + {#if askingForConfirmation?.preContent} +
{askingForConfirmation.preContent}
+ + {#if forceCancelInPopup} +
+

+ Force cancel is enabled. This is dangerous, only do this if you have no alternatives. + Instead of being gracefully cancelled, all jobs will be immediately sent to the completed + job table regardless of them being processed or not or part of running flows. You may end + up in an inconsistent state. +

+
+ {/if} + {/if} +
+ + + + {#if selectedIds.length === 1} + {#if selectedIds[0] === '-'} +
There is no information available for this job
+ {:else} + + {/if} + {/if} +
+
+ + { + reset() + loadFromQuery() + }} +/> + +{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.runs} + +{:else} +
+ +
+
+
+

+ Runs +

+ + + All past and schedule executions of scripts and flows, including previews. You only see + your own runs or runs of groups you belong to unless you are an admin. + +
+ + + { + jobsFilter('waiting') + }} + onJobsSuspended={() => { + jobsFilter('suspended') + }} + small={innerWidth < smallScreenWidth} + /> +
+ +
+ +
+ + {#if minTs || maxTs} + + {/if} + { + minTs = new Date(detail).toISOString() + calendarChangeTimeout && clearTimeout(calendarChangeTimeout) + calendarChangeTimeout = setTimeout(() => { + jobsLoader?.loadJobs(minTs, maxTs, true) + }, 1000) + }} + on:clear={async () => { + minTs = undefined + calendarChangeTimeout && clearTimeout(calendarChangeTimeout) + calendarChangeTimeout = setTimeout(() => { + jobsLoader?.loadJobs(minTs, maxTs, true) + }, 1000) + }} + /> + + + + {#if maxTs || minTs} + + {/if} + { + maxTs = new Date(detail).toISOString() + calendarChangeTimeout && clearTimeout(calendarChangeTimeout) + calendarChangeTimeout = setTimeout(() => { + jobsLoader?.loadJobs(minTs, maxTs, true) + }, 1000) + }} + on:clear={async () => { + maxTs = undefined + calendarChangeTimeout && clearTimeout(calendarChangeTimeout) + calendarChangeTimeout = setTimeout(() => { + jobsLoader?.loadJobs(minTs, maxTs, true) + }, 1000) + }} + /> + + + {#if minTs || maxTs} + + + + {/if} +
+ + +
+ { + if (e.detail == 'running' && maxTs != undefined) { + maxTs = undefined + } + }} + {usernames} + {folders} + {paths} + mobile={innerWidth < verySmallScreenWidth} + small={innerWidth < smallScreenWidth} + calendarSmall={!minTs && !maxTs} + /> +
+
+
+ + +
+
+
+ { + graph = detail + graphIsRunsChart = graph === 'RunChart' + }} + > + {#snippet children({ item })} + + + {/snippet} + + + {#if !graphIsRunsChart} + setLookback(0), + id: '0' + }, + { + displayName: '1 day', + action: () => setLookback(1), + id: '1' + }, + { + displayName: '3 days', + action: () => setLookback(3), + id: '3' + }, + { + displayName: '7 days', + action: () => setLookback(7), + id: '7' + } + ]} + selected={lookback.toString()} + selectedDisplayName={`${lookback} days lookback`} + > + {#snippet extraLabel()} + + {#snippet text()} + How far behind the min datetime to start considering jobs for the concurrency + graph. Change this value to include jobs started before the set time window for + the computation of the graph + {/snippet} + + {/snippet} + + {/if} +
+
+ {#if graph === 'RunChart'} + { + minTs = zoom.min.toISOString() + maxTs = zoom.max.toISOString() + manualDatePicker?.resetChoice() + jobsLoader?.loadJobs(minTs, maxTs, true) + }} + onPointClicked={(ids) => { + runsTable?.scrollToRun(ids) + }} + /> + {:else if graph === 'ConcurrencyChart'} + { + minTs = zoom.min.toISOString() + maxTs = zoom.max.toISOString() + jobsLoader?.loadJobs(minTs, maxTs, true) + }} + /> + {/if} +
+ +
+ + +
+ +
+
+ {#if selectionMode && selectableJobCount} +
+
+ +
+ +
+ {/if} + + +
+ +
+ {#if !jobTriggerKind} +
+ { + localStorage.setItem( + 'show_schedules_in_run', + showSchedules ? 'true' : 'false' + ) + }} + options={tableTopBarWidth < 800 || selectionMode + ? {} + : { right: 'Schedules' }} + /> + + + +
+ {/if} + +
+ { + localStorage.setItem('show_future_jobs', showFutureJobs ? 'true' : 'false') + }} + id="planned-later" + options={tableTopBarWidth < 800 || selectionMode + ? {} + : { right: 'Planned later' }} + /> + + + +
+
+ { + lastFetchWentToEnd = false + jobsLoader?.loadJobs(minTs, maxTs, true) + }} + bind:minTs + bind:maxTs + bind:selectedManualDate + {loading} + bind:this={manualDatePicker} + numberOfLastJobsToFetch={perPage} + /> + { + localStorage.setItem('auto_refresh_in_runs', autoRefresh ? 'true' : 'false') + }} + options={{ right: 'Auto-refresh' }} + textClass="whitespace-nowrap" + /> +
+
+
+ + +
+ {#if jobs} + + {:else} +
+ {#each new Array(8) as _} + + {/each} +
+ {/if} +
+
+ Per page: + - {#if usernames} - {#if $userStore?.is_admin || $userStore?.is_super_admin} - - {/if} - {#each usernames as e} - {#if e == username || $userStore?.is_admin || $userStore?.is_super_admin} - - {:else} - - {/if} - {/each} - {/if} - + (resources.value?.push(r), (resource = r))} createText="Press enter to use this value" bind:value={resource} @@ -423,6 +401,7 @@ Operation - - {#each ['Create', 'Update', 'Delete', 'Execute'] as e} - - {/each} - + ({ + label: triggerDisplayNamesMap[value], + value + }))} + bind:value={jobTriggerKind} + clearable + />
diff --git a/frontend/src/lib/components/runs/RunsTable.svelte b/frontend/src/lib/components/runs/RunsTable.svelte index 341d40d834..3eb22b2af2 100644 --- a/frontend/src/lib/components/runs/RunsTable.svelte +++ b/frontend/src/lib/components/runs/RunsTable.svelte @@ -22,6 +22,7 @@ activeLabel?: string | null // const loadMoreQuantity: number = 100 lastFetchWentToEnd?: boolean + perPage?: number } let { @@ -33,7 +34,8 @@ selectedIds = $bindable([]), selectedWorkspace = $bindable(undefined), activeLabel = null, - lastFetchWentToEnd = $bindable(false) + lastFetchWentToEnd = $bindable(false), + perPage = 1000 }: Props = $props() function getTime(job: Job): string | undefined { @@ -116,7 +118,7 @@ let tableHeight: number = $state(0) let headerHeight: number = $state(0) let containerWidth: number = $state(0) - // const MAX_ITEMS = 1000 + // const MAX_ITEMS = perPage /* function infiniteHandler({ detail: { loaded, error, complete } }) { @@ -139,7 +141,7 @@ return '' } const jc = jobCount - const isTruncated = jc >= 1000 && !lastFetchWentToEnd + const isTruncated = jc >= perPage && !lastFetchWentToEnd return `${jc}${isTruncated ? '+' : ''} job${jc != 1 ? 's' : ''}` } @@ -338,13 +340,15 @@ {/snippet} {#snippet footer()}
{#if !lastFetchWentToEnd && jobs && jobs.length >= 1000} + >{#if !lastFetchWentToEnd && jobs && jobs.length >= perPage} + Load next {perPage} jobs + {/if}
{/snippet} diff --git a/frontend/src/lib/components/select/Select.svelte b/frontend/src/lib/components/select/Select.svelte index 287c620a7b..0327444bcf 100644 --- a/frontend/src/lib/components/select/Select.svelte +++ b/frontend/src/lib/components/select/Select.svelte @@ -1,4 +1,7 @@ - - - - { - const func = askingForConfirmation?.onConfirm - await func?.(forceCancelInPopup) - askingForConfirmation = undefined - }} - type={askingForConfirmation?.type} - loading={askingForConfirmation?.loading} - on:canceled={() => { - askingForConfirmation = undefined - }} -> - {#if askingForConfirmation?.preContent} -
{askingForConfirmation.preContent}
- - {#if forceCancelInPopup} -
-

- Force cancel is enabled. This is dangerous, only do this if you have no alternatives. - Instead of being gracefully cancelled, all jobs will be immediately sent to the completed - job table regardless of them being processed or not or part of running flows. You may end - up in an inconsistent state. -

-
- {/if} - {/if} -
- - - - {#if selectedIds.length === 1} - {#if selectedIds[0] === '-'} -
There is no information available for this job
- {:else} - - {/if} - {/if} -
-
- - { - reset() - loadFromQuery() - }} -/> - -{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.runs} - -{:else} -
- -
-
-
-

- Runs -

- - - All past and schedule executions of scripts and flows, including previews. You only see - your own runs or runs of groups you belong to unless you are an admin. - -
- - - { - jobsFilter('waiting') - }} - onJobsSuspended={() => { - jobsFilter('suspended') - }} - small={innerWidth < smallScreenWidth} - /> -
- -
- -
- - {#if minTs || maxTs} - - {/if} - { - minTs = new Date(detail).toISOString() - calendarChangeTimeout && clearTimeout(calendarChangeTimeout) - calendarChangeTimeout = setTimeout(() => { - jobsLoader?.loadJobs(minTs, maxTs, true) - }, 1000) - }} - on:clear={async () => { - minTs = undefined - calendarChangeTimeout && clearTimeout(calendarChangeTimeout) - calendarChangeTimeout = setTimeout(() => { - jobsLoader?.loadJobs(minTs, maxTs, true) - }, 1000) - }} - /> - - - - {#if maxTs || minTs} - - {/if} - { - maxTs = new Date(detail).toISOString() - calendarChangeTimeout && clearTimeout(calendarChangeTimeout) - calendarChangeTimeout = setTimeout(() => { - jobsLoader?.loadJobs(minTs, maxTs, true) - }, 1000) - }} - on:clear={async () => { - maxTs = undefined - calendarChangeTimeout && clearTimeout(calendarChangeTimeout) - calendarChangeTimeout = setTimeout(() => { - jobsLoader?.loadJobs(minTs, maxTs, true) - }, 1000) - }} - /> - - - {#if minTs || maxTs} - - - - {/if} -
- - -
- { - if (e.detail == 'running' && maxTs != undefined) { - maxTs = undefined - } - }} - {usernames} - {folders} - {paths} - mobile={innerWidth < verySmallScreenWidth} - small={innerWidth < smallScreenWidth} - calendarSmall={!minTs && !maxTs} - /> -
-
-
- - -
-
-
- { - graph = detail - graphIsRunsChart = graph === 'RunChart' - }} - > - {#snippet children({ item })} - - - {/snippet} - - - {#if !graphIsRunsChart} - setLookback(0), - id: '0' - }, - { - displayName: '1 day', - action: () => setLookback(1), - id: '1' - }, - { - displayName: '3 days', - action: () => setLookback(3), - id: '3' - }, - { - displayName: '7 days', - action: () => setLookback(7), - id: '7' - } - ]} - selected={lookback.toString()} - selectedDisplayName={`${lookback} days lookback`} - > - {#snippet extraLabel()} - - {#snippet text()} - How far behind the min datetime to start considering jobs for the concurrency - graph. Change this value to include jobs started before the set time window for - the computation of the graph - {/snippet} - - {/snippet} - - {/if} -
-
- {#if graph === 'RunChart'} - { - minTs = zoom.min.toISOString() - maxTs = zoom.max.toISOString() - manualDatePicker?.resetChoice() - jobsLoader?.loadJobs(minTs, maxTs, true) - }} - onPointClicked={(ids) => { - runsTable?.scrollToRun(ids) - }} - /> - {:else if graph === 'ConcurrencyChart'} - { - minTs = zoom.min.toISOString() - maxTs = zoom.max.toISOString() - jobsLoader?.loadJobs(minTs, maxTs, true) - }} - /> - {/if} -
- -
- - -
- -
-
- {#if selectionMode && selectableJobCount} -
-
- -
- -
- {/if} - - -
- -
-
- { - localStorage.setItem( - 'show_schedules_in_run', - showSchedules ? 'true' : 'false' - ) - }} - options={tableTopBarWidth < 800 || selectionMode - ? {} - : { right: 'Cron schedules' }} - /> - - - -
- -
- { - localStorage.setItem('show_future_jobs', showFutureJobs ? 'true' : 'false') - }} - id="planned-later" - options={tableTopBarWidth < 800 || selectionMode - ? {} - : { right: 'Planned later' }} - /> - - - -
-
- { - lastFetchWentToEnd = false - jobsLoader?.loadJobs(minTs, maxTs, true) - }} - bind:minTs - bind:maxTs - bind:selectedManualDate - {loading} - bind:this={manualDatePicker} - /> - { - localStorage.setItem('auto_refresh_in_runs', autoRefresh ? 'true' : 'false') - }} - options={{ right: 'Auto-refresh' }} - textClass="whitespace-nowrap" - /> -
-
-
- - -
- {#if jobs} - - {:else} -
- {#each new Array(8) as _} - - {/each} -
- {/if} -
-
-
- 0}> - {#if selectionMode === 're-run'} - - {:else if selectedIds.length === 1} - {#if selectedIds[0] === '-'} -
There is no information available for this job
- {:else} - - {/if} - {:else if selectedIds.length > 1} -
There are {selectedIds.length} jobs selected. Choose 1 to see detailed information
- {/if} -
-
-
-
-{/if} +{#key perPage} + +{/key} diff --git a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte index cc7d9e67b4..76f22642bd 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte @@ -791,7 +791,7 @@ {#if script} import { BROWSER } from 'esm-env' - import { base } from '$lib/base' - import AppPreview from '$lib/components/apps/editor/AppPreview.svelte' - import { IS_APP_PUBLIC_CONTEXT_KEY, type EditorBreakpoint } from '$lib/components/apps/types' - import { Alert, Skeleton } from '$lib/components/common' - import { WindmillIcon } from '$lib/components/icons' import { AppService, OpenAPI, type AppWithLastVersion } from '$lib/gen' - import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores' - import { twMerge } from 'tailwind-merge' + import { userStore, workspaceStore } from '$lib/stores' import { setContext } from 'svelte' - import { writable } from 'svelte/store' import { setLicense } from '$lib/enterpriseUtils' - import { isCloudHosted } from '$lib/cloud' - import Login from '$lib/components/Login.svelte' + import { getUserExt } from '$lib/user' - import { User, UserRoundX } from 'lucide-svelte' - import { goto } from '$app/navigation' import { sendUserToast } from '$lib/toast' import { page } from '$app/state' - import { urlParamsToObject } from '$lib/utils' + import PublicApp from '$lib/components/apps/editor/PublicApp.svelte' let app: (AppWithLastVersion & { value: any }) | undefined = $state(undefined) let notExists = $state(false) let noPermission = $state(false) - let jwtError = $state(false) - setContext(IS_APP_PUBLIC_CONTEXT_KEY, true) function isJwt(t: string) { // simply check that the first part is a valid base64 encoded json @@ -56,9 +44,10 @@ } } - const parsedCustomPath = parseCustomPath(page.params.path ?? '') - + let workspace: string | undefined = $state(undefined) async function loadApp() { + const parsedCustomPath = parseCustomPath(page.params.path ?? '') + if (parsedCustomPath.jwt) { const token = 'jwt_ext_' + parsedCustomPath.jwt OpenAPI.TOKEN = token @@ -69,6 +58,7 @@ app = await AppService.getPublicAppByCustomPath({ customPath: parsedCustomPath.path }) + workspace = app.workspace_id workspaceStore.set(app.workspace_id) noPermission = false notExists = false @@ -95,93 +85,15 @@ setLicense() loadApp() } - - const breakpoint = writable('lg') - - const darkMode = - window.localStorage.getItem('dark-mode') ?? - (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') - - if (darkMode === 'dark') { - document.documentElement.classList.add('dark') - } else { - document.documentElement.classList.remove('dark') - } - - -
{#if $userStore} -
{$userStore.username}
- {:else}{/if} -
- -{#if notExists} -
There was an error loading the app, is the url correct? Go to Windmill -
-{:else if noPermission} -
{#if $userStore}You are logged in but have no read access to this app{:else}You must be logged - in and have read access to this app{/if}
-
- {#if !jwtError} - { - - // window.location.reload() - loadApp() - app = app - }} - popup - rd={page.url.toString()} - /> - {/if} -
-{:else if app} - {#key app} -
- goto(path)} - gotoFn={(path, opt) => goto(path, opt)} - /> -
- {/key} -{:else} - -{/if} + { + loadApp() + }} +> diff --git a/frontend/src/routes/(root)/(logged)/oauth/callback/[client_name]/+page.svelte b/frontend/src/routes/oauth/callback/[client_name]/+page.svelte similarity index 100% rename from frontend/src/routes/(root)/(logged)/oauth/callback/[client_name]/+page.svelte rename to frontend/src/routes/oauth/callback/[client_name]/+page.svelte diff --git a/frontend/src/routes/(root)/(logged)/oauth/callback_slack/+page.svelte b/frontend/src/routes/oauth/callback_slack/+page.svelte similarity index 100% rename from frontend/src/routes/(root)/(logged)/oauth/callback_slack/+page.svelte rename to frontend/src/routes/oauth/callback_slack/+page.svelte diff --git a/frontend/src/routes/(root)/(logged)/oauth/callback_slack/instance/+page.svelte b/frontend/src/routes/oauth/callback_slack/instance/+page.svelte similarity index 100% rename from frontend/src/routes/(root)/(logged)/oauth/callback_slack/instance/+page.svelte rename to frontend/src/routes/oauth/callback_slack/instance/+page.svelte diff --git a/frontend/src/routes/(root)/(logged)/oauth/callback_supabase/+page.svelte b/frontend/src/routes/oauth/callback_supabase/+page.svelte similarity index 100% rename from frontend/src/routes/(root)/(logged)/oauth/callback_supabase/+page.svelte rename to frontend/src/routes/oauth/callback_supabase/+page.svelte diff --git a/frontend/src/routes/public/[workspace]/[...secret]/+page.svelte b/frontend/src/routes/public/[workspace]/[...secret]/+page.svelte index fadae2baf9..c661aa48c6 100644 --- a/frontend/src/routes/public/[workspace]/[...secret]/+page.svelte +++ b/frontend/src/routes/public/[workspace]/[...secret]/+page.svelte @@ -1,34 +1,22 @@ - - -
{#if $userStore} -
{$userStore.username}
- {:else}{/if} -
- -{#if notExists} -
There was an error loading the app, is the url correct? Go to Windmill -
-{:else if noPermission} -
{#if $userStore}You are logged in but have no read access to this app{:else}You must be logged - in and have read access to this app{/if}
-
- {#if !jwtError} - { - console.log('login') - // window.location.reload() - loadUser().then(() => { - loadApp() - }) - app = app - }} - popup - rd={page.url.toString()} - /> - {/if} -
-{:else if app} - {#key app} -
- goto(path)} - gotoFn={(path, opt) => goto(path, opt)} - /> -
- {/key} -{:else} - -{/if} + { + loadAll() + }} +> diff --git a/frontend/src/routes/user/login_callback/[client_name]/+page.svelte b/frontend/src/routes/user/login_callback/[client_name]/+page.svelte index 902a9e4a15..ac05ae04b5 100644 --- a/frontend/src/routes/user/login_callback/[client_name]/+page.svelte +++ b/frontend/src/routes/user/login_callback/[client_name]/+page.svelte @@ -12,6 +12,8 @@ import { parseQueryParams } from '$lib/utils' import { page } from '$app/state' import { isCloudHosted } from '$lib/cloud' + import { getCookie } from '$lib/cookies' + // import { getAndDeleteCookie } from '$lib/cookies' let error = page.url.searchParams.get('error') let clientName = page.params.client_name ?? '' @@ -19,11 +21,14 @@ let state = page.url.searchParams.get('state') ?? undefined onMount(async () => { + // const closeCookie = getAndDeleteCookie('close') + // console.log('closeCookie', closeCookie) const rd = localStorage.getItem('rd') if (rd) { localStorage.removeItem('rd') } - const closeUponLogin = localStorage.getItem('closeUponLogin') == 'true' + const cookieCloseUponLogin = getCookie('close') == 'true' + const closeUponLogin = cookieCloseUponLogin ?? localStorage.getItem('closeUponLogin') == 'true' if (error) { sendUserToast(`Error trying to login with ${clientName} ${error}`, true) if (closeUponLogin) { diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 9fc6660220..522a6be139 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -11,7 +11,15 @@ const version = JSON.parse(json) const config = { server: { https: process.env.HTTPS === 'true', - allowedHosts: ['localhost', '127.0.0.1', '0.0.0.0', 'rubendev.wimill.xyz', 'windmill.xyz'], + allowedHosts: [ + 'localhost', + '127.0.0.1', + '0.0.0.0', + 'rubendev.wimill.xyz', + 'windmill.xyz', + 'app.windmill.xyz', + 'public.windmill.xyz' + ], port: 3000, proxy: { '^/api/w/[^/]+/s3_proxy/.*': { @@ -64,7 +72,7 @@ const config = { exclude: [ '@codingame/monaco-vscode-standalone-typescript-language-features', '@codingame/monaco-vscode-standalone-languages' - ], + ] }, worker: { format: 'es' diff --git a/lsp/Pipfile b/lsp/Pipfile index 8e69ddb427..a06a6fb27f 100644 --- a/lsp/Pipfile +++ b/lsp/Pipfile @@ -4,8 +4,8 @@ verify_ssl = true name = "pypi" [packages] -wmill = ">=1.575.0" -wmill_pg = ">=1.575.0" +wmill = ">=1.576.3" +wmill_pg = ">=1.576.3" sendgrid = "*" mysql-connector-python = "*" pymongo = "*" diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index bd728d0b37..e9460ad8b6 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.575.0 + version: 1.576.3 title: OpenFlow Spec contact: name: Ruben Fiszel diff --git a/powershell-client/WindmillClient/WindmillClient.psd1 b/powershell-client/WindmillClient/WindmillClient.psd1 index b75c5d2e72..74929769d0 100644 --- a/powershell-client/WindmillClient/WindmillClient.psd1 +++ b/powershell-client/WindmillClient/WindmillClient.psd1 @@ -12,7 +12,7 @@ RootModule = 'WindmillClient.psm1' # Version number of this module. - ModuleVersion = '1.575.0' + ModuleVersion = '1.576.3' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/python-client/wmill/pyproject.toml b/python-client/wmill/pyproject.toml index c4d9c9329e..0b2e9a3053 100644 --- a/python-client/wmill/pyproject.toml +++ b/python-client/wmill/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill" -version = "1.575.0" +version = "1.576.3" description = "A client library for accessing Windmill server wrapping the Windmill client API" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/python-client/wmill_pg/pyproject.toml b/python-client/wmill_pg/pyproject.toml index b148a05a68..3be1d00ecc 100644 --- a/python-client/wmill_pg/pyproject.toml +++ b/python-client/wmill_pg/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill-pg" -version = "1.575.0" +version = "1.576.3" description = "An extension client for the wmill client library focused on pg" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/typescript-client/jsr.json b/typescript-client/jsr.json index 564bf9b984..f0fccc57c0 100644 --- a/typescript-client/jsr.json +++ b/typescript-client/jsr.json @@ -1,6 +1,6 @@ { "name": "@windmill/windmill", - "version": "1.575.0", + "version": "1.576.3", "exports": "./src/index.ts", "publish": { "exclude": ["!src", "./s3Types.ts", "./client.ts"] diff --git a/typescript-client/package.json b/typescript-client/package.json index 1efa54ef76..4cbcbdca2f 100644 --- a/typescript-client/package.json +++ b/typescript-client/package.json @@ -1,7 +1,7 @@ { "name": "windmill-client", "description": "Windmill SDK client for browsers and Node.js", - "version": "1.575.0", + "version": "1.576.3", "author": "Ruben Fiszel", "license": "Apache 2.0", "devDependencies": { diff --git a/version.txt b/version.txt index f227cd375d..49974777a8 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.575.0 +1.576.3