Compare commits

..

30 Commits

Author SHA1 Message Date
Ruben Fiszel
a9bac09b14 all 2026-02-11 18:37:15 +00:00
Ruben Fiszel
607a861b80 all 2026-02-11 18:20:14 +00:00
Ruben Fiszel
5ecfa92ac3 fix: install powershell via dotnet tool for arm64 ci runner
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 16:48:41 +00:00
Ruben Fiszel
a3d5ae5425 all 2026-02-11 16:37:11 +00:00
Ruben Fiszel
d363fa6098 all 2026-02-11 16:28:20 +00:00
Ruben Fiszel
44143e7576 replace leftover common:: references in dependency_map test (#7911)
* fix: replace leftover common:: references in dependency_map test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add missing deno_core/mcp features and gate dead code in permissions test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 16:05:34 +00:00
Ruben Fiszel
4d94d80835 add missing feature-gated deps to api-integration-tests crate
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 15:09:14 +00:00
Ruben Fiszel
c9d5c24c34 chore(main): release 1.631.2 (#7906)
* chore(main): release 1.631.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-02-11 14:43:13 +00:00
Ruben Fiszel
b11d6ed794 fix(frontend): revert CloseButton refactor that broke tag removal in MultiSelect (#7909)
The recent refactor of CloseButton (from on:close component events to
onClick prop) broke tag removal in MultiSelect/TagsToListenTo. The
refactor changed on:pointerdown (component event) to onPointerdown
(native DOM event), which stopped native pointerdown propagation and
broke the drag tracking in DraggableTags, causing the dropdown to open
on every close button click.

Reverts CloseButton and all callers back to using createEventDispatcher
and on:close.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 14:27:50 +00:00
Ruben Fiszel
ea52a8b8ce refactor: move integration tests to subcrates to reduce recompilation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 14:04:54 +00:00
Ruben Fiszel
b9e0ccaefd chore: split api-scripts and api-flows 2026-02-11 13:27:00 +00:00
Devdatta Talele
5effb87a36 fix: strip unsupported schema fields for Google AI (#7894)
Closes #7759
2026-02-11 13:26:08 +00:00
wendrul
900c76ccad fix: nix flake libz.so for deno_core (#7905) 2026-02-11 13:13:36 +00:00
Ruben Fiszel
ff72b81004 chore(main): release 1.631.1 (#7904)
* chore(main): release 1.631.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-02-11 12:45:39 +00:00
Ruben Fiszel
2815cfae1a fix: add kafka-gssapi support to ee builds 2026-02-11 12:40:59 +00:00
Ruben Fiszel
700c61243f chore(main): release 1.631.0 (#7898)
* chore(main): release 1.631.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-02-11 12:08:25 +00:00
centdix
f22eb964e4 feat(ai): support 1M context window for Anthropic resources (#7891)
* feat(ai): support 1M context window for Anthropic resources

Add `enable_1m_context` boolean field to Anthropic resource configuration.
When enabled (and not using Vertex AI), sends the `anthropic-beta: context-1m-2025-08-07`
header in both the API proxy layer and the AI agent worker layer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ai): add serde alias for enable_1M_context DB field name

The resource_type schema uses `enable_1M_context` (uppercase M) but
serde only matched `enable_1m_context` and `enable1mContext`, causing
the field to always deserialize as false.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 12:01:46 +00:00
hugocasa
754b48cb89 fix: waitJob getJob and streamJob in raw apps (#7901)
* fix: waitJob getJob and streamJob in raw apps

* nits

* use latest ui builder

* fix

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-02-11 11:41:37 +00:00
Pyra
097d9288c5 feat(uv): index resolve strategy (#7885)
* fix

* fix

Signed-off-by: pyranota <pyra@duck.com>

* Update backend/windmill-worker/src/python_executor.rs

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* Update backend/windmill-worker/src/python_executor.rs

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

---------

Signed-off-by: pyranota <pyra@duck.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-02-11 11:18:51 +00:00
Ruben Fiszel
92c601860f fix: otel gracefully handle no native ts 2026-02-11 11:15:01 +00:00
Ruben Fiszel
e26e437dd1 refactor: extract 12 leaf crates from windmill-api (#7899)
* feat(backend): extract 12 leaf crates from windmill-api to improve incremental compilation

Extract independent modules from windmill-api (90k LOC monolith) into
separate leaf crates to reduce incremental compilation times. Modules
extracted: assets, configs, debug, flow-conversations, inputs,
npm-proxy, openapi, schedule, settings, workers, agent-workers, and
alerting (from windmill-common).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add windmill-api-settings dep to root crate, make ee_oss public

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add agent_workers integration tests

8 tests covering agent worker lifecycle:
- Simple script execution (bun)
- Script with arguments
- Script with logs (verified in job_logs table)
- Script failure handling
- Complex result (nested objects/arrays)
- Agent token creation via API
- Token creation + Initial/MainLoop ping cycle
- Multiple sequential job execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 10:31:03 +00:00
Ruben Fiszel
9e8d77436b dotNet (#7900) 2026-02-11 10:23:31 +00:00
Ruben Fiszel
7eb239f1e2 fix: worker do not apply migrations anymore but wait for servers to do so 2026-02-11 08:16:06 +00:00
Ruben Fiszel
317ae9196b update refs 2026-02-11 07:12:45 +00:00
Ruben Fiszel
20bb97fc1f reorg mcp_tools 2026-02-11 06:44:10 +00:00
Ruben Fiszel
5e9b4cfa99 nit UI + more tests 2026-02-11 06:27:21 +00:00
Guilhem
31bfccc745 fix(frontend): improve time picker (#7893)
* clean layout

* remove useless toggle

* improve layout

* nit

* Fix input type seconds

* convert secondInputs to svelte 5

* redesign seconds input

* Adapt layout where time input is used

* nit
2026-02-11 00:48:18 +00:00
Ruben Fiszel
0ad4f0232e chore(main): release 1.630.2 (#7897)
* chore(main): release 1.630.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-02-11 00:29:42 +00:00
Ruben Fiszel
02f4359ba4 bump rust to 1.93.0 2026-02-11 00:02:24 +00:00
Ruben Fiszel
1a109a7797 fix: bump rust version from 1.90.0 to 1.93.0 2026-02-10 23:55:36 +00:00
172 changed files with 4135 additions and 1060 deletions

View File

@@ -23,7 +23,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false
toolchain: 1.90.0
toolchain: 1.93.0
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
@@ -44,7 +44,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false
toolchain: 1.90.0
toolchain: 1.93.0
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
@@ -81,7 +81,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false
toolchain: 1.90.0
toolchain: 1.93.0
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
@@ -118,7 +118,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.90.0
toolchain: 1.93.0
- name: cargo check
timeout-minutes: 16
working-directory: ./backend

View File

@@ -19,7 +19,7 @@ defaults:
jobs:
cargo_test:
runs-on: ubicloud-standard-16
runs-on: blacksmith-32vcpu-ubuntu-2404
services:
postgres:
image: postgres
@@ -50,9 +50,9 @@ jobs:
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- uses: actions/setup-go@v2
- uses: actions/setup-go@v6
with:
go-version: 1.21.5
go-version: 1.25.0
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.8
@@ -72,12 +72,14 @@ jobs:
bundler-cache: false
- name: Install PowerShell, mold and clang
run: |
sudo apt-get update && sudo apt-get install -y powershell mold clang libcurl4-openssl-dev
dotnet tool install --global PowerShell
echo "$HOME/.dotnet/tools" >> $GITHUB_PATH
sudo apt-get update && sudo apt-get install -y mold clang libcurl4-openssl-dev
working-directory: /
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false
toolchain: 1.90.0
cache: true
toolchain: 1.93.0
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./ee-repo-ref.txt)" >> "$GITHUB_ENV"
@@ -214,7 +216,7 @@ jobs:
RUST_LOG: "off"
RUST_LOG_STYLE: never
CARGO_NET_GIT_FETCH_WITH_CLI: true
CARGO_BUILD_JOBS: 12
CARGO_BUILD_JOBS: 16
WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES: 1
WMDEBUG_FORCE_RUNNABLE_SETTINGS_V0: 1
WMDEBUG_FORCE_NO_LEGACY_DEBOUNCING_COMPAT: 1
@@ -222,4 +224,4 @@ jobs:
run: |
deno --version && bun -v && node --version && go version && python3 --version && php --version && ruby --version && pwsh --version && dotnet --version
cd windmill-duckdb-ffi-internal && ./build_dev.sh && cd ..
DENO_PATH=$(which deno) BUN_PATH=$(which bun) NODE_BIN_PATH=$(which node) GO_PATH=$(which go) UV_PATH=$(which uv) PHP_PATH=$(which php) COMPOSER_PATH=$(which composer) RUBY_PATH=$(which ruby) RUBY_BUNDLE_PATH=$(which bundle) RUBY_GEM_PATH=$(which gem) POWERSHELL_PATH=$(which pwsh) DOTNET_PATH=$(which dotnet) cargo test --features enterprise,deno_core,duckdb,license,python,rust,scoped_cache,parquet,private,private_registry_test,csharp,php,ruby,mysql,quickjs,mcp --all -- --nocapture --test-threads=10
DENO_PATH=$(which deno) BUN_PATH=$(which bun) NODE_BIN_PATH=$(which node) GO_PATH=$(which go) UV_PATH=$(which uv) PHP_PATH=$(which php) COMPOSER_PATH=$(which composer) RUBY_PATH=$(which ruby) RUBY_BUNDLE_PATH=$(which bundle) RUBY_GEM_PATH=$(which gem) POWERSHELL_PATH=$(which pwsh) DOTNET_PATH=$(which dotnet) cargo test --features enterprise,deno_core,duckdb,license,python,rust,scoped_cache,parquet,private,private_registry_test,csharp,php,ruby,mysql,quickjs,mcp --all -- --nocapture --test-threads=12

View File

@@ -33,7 +33,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.90.0
toolchain: 1.93.0
- name: Substitute EE code
shell: bash

View File

@@ -83,7 +83,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.90.0
toolchain: 1.93.0
- name: cargo check
working-directory: ./backend

View File

@@ -66,7 +66,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.90.0
toolchain: 1.93.0
- name: Install xmlsec and gssapi build-time deps
run: |

View File

@@ -35,7 +35,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.90.0
toolchain: 1.93.0
- name: Substitute EE code
shell: bash

1
.gitignore vendored
View File

@@ -19,3 +19,4 @@ backend/target
frontend/node_modules
typescript-client/node_modules
frontend/.svelte-kit
backend/chrome_profiler.json

View File

@@ -1,5 +1,44 @@
# Changelog
## [1.631.2](https://github.com/windmill-labs/windmill/compare/v1.631.1...v1.631.2) (2026-02-11)
### Bug Fixes
* **frontend:** revert CloseButton refactor that broke tag removal in MultiSelect ([#7909](https://github.com/windmill-labs/windmill/issues/7909)) ([b11d6ed](https://github.com/windmill-labs/windmill/commit/b11d6ed7940faddfe74a22b25bcb132527cbcec8))
* nix flake libz.so for deno_core ([#7905](https://github.com/windmill-labs/windmill/issues/7905)) ([900c76c](https://github.com/windmill-labs/windmill/commit/900c76ccad09f58fc6adcfa8151db780249617f6))
* strip unsupported schema fields for Google AI ([#7894](https://github.com/windmill-labs/windmill/issues/7894)) ([5effb87](https://github.com/windmill-labs/windmill/commit/5effb87a36793e20d17e678619091ef458fe8f0a)), closes [#7759](https://github.com/windmill-labs/windmill/issues/7759)
## [1.631.1](https://github.com/windmill-labs/windmill/compare/v1.631.0...v1.631.1) (2026-02-11)
### Bug Fixes
* add kafka-gssapi support to ee builds ([2815cfa](https://github.com/windmill-labs/windmill/commit/2815cfae1a5eefb2c553e893dc926e62ad1df528))
## [1.631.0](https://github.com/windmill-labs/windmill/compare/v1.630.2...v1.631.0) (2026-02-11)
### Features
* **ai:** support 1M context window for Anthropic resources ([#7891](https://github.com/windmill-labs/windmill/issues/7891)) ([f22eb96](https://github.com/windmill-labs/windmill/commit/f22eb964e47defe9922ecdb6ab471dd4ca267952))
* **uv:** index resolve strategy ([#7885](https://github.com/windmill-labs/windmill/issues/7885)) ([097d928](https://github.com/windmill-labs/windmill/commit/097d9288c58076882f1991e2fb33e4441fe332d3))
### Bug Fixes
* **frontend:** improve time picker ([#7893](https://github.com/windmill-labs/windmill/issues/7893)) ([31bfccc](https://github.com/windmill-labs/windmill/commit/31bfccc74588af10fc11fbbb0bc4833d65ff6421))
* otel gracefully handle no native ts ([92c6018](https://github.com/windmill-labs/windmill/commit/92c601860f1e1211aa34838cd08900ba7334a20c))
* waitJob getJob and streamJob in raw apps ([#7901](https://github.com/windmill-labs/windmill/issues/7901)) ([754b48c](https://github.com/windmill-labs/windmill/commit/754b48cb898dffe196339cea1c1598c9e1765cdc))
* worker do not apply migrations anymore but wait for servers to do so ([7eb239f](https://github.com/windmill-labs/windmill/commit/7eb239f1e2eb1b71234a8d4265c7c5813e5861ae))
## [1.630.2](https://github.com/windmill-labs/windmill/compare/v1.630.1...v1.630.2) (2026-02-11)
### Bug Fixes
* bump rust version from 1.90.0 to 1.93.0 ([1a109a7](https://github.com/windmill-labs/windmill/commit/1a109a7797d1a50a0d85f3fff236d707b2cfb81d))
## [1.630.1](https://github.com/windmill-labs/windmill/compare/v1.630.0...v1.630.1) (2026-02-10)

View File

@@ -1,5 +1,5 @@
ARG DEBIAN_IMAGE=debian:bookworm-slim
ARG RUST_IMAGE=rust:1.90-slim-bookworm
ARG RUST_IMAGE=rust:1.93-slim-bookworm
FROM debian:bookworm-slim AS nsjail

View File

@@ -46,11 +46,11 @@
]
},
"nullable": [
true,
true,
true,
true,
true,
false,
false,
false,
false,
false,
true,
true
]

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT error_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "12c3101e02f197ad731b66f539fe677ad25520fcc9c5a2378a293122956bed4c"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET error_handler = '{\"path\": \"script/f/test/error_handler\", \"extra_args\": {\"notify\": true}, \"muted_on_cancel\": true, \"muted_on_user_path\": false}'::jsonb\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "18d1a6ac94f2c87d5ea8c48a228f061135be4065dca33fbc429d8b0186c5ccb3"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT (error_handler->>'muted_on_cancel')::boolean FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "bool",
"type_info": "Bool"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "25bf3956b4365a4d4f96368bcc6e33817ae284ab26c195eee53d988d74263e86"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT (error_handler->>'muted_on_user_path')::boolean FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "bool",
"type_info": "Bool"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "28b8264a427e8c19be823d2b8e40736a7b7675780672b503d91737cd0c617657"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', $1, 'f/test/success_script', 'export function main() { return \"ok\"; }', 'deno', 'script', 'test-user', '{}', '', '', '')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "51f18c817d4014e847486f5a8dbf205ffa0e53d5bd3fdb97777e988389b1d69e"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO group_ (workspace_id, name, summary, extra_perms)\n VALUES ('test-workspace', 'error_handler', 'The group the error handler acts on behalf of', '{\"u/test-user\": true}')\n ON CONFLICT DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "5f5bdd667f699a5a1c05e07d805a97afb06252541d06b4785063819c3893b674"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, ws_error_handler_muted)\n VALUES ('test-workspace', $1, 'f/test/muted_failing_script', 'export function main() { throw new Error(\"fail\"); }', 'deno', 'script', 'test-user', '{}', '', '', '', true)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "782c259ccdf71c5e2fa8dcb27515e7c5ff83009774dc15095ed0dbcb0ecdbdde"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET error_handler = NULL\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "79648f03eb50385183c34d57bda1ac984cf6502609df9ee51b7d684b585b8447"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', 3333333333, 'f/test/error_handler', 'export function main() { return \"handled\"; }', 'deno', 'script', 'test-user', '{}', '', '', '')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "7f98c013682aca6d269ef6ef754a7247b6e2dd4958d0968a3f2eb49cc320e877"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE alerts \n SET\n acknowledged = true,\n acknowledged_workspace = CASE\n WHEN $2 THEN\n CASE\n WHEN $1::text IS NOT NULL THEN true\n ELSE acknowledged_workspace\n END\n ELSE true\n END\n WHERE ($1::text IS NOT NULL AND workspace_id = $1)\n OR ($1::text IS NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "80864158f61adaad8df934acc54ba523c9f17d106298d8781885134d28553d36"
}

View File

@@ -18,8 +18,8 @@
"Left": []
},
"nullable": [
true,
false
false,
true
]
},
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"

View File

@@ -59,9 +59,7 @@
"failure",
"command",
"approval",
"preprocessor",
"schedule_handler_old",
"dynamic_skip"
"preprocessor"
]
}
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO group_ (workspace_id, name, summary, extra_perms)\n VALUES ('test-workspace', 'error_handler', 'Error handler group', '{\"u/test-user\": true}')\n ON CONFLICT DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "ba0c3dad5d2a77922821c9c6d58e06a65e6666e197f3e59b880470df35c27bec"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET error_handler = '{\"path\": \"script/f/test/error_handler\"}'::jsonb\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "bb4cde84157126f29680cb36075848e7cde2ab7c6b7f0861becd4c0b8c330a8d"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', 5555555555, 'f/test/error_handler', 'export function main() { return \"handled\"; }', 'deno', 'script', 'test-user', '{}', '', '', '')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "cdc594b30855e9384481f561b2c16200d4e6dacb1eb274513c8f1795235dfe20"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', $1, 'f/test/failing_script', $2, 'deno', 'script', 'test-user', '{}', 'Failing test script', 'A script that always fails', '')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": []
},
"hash": "d2a9a9af4d00c1c05276c14e6f32044437800a4b0c60aa364ab2f362a4fd9a2c"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT error_handler->'extra_args' FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "d5f1a2bfbac206a33b991d58f9ff4b8e8e4a3840ea1c65d409576379dede41ed"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE alerts\n SET\n acknowledged = true,\n acknowledged_workspace = CASE\n WHEN $2 THEN\n CASE\n WHEN $1::text IS NOT NULL THEN true\n ELSE acknowledged_workspace\n END\n ELSE true\n END\n WHERE ($1::text IS NOT NULL AND workspace_id = $1)\n OR ($1::text IS NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "e00f7877d7bd33cf81e7bf48b1a37a507ab0a8f8b4bd7a38cf44d181f8f2d940"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id\n FROM v2_job\n WHERE workspace_id = 'test-workspace'\n AND permissioned_as_email = 'error_handler@windmill.dev'\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "e4c9f3bca6bd636c3ee31fc643182ed5ca48a45da895fb992d820682f5169f7c"
}

View File

@@ -0,0 +1,38 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n id,\n runnable_path,\n permissioned_as_email,\n parent_job\n FROM v2_job\n WHERE workspace_id = 'test-workspace'\n AND permissioned_as_email = 'error_handler@windmill.dev'\n ORDER BY created_at DESC\n LIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "runnable_path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "permissioned_as_email",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "parent_job",
"type_info": "Uuid"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
true,
false,
true
]
},
"hash": "fdf0a16a72bc2eca2025d64750e2e6a7e7ff90b5ad4b42e6aab53992b447a20e"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', 1111111111, 'f/test/error_handler', $1, 'deno', 'script', 'test-user', '{}', 'Error handler script', 'Handles failed job completions', '')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "fe17aff543f28cf4d6d09ea7c4c7d18ff6a95a8402468131f5a5f33df91c59e4"
}

View File

@@ -37,14 +37,22 @@ Windmill uses a workspace-based architecture with multiple crates:
- Update database schema with migration if necessary
- Use `sqlx` for database operations with prepared statements
- Use transactions for multi-step operations
- To apply pending migrations: `sqlx migrate run` (never manually run .sql files)
- **Never use `SQLX_OFFLINE=true`** — a live database is always available for compilation
- After all code changes are done, run `./update-sqlx` to regenerate the offline query cache
## Enterprise Features
- Enterprise files use the `*_ee.rs` suffix
- Enterprise source is in `windmill-ee-private` folder (sibling directory), symlinked into each crate's `src/`
- Enterprise source is in `windmill-ee-private` folder (sibling directory at `../../windmill-ee-private`), symlinked into each crate's `src/`
- You can and should modify `windmill-ee-private` directly when needed (e.g., when creating new crates that need EE code, mirror the package structure there)
- Use feature flags: `#[cfg(feature = "enterprise")]`
- Isolate enterprise code in separate modules
## Git Workflow
- **Never push directly to main** — always create a branch and open a pull request
## Testing
- Write unit tests for core functionality

429
backend/Cargo.lock generated
View File

@@ -2322,9 +2322,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.57"
version = "4.5.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a"
checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806"
dependencies = [
"clap_builder",
"clap_derive",
@@ -2332,9 +2332,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.57"
version = "4.5.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238"
checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2"
dependencies = [
"anstream",
"anstyle",
@@ -2356,9 +2356,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.7"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "clipboard-win"
@@ -7858,9 +7858,9 @@ checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553"
[[package]]
name = "lazy-regex"
version = "3.5.1"
version = "3.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5c13b6857ade4c8ee05c3c3dc97d2ab5415d691213825b90d3211c425c1f907"
checksum = "6bae91019476d3ec7147de9aa291cadb6d870abf2f3015d2da73a90325ac1496"
dependencies = [
"lazy-regex-proc_macros",
"once_cell",
@@ -7869,9 +7869,9 @@ dependencies = [
[[package]]
name = "lazy-regex-proc_macros"
version = "3.5.1"
version = "3.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a95c68db5d41694cea563c86a4ba4dc02141c16ef64814108cb23def4d5438"
checksum = "4de9c1e1439d8b7b3061b2d209809f447ca33241733d9a3c01eabf2dc8d94358"
dependencies = [
"proc-macro2",
"quote",
@@ -14840,9 +14840,9 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typed-path"
version = "0.12.2"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3015e6ce46d5ad8751e4a772543a30c7511468070e98e64e20165f8f81155b64"
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
[[package]]
name = "typeid"
@@ -15725,7 +15725,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-nats",
@@ -15768,7 +15768,9 @@ dependencies = [
"url",
"uuid",
"windmill-api",
"windmill-api-agent-workers",
"windmill-api-client",
"windmill-api-settings",
"windmill-autoscaling",
"windmill-common",
"windmill-dep-map",
@@ -15776,14 +15778,28 @@ dependencies = [
"windmill-indexer",
"windmill-queue",
"windmill-runtime-nativets",
"windmill-test-utils",
"windmill-worker",
"windows-service",
"windows-sys 0.52.0",
]
[[package]]
name = "windmill-alerting"
version = "1.631.2"
dependencies = [
"axum 0.7.9",
"chrono",
"serde",
"serde_json",
"sqlx",
"tracing",
"windmill-common",
]
[[package]]
name = "windmill-api"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"argon2",
@@ -15867,13 +15883,26 @@ dependencies = [
"url",
"urlencoding",
"uuid",
"windmill-alerting",
"windmill-api-agent-workers",
"windmill-api-assets",
"windmill-api-auth",
"windmill-api-configs",
"windmill-api-debug",
"windmill-api-embeddings",
"windmill-api-flow-conversations",
"windmill-api-flows",
"windmill-api-groups",
"windmill-api-inputs",
"windmill-api-jobs",
"windmill-api-npm-proxy",
"windmill-api-openapi",
"windmill-api-schedule",
"windmill-api-scripts",
"windmill-api-settings",
"windmill-api-sse",
"windmill-api-users",
"windmill-api-workers",
"windmill-api-workspaces",
"windmill-audit",
"windmill-autoscaling",
@@ -15905,9 +15934,45 @@ dependencies = [
"windmill-worker",
]
[[package]]
name = "windmill-api-agent-workers"
version = "1.631.2"
dependencies = [
"axum 0.7.9",
"chrono",
"http 1.4.0",
"hyper 1.8.1",
"lazy_static",
"quick_cache",
"serde",
"serde_json",
"sqlx",
"tokio",
"tracing",
"uuid",
"windmill-api-auth",
"windmill-common",
"windmill-parser-py-imports",
"windmill-queue",
"windmill-worker",
]
[[package]]
name = "windmill-api-assets"
version = "1.631.2"
dependencies = [
"axum 0.7.9",
"chrono",
"serde",
"serde_json",
"sqlx",
"windmill-api-auth",
"windmill-common",
]
[[package]]
name = "windmill-api-auth"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -15933,7 +15998,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -15941,9 +16006,49 @@ dependencies = [
"urlencoding",
]
[[package]]
name = "windmill-api-configs"
version = "1.631.2"
dependencies = [
"axum 0.7.9",
"chrono",
"itertools 0.14.0",
"serde",
"serde_json",
"sqlx",
"windmill-api-auth",
"windmill-audit",
"windmill-autoscaling",
"windmill-common",
"windmill-worker",
]
[[package]]
name = "windmill-api-debug"
version = "1.631.2"
dependencies = [
"axum 0.7.9",
"base64 0.22.1",
"chrono",
"ed25519-dalek",
"hex",
"lazy_static",
"rand 0.9.0",
"serde",
"serde_json",
"sha2 0.10.9",
"sqlx",
"tokio",
"tracing",
"uuid",
"windmill-api-auth",
"windmill-audit",
"windmill-common",
]
[[package]]
name = "windmill-api-embeddings"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -15964,9 +16069,45 @@ dependencies = [
"windmill-store",
]
[[package]]
name = "windmill-api-flow-conversations"
version = "1.631.2"
dependencies = [
"axum 0.7.9",
"chrono",
"serde",
"sql-builder",
"sqlx",
"tokio",
"tracing",
"uuid",
"windmill-api-auth",
"windmill-common",
]
[[package]]
name = "windmill-api-flows"
version = "1.631.2"
dependencies = [
"axum 0.7.9",
"chrono",
"hyper 1.8.1",
"serde",
"serde_json",
"sql-builder",
"sqlx",
"tracing",
"windmill-api-auth",
"windmill-audit",
"windmill-common",
"windmill-dep-map",
"windmill-git-sync",
"windmill-queue",
]
[[package]]
name = "windmill-api-groups"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15984,9 +16125,47 @@ dependencies = [
"windmill-git-sync",
]
[[package]]
name = "windmill-api-inputs"
version = "1.631.2"
dependencies = [
"axum 0.7.9",
"chrono",
"serde",
"serde_json",
"sql-builder",
"sqlx",
"windmill-api-auth",
"windmill-common",
]
[[package]]
name = "windmill-api-integration-tests"
version = "1.631.2"
dependencies = [
"anyhow",
"async-nats",
"aws-config",
"aws-credential-types",
"aws-sdk-sqs",
"base64 0.22.1",
"rand 0.9.0",
"rdkafka",
"reqwest 0.13.1",
"rumqttc",
"serde",
"serde_json",
"sqlx",
"tokio",
"uuid",
"windmill-api-client",
"windmill-common",
"windmill-test-utils",
]
[[package]]
name = "windmill-api-jobs"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16009,9 +16188,66 @@ dependencies = [
"windmill-queue",
]
[[package]]
name = "windmill-api-npm-proxy"
version = "1.631.2"
dependencies = [
"axum 0.7.9",
"flate2",
"serde",
"serde_json",
"sqlx",
"tar",
"tower-http",
"tracing",
"windmill-api-auth",
"windmill-common",
]
[[package]]
name = "windmill-api-openapi"
version = "1.631.2"
dependencies = [
"anyhow",
"axum 0.7.9",
"http 1.4.0",
"indexmap 2.11.1",
"itertools 0.14.0",
"lazy_static",
"serde",
"serde_json",
"serde_yml",
"sqlx",
"url",
"windmill-api-auth",
"windmill-common",
"windmill-store",
"windmill-trigger-http",
]
[[package]]
name = "windmill-api-schedule"
version = "1.631.2"
dependencies = [
"axum 0.7.9",
"chrono",
"chrono-tz",
"serde",
"serde_json",
"sql-builder",
"sqlx",
"tracing",
"windmill-api-auth",
"windmill-api-settings",
"windmill-audit",
"windmill-common",
"windmill-git-sync",
"windmill-queue",
]
[[package]]
name = "windmill-api-scripts"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16038,9 +16274,35 @@ dependencies = [
"windmill-queue",
]
[[package]]
name = "windmill-api-settings"
version = "1.631.2"
dependencies = [
"anyhow",
"axum 0.7.9",
"base64 0.22.1",
"bytes",
"chrono",
"futures",
"lazy_static",
"object_store",
"regex",
"rsa",
"serde",
"serde_json",
"sha2 0.10.9",
"sqlx",
"tokio",
"tracing",
"uuid",
"windmill-alerting",
"windmill-api-auth",
"windmill-common",
]
[[package]]
name = "windmill-api-sse"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"lazy_static",
"serde",
@@ -16052,7 +16314,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"argon2",
"axum 0.7.9",
@@ -16073,9 +16335,23 @@ dependencies = [
"windmill-git-sync",
]
[[package]]
name = "windmill-api-workers"
version = "1.631.2"
dependencies = [
"axum 0.7.9",
"chrono",
"serde",
"serde_json",
"sqlx",
"uuid",
"windmill-api-auth",
"windmill-common",
]
[[package]]
name = "windmill-api-workspaces"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16104,7 +16380,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"chrono",
"lazy_static",
@@ -16118,7 +16394,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16137,7 +16413,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"aes-gcm",
"anyhow",
@@ -16236,7 +16512,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -16255,7 +16531,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"regex",
"serde",
@@ -16270,7 +16546,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -16294,7 +16570,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"futures",
@@ -16311,7 +16587,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -16327,7 +16603,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-trait",
@@ -16348,7 +16624,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-trait",
@@ -16379,7 +16655,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-oauth2",
@@ -16403,7 +16679,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -16412,7 +16688,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -16424,7 +16700,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"serde_json",
@@ -16436,7 +16712,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"gosyn",
@@ -16448,7 +16724,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -16460,7 +16736,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"serde_json",
@@ -16472,7 +16748,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"nu-parser",
@@ -16483,7 +16759,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16494,7 +16770,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16507,7 +16783,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-recursion",
@@ -16531,7 +16807,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -16545,7 +16821,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -16562,7 +16838,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -16576,7 +16852,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -16595,7 +16871,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"serde",
@@ -16606,7 +16882,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-recursion",
@@ -16643,7 +16919,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"const_format",
@@ -16681,7 +16957,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -16691,7 +16967,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-recursion",
@@ -16718,9 +16994,32 @@ dependencies = [
"windmill-oauth",
]
[[package]]
name = "windmill-test-utils"
version = "1.631.2"
dependencies = [
"anyhow",
"axum 0.7.9",
"chrono",
"futures",
"serde",
"serde_json",
"sqlx",
"tokio",
"tracing",
"uuid",
"windmill-api",
"windmill-api-agent-workers",
"windmill-api-client",
"windmill-common",
"windmill-queue",
"windmill-runtime-nativets",
"windmill-worker",
]
[[package]]
name = "windmill-trigger"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-trait",
@@ -16753,7 +17052,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-trait",
@@ -16773,7 +17072,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-trait",
@@ -16807,7 +17106,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-trait",
@@ -16841,7 +17140,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-trait",
@@ -16864,7 +17163,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-trait",
@@ -16888,7 +17187,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-nats",
@@ -16912,7 +17211,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-trait",
@@ -16947,7 +17246,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-trait",
@@ -16975,7 +17274,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-trait",
@@ -16998,7 +17297,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"chrono",
@@ -17015,7 +17314,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.630.1"
version = "1.631.2"
dependencies = [
"anyhow",
"async-once-cell",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.630.1"
version = "1.631.2"
authors.workspace = true
edition.workspace = true
@@ -9,6 +9,7 @@ resolver = "2"
members = [
"./windmill-api",
"./windmill-api-scripts",
"./windmill-api-flows",
"./windmill-api-users",
"./windmill-api-workspaces",
"./windmill-api-groups",
@@ -26,7 +27,19 @@ members = [
"./windmill-trigger-gcp",
"./windmill-trigger-http",
"./windmill-native-triggers",
"./windmill-alerting",
"./windmill-api-agent-workers",
"./windmill-api-assets",
"./windmill-api-configs",
"./windmill-api-debug",
"./windmill-api-embeddings",
"./windmill-api-flow-conversations",
"./windmill-api-inputs",
"./windmill-api-npm-proxy",
"./windmill-api-openapi",
"./windmill-api-schedule",
"./windmill-api-settings",
"./windmill-api-workers",
"./windmill-store",
"./windmill-queue",
"./windmill-worker",
@@ -54,12 +67,14 @@ members = [
"./parsers/windmill-parser-py",
"./parsers/windmill-parser-py-imports",
"./parsers/windmill-sql-datatype-parser-wasm",
"./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu"
"./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu",
"./windmill-test-utils",
"./windmill-api-integration-tests",
]
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.630.1"
version = "1.631.2"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -80,9 +95,9 @@ lto = "thin"
[features]
default = []
private = ["windmill-api/private", "windmill-autoscaling/private", "windmill-common/private", "windmill-git-sync/private", "windmill-indexer/private", "windmill-queue/private", "windmill-worker/private"]
agent_worker_server = ["windmill-api/agent_worker_server"]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise"]
private = ["windmill-api/private", "windmill-api-agent-workers?/private", "windmill-autoscaling/private", "windmill-common/private", "windmill-git-sync/private", "windmill-indexer/private", "windmill-queue/private", "windmill-worker/private"]
agent_worker_server = ["windmill-api/agent_worker_server", "dep:windmill-api-agent-workers"]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "windmill-api-agent-workers?/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise"]
local_reports = ["windmill-common/local_reports"]
enterprise_saml = ["windmill-api/enterprise_saml", "oauth2"]
stripe = ["windmill-api/stripe"]
@@ -114,7 +129,7 @@ native_trigger = ["windmill-api/native_trigger"]
sqs_trigger = ["windmill-api/sqs_trigger", "windmill-common/aws_auth", "windmill-api/openidconnect"]
gcp_trigger = ["windmill-api/gcp_trigger"]
smtp = ["windmill-api/smtp", "windmill-common/smtp", "windmill-queue/smtp"]
license = ["windmill-api/license"]
license = ["windmill-api/license", "windmill-api-settings/license"]
oauth2 = ["windmill-api/oauth2"]
zip = ["windmill-api/zip"]
static_frontend = ["windmill-api/static_frontend"]
@@ -160,8 +175,8 @@ oss = ["oss_core", "all_languages", "no_auth"]
ce_rpi = ["ce_core", "all_languages"]
ce = ["ce_rpi", "jemalloc", "dind", "agent_worker_server"]
# Edition meta-features: EE variants
ee = ["ce", "ee_core", "ee_server"]
ee_rhel = ["ce_core", "ee_core", "all_languages"]
ee = ["ce", "ee_core", "ee_server", "kafka-gssapi"]
ee_rhel = ["ce_core", "ee_core", "kafka-gssapi", "all_languages"]
ee_windows = ["ce_core", "ee_core", "all_languages_windows"]
all_sqlx_features = ["all_languages", "enterprise", "enterprise_saml", "embedding", "parquet", "prometheus", "flow_testing",
"openidconnect", "cloud", "jemalloc", "tantivy", "sqlx", "kafka", "kafka-gssapi", "nats", "otel", "dind", "websocket", "http_trigger",
@@ -182,6 +197,8 @@ windmill-queue.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-git-sync.workspace = true
windmill-api = { workspace = true, default-features = false }
windmill-api-agent-workers = { workspace = true, optional = true }
windmill-api-settings.workspace = true
windmill-worker.workspace = true
windmill-indexer = { workspace = true, optional = true }
windmill-autoscaling = { workspace = true, optional = true }
@@ -225,6 +242,7 @@ serde_json.workspace = true
reqwest.workspace = true
windmill-queue = { workspace = true, features = ["failpoints"] }
windmill-dep-map.workspace = true
windmill-test-utils.workspace = true
axum.workspace = true
serde.workspace = true
windmill-api-client.workspace = true
@@ -253,6 +271,7 @@ windmill-oauth = {path = "./windmill-oauth"}
windmill-macros = {path = "./windmill-macros"}
windmill-api-auth = { path = "./windmill-api-auth" }
windmill-api-scripts = { path = "./windmill-api-scripts" }
windmill-api-flows = { path = "./windmill-api-flows" }
windmill-api-users = { path = "./windmill-api-users" }
windmill-api-workspaces = { path = "./windmill-api-workspaces" }
windmill-api-groups = { path = "./windmill-api-groups" }
@@ -269,7 +288,19 @@ windmill-trigger-sqs = { path = "./windmill-trigger-sqs" }
windmill-trigger-gcp = { path = "./windmill-trigger-gcp" }
windmill-trigger-http = { path = "./windmill-trigger-http" }
windmill-native-triggers = { path = "./windmill-native-triggers" }
windmill-alerting = { path = "./windmill-alerting" }
windmill-api-agent-workers = { path = "./windmill-api-agent-workers" }
windmill-api-assets = { path = "./windmill-api-assets" }
windmill-api-configs = { path = "./windmill-api-configs" }
windmill-api-debug = { path = "./windmill-api-debug" }
windmill-api-embeddings = { path = "./windmill-api-embeddings" }
windmill-api-flow-conversations = { path = "./windmill-api-flow-conversations" }
windmill-api-inputs = { path = "./windmill-api-inputs" }
windmill-api-npm-proxy = { path = "./windmill-api-npm-proxy" }
windmill-api-openapi = { path = "./windmill-api-openapi" }
windmill-api-schedule = { path = "./windmill-api-schedule" }
windmill-api-settings = { path = "./windmill-api-settings" }
windmill-api-workers = { path = "./windmill-api-workers" }
windmill-store = { path = "./windmill-store" }
windmill-parser = { path = "./parsers/windmill-parser" }
windmill-parser-ts = { path = "./parsers/windmill-parser-ts" }
@@ -289,6 +320,7 @@ windmill-parser-php = { path = "./parsers/windmill-parser-php" }
windmill-jseval = { path = "./windmill-jseval" }
windmill-runtime-nativets = { path = "./windmill-runtime-nativets" }
windmill-api-client = { path = "./windmill-api-client" }
windmill-test-utils = { path = "./windmill-test-utils" }
reqwest-retry = "^0"
reqwest-middleware = { version = "^0", features = ["json"] }

View File

@@ -1 +1 @@
de9ff0d258f5a1a3739d9693531bad3972ddfd22
ab082a1a65830bf362a32b30fa86741999da091d

View File

@@ -46,8 +46,8 @@ use windmill_common::{
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING,
MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NO_DEFAULT_MAVEN_SETTING,
NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING,
OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING,
POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, UV_INDEX_STRATEGY_SETTING,
POWERSHELL_REPO_PAT_SETTING, POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
RUBY_REPOS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TEAMS_SETTING,
TIMEOUT_WAIT_RESULT_SETTING,
@@ -103,7 +103,7 @@ use crate::monitor::{
reload_jwt_secret_setting, reload_license_key, reload_npm_config_registry_setting,
reload_otel_tracing_proxy_setting, reload_pip_index_url_setting,
reload_retention_period_setting, reload_scim_token_setting, reload_smtp_config,
reload_worker_config, MonitorIteration,
reload_uv_index_strategy_setting, reload_worker_config, MonitorIteration,
};
#[cfg(feature = "parquet")]
@@ -735,8 +735,12 @@ async fn windmill_main() -> anyhow::Result<()> {
.unwrap_or(false);
if !skip_migration {
// migration code to avoid break
migration_handle = windmill_api::migrate_db(&db, killpill_rx.resubscribe()).await?;
if mode == Mode::Worker {
windmill_api::wait_for_db_migrations(&db, killpill_rx.resubscribe()).await?;
} else {
migration_handle =
windmill_api::migrate_db(&db, killpill_rx.resubscribe()).await?;
}
} else {
tracing::info!("SKIP_MIGRATION set, skipping db migration...")
}
@@ -1560,6 +1564,7 @@ async fn process_notify_event(
SCIM_TOKEN_SETTING => reload_scim_token_setting(conn).await,
EXTRA_PIP_INDEX_URL_SETTING => reload_extra_pip_index_url_setting(conn).await,
PIP_INDEX_URL_SETTING => reload_pip_index_url_setting(conn).await,
UV_INDEX_STRATEGY_SETTING => reload_uv_index_strategy_setting(conn).await,
INSTANCE_PYTHON_VERSION_SETTING => {
reload_instance_python_version_setting(conn).await
}

View File

@@ -57,6 +57,7 @@ use windmill_common::{
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING,
LICENSE_KEY_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPM_CONFIG_REGISTRY_SETTING,
NUGET_CONFIG_SETTING, OTEL_SETTING, OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING,
UV_INDEX_STRATEGY_SETTING,
POWERSHELL_REPO_PAT_SETTING, POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
@@ -88,7 +89,7 @@ use windmill_worker::{
SameWorkerSender, BUNFIG_INSTALL_SCOPES, INSTANCE_PYTHON_VERSION, JOB_DEFAULT_TIMEOUT,
KEEP_JOB_DIR, MAVEN_REPOS, NO_DEFAULT_MAVEN, NPM_CONFIG_REGISTRY, NUGET_CONFIG,
OTEL_TRACING_PROXY_SETTINGS, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, POWERSHELL_REPO_PAT,
POWERSHELL_REPO_URL,
POWERSHELL_REPO_URL, UV_INDEX_STRATEGY,
};
#[cfg(feature = "parquet")]
@@ -317,6 +318,7 @@ pub async fn initial_load(
reload_job_default_timeout_setting(&conn).await;
reload_extra_pip_index_url_setting(&conn).await;
reload_pip_index_url_setting(&conn).await;
reload_uv_index_strategy_setting(&conn).await;
reload_npm_config_registry_setting(&conn).await;
reload_bunfig_install_scopes_setting(&conn).await;
reload_instance_python_version_setting(&conn).await;
@@ -1250,6 +1252,16 @@ pub async fn reload_pip_index_url_setting(conn: &Connection) {
.await;
}
pub async fn reload_uv_index_strategy_setting(conn: &Connection) {
reload_option_setting_with_tracing(
conn,
UV_INDEX_STRATEGY_SETTING,
"UV_INDEX_STRATEGY",
UV_INDEX_STRATEGY.clone(),
)
.await;
}
pub async fn reload_instance_python_version_setting(conn: &Connection) {
reload_option_setting_with_tracing(
conn,

View File

@@ -0,0 +1,267 @@
#![cfg(all(feature = "private", feature = "agent_worker_server"))]
use windmill_test_utils::*;
use serde_json::json;
use sqlx::{Pool, Postgres};
use windmill_common::{
jobs::{JobPayload, RawCode},
scripts::ScriptLang,
};
fn bun_code(code: &str) -> RawCode {
RawCode {
hash: None,
content: code.to_string(),
path: None,
language: ScriptLang::Bun,
lock: None,
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
}
}
#[sqlx::test(fixtures("base"))]
async fn test_agent_worker_simple_script(db: Pool<Postgres>) -> anyhow::Result<()> {
let (_client, port, _server) = init_client_agent_mode(db.clone()).await;
let result = RunJob::from(JobPayload::Code(bun_code(
"export function main() { return 42; }",
)))
.run_until_complete(&db, false, port)
.await;
assert!(result.success, "job should succeed");
assert_eq!(result.json_result(), Some(json!(42)));
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_agent_worker_script_with_args(db: Pool<Postgres>) -> anyhow::Result<()> {
let (_client, port, _server) = init_client_agent_mode(db.clone()).await;
let result = RunJob::from(JobPayload::Code(bun_code(
"export function main(x: number, y: number) { return x + y; }",
)))
.arg("x", json!(10))
.arg("y", json!(32))
.run_until_complete(&db, false, port)
.await;
assert!(result.success, "job should succeed");
assert_eq!(result.json_result(), Some(json!(42)));
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_agent_worker_script_with_logs(db: Pool<Postgres>) -> anyhow::Result<()> {
let (_client, port, _server) = init_client_agent_mode(db.clone()).await;
let result = RunJob::from(JobPayload::Code(bun_code(
r#"export function main() {
console.log("hello from agent worker");
console.log("processing step 1");
console.log("processing step 2");
return "done";
}"#,
)))
.run_until_complete(&db, false, port)
.await;
assert!(result.success, "job should succeed");
assert_eq!(result.json_result(), Some(json!("done")));
let logs = sqlx::query_scalar::<_, String>(
"SELECT logs FROM job_logs WHERE job_id = $1 AND workspace_id = 'test-workspace'",
)
.bind(result.id)
.fetch_optional(&db)
.await?;
let logs = logs.expect("logs should exist");
assert!(
logs.contains("hello from agent worker"),
"logs should contain the printed output, got: {logs}"
);
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_agent_worker_script_failure(db: Pool<Postgres>) -> anyhow::Result<()> {
let (_client, port, _server) = init_client_agent_mode(db.clone()).await;
let result = RunJob::from(JobPayload::Code(bun_code(
"export function main() { throw new Error('test error'); }",
)))
.run_until_complete(&db, false, port)
.await;
assert!(!result.success, "job should fail");
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_agent_worker_complex_result(db: Pool<Postgres>) -> anyhow::Result<()> {
let (_client, port, _server) = init_client_agent_mode(db.clone()).await;
let result = RunJob::from(JobPayload::Code(bun_code(
r#"export function main() {
return {
items: [1, 2, 3],
metadata: { key: "value" },
count: 3,
};
}"#,
)))
.run_until_complete(&db, false, port)
.await;
assert!(result.success, "job should succeed");
let json = result.json_result().unwrap();
assert_eq!(json["items"], json!([1, 2, 3]));
assert_eq!(json["metadata"]["key"], json!("value"));
assert_eq!(json["count"], json!(3));
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_agent_worker_token_creation(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, _port, _server) = init_client_agent_mode(db.clone()).await;
// client.baseurl() already includes /api
let resp = client
.client()
.post(format!(
"{}/agent_workers/create_agent_token",
client.baseurl()
))
.json(&json!({
"worker_group": "lifecycle-test",
"tags": ["bun", "flow", "dependency"],
"exp": usize::MAX
}))
.send()
.await?;
assert!(
resp.status().is_success(),
"create_agent_token should succeed, got: {}",
resp.status()
);
let token = resp.text().await?;
let token = token.trim_matches('"');
assert!(
token.starts_with("jwt_agent_"),
"token should start with jwt_agent_ prefix, got: {token}"
);
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_agent_worker_token_and_ping(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, port, _server) = init_client_agent_mode(db.clone()).await;
let resp = client
.client()
.post(format!(
"{}/agent_workers/create_agent_token",
client.baseurl()
))
.json(&json!({
"worker_group": "lifecycle-test",
"tags": ["bun", "flow", "dependency"],
"exp": usize::MAX
}))
.send()
.await?;
assert!(resp.status().is_success());
let token = resp.text().await?;
let token = token.trim_matches('"');
let suffix = windmill_common::utils::create_default_worker_suffix("lifecycle-test");
let base_url = format!("http://localhost:{port}");
let http_client =
windmill_common::agent_workers::build_agent_http_client(&suffix, &token, &base_url);
// Initial ping inserts the worker record into the database
let resp = http_client
.client
.post(format!("{}/api/agent_workers/update_ping", base_url))
.json(&json!({
"worker_instance": "test-instance",
"ip": "127.0.0.1",
"tags": ["bun"],
"version": "test",
"vcpus": 4,
"memory": 8192,
"ping_type": "Initial"
}))
.send()
.await?;
assert!(
resp.status().is_success(),
"initial ping should succeed, got: {}",
resp.status()
);
// Verify the ping was recorded in the database
let worker_count = sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM worker_ping WHERE worker_instance = 'test-instance'",
)
.fetch_one(&db)
.await?;
assert!(worker_count > 0, "worker ping should be recorded in database");
// MainLoop ping updates the existing record
let resp = http_client
.client
.post(format!("{}/api/agent_workers/update_ping", base_url))
.json(&json!({
"tags": ["bun"],
"vcpus": 4,
"memory": 8192,
"jobs_executed": 0,
"ping_type": "MainLoop"
}))
.send()
.await?;
assert!(
resp.status().is_success(),
"main loop ping should succeed, got: {}",
resp.status()
);
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_agent_worker_multiple_jobs_sequential(db: Pool<Postgres>) -> anyhow::Result<()> {
let (_client, port, _server) = init_client_agent_mode(db.clone()).await;
for i in 0..3 {
let result = RunJob::from(JobPayload::Code(bun_code(&format!(
"export function main() {{ return {i}; }}"
))))
.run_until_complete(&db, false, port)
.await;
assert!(result.success, "job {i} should succeed");
assert_eq!(result.json_result(), Some(json!(i)));
}
Ok(())
}

View File

@@ -1,5 +1,4 @@
mod common;
use crate::common::*;
use windmill_test_utils::*;
use sqlx::postgres::Postgres;
use sqlx::Pool;
use windmill_common::jobs::{JobPayload, RawCode};
@@ -860,7 +859,7 @@ export function main() {
// ============================================================================
mod dedicated_worker_protocol {
use crate::common::{parse_dedicated_worker_line, DedicatedWorkerResult};
use windmill_test_utils::{parse_dedicated_worker_line, DedicatedWorkerResult};
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
use windmill_worker::{

View File

@@ -2,8 +2,9 @@ use sqlx::{Pool, Postgres};
use tokio_stream::StreamExt;
use windmill_api_client::types::NewScript;
mod common;
use common::{in_test_worker, init_client, listen_for_completed_jobs, ApiServer};
use windmill_test_utils::{
in_test_worker, init_client, listen_for_completed_jobs, rebuild_dmap, ApiServer,
};
mod dependency_map {
use super::*;
@@ -170,7 +171,7 @@ mod dependency_map {
let (client, _port, _s) = init(db.clone()).await;
assert_dmap(&db, None, CORRECT_DMAP.clone()).await;
// rebuild map
assert!(common::rebuild_dmap(&client).await);
assert!(rebuild_dmap(&client).await);
assert_dmap(&db, None, CORRECT_DMAP.clone()).await;
Ok(())
}
@@ -183,7 +184,7 @@ mod dependency_map {
// Spawn first rebuild
let handle = {
let client = client.clone();
tokio::spawn(async move { common::rebuild_dmap(&client).await })
tokio::spawn(async move { rebuild_dmap(&client).await })
};
// Immidiately spawn another

View File

@@ -0,0 +1,410 @@
use sqlx::{Pool, Postgres};
use windmill_test_utils::*;
/// Test that workspace error handler can be set and removed via database operations
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_settings(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let _server = ApiServer::start(db.clone()).await?;
// Initially error_handler should be NULL
let initial = sqlx::query_scalar!(
r#"SELECT error_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert!(initial.is_none());
// Set error handler with all options
sqlx::query!(
r#"
UPDATE workspace_settings
SET error_handler = '{"path": "script/f/test/error_handler", "extra_args": {"notify": true}, "muted_on_cancel": true, "muted_on_user_path": false}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
let after_set = sqlx::query_scalar!(
r#"SELECT error_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert_eq!(
after_set,
Some("script/f/test/error_handler".to_string())
);
// Verify extra_args
let extra_args = sqlx::query_scalar!(
r#"SELECT error_handler->'extra_args' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert!(extra_args.is_some());
// Verify muted_on_cancel
let muted_on_cancel = sqlx::query_scalar!(
r#"SELECT (error_handler->>'muted_on_cancel')::boolean FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert_eq!(muted_on_cancel, Some(true));
// Verify muted_on_user_path
let muted_on_user_path = sqlx::query_scalar!(
r#"SELECT (error_handler->>'muted_on_user_path')::boolean FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert_eq!(muted_on_user_path, Some(false));
// Remove error handler
sqlx::query!(
r#"
UPDATE workspace_settings
SET error_handler = NULL
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
let after_remove = sqlx::query_scalar!(
r#"SELECT error_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert!(after_remove.is_none());
Ok(())
}
/// Test that a failed job triggers the workspace error handler
#[cfg(all(feature = "deno_core", feature = "enterprise", feature = "private"))]
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_triggered_on_failure(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::jobs::JobPayload;
use windmill_common::runnable_settings::{ConcurrencySettings, DebouncingSettings};
use windmill_common::scripts::{ScriptHash, ScriptLang};
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
// Create the error handler script
let error_handler_code = r#"
export async function main(path: string, email: string, job_id: string, is_flow: boolean, workspace_id: string, error: any) {
console.log("Error handler called for job:", job_id);
return { handled: true, original_path: path };
}
"#;
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', 1111111111, 'f/test/error_handler', $1, 'deno', 'script', 'test-user', '{}', 'Error handler script', 'Handles failed job completions', '')
"#,
error_handler_code
)
.execute(&db)
.await?;
// Create a script that will fail
let failing_script_code = "export function main() { throw new Error('intentional failure'); }";
let failing_script_hash: i64 = 2222222222;
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', $1, 'f/test/failing_script', $2, 'deno', 'script', 'test-user', '{}', 'Failing test script', 'A script that always fails', '')
"#,
failing_script_hash,
failing_script_code
)
.execute(&db)
.await?;
// Set up the error handler in workspace_settings
sqlx::query!(
r#"
UPDATE workspace_settings
SET error_handler = '{"path": "script/f/test/error_handler"}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
// Create the error_handler group
sqlx::query!(
r#"
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
VALUES ('test-workspace', 'error_handler', 'The group the error handler acts on behalf of', '{"u/test-user": true}')
ON CONFLICT DO NOTHING
"#
)
.execute(&db)
.await?;
// Run the failing script
let completed_job = RunJob::from(JobPayload::ScriptHash {
hash: ScriptHash(failing_script_hash),
path: "f/test/failing_script".to_string(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
language: ScriptLang::Deno,
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(),
})
.run_until_complete(&db, false, server.addr.port())
.await;
// Verify the job actually failed
assert!(!completed_job.success, "Job should have failed");
let main_job_id = completed_job.id;
// Wait for the error handler job to be created
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// Verify the error handler job was created
let error_handler_job = sqlx::query!(
r#"
SELECT
id,
runnable_path,
permissioned_as_email,
parent_job
FROM v2_job
WHERE workspace_id = 'test-workspace'
AND permissioned_as_email = 'error_handler@windmill.dev'
ORDER BY created_at DESC
LIMIT 1
"#
)
.fetch_optional(&db)
.await?;
assert!(
error_handler_job.is_some(),
"Error handler job should have been created"
);
let handler_job = error_handler_job.unwrap();
assert_eq!(
handler_job.runnable_path.as_deref(),
Some("f/test/error_handler"),
"Error handler should run the configured script"
);
assert_eq!(
handler_job.permissioned_as_email.as_str(),
"error_handler@windmill.dev",
"Error handler should run as error_handler user"
);
assert_eq!(
handler_job.parent_job,
Some(main_job_id),
"Error handler should have the failed job as parent"
);
Ok(())
}
/// Test that error handler is NOT triggered when ws_error_handler_muted is set on the script
#[cfg(all(feature = "deno_core", feature = "enterprise", feature = "private"))]
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_muted_on_script(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::jobs::JobPayload;
use windmill_common::runnable_settings::{ConcurrencySettings, DebouncingSettings};
use windmill_common::scripts::{ScriptHash, ScriptLang};
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
// Create the error handler script
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', 3333333333, 'f/test/error_handler', 'export function main() { return "handled"; }', 'deno', 'script', 'test-user', '{}', '', '', '')
"#,
)
.execute(&db)
.await?;
// Create a failing script with ws_error_handler_muted = true
let failing_script_hash: i64 = 4444444444;
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, ws_error_handler_muted)
VALUES ('test-workspace', $1, 'f/test/muted_failing_script', 'export function main() { throw new Error("fail"); }', 'deno', 'script', 'test-user', '{}', '', '', '', true)
"#,
failing_script_hash,
)
.execute(&db)
.await?;
// Set up the error handler
sqlx::query!(
r#"
UPDATE workspace_settings
SET error_handler = '{"path": "script/f/test/error_handler"}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
sqlx::query!(
r#"
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
VALUES ('test-workspace', 'error_handler', 'Error handler group', '{"u/test-user": true}')
ON CONFLICT DO NOTHING
"#
)
.execute(&db)
.await?;
// Run the muted failing script
let completed_job = RunJob::from(JobPayload::ScriptHash {
hash: ScriptHash(failing_script_hash),
path: "f/test/muted_failing_script".to_string(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
language: ScriptLang::Deno,
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(),
})
.run_until_complete(&db, false, server.addr.port())
.await;
assert!(!completed_job.success, "Job should have failed");
// Wait and check that NO error handler job was created
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let error_handler_job = sqlx::query_scalar!(
r#"
SELECT id
FROM v2_job
WHERE workspace_id = 'test-workspace'
AND permissioned_as_email = 'error_handler@windmill.dev'
"#
)
.fetch_optional(&db)
.await?;
assert!(
error_handler_job.is_none(),
"Error handler should NOT have been triggered for a muted script"
);
Ok(())
}
/// Test that error handler is NOT triggered on successful job completion
#[cfg(all(feature = "deno_core", feature = "enterprise", feature = "private"))]
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_not_triggered_on_success(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::jobs::JobPayload;
use windmill_common::runnable_settings::{ConcurrencySettings, DebouncingSettings};
use windmill_common::scripts::{ScriptHash, ScriptLang};
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
// Create the error handler script
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', 5555555555, 'f/test/error_handler', 'export function main() { return "handled"; }', 'deno', 'script', 'test-user', '{}', '', '', '')
"#,
)
.execute(&db)
.await?;
// Create a successful script
let success_script_hash: i64 = 6666666666;
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', $1, 'f/test/success_script', 'export function main() { return "ok"; }', 'deno', 'script', 'test-user', '{}', '', '', '')
"#,
success_script_hash,
)
.execute(&db)
.await?;
// Set up the error handler
sqlx::query!(
r#"
UPDATE workspace_settings
SET error_handler = '{"path": "script/f/test/error_handler"}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
sqlx::query!(
r#"
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
VALUES ('test-workspace', 'error_handler', 'Error handler group', '{"u/test-user": true}')
ON CONFLICT DO NOTHING
"#
)
.execute(&db)
.await?;
// Run the successful script
let completed_job = RunJob::from(JobPayload::ScriptHash {
hash: ScriptHash(success_script_hash),
path: "f/test/success_script".to_string(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
language: ScriptLang::Deno,
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(),
})
.run_until_complete(&db, false, server.addr.port())
.await;
assert!(completed_job.success, "Job should have succeeded");
// Wait and check that NO error handler job was created
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let error_handler_job = sqlx::query_scalar!(
r#"
SELECT id
FROM v2_job
WHERE workspace_id = 'test-workspace'
AND permissioned_as_email = 'error_handler@windmill.dev'
"#
)
.fetch_optional(&db)
.await?;
assert!(
error_handler_job.is_none(),
"Error handler should NOT have been triggered for a successful job"
);
Ok(())
}

View File

@@ -25,8 +25,7 @@ use windmill_common::{
scripts::ScriptLang,
};
mod common;
use common::*;
use windmill_test_utils::*;
/// Helper to create a FlowModule with default fields
fn flow_module(id: &str, value: FlowModuleValue) -> FlowModule {

View File

@@ -1,5 +1,3 @@
mod common;
mod job_payload {
use serde_json::json;
use sqlx::{Pool, Postgres};
@@ -8,7 +6,7 @@ mod job_payload {
use windmill_common::scripts::{ScriptHash, ScriptLang};
use windmill_common::flow_status::RestartedFrom;
use crate::common::*;
use windmill_test_utils::*;
use windmill_common::min_version::{
MIN_VERSION, MIN_VERSION_IS_AT_LEAST_1_427, MIN_VERSION_IS_AT_LEAST_1_432,
MIN_VERSION_IS_AT_LEAST_1_440,

View File

@@ -7,8 +7,7 @@ use windmill_common::{
scripts::ScriptLang,
};
mod common;
use common::*;
use windmill_test_utils::*;
#[derive(Debug, Deserialize)]
struct ListJobsResponse {

View File

@@ -14,10 +14,7 @@
*/
#[cfg(feature = "deno_core")]
mod common;
#[cfg(feature = "deno_core")]
use common::*;
use windmill_test_utils::*;
#[cfg(feature = "deno_core")]
use futures::StreamExt;

View File

@@ -11,10 +11,7 @@
*/
#[cfg(feature = "deno_core")]
mod common;
#[cfg(feature = "deno_core")]
use common::*;
use windmill_test_utils::*;
#[cfg(feature = "deno_core")]
use std::time::Instant;

View File

@@ -1,5 +1,4 @@
mod common;
use crate::common::*;
use windmill_test_utils::*;
use sqlx::postgres::Postgres;
use sqlx::Pool;
use windmill_common::scripts::ScriptLang;

View File

@@ -1,8 +1,6 @@
mod common;
#[cfg(feature = "deno_core")]
mod retry {
use crate::common::*;
use windmill_test_utils::*;
use serde_json::json;
use sqlx::{Pool, Postgres};
use tokio::io::{AsyncReadExt, AsyncWriteExt};

View File

@@ -1,7 +1,6 @@
use sqlx::{Pool, Postgres};
mod common;
use common::*;
use windmill_test_utils::*;
/// Test that the workspace success handler cache works correctly with 60s TTL
#[cfg(feature = "deno_core")]

View File

@@ -1,11 +1,9 @@
mod common;
mod suspend_resume {
#[cfg(feature = "deno_core")]
use serde_json::json;
#[cfg(feature = "deno_core")]
use crate::common::*;
use windmill_test_utils::*;
#[cfg(feature = "deno_core")]
use futures::{Stream, StreamExt};

View File

@@ -25,8 +25,7 @@ use windmill_common::{
jobs::{JobPayload, RawCode},
scripts::ScriptLang,
};
mod common;
use common::*;
use windmill_test_utils::*;
#[cfg(feature = "enterprise")]
use futures::StreamExt;

View File

@@ -1,9 +1,8 @@
mod common;
mod workspace_dependencies {
use crate::common::in_test_worker;
use crate::common::init_client;
use crate::common::listen_for_completed_jobs;
use windmill_test_utils::in_test_worker;
use windmill_test_utils::init_client;
use windmill_test_utils::listen_for_completed_jobs;
use sqlx::{Pool, Postgres};
use tokio_stream::StreamExt;
use windmill_common::scripts::ScriptLang;

View File

@@ -0,0 +1,18 @@
[package]
name = "windmill-alerting"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_alerting"
path = "src/lib.rs"
[dependencies]
windmill-common = { workspace = true, default-features = false }
axum.workspace = true
chrono.workspace = true
serde.workspace = true
serde_json.workspace = true
sqlx.workspace = true
tracing.workspace = true

View File

@@ -0,0 +1,208 @@
use axum::Json;
use serde::Deserialize;
use windmill_common::error::{self, JsonResult};
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::DB;
#[derive(serde::Serialize)]
pub struct CriticalAlert {
id: i32,
alert_type: String,
message: String,
created_at: chrono::DateTime<chrono::Utc>,
acknowledged: Option<bool>,
workspace_id: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct AlertQueryParams {
pub page: Option<i32>,
pub page_size: Option<i32>,
pub acknowledged: Option<bool>,
}
pub async fn get_critical_alerts(
db: DB,
params: AlertQueryParams,
workspace_id: Option<String>,
) -> JsonResult<serde_json::Value> {
let page = params.page.unwrap_or(1).max(1);
let page_size = params.page_size.unwrap_or(10).min(100) as i64;
let offset = ((page - 1) * page_size as i32) as i64;
let total_rows = if let Some(workspace_id) = &workspace_id {
if params.acknowledged.is_none() {
sqlx::query_scalar!(
"SELECT COUNT(*)
FROM alerts
WHERE workspace_id = $1",
workspace_id
)
.fetch_one(&db)
.await?
} else {
sqlx::query_scalar!(
"SELECT COUNT(*)
FROM alerts
WHERE workspace_id = $1 AND COALESCE(acknowledged_workspace, false) = $2",
workspace_id,
params.acknowledged
)
.fetch_one(&db)
.await?
}
} else {
if params.acknowledged.is_none() {
sqlx::query_scalar!(
"SELECT COUNT(*)
FROM alerts"
)
.fetch_one(&db)
.await?
} else {
sqlx::query_scalar!(
"SELECT COUNT(*)
FROM alerts
WHERE COALESCE(acknowledged, false) = $1",
params.acknowledged
)
.fetch_one(&db)
.await?
}
};
let alerts = if let Some(workspace_id) = workspace_id {
if params.acknowledged.is_none() {
sqlx::query_as!(
CriticalAlert,
"SELECT id, alert_type, message, created_at, COALESCE(acknowledged_workspace, false) AS acknowledged, workspace_id
FROM alerts
WHERE workspace_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3",
workspace_id,
page_size,
offset
)
.fetch_all(&db)
.await?
} else {
sqlx::query_as!(
CriticalAlert,
"SELECT id, alert_type, message, created_at, COALESCE(acknowledged_workspace, false) AS acknowledged, workspace_id
FROM alerts
WHERE workspace_id = $1 AND COALESCE(acknowledged_workspace, false) = $2
ORDER BY created_at DESC
LIMIT $3 OFFSET $4",
workspace_id,
params.acknowledged,
page_size,
offset
)
.fetch_all(&db)
.await?
}
} else {
if params.acknowledged.is_none() {
sqlx::query_as!(
CriticalAlert,
"SELECT id, alert_type, message, created_at, COALESCE(acknowledged, false) AS acknowledged, workspace_id
FROM alerts
ORDER BY created_at DESC
LIMIT $1 OFFSET $2",
page_size,
offset
)
.fetch_all(&db)
.await?
} else {
sqlx::query_as!(
CriticalAlert,
"SELECT id, alert_type, message, created_at, COALESCE(acknowledged, false) AS acknowledged, workspace_id
FROM alerts
WHERE COALESCE(acknowledged, false) = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3",
params.acknowledged,
page_size,
offset
)
.fetch_all(&db)
.await?
}
};
let total_rows = total_rows.unwrap_or(0);
let total_pages = ((total_rows as f64) / (page_size as f64)).ceil() as i64;
Ok(Json(serde_json::json!({
"alerts": alerts,
"total_rows": total_rows,
"total_pages": total_pages
})))
}
pub async fn acknowledge_critical_alert(
db: DB,
workspace_id: Option<String>,
id: i32,
) -> error::Result<String> {
sqlx::query!(
"UPDATE alerts
SET
acknowledged = true,
acknowledged_workspace = CASE
WHEN $3 THEN
CASE
WHEN $2::text IS NOT NULL AND workspace_id = $2 THEN true
ELSE acknowledged_workspace
END
ELSE true
END
WHERE id = $1",
id,
workspace_id,
*CLOUD_HOSTED
)
.execute(&db)
.await?;
tracing::info!(
"Acknowledged critical alert with id: {}{}",
id,
workspace_id.map_or_else(|| "".to_string(), |w| format!(" for workspace_id: {}", w))
);
Ok("Critical alert acknowledged".to_string())
}
pub async fn acknowledge_all_critical_alerts(
db: DB,
workspace_id: Option<String>,
) -> error::Result<String> {
sqlx::query!(
"UPDATE alerts
SET
acknowledged = true,
acknowledged_workspace = CASE
WHEN $2 THEN
CASE
WHEN $1::text IS NOT NULL THEN true
ELSE acknowledged_workspace
END
ELSE true
END
WHERE ($1::text IS NOT NULL AND workspace_id = $1)
OR ($1::text IS NULL)",
workspace_id,
*CLOUD_HOSTED
)
.execute(&db)
.await?;
tracing::info!(
"Acknowledged all unacknowledged critical alerts{}",
workspace_id.map_or_else(|| "".to_string(), |w| format!(" for workspace_id: {}", w))
);
Ok("All unacknowledged critical alerts acknowledged".to_string())
}

View File

@@ -0,0 +1,35 @@
[package]
name = "windmill-api-agent-workers"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_api_agent_workers"
path = "src/lib.rs"
[features]
default = []
enterprise = ["windmill-common/enterprise", "windmill-queue/enterprise"]
private = ["windmill-common/private", "windmill-queue/private"]
python = ["dep:windmill-parser-py-imports"]
benchmark = []
[dependencies]
windmill-api-auth.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-queue.workspace = true
windmill-worker.workspace = true
axum.workspace = true
lazy_static.workspace = true
chrono.workspace = true
http.workspace = true
hyper.workspace = true
quick_cache.workspace = true
serde.workspace = true
serde_json.workspace = true
sqlx.workspace = true
tokio.workspace = true
tracing.workspace = true
uuid.workspace = true
windmill-parser-py-imports = { workspace = true, optional = true }

View File

@@ -1,6 +1,8 @@
#[cfg(feature = "private")]
mod ee;
#[cfg(feature = "private")]
#[allow(unused)]
pub use crate::agent_workers_ee::*;
pub use ee::*;
/*
* Author: Ruben Fiszel
@@ -11,7 +13,7 @@ pub use crate::agent_workers_ee::*;
*/
#[cfg(not(feature = "private"))]
use crate::db::DB;
use windmill_common::DB;
#[cfg(not(feature = "private"))]
use axum::Router;

View File

@@ -0,0 +1,18 @@
[package]
name = "windmill-api-assets"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_api_assets"
path = "src/lib.rs"
[dependencies]
windmill-api-auth.workspace = true
windmill-common = { workspace = true, default-features = false }
axum.workspace = true
chrono.workspace = true
serde.workspace = true
serde_json.workspace = true
sqlx.workspace = true

View File

@@ -12,7 +12,7 @@ use windmill_common::{
error::JsonResult,
};
use crate::db::ApiAuthed;
use windmill_api_auth::ApiAuthed;
pub fn workspaced_service() -> Router {
Router::new()

View File

@@ -0,0 +1,29 @@
[package]
name = "windmill-api-configs"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_api_configs"
path = "src/lib.rs"
[features]
default = []
enterprise = ["dep:windmill-autoscaling"]
private = []
python = []
inline_preview = ["dep:windmill-worker", "dep:itertools"]
[dependencies]
windmill-api-auth.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-audit.workspace = true
windmill-autoscaling = { workspace = true, optional = true }
windmill-worker = { workspace = true, optional = true }
axum.workspace = true
chrono.workspace = true
itertools = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
sqlx.workspace = true

View File

@@ -23,7 +23,7 @@ use windmill_common::{
DB,
};
use crate::{db::ApiAuthed, utils::require_devops_role};
use windmill_api_auth::{ApiAuthed, require_devops_role};
pub fn global_service() -> Router {
Router::new()

View File

@@ -0,0 +1,28 @@
[package]
name = "windmill-api-debug"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_api_debug"
path = "src/lib.rs"
[dependencies]
windmill-api-auth.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-audit.workspace = true
axum.workspace = true
base64.workspace = true
chrono.workspace = true
ed25519-dalek.workspace = true
hex.workspace = true
lazy_static.workspace = true
rand.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
sqlx.workspace = true
tokio.workspace = true
tracing.workspace = true
uuid.workspace = true

View File

@@ -41,7 +41,7 @@ use windmill_common::{
users::username_to_permissioned_as,
};
use crate::db::ApiAuthed;
use windmill_api_auth::ApiAuthed;
/// TTL for debug tokens in seconds (60 seconds)
pub const DEBUG_TOKEN_TTL_SECS: i64 = 60;

View File

@@ -6,6 +6,8 @@ use std::{collections::HashMap, path::PathBuf, sync::Arc};
use windmill_common::DEFAULT_HUB_BASE_URL;
#[cfg(feature = "embedding")]
use windmill_common::HUB_BASE_URL;
#[cfg(feature = "embedding")]
use windmill_common::utils::HTTP_CLIENT_PERMISSIVE as HTTP_CLIENT;
use axum::Router;
@@ -54,13 +56,6 @@ lazy_static::lazy_static! {
pub static ref EMBEDDINGS_DB: Arc<RwLock<Option<EmbeddingsDb>>> = Arc::new(RwLock::new(None));
pub static ref MODEL_INSTANCE: Arc<RwLock<Option<Arc<ModelInstance>>>> = Arc::new(RwLock::new(None));
pub static ref HUB_EMBEDDINGS_PULLING_INTERVAL_SECS: u64 = std::env::var("HUB_EMBEDDINGS_PULLING_INTERVAL_SECS").ok().map(|x| x.parse::<u64>().ok()).flatten().unwrap_or(3600 * 24);
// Match windmill-api's HTTP_CLIENT config: 30s timeout, ACCEPT_INVALID_CERTS support
static ref HTTP_CLIENT: reqwest::Client = windmill_common::utils::configure_client(reqwest::ClientBuilder::new()
.user_agent("windmill/beta")
.connect_timeout(std::time::Duration::from_secs(10))
.timeout(std::time::Duration::from_secs(30))
.danger_accept_invalid_certs(std::env::var("ACCEPT_INVALID_CERTS").is_ok()))
.build().unwrap();
}
#[cfg(feature = "embedding")]

View File

@@ -0,0 +1,21 @@
[package]
name = "windmill-api-flow-conversations"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_api_flow_conversations"
path = "src/lib.rs"
[dependencies]
windmill-api-auth.workspace = true
windmill-common = { workspace = true, default-features = false }
axum.workspace = true
chrono.workspace = true
serde.workspace = true
sql-builder.workspace = true
sqlx.workspace = true
tokio.workspace = true
tracing.workspace = true
uuid.workspace = true

View File

@@ -9,7 +9,7 @@ use sql_builder::prelude::*;
use sqlx::{FromRow, Postgres};
use uuid::Uuid;
use crate::db::ApiAuthed;
use windmill_api_auth::ApiAuthed;
pub use windmill_common::flow_conversations::FlowConversation;
use windmill_common::{
db::{UserDB, DB},

View File

@@ -0,0 +1,31 @@
[package]
name = "windmill-api-flows"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_api_flows"
path = "src/lib.rs"
[features]
default = []
enterprise = ["windmill-common/enterprise"]
private = ["windmill-common/private"]
[dependencies]
windmill-common = { workspace = true, default-features = false }
windmill-api-auth.workspace = true
windmill-queue.workspace = true
windmill-audit.workspace = true
windmill-git-sync.workspace = true
windmill-dep-map.workspace = true
axum.workspace = true
hyper.workspace = true
serde.workspace = true
serde_json.workspace = true
sql-builder.workspace = true
sqlx.workspace = true
tracing.workspace = true
chrono.workspace = true

View File

@@ -0,0 +1,9 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
pub mod flows;

View File

@@ -0,0 +1,19 @@
[package]
name = "windmill-api-inputs"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_api_inputs"
path = "src/lib.rs"
[dependencies]
windmill-api-auth.workspace = true
windmill-common = { workspace = true, default-features = false }
axum.workspace = true
chrono.workspace = true
serde.workspace = true
serde_json.workspace = true
sql-builder.workspace = true
sqlx.workspace = true

View File

@@ -6,7 +6,7 @@
* LICENSE-AGPL for a copy of the license.
*/
use crate::db::ApiAuthed;
use windmill_api_auth::ApiAuthed;
use axum::{
extract::{Path, Query},
routing::{get, post},

View File

@@ -0,0 +1,36 @@
[package]
name = "windmill-api-integration-tests"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_api_integration_tests"
path = "src/lib.rs"
[features]
default = []
private = ["windmill-test-utils/private", "dep:aws-config", "dep:aws-credential-types", "dep:aws-sdk-sqs"]
enterprise = ["windmill-test-utils/enterprise", "dep:base64"]
deno_core = ["windmill-test-utils/deno_core"]
mcp = []
[dependencies]
windmill-test-utils.workspace = true
windmill-api-client.workspace = true
windmill-common = { workspace = true, default-features = false }
sqlx.workspace = true
serde_json.workspace = true
serde.workspace = true
reqwest.workspace = true
tokio.workspace = true
anyhow.workspace = true
uuid.workspace = true
rand.workspace = true
rumqttc.workspace = true
rdkafka.workspace = true
async-nats.workspace = true
aws-config = { workspace = true, optional = true }
aws-credential-types = { workspace = true, optional = true }
aws-sdk-sqs = { workspace = true, optional = true }
base64 = { workspace = true, optional = true }

View File

@@ -0,0 +1,2 @@
// This crate exists only to host integration tests.
// See tests/ directory for the actual test files.

View File

@@ -1,8 +1,7 @@
use serde_json::json;
use sqlx::{Pool, Postgres};
mod common;
use common::*;
use windmill_test_utils::*;
fn app_url(port: u16, endpoint: &str, path: &str) -> String {
format!("http://localhost:{port}/api/w/test-workspace/apps/{endpoint}/{path}")
@@ -40,7 +39,7 @@ fn new_app(path: &str, summary: &str) -> serde_json::Value {
})
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_app_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;

View File

@@ -1,8 +1,7 @@
use serde_json::json;
use sqlx::{Pool, Postgres};
mod common;
use common::*;
use windmill_test_utils::*;
fn client() -> reqwest::Client {
reqwest::Client::new()
@@ -12,7 +11,7 @@ fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
builder.header("Authorization", "Bearer SECRET_TOKEN")
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_draft_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;

View File

@@ -0,0 +1,146 @@
-- used for backend automated testing
-- https://docs.rs/sqlx/latest/sqlx/attr.test.html
INSERT INTO workspace
(id, name, owner)
VALUES ('test-workspace', 'test-workspace', 'test-user');
INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES
('test-workspace', 'test@windmill.dev', 'test-user', true, 'Admin');
INSERT INTO workspace_key(workspace_id, kind, key) VALUES
('test-workspace', 'cloud', 'test-key');
INSERT INTO workspace_settings (workspace_id) VALUES
('test-workspace');
INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES
('test-workspace', 'all', 'All users', '{}');
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name, username)
VALUES ('test@windmill.dev', 'not-a-real-hash', 'password', true, true, 'Test User', 'test-user');
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name)
VALUES ('test2@windmill.dev', 'not-a-real-hash', 'password', false, true, 'Test User 2');
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name)
VALUES ('test3@windmill.dev', 'not-a-real-hash', 'password', false, true, 'Test User 3');
INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES
('test-workspace', 'test2@windmill.dev', 'test-user-2', false, 'User');
INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES
('test-workspace', 'test3@windmill.dev', 'test-user-3', false, 'User');
insert INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN', 'test@windmill.dev', 'test token', true);
insert INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN_2', 'test2@windmill.dev', 'test token 2', false);
insert INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN_3', 'test3@windmill.dev', 'test token 3', false);
GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_admin;
GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_user;
CREATE FUNCTION "notify_insert_on_completed_job" ()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('completed', NEW.id::text);
RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;
CREATE TRIGGER "notify_insert_on_completed_job"
AFTER INSERT ON "v2_job_completed"
FOR EACH ROW
EXECUTE FUNCTION "notify_insert_on_completed_job" ();
CREATE FUNCTION "notify_queue" ()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('queued', NEW.id::text);
RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;
CREATE TRIGGER "notify_queue_after_insert"
AFTER INSERT ON "v2_job_queue"
FOR EACH ROW
EXECUTE FUNCTION "notify_queue" ();
CREATE TRIGGER "notify_queue_after_flow_status_update"
AFTER UPDATE ON "v2_job_status"
FOR EACH ROW
WHEN (NEW.flow_status IS DISTINCT FROM OLD.flow_status)
EXECUTE FUNCTION "notify_queue" ();
-- Apply phase 4:
DROP FUNCTION IF EXISTS v2_job_after_update CASCADE;
DROP FUNCTION IF EXISTS v2_job_completed_before_insert CASCADE;
DROP FUNCTION IF EXISTS v2_job_completed_before_update CASCADE;
DROP FUNCTION IF EXISTS v2_job_queue_after_insert CASCADE;
DROP FUNCTION IF EXISTS v2_job_queue_before_insert CASCADE;
DROP FUNCTION IF EXISTS v2_job_queue_before_update CASCADE;
DROP FUNCTION IF EXISTS v2_job_runtime_before_insert CASCADE;
DROP FUNCTION IF EXISTS v2_job_runtime_before_update CASCADE;
DROP FUNCTION IF EXISTS v2_job_status_before_insert CASCADE;
DROP FUNCTION IF EXISTS v2_job_status_before_update CASCADE;
DROP VIEW IF EXISTS completed_job, completed_job_view, job, queue, queue_view CASCADE;
ALTER TABLE v2_job_queue
DROP COLUMN IF EXISTS __parent_job CASCADE,
DROP COLUMN IF EXISTS __created_by CASCADE,
DROP COLUMN IF EXISTS __script_hash CASCADE,
DROP COLUMN IF EXISTS __script_path CASCADE,
DROP COLUMN IF EXISTS __args CASCADE,
DROP COLUMN IF EXISTS __logs CASCADE,
DROP COLUMN IF EXISTS __raw_code CASCADE,
DROP COLUMN IF EXISTS __canceled CASCADE,
DROP COLUMN IF EXISTS __last_ping CASCADE,
DROP COLUMN IF EXISTS __job_kind CASCADE,
DROP COLUMN IF EXISTS __env_id CASCADE,
DROP COLUMN IF EXISTS __schedule_path CASCADE,
DROP COLUMN IF EXISTS __permissioned_as CASCADE,
DROP COLUMN IF EXISTS __flow_status CASCADE,
DROP COLUMN IF EXISTS __raw_flow CASCADE,
DROP COLUMN IF EXISTS __is_flow_step CASCADE,
DROP COLUMN IF EXISTS __language CASCADE,
DROP COLUMN IF EXISTS __same_worker CASCADE,
DROP COLUMN IF EXISTS __raw_lock CASCADE,
DROP COLUMN IF EXISTS __pre_run_error CASCADE,
DROP COLUMN IF EXISTS __email CASCADE,
DROP COLUMN IF EXISTS __visible_to_owner CASCADE,
DROP COLUMN IF EXISTS __mem_peak CASCADE,
DROP COLUMN IF EXISTS __root_job CASCADE,
DROP COLUMN IF EXISTS __leaf_jobs CASCADE,
DROP COLUMN IF EXISTS __concurrent_limit CASCADE,
DROP COLUMN IF EXISTS __concurrency_time_window_s CASCADE,
DROP COLUMN IF EXISTS __timeout CASCADE,
DROP COLUMN IF EXISTS __flow_step_id CASCADE,
DROP COLUMN IF EXISTS __cache_ttl CASCADE;
LOCK TABLE v2_job_queue IN ACCESS EXCLUSIVE MODE;
ALTER TABLE v2_job_completed
DROP COLUMN IF EXISTS __parent_job CASCADE,
DROP COLUMN IF EXISTS __created_by CASCADE,
DROP COLUMN IF EXISTS __created_at CASCADE,
DROP COLUMN IF EXISTS __success CASCADE,
DROP COLUMN IF EXISTS __script_hash CASCADE,
DROP COLUMN IF EXISTS __script_path CASCADE,
DROP COLUMN IF EXISTS __args CASCADE,
DROP COLUMN IF EXISTS __logs CASCADE,
DROP COLUMN IF EXISTS __raw_code CASCADE,
DROP COLUMN IF EXISTS __canceled CASCADE,
DROP COLUMN IF EXISTS __job_kind CASCADE,
DROP COLUMN IF EXISTS __env_id CASCADE,
DROP COLUMN IF EXISTS __schedule_path CASCADE,
DROP COLUMN IF EXISTS __permissioned_as CASCADE,
DROP COLUMN IF EXISTS __raw_flow CASCADE,
DROP COLUMN IF EXISTS __is_flow_step CASCADE,
DROP COLUMN IF EXISTS __language CASCADE,
DROP COLUMN IF EXISTS __is_skipped CASCADE,
DROP COLUMN IF EXISTS __raw_lock CASCADE,
DROP COLUMN IF EXISTS __email CASCADE,
DROP COLUMN IF EXISTS __visible_to_owner CASCADE,
DROP COLUMN IF EXISTS __tag CASCADE,
DROP COLUMN IF EXISTS __priority CASCADE;

View File

@@ -0,0 +1,238 @@
-- Permissions test fixture
-- This sets up a comprehensive permission testing scenario
-- ============================================
-- USERS
-- ============================================
-- Admin user (may already exist from base.sql, use ON CONFLICT)
INSERT INTO usr (workspace_id, email, username, is_admin, role)
VALUES ('test-workspace', 'admin@windmill.dev', 'admin', true, 'Admin')
ON CONFLICT (workspace_id, username) DO NOTHING;
-- Regular users (non-admin)
INSERT INTO usr (workspace_id, email, username, is_admin, role)
VALUES
('test-workspace', 'alice@windmill.dev', 'alice', false, 'Developer'),
('test-workspace', 'bob@windmill.dev', 'bob', false, 'Developer'),
('test-workspace', 'charlie@windmill.dev', 'charlie', false, 'Developer');
-- Operator user (can execute but cannot create/update scripts, flows, apps)
INSERT INTO usr (workspace_id, email, username, is_admin, operator, role)
VALUES
('test-workspace', 'operator@windmill.dev', 'operator', false, true, 'Operator');
-- Add users to password table (use ON CONFLICT since admin may exist from base.sql)
INSERT INTO password (email, password_hash, login_type, super_admin, verified, name)
VALUES
('admin@windmill.dev', 'dummy_hash', 'password', false, true, 'Admin User'),
('alice@windmill.dev', 'dummy_hash', 'password', false, true, 'Alice'),
('bob@windmill.dev', 'dummy_hash', 'password', false, true, 'Bob'),
('charlie@windmill.dev', 'dummy_hash', 'password', false, true, 'Charlie'),
('operator@windmill.dev', 'dummy_hash', 'password', false, true, 'Operator')
ON CONFLICT (email) DO NOTHING;
-- ============================================
-- TOKENS for authentication
-- ============================================
-- Tokens associated with emails (workspace-scoped)
-- The auth system will look up the user by email in the usr table
-- Note: tokens must be at least 10 characters (TOKEN_PREFIX_LEN)
INSERT INTO token (token, email, label, super_admin, owner, workspace_id)
VALUES
('ADMIN_TOKEN_TEST', 'admin@windmill.dev', 'Admin token', false, 'u/admin', 'test-workspace'),
('ALICE_TOKEN_TEST', 'alice@windmill.dev', 'Alice token', false, 'u/alice', 'test-workspace'),
('BOB_TOKEN_TEST12', 'bob@windmill.dev', 'Bob token', false, 'u/bob', 'test-workspace'),
('CHARLIE_TOKEN_01', 'charlie@windmill.dev', 'Charlie token', false, 'u/charlie', 'test-workspace'),
('OPERATOR_TOKEN_1', 'operator@windmill.dev', 'Operator token', false, 'u/operator', 'test-workspace');
-- ============================================
-- GROUPS
-- ============================================
-- 'developers' group - Charlie is a member
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
VALUES ('test-workspace', 'developers', 'Developer group', '{}');
-- 'editors' group - Bob is a member (has write access to some folders)
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
VALUES ('test-workspace', 'editors', 'Editor group', '{}');
-- Group memberships
INSERT INTO usr_to_group (workspace_id, group_, usr)
VALUES
('test-workspace', 'developers', 'charlie'),
('test-workspace', 'editors', 'bob');
-- ============================================
-- FOLDERS
-- ============================================
-- 'shared' folder:
-- - Alice has read-only access (extra_perms: u/alice -> false)
-- - Bob has write access (extra_perms: u/bob -> true)
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by)
VALUES ('test-workspace', 'shared', 'Shared Folder', '{"u/admin"}',
'{"u/alice": false, "u/bob": true}', 'admin');
-- 'team' folder:
-- - 'developers' group has read-only access
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by)
VALUES ('test-workspace', 'team', 'Team Folder', '{"u/admin"}',
'{"g/developers": false}', 'admin');
-- 'editable' folder:
-- - 'editors' group has write access
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by)
VALUES ('test-workspace', 'editable', 'Editable Folder', '{"u/admin"}',
'{"g/editors": true}', 'admin');
-- 'admin_only' folder:
-- - Only admin has access (but we'll add item-level perms for specific items)
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by)
VALUES ('test-workspace', 'admin_only', 'Admin Only Folder', '{"u/admin"}',
'{}', 'admin');
-- 'alice_owned' folder:
-- - Alice is an owner (owners must also be in extra_perms for get_folders_for_user to find them)
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by)
VALUES ('test-workspace', 'alice_owned', 'Alice Owned Folder', '{"u/alice"}',
'{"u/alice": true}', 'alice');
-- ============================================
-- SCRIPTS
-- ============================================
-- Alice's personal script
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
VALUES ('test-workspace', 1001, 'u/alice/my_script',
'export function main() { return "alice script"; }',
'deno', 'script', 'alice', '{}', 'Alice script', '', '', '{}');
-- Bob's personal script
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
VALUES ('test-workspace', 1002, 'u/bob/my_script',
'export function main() { return "bob script"; }',
'deno', 'script', 'bob', '{}', 'Bob script', '', '', '{}');
-- Script in shared folder (accessible by Alice read-only, Bob write)
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
VALUES ('test-workspace', 1003, 'f/shared/public_script',
'export function main() { return "public"; }',
'deno', 'script', 'admin', '{}', 'Public script', '', '', '{}');
-- Script in team folder (accessible by developers group)
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
VALUES ('test-workspace', 1004, 'f/team/team_script',
'export function main() { return "team"; }',
'deno', 'script', 'admin', '{}', 'Team script', '', '', '{}');
-- Script in admin_only folder but with explicit share to Alice
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
VALUES ('test-workspace', 1005, 'f/admin_only/shared_with_alice',
'export function main() { return "shared"; }',
'deno', 'script', 'admin', '{}', 'Shared with Alice', '', '',
'{"u/alice": false}');
-- Script in alice_owned folder
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
VALUES ('test-workspace', 1006, 'f/alice_owned/owner_script',
'export function main() { return "owner"; }',
'deno', 'script', 'alice', '{}', 'Owner script', '', '', '{}');
-- Alice's script with extra_perms sharing to Bob (read-only)
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
VALUES ('test-workspace', 1007, 'u/alice/extra_shared_script',
'export function main() { return "extra shared"; }',
'deno', 'script', 'alice', '{}', 'Extra shared script', '', '',
'{"u/bob": false}');
-- ============================================
-- FLOWS (with flow_versions)
-- ============================================
-- Create flows first (without versions)
INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, schema, extra_perms)
VALUES
('test-workspace', 'u/alice/my_flow', 'Alice flow', '', '{"modules": []}', 'alice', NOW(), '{}', '{}'),
('test-workspace', 'u/bob/my_flow', 'Bob flow', '', '{"modules": []}', 'bob', NOW(), '{}', '{}'),
('test-workspace', 'f/shared/shared_flow', 'Shared flow', '', '{"modules": []}', 'admin', NOW(), '{}', '{}');
-- Create flow versions
INSERT INTO flow_version (id, workspace_id, path, value, schema, created_by, created_at)
VALUES
(1001, 'test-workspace', 'u/alice/my_flow', '{"modules": []}', '{}', 'alice', NOW()),
(1002, 'test-workspace', 'u/bob/my_flow', '{"modules": []}', '{}', 'bob', NOW()),
(1003, 'test-workspace', 'f/shared/shared_flow', '{"modules": []}', '{}', 'admin', NOW());
-- Update flows with version references
UPDATE flow SET versions = ARRAY[1001::bigint] WHERE path = 'u/alice/my_flow' AND workspace_id = 'test-workspace';
UPDATE flow SET versions = ARRAY[1002::bigint] WHERE path = 'u/bob/my_flow' AND workspace_id = 'test-workspace';
UPDATE flow SET versions = ARRAY[1003::bigint] WHERE path = 'f/shared/shared_flow' AND workspace_id = 'test-workspace';
-- ============================================
-- RESOURCES
-- ============================================
-- Alice's personal resource
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/alice/my_resource', '{"key": "alice_value"}',
'Alice resource', 'object', '{}', 'alice');
-- Bob's personal resource
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/bob/my_resource', '{"key": "bob_value"}',
'Bob resource', 'object', '{}', 'bob');
-- ============================================
-- VARIABLES
-- ============================================
-- Alice's personal variable (non-secret for testing permissions, not encryption)
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
VALUES ('test-workspace', 'u/alice/my_variable', 'alice_value', false,
'Alice variable', '{}');
-- Bob's personal variable (non-secret for testing permissions, not encryption)
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
VALUES ('test-workspace', 'u/bob/my_variable', 'bob_value', false,
'Bob variable', '{}');
-- ============================================
-- SCHEDULES
-- ============================================
-- Alice's personal schedule
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms)
VALUES ('test-workspace', 'u/alice/my_schedule', 'alice', NOW(), '0 * * * *', false,
'u/alice/my_script', '{}', false, 'alice@windmill.dev', 'UTC', '{}');
-- Bob's personal schedule
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms)
VALUES ('test-workspace', 'u/bob/my_schedule', 'bob', NOW(), '0 * * * *', false,
'u/bob/my_script', '{}', false, 'bob@windmill.dev', 'UTC', '{}');
-- ============================================
-- APPS (with app_versions)
-- ============================================
-- Alice's personal app
INSERT INTO app (id, workspace_id, path, summary, versions, policy, extra_perms)
VALUES (2001, 'test-workspace', 'u/alice/my_app', 'Alice app', '{}',
'{"on_behalf_of": "u/alice", "on_behalf_of_email": "alice@windmill.dev", "execution_mode": "viewer"}', '{}');
-- Shared folder app
INSERT INTO app (id, workspace_id, path, summary, versions, policy, extra_perms)
VALUES (2002, 'test-workspace', 'f/shared/shared_app', 'Shared app', '{}',
'{"on_behalf_of": "u/admin", "on_behalf_of_email": "admin@windmill.dev", "execution_mode": "viewer"}', '{}');
-- Create app versions
INSERT INTO app_version (id, app_id, value, created_by, created_at)
VALUES
(2001, 2001, '{"grid": []}', 'alice', NOW()),
(2002, 2002, '{"grid": []}', 'admin', NOW());
-- Update apps with version references
UPDATE app SET versions = ARRAY[2001::bigint] WHERE id = 2001;
UPDATE app SET versions = ARRAY[2002::bigint] WHERE id = 2002;

View File

@@ -0,0 +1,69 @@
-- Combined fixture for resource endpoint tests
-- === resource type test data ===
INSERT INTO resource_type (workspace_id, name, schema, description, created_by)
VALUES ('test-workspace', 'test_db', '{"type": "object", "properties": {"host": {"type": "string"}}}',
'Test DB type', 'test-user');
-- === get_value_interpolated test data ===
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/test-user/simple_resource', '{"host": "localhost", "port": 5432}',
'Simple resource', 'object', '{}', 'test-user');
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
VALUES ('test-workspace', 'u/test-user/db_password', 'hunter2', false, 'DB password', '{}');
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/test-user/resource_with_var', '{"host": "localhost", "password": "$var:u/test-user/db_password"}',
'Resource with var ref', 'object', '{}', 'test-user');
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/test-user/db_credentials', '{"user": "admin", "password": "secret123"}',
'DB credentials', 'object', '{}', 'test-user');
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/test-user/resource_with_res', '{"host": "localhost", "credentials": "$res:u/test-user/db_credentials"}',
'Resource with res ref', 'object', '{}', 'test-user');
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/test-user/resource_mixed', '{"host": "localhost", "password": "$var:u/test-user/db_password", "credentials": "$res:u/test-user/db_credentials"}',
'Resource with mixed refs', 'object', '{}', 'test-user');
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/test-user/null_resource', null,
'Null resource', 'object', '{}', 'test-user');
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
VALUES ('test-workspace', 'u/test-user/api_key', 'sk-abc123', false, 'API key', '{}');
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/test-user/inner_resource', '{"key": "$var:u/test-user/api_key"}',
'Inner resource', 'object', '{}', 'test-user');
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/test-user/chained_resource', '{"service": "myapi", "auth": "$res:u/test-user/inner_resource"}',
'Chained resource', 'object', '{}', 'test-user');
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/test-user/resource_with_array', '{"hosts": ["host1", "host2"], "port": 5432}',
'Resource with array', 'object', '{}', 'test-user');
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/test-user/scalar_var_resource', '"$var:u/test-user/db_password"',
'Scalar var ref', 'string', '{}', 'test-user');
-- === mcp_tools test data ===
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/test-user/mcp_valid', '{"name": "test-mcp", "url": "http://127.0.0.1:19999/mcp"}',
'Valid MCP resource (unreachable)', 'mcp_server', '{}', 'test-user');
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/test-user/mcp_invalid_format', '{"host": "localhost", "port": 5432}',
'Not an MCP resource', 'object', '{}', 'test-user');
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/test-user/mcp_null', null,
'Null MCP resource', 'mcp_server', '{}', 'test-user');

View File

@@ -0,0 +1,10 @@
-- Fixture for variable endpoint tests
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
VALUES ('test-workspace', 'u/test-user/plain_var', 'hello world', false, 'A plain variable', '{}');
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
VALUES ('test-workspace', 'u/test-user/secret_var', 'supersecret', true, 'A secret variable', '{}');
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
VALUES ('test-workspace', 'u/test-user/another_var', 'foobar', false, 'Another variable', '{}');

View File

@@ -1,8 +1,7 @@
use serde_json::json;
use sqlx::{Pool, Postgres};
mod common;
use common::*;
use windmill_test_utils::*;
fn flow_url(port: u16, endpoint: &str, path: &str) -> String {
format!("http://localhost:{port}/api/w/test-workspace/flows/{endpoint}/{path}")
@@ -40,7 +39,7 @@ fn new_flow(path: &str, summary: &str) -> serde_json::Value {
})
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_flow_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;

View File

@@ -1,8 +1,7 @@
use serde_json::json;
use sqlx::{Pool, Postgres};
mod common;
use common::*;
use windmill_test_utils::*;
fn folder_url(port: u16, endpoint: &str, name: &str) -> String {
format!("http://localhost:{port}/api/w/test-workspace/folders/{endpoint}/{name}")
@@ -16,7 +15,7 @@ fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
builder.header("Authorization", "Bearer SECRET_TOKEN")
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_folder_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;

View File

@@ -1,8 +1,7 @@
use serde_json::json;
use sqlx::{Pool, Postgres};
mod common;
use common::*;
use windmill_test_utils::*;
fn group_url(port: u16, endpoint: &str, name: &str) -> String {
format!("http://localhost:{port}/api/w/test-workspace/groups/{endpoint}/{name}")
@@ -16,7 +15,7 @@ fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
builder.header("Authorization", "Bearer SECRET_TOKEN")
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_group_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;

View File

@@ -1,10 +1,13 @@
#[cfg(feature = "deno_core")]
use serde_json::json;
#[cfg(feature = "deno_core")]
use sqlx::{Pool, Postgres};
mod common;
use common::*;
#[cfg(feature = "deno_core")]
use windmill_test_utils::*;
/// Helper to create a client authenticated as a specific user
#[cfg(feature = "deno_core")]
async fn create_client_for_user(_port: u16, token: &str) -> reqwest::Client {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
@@ -18,12 +21,14 @@ async fn create_client_for_user(_port: u16, token: &str) -> reqwest::Client {
}
/// Test helper to check if a GET request succeeds
#[cfg(feature = "deno_core")]
async fn can_read(client: &reqwest::Client, url: &str) -> bool {
let resp = client.get(url).send().await.unwrap();
resp.status().is_success()
}
/// Test helper to check if a POST request succeeds (for write operations)
#[cfg(feature = "deno_core")]
async fn can_write(client: &reqwest::Client, url: &str, body: serde_json::Value) -> bool {
let resp = client.post(url).json(&body).send().await.unwrap();
let status = resp.status();
@@ -44,7 +49,7 @@ async fn can_write(client: &reqwest::Client, url: &str, body: serde_json::Value)
/// `cargo test --features deno_core permissions -- --ignored`
#[ignore]
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base", "permissions_test"))]
#[sqlx::test(migrations = "../migrations", fixtures("base", "permissions_test"))]
async fn test_permissions_exhaustive(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
@@ -477,7 +482,7 @@ async fn test_permissions_exhaustive(db: Pool<Postgres>) -> anyhow::Result<()> {
/// Additional test for verifying group permission inheritance
#[ignore]
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base", "permissions_test"))]
#[sqlx::test(migrations = "../migrations", fixtures("base", "permissions_test"))]
async fn test_group_permission_inheritance(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
@@ -547,7 +552,7 @@ async fn test_group_permission_inheritance(db: Pool<Postgres>) -> anyhow::Result
/// Test that permissions work correctly for all item types
#[ignore]
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base", "permissions_test"))]
#[sqlx::test(migrations = "../migrations", fixtures("base", "permissions_test"))]
async fn test_all_item_types_permissions(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
@@ -588,7 +593,7 @@ async fn test_all_item_types_permissions(db: Pool<Postgres>) -> anyhow::Result<(
/// Operators have limited permissions - they can execute but cannot manage resources
#[ignore]
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base", "permissions_test"))]
#[sqlx::test(migrations = "../migrations", fixtures("base", "permissions_test"))]
async fn test_operator_cannot_create_update(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;

View File

@@ -7,8 +7,7 @@ use serde_json::json;
use sqlx::{Pool, Postgres};
use windmill_common::workspaces::invalidate_protection_rules_cache;
mod common;
use common::*;
use windmill_test_utils::*;
fn client() -> reqwest::Client {
reqwest::Client::new()
@@ -51,7 +50,7 @@ fn new_flow(path: &str, summary: &str) -> serde_json::Value {
/// Comprehensive test for protection rules functionality.
/// Tests all essential cases in a single test to avoid cache interference.
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_protection_rules(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
invalidate_protection_rules_cache("test-workspace");

View File

@@ -1,8 +1,7 @@
use serde_json::json;
use sqlx::{Pool, Postgres};
mod common;
use common::*;
use windmill_test_utils::*;
fn resource_url(port: u16, endpoint: &str, path: &str) -> String {
format!("http://localhost:{port}/api/w/test-workspace/resources/{endpoint}/{path}")
@@ -23,7 +22,7 @@ async fn authed_get(port: u16, endpoint: &str, path: &str) -> reqwest::Response
.unwrap()
}
#[sqlx::test(fixtures("base", "resources_test"))]
#[sqlx::test(migrations = "../migrations", fixtures("base", "resources_test"))]
async fn test_resource_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
@@ -400,7 +399,7 @@ async fn test_resource_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
}
#[cfg(feature = "mcp")]
#[sqlx::test(fixtures("base", "resources_test"))]
#[sqlx::test(migrations = "../migrations", fixtures("base", "resources_test"))]
async fn test_mcp_tools(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;

View File

@@ -1,8 +1,7 @@
use serde_json::json;
use sqlx::{Pool, Postgres};
mod common;
use common::*;
use windmill_test_utils::*;
fn schedule_url(port: u16, endpoint: &str, path: &str) -> String {
format!("http://localhost:{port}/api/w/test-workspace/schedules/{endpoint}/{path}")
@@ -23,7 +22,7 @@ async fn authed_get(port: u16, endpoint: &str, path: &str) -> reqwest::Response
.unwrap()
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_schedule_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;

View File

@@ -1,8 +1,7 @@
use serde_json::json;
use sqlx::{Pool, Postgres};
mod common;
use common::*;
use windmill_test_utils::*;
fn script_url(port: u16, endpoint: &str, path: &str) -> String {
format!("http://localhost:{port}/api/w/test-workspace/scripts/{endpoint}/{path}")
@@ -39,7 +38,7 @@ fn new_script(path: &str, summary: &str, content: &str) -> serde_json::Value {
})
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_script_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;

View File

@@ -24,8 +24,7 @@ use serde_json::json;
use sqlx::{Pool, Postgres};
use std::time::Duration;
mod common;
use common::*;
use windmill_test_utils::*;
// ============================================================================
// Helpers
@@ -137,7 +136,7 @@ async fn insert_resource(
/// -- --ignored --nocapture
/// ```
#[ignore = "requires running MQTT broker on localhost:1883"]
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_mqtt_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
@@ -230,7 +229,7 @@ async fn test_mqtt_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
/// -- --ignored --nocapture
/// ```
#[ignore = "requires running WebSocket echo server on localhost:8765"]
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_websocket_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
@@ -286,7 +285,7 @@ async fn test_websocket_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
/// -- --ignored --nocapture
/// ```
#[ignore = "requires PostgreSQL with wal_level=logical"]
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_postgres_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
@@ -387,7 +386,7 @@ async fn test_postgres_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
/// ```
#[cfg(all(feature = "enterprise", feature = "private"))]
#[ignore = "requires running Kafka broker on localhost:9092"]
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_kafka_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
@@ -478,7 +477,7 @@ async fn test_kafka_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
/// ```
#[cfg(all(feature = "enterprise", feature = "private"))]
#[ignore = "requires running NATS server on localhost:4222"]
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_nats_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
@@ -553,7 +552,7 @@ async fn test_nats_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
/// ```
#[cfg(all(feature = "enterprise", feature = "private"))]
#[ignore = "requires LocalStack SQS on localhost:4566"]
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_sqs_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
@@ -641,7 +640,7 @@ async fn test_sqs_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
/// ```
#[cfg(all(feature = "enterprise", feature = "private"))]
#[ignore = "requires GCP Pub/Sub emulator on localhost:8085"]
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_gcp_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;

View File

@@ -12,14 +12,13 @@ use serde::Deserialize;
use serde_json::json;
use sqlx::{Pool, Postgres};
mod common;
use common::*;
use windmill_test_utils::*;
// ============================================================================
// Capture Config Tests (direct DB)
// ============================================================================
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_capture_config_insert_and_query(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
@@ -56,7 +55,7 @@ async fn test_capture_config_insert_and_query(db: Pool<Postgres>) -> anyhow::Res
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_capture_config_upsert(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
@@ -104,7 +103,7 @@ async fn test_capture_config_upsert(db: Pool<Postgres>) -> anyhow::Result<()> {
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_capture_config_ping_updates_timestamp(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
@@ -159,7 +158,7 @@ async fn test_capture_config_ping_updates_timestamp(db: Pool<Postgres>) -> anyho
// Capture Payload Tests (direct DB)
// ============================================================================
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_capture_insert_and_list(db: Pool<Postgres>) -> anyhow::Result<()> {
for i in 0..2 {
sqlx::query!(
@@ -192,7 +191,7 @@ async fn test_capture_insert_and_list(db: Pool<Postgres>) -> anyhow::Result<()>
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_capture_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
let id = sqlx::query_scalar!(
r#"
@@ -227,7 +226,7 @@ async fn test_capture_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_capture_filter_by_trigger_kind(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
@@ -304,7 +303,7 @@ struct CaptureResponse {
main_args: serde_json::Value,
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_capture_api_set_config_and_list(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
@@ -348,7 +347,7 @@ async fn test_capture_api_set_config_and_list(db: Pool<Postgres>) -> anyhow::Res
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_capture_api_list_captures(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
@@ -394,7 +393,7 @@ async fn test_capture_api_list_captures(db: Pool<Postgres>) -> anyhow::Result<()
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_capture_api_get_single(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
@@ -441,7 +440,7 @@ async fn test_capture_api_get_single(db: Pool<Postgres>) -> anyhow::Result<()> {
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_capture_api_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
@@ -493,7 +492,7 @@ async fn test_capture_api_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_capture_api_pagination(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
@@ -555,7 +554,7 @@ async fn test_capture_api_pagination(db: Pool<Postgres>) -> anyhow::Result<()> {
// HTTP Trigger Tests (direct DB)
// ============================================================================
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_http_trigger_insert_and_query(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
@@ -610,7 +609,7 @@ async fn test_http_trigger_insert_and_query(db: Pool<Postgres>) -> anyhow::Resul
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_http_trigger_multiple_methods(db: Pool<Postgres>) -> anyhow::Result<()> {
let methods = ["get", "post", "put", "delete", "patch"];
@@ -657,7 +656,7 @@ async fn test_http_trigger_multiple_methods(db: Pool<Postgres>) -> anyhow::Resul
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_http_trigger_authentication_methods(db: Pool<Postgres>) -> anyhow::Result<()> {
let auth_methods = ["none", "windmill", "api_key", "basic_http", "signature"];
@@ -708,7 +707,7 @@ async fn test_http_trigger_authentication_methods(db: Pool<Postgres>) -> anyhow:
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_http_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
@@ -761,7 +760,7 @@ async fn test_http_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_http_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
@@ -813,7 +812,7 @@ async fn test_http_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_http_trigger_mode_filtering(db: Pool<Postgres>) -> anyhow::Result<()> {
let modes = ["enabled", "disabled", "suspended"];
@@ -870,7 +869,7 @@ async fn test_http_trigger_mode_filtering(db: Pool<Postgres>) -> anyhow::Result<
// Other Trigger Types Tests (DB schema validation)
// ============================================================================
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_websocket_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
@@ -909,7 +908,7 @@ async fn test_websocket_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()>
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_kafka_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
@@ -952,7 +951,7 @@ async fn test_kafka_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_postgres_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
@@ -995,7 +994,7 @@ async fn test_postgres_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()>
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_nats_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
@@ -1038,7 +1037,7 @@ async fn test_nats_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_sqs_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
@@ -1089,7 +1088,7 @@ async fn test_sqs_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
// Cross-trigger tests
// ============================================================================
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_trigger_server_state_tracking(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
@@ -1144,7 +1143,7 @@ async fn test_trigger_server_state_tracking(db: Pool<Postgres>) -> anyhow::Resul
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_trigger_mode_filtering(db: Pool<Postgres>) -> anyhow::Result<()> {
let modes = ["enabled", "disabled", "enabled"];
@@ -1191,7 +1190,7 @@ async fn test_trigger_mode_filtering(db: Pool<Postgres>) -> anyhow::Result<()> {
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_multiple_capture_configs_per_path(db: Pool<Postgres>) -> anyhow::Result<()> {
let trigger_kinds = ["webhook", "email", "kafka"];
@@ -1229,7 +1228,7 @@ async fn test_multiple_capture_configs_per_path(db: Pool<Postgres>) -> anyhow::R
// Schedule Tests (DB-level)
// ============================================================================
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_schedule_insert_and_query(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
@@ -1277,7 +1276,7 @@ async fn test_schedule_insert_and_query(db: Pool<Postgres>) -> anyhow::Result<()
// MQTT Trigger Tests (DB-level)
// ============================================================================
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_mqtt_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
r#"
@@ -1321,7 +1320,7 @@ async fn test_mqtt_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_mqtt_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
r#"
@@ -1366,7 +1365,7 @@ async fn test_mqtt_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_mqtt_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
r#"
@@ -1414,7 +1413,7 @@ async fn test_mqtt_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
// GCP Trigger Tests (DB-level)
// ============================================================================
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_gcp_trigger_insert_pull(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
r#"
@@ -1465,7 +1464,7 @@ async fn test_gcp_trigger_insert_pull(db: Pool<Postgres>) -> anyhow::Result<()>
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_gcp_trigger_insert_push(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
r#"
@@ -1515,7 +1514,7 @@ async fn test_gcp_trigger_insert_push(db: Pool<Postgres>) -> anyhow::Result<()>
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_gcp_trigger_unique_constraint(db: Pool<Postgres>) -> anyhow::Result<()> {
let insert_query = r#"
INSERT INTO gcp_trigger (
@@ -1569,7 +1568,7 @@ async fn test_gcp_trigger_unique_constraint(db: Pool<Postgres>) -> anyhow::Resul
// Email Trigger Tests (DB-level)
// ============================================================================
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_email_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
@@ -1612,7 +1611,7 @@ async fn test_email_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_email_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
@@ -1658,7 +1657,7 @@ async fn test_email_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
Ok(())
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_email_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"

View File

@@ -1,8 +1,7 @@
use serde_json::json;
use sqlx::{Pool, Postgres};
mod common;
use common::*;
use windmill_test_utils::*;
fn user_url(port: u16, endpoint: &str, name: &str) -> String {
format!("http://localhost:{port}/api/w/test-workspace/users/{endpoint}/{name}")
@@ -16,7 +15,7 @@ fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
builder.header("Authorization", "Bearer SECRET_TOKEN")
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_user_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;

View File

@@ -1,8 +1,7 @@
use serde_json::json;
use sqlx::{Pool, Postgres};
mod common;
use common::*;
use windmill_test_utils::*;
fn variable_url(port: u16, endpoint: &str, path: &str) -> String {
format!("http://localhost:{port}/api/w/test-workspace/variables/{endpoint}/{path}")
@@ -23,7 +22,7 @@ async fn authed_get(port: u16, endpoint: &str, path: &str) -> reqwest::Response
.unwrap()
}
#[sqlx::test(fixtures("base", "variables_test"))]
#[sqlx::test(migrations = "../migrations", fixtures("base", "variables_test"))]
async fn test_variable_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;

View File

@@ -1,8 +1,7 @@
use serde_json::json;
use sqlx::{Pool, Postgres};
mod common;
use common::*;
use windmill_test_utils::*;
/// Comprehensive integration test for the compare_workspaces endpoint.
///
@@ -12,7 +11,7 @@ use common::*;
/// 3. Making various changes in both workspaces (new items, modifications, conflicts, deletions, renames)
/// 4. Populating the workspace_diff table to simulate Git sync tracking
/// 5. Calling compare_workspaces and verifying all aspects of the comparison
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_compare_workspaces_comprehensive(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;

View File

@@ -1,8 +1,7 @@
use serde_json::json;
use sqlx::{Pool, Postgres};
mod common;
use common::*;
use windmill_test_utils::*;
fn client() -> reqwest::Client {
reqwest::Client::new()
@@ -12,7 +11,7 @@ fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
builder.header("Authorization", "Bearer SECRET_TOKEN")
}
#[sqlx::test(fixtures("base"))]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_workspace_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;

View File

@@ -0,0 +1,21 @@
[package]
name = "windmill-api-npm-proxy"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_api_npm_proxy"
path = "src/lib.rs"
[dependencies]
windmill-api-auth.workspace = true
windmill-common = { workspace = true, default-features = false }
axum.workspace = true
flate2.workspace = true
serde.workspace = true
serde_json.workspace = true
sqlx.workspace = true
tar.workspace = true
tower-http.workspace = true
tracing.workspace = true

View File

@@ -18,7 +18,8 @@ use windmill_common::{
utils::StripPath,
};
use crate::{db::ApiAuthed, HTTP_CLIENT};
use windmill_api_auth::ApiAuthed;
use windmill_common::utils::HTTP_CLIENT_PERMISSIVE as HTTP_CLIENT;
#[derive(Deserialize)]
struct ProxyQuery {

View File

@@ -0,0 +1,26 @@
[package]
name = "windmill-api-openapi"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_api_openapi"
path = "src/lib.rs"
[dependencies]
windmill-api-auth.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-store.workspace = true
windmill-trigger-http.workspace = true
anyhow.workspace = true
axum.workspace = true
http.workspace = true
indexmap.workspace = true
itertools.workspace = true
lazy_static.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_yml.workspace = true
sqlx.workspace = true
url.workspace = true

View File

@@ -20,13 +20,11 @@ use windmill_common::{
utils::{deserialize_url, empty_as_none, is_empty, RunnableKind},
DB,
};
use windmill_store::resources::try_get_resource_from_db_as;
use crate::{
db::ApiAuthed,
resources::try_get_resource_from_db_as,
triggers::http::{
http_trigger_auth::ApiKeyAuthentication, AuthenticationMethod, HttpMethod, RequestType,
},
use windmill_api_auth::ApiAuthed;
use windmill_trigger_http::{
http_trigger_auth::ApiKeyAuthentication, AuthenticationMethod, HttpMethod, RequestType,
};
lazy_static::lazy_static! {

View File

@@ -0,0 +1,29 @@
[package]
name = "windmill-api-schedule"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_api_schedule"
path = "src/lib.rs"
[features]
default = []
enterprise = []
[dependencies]
windmill-api-auth.workspace = true
windmill-api-settings.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-audit.workspace = true
windmill-git-sync.workspace = true
windmill-queue.workspace = true
axum.workspace = true
chrono.workspace = true
chrono-tz.workspace = true
serde.workspace = true
serde_json.workspace = true
sql-builder.workspace = true
sqlx.workspace = true
tracing.workspace = true

View File

@@ -6,12 +6,8 @@
* LICENSE-AGPL for a copy of the license.
*/
use crate::{
db::{ApiAuthed, DB},
settings::{delete_global_setting, set_global_setting_internal},
users::maybe_refresh_folders,
utils::{check_scopes, require_super_admin},
};
use windmill_api_auth::{check_scopes, maybe_refresh_folders, require_super_admin, ApiAuthed};
use windmill_common::DB;
use axum::{
extract::{Extension, Path, Query},
routing::{delete, get, post},
@@ -922,9 +918,9 @@ async fn set_default_error_handler(
};
if let Some(value_content) = value {
set_global_setting_internal(&db, key, value_content).await?;
windmill_api_settings::set_global_setting_internal(&db, key, value_content).await?;
} else {
delete_global_setting(&db, key.as_str()).await?;
windmill_api_settings::delete_global_setting(&db, key.as_str()).await?;
}
if payload.override_existing {

View File

@@ -6,5 +6,4 @@
* LICENSE-AGPL for a copy of the license.
*/
pub mod flows;
pub mod scripts;

View File

@@ -0,0 +1,38 @@
[package]
name = "windmill-api-settings"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_api_settings"
path = "src/lib.rs"
[features]
default = []
enterprise = []
private = ["windmill-common/private"]
parquet = ["dep:object_store", "windmill-common/parquet"]
license = ["dep:rsa"]
[dependencies]
windmill-alerting.workspace = true
windmill-api-auth.workspace = true
windmill-common = { workspace = true, default-features = false }
axum.workspace = true
anyhow.workspace = true
bytes.workspace = true
chrono.workspace = true
futures.workspace = true
lazy_static.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
sqlx.workspace = true
tokio.workspace = true
tracing.workspace = true
uuid.workspace = true
base64.workspace = true
sha2.workspace = true
rsa = { workspace = true, optional = true }
object_store = { workspace = true, optional = true }

Some files were not shown because too many files have changed in this diff Show More