Compare commits
75 Commits
removeplug
...
di/db-mana
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bafef661c | ||
|
|
2828616a79 | ||
|
|
73d27e92dd | ||
|
|
41e523f827 | ||
|
|
8b1fe8f9de | ||
|
|
c97cf604ab | ||
|
|
5ba4029d86 | ||
|
|
e75763dbe5 | ||
|
|
ce8ac9cf52 | ||
|
|
7e7d7645e2 | ||
|
|
037035e094 | ||
|
|
24078d736c | ||
|
|
3a2258745d | ||
|
|
0330993cb6 | ||
|
|
1d78589940 | ||
|
|
822723ae69 | ||
|
|
c40ad129bc | ||
|
|
7859bca6ae | ||
|
|
1ac391a795 | ||
|
|
5d79f33590 | ||
|
|
86065aaac8 | ||
|
|
e3f4130c68 | ||
|
|
2e582b1bc1 | ||
|
|
2d583826dc | ||
|
|
972ae7aa29 | ||
|
|
d46913b74a | ||
|
|
90f4c64ee1 | ||
|
|
a8cbe9396f | ||
|
|
ce041e8a5e | ||
|
|
65082159d8 | ||
|
|
5f0ef936d1 | ||
|
|
bee50b83d1 | ||
|
|
e56ccd200b | ||
|
|
eab789beeb | ||
|
|
077779ec52 | ||
|
|
63ebae8829 | ||
|
|
87ebeaa51d | ||
|
|
62382fd286 | ||
|
|
19c065bed5 | ||
|
|
164e499c64 | ||
|
|
8a859ff7b9 | ||
|
|
c9c3baecb3 | ||
|
|
baf2bcf14d | ||
|
|
7fe1594d22 | ||
|
|
c0c9388415 | ||
|
|
4bf827bea4 | ||
|
|
53caecf1da | ||
|
|
424ca59dfe | ||
|
|
fafa809670 | ||
|
|
c97d8b4715 | ||
|
|
f6ceb2e366 | ||
|
|
ef7b2ec81c | ||
|
|
ee01acd9a6 | ||
|
|
7b6f1deeb1 | ||
|
|
f331e1f0ad | ||
|
|
aafe716823 | ||
|
|
e97da86067 | ||
|
|
26f4f2b399 | ||
|
|
cac4bdd54f | ||
|
|
4a14e9436e | ||
|
|
e6f7775d4d | ||
|
|
c5b440e569 | ||
|
|
2b2be38f12 | ||
|
|
50defdded1 | ||
|
|
759eb68a7f | ||
|
|
3e6b1bee59 | ||
|
|
f412fbc3b7 | ||
|
|
cf3ddce68a | ||
|
|
e906818982 | ||
|
|
18552046c2 | ||
|
|
a111653c6d | ||
|
|
e0d4a4b38e | ||
|
|
9e92445fae | ||
|
|
5faeae9486 | ||
|
|
cfd9541ab1 |
@@ -110,7 +110,6 @@
|
||||
]
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"rust-analyzer-lsp@claude-plugins-official": true,
|
||||
"typescript-lsp@claude-plugins-official": true,
|
||||
"code-review@claude-plugins-official": true
|
||||
}
|
||||
|
||||
165
.github/workflows/backend-test-windows.yml
vendored
Normal file
165
.github/workflows/backend-test-windows.yml
vendored
Normal file
@@ -0,0 +1,165 @@
|
||||
name: Backend integration tests (Windows)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "ci-windows-tests"
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
SQLX_OFFLINE: true
|
||||
DISABLE_EMBEDDING: true
|
||||
|
||||
jobs:
|
||||
cargo_test_windows:
|
||||
runs-on: blacksmith-16vcpu-windows-2025
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Read EE repo commit hash
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ee_repo_ref = Get-Content .\backend\ee-repo-ref.txt
|
||||
echo "ee_repo_ref=$ee_repo_ref" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
|
||||
- name: Checkout windmill-ee-private repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: windmill-labs/windmill-ee-private
|
||||
path: ./windmill-ee-private
|
||||
ref: ${{ env.ee_repo_ref }}
|
||||
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Substitute EE code
|
||||
shell: bash
|
||||
run: |
|
||||
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
|
||||
|
||||
- name: Setup PostgreSQL
|
||||
uses: ikalnytskyi/action-setup-postgres@v6
|
||||
with:
|
||||
username: postgres
|
||||
password: changeme
|
||||
database: windmill
|
||||
port: 5432
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.93.0
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "9.0.x"
|
||||
|
||||
- uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v2.x
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.10
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- uses: astral-sh/setup-uv@v6.2.1
|
||||
with:
|
||||
version: "0.9.24"
|
||||
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: "8.3"
|
||||
tools: composer
|
||||
|
||||
- name: Install windmill CLI
|
||||
shell: bash
|
||||
run: |
|
||||
cd cli
|
||||
bash gen_wm_client.sh
|
||||
bun install
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
printf '#!/bin/sh\nexec bun run "%s/cli/src/main.ts" "$@"\n' "$GITHUB_WORKSPACE" > "$HOME/.local/bin/wmill"
|
||||
chmod +x "$HOME/.local/bin/wmill"
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install OpenSSL via vcpkg
|
||||
run: |
|
||||
vcpkg.exe install openssl-windows:x64-windows
|
||||
vcpkg.exe install openssl:x64-windows-static
|
||||
vcpkg.exe integrate install
|
||||
|
||||
- name: Get runtime paths
|
||||
id: runtime-paths
|
||||
shell: pwsh
|
||||
run: |
|
||||
echo "DENO_PATH=$($(Get-Command deno).Source)" >> $env:GITHUB_OUTPUT
|
||||
echo "BUN_PATH=$($(Get-Command bun).Source)" >> $env:GITHUB_OUTPUT
|
||||
echo "NODE_BIN_PATH=$($(Get-Command node).Source)" >> $env:GITHUB_OUTPUT
|
||||
echo "GO_PATH=$($(Get-Command go).Source)" >> $env:GITHUB_OUTPUT
|
||||
echo "UV_PATH=$($(Get-Command uv).Source)" >> $env:GITHUB_OUTPUT
|
||||
echo "PHP_PATH=$($(Get-Command php).Source)" >> $env:GITHUB_OUTPUT
|
||||
echo "COMPOSER_PATH=$($(Get-Command composer).Source)" >> $env:GITHUB_OUTPUT
|
||||
echo "POWERSHELL_PATH=$($(Get-Command pwsh).Source)" >> $env:GITHUB_OUTPUT
|
||||
echo "DOTNET_PATH=$($(Get-Command dotnet).Source)" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Build DuckDB FFI module
|
||||
working-directory: backend/windmill-duckdb-ffi-internal
|
||||
timeout-minutes: 30
|
||||
run: |
|
||||
cargo build --release -p windmill_duckdb_ffi_internal
|
||||
New-Item -ItemType Directory -Path ..\target\debug -Force
|
||||
Copy-Item target\release\windmill_duckdb_ffi_internal.dll ..\target\debug\
|
||||
|
||||
- name: Print runtime versions and env
|
||||
shell: pwsh
|
||||
run: |
|
||||
deno --version
|
||||
bun -v
|
||||
node --version
|
||||
go version
|
||||
python3 --version
|
||||
php --version
|
||||
pwsh --version
|
||||
dotnet --version
|
||||
echo "TEMP=$env:TEMP"
|
||||
echo "TMP=$env:TMP"
|
||||
echo "USERPROFILE=$env:USERPROFILE"
|
||||
echo "HOME=$env:HOME"
|
||||
|
||||
- name: cargo test
|
||||
working-directory: backend
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@localhost:5432/windmill
|
||||
RUST_LOG: "off"
|
||||
RUST_LOG_STYLE: never
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: true
|
||||
CARGO_BUILD_JOBS: 12
|
||||
VCPKGRS_DYNAMIC: 1
|
||||
OPENSSL_DIR: ${{ env.VCPKG_INSTALLATION_ROOT }}\installed\x64-windows-static
|
||||
DENO_PATH: ${{ steps.runtime-paths.outputs.DENO_PATH }}
|
||||
BUN_PATH: ${{ steps.runtime-paths.outputs.BUN_PATH }}
|
||||
NODE_BIN_PATH: ${{ steps.runtime-paths.outputs.NODE_BIN_PATH }}
|
||||
GO_PATH: ${{ steps.runtime-paths.outputs.GO_PATH }}
|
||||
UV_PATH: ${{ steps.runtime-paths.outputs.UV_PATH }}
|
||||
PHP_PATH: ${{ steps.runtime-paths.outputs.PHP_PATH }}
|
||||
COMPOSER_PATH: ${{ steps.runtime-paths.outputs.COMPOSER_PATH }}
|
||||
POWERSHELL_PATH: ${{ steps.runtime-paths.outputs.POWERSHELL_PATH }}
|
||||
DOTNET_PATH: ${{ steps.runtime-paths.outputs.DOTNET_PATH }}
|
||||
WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES: 1
|
||||
WMDEBUG_FORCE_RUNNABLE_SETTINGS_V0: 1
|
||||
WMDEBUG_FORCE_NO_LEGACY_DEBOUNCING_COMPAT: 1
|
||||
run: >
|
||||
cargo test
|
||||
--no-fail-fast
|
||||
--features enterprise,deno_core,duckdb,license,python,rust,scoped_cache,parquet,private,csharp,php,quickjs,mcp,run_inline
|
||||
--all
|
||||
-- --nocapture --test-threads=10
|
||||
19
.github/workflows/cli-tests.yml
vendored
19
.github/workflows/cli-tests.yml
vendored
@@ -4,13 +4,13 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'cli/**'
|
||||
- '.github/workflows/cli-tests.yml'
|
||||
- "cli/**"
|
||||
- ".github/workflows/cli-tests.yml"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'cli/**'
|
||||
- '.github/workflows/cli-tests.yml'
|
||||
- "cli/**"
|
||||
- ".github/workflows/cli-tests.yml"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: "20"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: "20"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: "20"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
@@ -163,11 +163,6 @@ jobs:
|
||||
NODE_BIN_PATH: ${{ steps.runtime-paths.outputs.NODE_BIN_PATH }}
|
||||
run: bun test --timeout 120000 test/
|
||||
|
||||
- name: Keep runner alive for SSH debug
|
||||
if: failure()
|
||||
shell: pwsh
|
||||
run: Start-Sleep -Seconds 3600
|
||||
|
||||
# Combined summary job for branch protection
|
||||
test-summary:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
52
.webmux.yaml
Normal file
52
.webmux.yaml
Normal file
@@ -0,0 +1,52 @@
|
||||
# Project display name in the dashboard
|
||||
name: windmill
|
||||
|
||||
workspace:
|
||||
mainBranch: main
|
||||
worktreeRoot: ../__worktrees
|
||||
defaultAgent: claude
|
||||
|
||||
startupEnvs:
|
||||
CARGO_FEATURES: "quickjs"
|
||||
WM_CLONE_DB: false
|
||||
USE_RUST_PLUGIN: false
|
||||
|
||||
lifecycleHooks:
|
||||
postCreate: bash ./scripts/post-create.sh
|
||||
preRemove: bash ./scripts/pre-remove.sh
|
||||
|
||||
# Each service defines a port env var that webmux injects into pane and agent
|
||||
# process environments when creating a worktree. Ports are auto-assigned:
|
||||
# base + (slot x step).
|
||||
services:
|
||||
- name: backend
|
||||
portEnv: BACKEND_PORT
|
||||
portStart: 8000
|
||||
portStep: 10
|
||||
- name: frontend
|
||||
portEnv: FRONTEND_PORT
|
||||
portStart: 3000
|
||||
portStep: 10
|
||||
|
||||
profiles:
|
||||
default:
|
||||
runtime: host
|
||||
envPassthrough: []
|
||||
panes:
|
||||
- id: agent
|
||||
kind: agent
|
||||
focus: true
|
||||
- id: backend
|
||||
kind: command
|
||||
split: right
|
||||
command: ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/backend" && PORT=${BACKEND_PORT:-8000} cargo watch -x "run ${CARGO_FEATURES:+--features $CARGO_FEATURES}"
|
||||
- id: frontend
|
||||
kind: command
|
||||
split: bottom
|
||||
command: ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/frontend" && npm run generate-backend-client && REMOTE=${REMOTE:-http://localhost:${BACKEND_PORT:-8000}} npm run dev -- --port ${FRONTEND_PORT:-3000} --host 0.0.0.0
|
||||
|
||||
integrations:
|
||||
github:
|
||||
linkedRepos: []
|
||||
linear:
|
||||
enabled: true
|
||||
106
.wmdev.yaml
106
.wmdev.yaml
@@ -1,106 +0,0 @@
|
||||
services:
|
||||
- name: BE
|
||||
portEnv: BACKEND_PORT
|
||||
- name: FE
|
||||
portEnv: FRONTEND_PORT
|
||||
|
||||
profiles:
|
||||
default:
|
||||
name: default
|
||||
|
||||
sandbox:
|
||||
name: sandbox
|
||||
image: windmill-sandbox
|
||||
envPassthrough:
|
||||
- AWS_ACCESS_KEY_ID
|
||||
- AWS_SECRET_ACCESS_KEY
|
||||
- R2_ENDPOINT
|
||||
- R2_BUCKET
|
||||
- R2_PUBLIC_URL
|
||||
extraMounts:
|
||||
- hostPath: ~/.ssh
|
||||
guestPath: /root/.ssh
|
||||
writable: true
|
||||
- hostPath: ~/.codex
|
||||
guestPath: /root/.codex
|
||||
writable: true
|
||||
- hostPath: ~/windmill-ee-private
|
||||
writable: true
|
||||
- hostPath: ~/windmill-ee-private__worktrees
|
||||
writable: true
|
||||
systemPrompt: >
|
||||
You are running inside a sandboxed container with full permissions.
|
||||
This worktree is configured with the following ports:
|
||||
|
||||
- Backend: port ${BACKEND_PORT}.
|
||||
Start with: cd backend && PORT=${BACKEND_PORT}
|
||||
DATABASE_URL=postgres://postgres:changeme@localhost:5432/windmill
|
||||
cargo watch -x run
|
||||
|
||||
- Frontend: port ${FRONTEND_PORT}.
|
||||
Start with: cd frontend && REMOTE=http://localhost:${BACKEND_PORT}
|
||||
npm run dev -- --port ${FRONTEND_PORT} --host 0.0.0.0
|
||||
|
||||
--- Screenshots ---
|
||||
You can take screenshots of the frontend UI and upload them to R2
|
||||
for use in PR descriptions.
|
||||
1) Take a screenshot:
|
||||
bunx playwright screenshot --browser chromium
|
||||
http://localhost:${FRONTEND_PORT}/path/to/page /tmp/screenshot.png
|
||||
2) Upload to R2:
|
||||
aws s3 cp /tmp/screenshot.png
|
||||
"s3://$(printenv R2_BUCKET)/$(git rev-parse --abbrev-ref HEAD)/screenshot.png"
|
||||
--endpoint-url "$(printenv R2_ENDPOINT)"
|
||||
3) The public URL will be:
|
||||
$(printenv R2_PUBLIC_URL)/<branch>/screenshot.png
|
||||
4) Include in PR descriptions using markdown image syntax.
|
||||
|
||||
--- Terminal Recordings (asciinema) ---
|
||||
You can record terminal sessions and upload them for sharing.
|
||||
asciinema is available on PATH.
|
||||
|
||||
1) Write a shell script with the commands to demo. Add sleep
|
||||
delays for readable pacing:
|
||||
- 0.5s after printing a "$ command" line (lets viewer read it)
|
||||
- 1.5-2s after command output (lets viewer absorb the result)
|
||||
- Set GIT_PAGER=cat and PAGER=cat to prevent pager hangs
|
||||
|
||||
2) Record headlessly:
|
||||
asciinema rec --headless --overwrite \
|
||||
-c "bash /tmp/demo.sh" \
|
||||
--window-size 120x50 \
|
||||
--title "Description of demo" \
|
||||
/tmp/demo.cast
|
||||
|
||||
3) Upload to asciinema.org:
|
||||
XDG_DATA_HOME=/tmp/.local/share \
|
||||
asciinema upload --server-url https://asciinema.org /tmp/demo.cast
|
||||
|
||||
--- Mermaid Diagrams ---
|
||||
You can render Mermaid diagrams to SVG using the pre-installed mmdc CLI.
|
||||
The puppeteer config (no-sandbox + Chromium path) is at /root/.puppeteerrc.json.
|
||||
|
||||
1) Write a .mmd file with your diagram:
|
||||
cat > /tmp/diagram.mmd << 'EOF'
|
||||
graph TD
|
||||
A[Start] --> B[End]
|
||||
EOF
|
||||
|
||||
2) Render to SVG (the -p flag is required):
|
||||
mmdc -i /tmp/diagram.mmd -o /tmp/diagram.svg -p /root/.puppeteerrc.json
|
||||
|
||||
3) Upload to R2:
|
||||
aws s3 cp /tmp/diagram.svg
|
||||
"s3://$(printenv R2_BUCKET)/$(git rev-parse --abbrev-ref HEAD)/diagram.svg"
|
||||
--endpoint-url "$(printenv R2_ENDPOINT)"
|
||||
|
||||
4) The public URL will be:
|
||||
$(printenv R2_PUBLIC_URL)/<branch>/diagram.svg
|
||||
|
||||
5) Include in PR descriptions using markdown image syntax.
|
||||
|
||||
IMPORTANT: Read docs/autonomous-mode.md before starting any work.
|
||||
|
||||
linkedRepos:
|
||||
- repo: windmill-labs/windmill-ee-private
|
||||
alias: ee
|
||||
81
CHANGELOG.md
81
CHANGELOG.md
@@ -1,5 +1,86 @@
|
||||
# Changelog
|
||||
|
||||
## [1.651.1](https://github.com/windmill-labs/windmill/compare/v1.651.0...v1.651.1) (2026-03-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* prevent slow loading toast interval from leaking on promise cancellation ([#8240](https://github.com/windmill-labs/windmill/issues/8240)) ([2e582b1](https://github.com/windmill-labs/windmill/commit/2e582b1bc1c299388a3c97cfddff9d0eb92858f2))
|
||||
* suppress unused variable warnings on windows builds ([#8241](https://github.com/windmill-labs/windmill/issues/8241)) ([2d58382](https://github.com/windmill-labs/windmill/commit/2d583826dc065c05684d4cd1d1510f0d1f2d9ae9))
|
||||
|
||||
## [1.651.0](https://github.com/windmill-labs/windmill/compare/v1.650.0...v1.651.0) (2026-03-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add sandbox annotations, volume mounts, for AI sandbox starting with claude ([#8058](https://github.com/windmill-labs/windmill/issues/8058)) ([5f0ef93](https://github.com/windmill-labs/windmill/commit/5f0ef936d1d5d07d01c8e07e26ec254feebef8fb))
|
||||
* hash-based MCP tool names for long paths ([#8133](https://github.com/windmill-labs/windmill/issues/8133)) ([ce041e8](https://github.com/windmill-labs/windmill/commit/ce041e8a5e7ff105df389875d9981f3843d4ce39))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **python-client:** add delete_s3_object ([#8216](https://github.com/windmill-labs/windmill/issues/8216)) ([90f4c64](https://github.com/windmill-labs/windmill/commit/90f4c64ee12e1d04ce846ff88d6658f667e194e0))
|
||||
* update CLI bun template to match UI template ([#8238](https://github.com/windmill-labs/windmill/issues/8238)) ([a8cbe93](https://github.com/windmill-labs/windmill/commit/a8cbe9396ffc51140dce5582d57f4dc59873304e))
|
||||
* write fallback package.json for codebase mode nsjail ([#8239](https://github.com/windmill-labs/windmill/issues/8239)) ([d46913b](https://github.com/windmill-labs/windmill/commit/d46913b74a0ffd41d2323e0355cc81954f09e29d))
|
||||
|
||||
## [1.650.0](https://github.com/windmill-labs/windmill/compare/v1.649.0...v1.650.0) (2026-03-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add move, delete, and duplicate to flow node context menu ([#8050](https://github.com/windmill-labs/windmill/issues/8050)) ([c0c9388](https://github.com/windmill-labs/windmill/commit/c0c9388415716ce77d841bd08a46f94e0a529685))
|
||||
* add variable and resource types to flow env variables ([#8214](https://github.com/windmill-labs/windmill/issues/8214)) ([164e499](https://github.com/windmill-labs/windmill/commit/164e499c64dc5eb76fcfb0f8cefbad2df244f610))
|
||||
* Ducklake typechecker ([#8118](https://github.com/windmill-labs/windmill/issues/8118)) ([53caecf](https://github.com/windmill-labs/windmill/commit/53caecf1da8d76e246178dfb9b86d330f0ec52fd))
|
||||
* make WINDMILL_DIR configurable via environment variable ([#8215](https://github.com/windmill-labs/windmill/issues/8215)) ([424ca59](https://github.com/windmill-labs/windmill/commit/424ca59dfe3e730f5388d9cac4ea7e69773614d3))
|
||||
* make WM_END_USER_EMAIL display users from different workspaces ([#8208](https://github.com/windmill-labs/windmill/issues/8208)) ([baf2bcf](https://github.com/windmill-labs/windmill/commit/baf2bcf14da0c8c95bdbbf511fcaee48be33948b))
|
||||
* persistent Db manager state in URI ([#8134](https://github.com/windmill-labs/windmill/issues/8134)) ([4bf827b](https://github.com/windmill-labs/windmill/commit/4bf827bea4d44aca8c5ff7aa67ad449dbcf00673))
|
||||
* replace hub error toasts with warning alerts and add disable hub setting ([#8225](https://github.com/windmill-labs/windmill/issues/8225)) ([63ebae8](https://github.com/windmill-labs/windmill/commit/63ebae8829a6dc47a4e23c8670b514f042c9d4be))
|
||||
* token expiration notifications ([#8190](https://github.com/windmill-labs/windmill/issues/8190)) ([e56ccd2](https://github.com/windmill-labs/windmill/commit/e56ccd200be29e6ac8ea2b04a341b1ce78a307f6))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* handle multipart stream errors gracefully instead of panicking ([#8226](https://github.com/windmill-labs/windmill/issues/8226)) ([19c065b](https://github.com/windmill-labs/windmill/commit/19c065bed5468c484c8e7a50a6b79ab90153cc0e))
|
||||
* improve windows compatibility ([077779e](https://github.com/windmill-labs/windmill/commit/077779ec52f7d3e5fcc93951544bf47bd6dc30b6))
|
||||
* wrap set_encryption_key in a single database transaction ([#8212](https://github.com/windmill-labs/windmill/issues/8212)) ([62382fd](https://github.com/windmill-labs/windmill/commit/62382fd2869ea0190dd0c0b714f9cbd35ceddd7a))
|
||||
|
||||
## [1.649.0](https://github.com/windmill-labs/windmill/compare/v1.648.0...v1.649.0) (2026-03-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** add script recorder for offline replay ([#8200](https://github.com/windmill-labs/windmill/issues/8200)) ([c97d8b4](https://github.com/windmill-labs/windmill/commit/c97d8b4715f86ea83ab2c0223ba859ced690829a))
|
||||
* move index management out of /srch/, add storage size reporting ([#8169](https://github.com/windmill-labs/windmill/issues/8169)) ([ee01acd](https://github.com/windmill-labs/windmill/commit/ee01acd9a6a2cd68a3f226988bfb46f6a6e64c08))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* clean up slow-load toast interval on component destroy ([#8207](https://github.com/windmill-labs/windmill/issues/8207)) ([26f4f2b](https://github.com/windmill-labs/windmill/commit/26f4f2b399b828185b553289d6560e12261030a3))
|
||||
* **frontend:** prevent subflow expansion from hiding all insertion points ([#8203](https://github.com/windmill-labs/windmill/issues/8203)) ([e97da86](https://github.com/windmill-labs/windmill/commit/e97da860672171e33054a77d71f4824bb09e540d))
|
||||
* gracefully handle malformed OAuth entries in instance config ([#8205](https://github.com/windmill-labs/windmill/issues/8205)) ([cac4bdd](https://github.com/windmill-labs/windmill/commit/cac4bdd54f0c3ea80844ac31f7597f418ff7d8ae))
|
||||
* skip stop_after_if evaluation for skipped (identity) flow steps ([#8201](https://github.com/windmill-labs/windmill/issues/8201)) ([e6f7775](https://github.com/windmill-labs/windmill/commit/e6f7775d4d9a052aefc37260c6ed161146841cd7))
|
||||
* use exact matching for python requirements directive parsing ([#8199](https://github.com/windmill-labs/windmill/issues/8199)) ([2b2be38](https://github.com/windmill-labs/windmill/commit/2b2be38f129bbe58b6bb3815c4bd94aa03a3da90))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* use two-step query in input history to leverage v2_job index ([#8197](https://github.com/windmill-labs/windmill/issues/8197)) ([50defdd](https://github.com/windmill-labs/windmill/commit/50defdded113b4d2cf0991b3fb642d1cd9a462b7))
|
||||
|
||||
## [1.648.0](https://github.com/windmill-labs/windmill/compare/v1.647.2...v1.648.0) (2026-03-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add right-click context menu to ObjectViewer ([#8181](https://github.com/windmill-labs/windmill/issues/8181)) ([1855204](https://github.com/windmill-labs/windmill/commit/18552046c29878b5cf115b9364c2ce829ab7aa59))
|
||||
* **frontend:** add drag-and-drop node movement in flow editor ([#8076](https://github.com/windmill-labs/windmill/issues/8076)) ([7a5e487](https://github.com/windmill-labs/windmill/commit/7a5e48787860c38aa3589c49ea9a70654d479c8a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't insert underscore after digit in PascalCase to snake_case conversion ([#8184](https://github.com/windmill-labs/windmill/issues/8184)) ([a111653](https://github.com/windmill-labs/windmill/commit/a111653c6d32fd1a3d2f45351eceb8d8d7df6f41))
|
||||
* **frontend:** preserve keycloak realm url between instance settings saves ([#8189](https://github.com/windmill-labs/windmill/issues/8189)) ([cfd9541](https://github.com/windmill-labs/windmill/commit/cfd9541ab1daf635c7d801cd3a7788db57b98257))
|
||||
* preserve debouncing settings for post-preprocessing arg accumulation ([#8191](https://github.com/windmill-labs/windmill/issues/8191)) ([9e92445](https://github.com/windmill-labs/windmill/commit/9e92445faed1a10b2406b97562e8df7a5b2dfd76))
|
||||
|
||||
## [1.647.2](https://github.com/windmill-labs/windmill/compare/v1.647.1...v1.647.2) (2026-03-02)
|
||||
|
||||
|
||||
|
||||
21
CLAUDE.md
21
CLAUDE.md
@@ -26,6 +26,27 @@ Open-source platform for internal tools, workflows, API integrations, background
|
||||
- **Login**: `admin@windmill.dev` / `changeme`
|
||||
- **Instance settings**: navigate to `/#superadmin-settings`
|
||||
|
||||
## Banned Patterns
|
||||
|
||||
### `$bindable(default_value)` on optional props
|
||||
|
||||
Using `$bindable(default_value)` on props that can be `undefined` is **banned**. This pattern causes subtle bugs because the default value masks the `undefined` state.
|
||||
|
||||
**Bad:**
|
||||
```svelte
|
||||
let { my_prop = $bindable(default_value) }: { my_prop?: string } = $props()
|
||||
```
|
||||
|
||||
**Correct alternatives:**
|
||||
|
||||
1. **Use `$derived` with nullish coalescing** — handle the potential `undefined` at the usage site:
|
||||
```svelte
|
||||
let { my_prop = $bindable() }: { my_prop?: string } = $props()
|
||||
let effective_value = $derived(my_prop ?? default_value)
|
||||
```
|
||||
|
||||
2. **Create a `useMyPropState()` helper** — encapsulate the undefined-handling logic in a reusable function and call it higher in the component tree, so the child component always receives a defined value.
|
||||
|
||||
## Core Principles
|
||||
|
||||
- Search for existing code to reuse before writing new code
|
||||
|
||||
@@ -262,6 +262,12 @@ COPY --from=oven/bun:1.3.10 /usr/local/bin/bun /usr/bin/bun
|
||||
RUN bun install -g windmill-cli \
|
||||
&& ln -s $(bun pm bin -g)/wmill /usr/bin/wmill
|
||||
|
||||
# Install Claude Code CLI (used by claude sandbox scripts)
|
||||
# The installer puts the binary in ~/.local/bin/claude (symlink to ~/.local/share/claude/versions/*)
|
||||
# Copy it to /usr/bin/claude so it's accessible inside nsjail sandbox (which mounts /usr but not /root)
|
||||
RUN curl -fsSL https://claude.ai/install.sh | bash \
|
||||
&& cp /root/.local/share/claude/versions/* /usr/bin/claude
|
||||
|
||||
COPY --from=php:8.3.7-cli /usr/local/bin/php /usr/bin/php
|
||||
COPY --from=composer:2.7.6 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
|
||||
16
backend/.sqlx/query-00bf3dbd9d3f51dd7fdefcbd654d55e0379cc84188954037165cbe2d198ef71f.json
generated
Normal file
16
backend/.sqlx/query-00bf3dbd9d3f51dd7fdefcbd654d55e0379cc84188954037165cbe2d198ef71f.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE volume SET lease_until = now() + interval '60 seconds'\n WHERE workspace_id = $1 AND name = $2 AND leased_by = $3 AND lease_until > now()",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "00bf3dbd9d3f51dd7fdefcbd654d55e0379cc84188954037165cbe2d198ef71f"
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value FROM variable WHERE workspace_id = $1 AND path = $2",
|
||||
"query": "SELECT group_ FROM usr_to_group WHERE usr = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value",
|
||||
"name": "group_",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
@@ -19,5 +19,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2c0ab7571e1a7c4290315bc3efccb4db9e0c9aee05596a594f81975a0cdb74d1"
|
||||
"hash": "015a8551c646f9b027fc23752c5c5c81e520e3ca97dd1cd1e4ebfe3e46c4ad11"
|
||||
}
|
||||
22
backend/.sqlx/query-083d69abc8a662bb364cf43b8ffc6e9b159a54c179cecb108068597536835f7e.json
generated
Normal file
22
backend/.sqlx/query-083d69abc8a662bb364cf43b8ffc6e9b159a54c179cecb108068597536835f7e.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT large_file_storage->>'volume_storage' FROM workspace_settings WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "083d69abc8a662bb364cf43b8ffc6e9b159a54c179cecb108068597536835f7e"
|
||||
}
|
||||
23
backend/.sqlx/query-0afd4ae50ff7e1b0dcca4b483816c595401dd2e1f7699a28bf3b79db5e3841f4.json
generated
Normal file
23
backend/.sqlx/query-0afd4ae50ff7e1b0dcca4b483816c595401dd2e1f7699a28bf3b79db5e3841f4.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT extra_perms FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0afd4ae50ff7e1b0dcca4b483816c595401dd2e1f7699a28bf3b79db5e3841f4"
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT token\n FROM token\n WHERE token LIKE concat($1::text, '%')\n LIMIT 1\n ",
|
||||
"query": "SELECT created_by FROM volume WHERE name = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"name": "created_by",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
@@ -18,5 +19,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "90092c0b3f7612373fcc8fb7a966200118ab308430d4a0cbb5cb16c397246492"
|
||||
"hash": "0eb54f04a8185085b3f80772f5c28e666f6fbd1ec5ee9d30ee0cdb5e30a68750"
|
||||
}
|
||||
25
backend/.sqlx/query-14004a7c1641a3157eddd571fea11a1dfb1422187200119268b2342b47a960c6.json
generated
Normal file
25
backend/.sqlx/query-14004a7c1641a3157eddd571fea11a1dfb1422187200119268b2342b47a960c6.json
generated
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO volume (workspace_id, name, size_bytes, created_by, lease_until, leased_by)\n VALUES ($1, $2, 0, $3, now() + interval '60 seconds', $4)\n ON CONFLICT (workspace_id, name) DO UPDATE\n SET lease_until = now() + interval '60 seconds', leased_by = $4\n WHERE volume.lease_until IS NULL OR volume.lease_until < now()\n RETURNING name",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "14004a7c1641a3157eddd571fea11a1dfb1422187200119268b2342b47a960c6"
|
||||
}
|
||||
17
backend/.sqlx/query-181e6fca7e0d0fd88eccd79303f0339b1f2194c52f6bd1245dfa8ff3f0db4051.json
generated
Normal file
17
backend/.sqlx/query-181e6fca7e0d0fd88eccd79303f0339b1f2194c52f6bd1245dfa8ff3f0db4051.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path, preprocessed)\n VALUES ($1, 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', $2, $3, $4)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "181e6fca7e0d0fd88eccd79303f0339b1f2194c52f6bd1245dfa8ff3f0db4051"
|
||||
}
|
||||
22
backend/.sqlx/query-19a7ebb2e7e8e57b6e7c974da8eb7c6841a5c4ff12ba7c12c73d691c49dd99ed.json
generated
Normal file
22
backend/.sqlx/query-19a7ebb2e7e8e57b6e7c974da8eb7c6841a5c4ff12ba7c12c73d691c49dd99ed.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT email FROM token WHERE token = $1 AND (expiration > NOW() OR expiration IS NULL)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "19a7ebb2e7e8e57b6e7c974da8eb7c6841a5c4ff12ba7c12c73d691c49dd99ed"
|
||||
}
|
||||
15
backend/.sqlx/query-1d2f765c2a71e1154ca5d9f5e52ef31e6d647377d37747f7bdc834748a59419e.json
generated
Normal file
15
backend/.sqlx/query-1d2f765c2a71e1154ca5d9f5e52ef31e6d647377d37747f7bdc834748a59419e.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE volume SET last_used_at = now() WHERE workspace_id = $1 AND name = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1d2f765c2a71e1154ca5d9f5e52ef31e6d647377d37747f7bdc834748a59419e"
|
||||
}
|
||||
17
backend/.sqlx/query-1e9b9a02f45e6200f4d101bd5336fc8ce983f857339e6fccf799dc6587964aab.json
generated
Normal file
17
backend/.sqlx/query-1e9b9a02f45e6200f4d101bd5336fc8ce983f857339e6fccf799dc6587964aab.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO volume (workspace_id, name, size_bytes, created_by, last_used_at)\n VALUES ($1, $2, $3, $4, now())\n ON CONFLICT (workspace_id, name) DO UPDATE\n SET size_bytes = $3, last_used_at = now()",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1e9b9a02f45e6200f4d101bd5336fc8ce983f857339e6fccf799dc6587964aab"
|
||||
}
|
||||
25
backend/.sqlx/query-23f47f5207abe0cfaede197aeee485957990eb92fa3ce515895eab0d3f28bfdc.json
generated
Normal file
25
backend/.sqlx/query-23f47f5207abe0cfaede197aeee485957990eb92fa3ce515895eab0d3f28bfdc.json
generated
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO volume (workspace_id, name, size_bytes, created_by, lease_until, leased_by)\n VALUES ($1, $2, 0, $3, now() + interval '60 seconds', $4)\n ON CONFLICT (workspace_id, name) DO UPDATE\n SET lease_until = now() + interval '60 seconds', leased_by = $4\n WHERE volume.lease_until IS NULL OR volume.lease_until < now()\n RETURNING name",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "23f47f5207abe0cfaede197aeee485957990eb92fa3ce515895eab0d3f28bfdc"
|
||||
}
|
||||
16
backend/.sqlx/query-28df7bbe1f54f69640bc76def9e580b4c7ba25f279644e3233b63f4f6db0ad98.json
generated
Normal file
16
backend/.sqlx/query-28df7bbe1f54f69640bc76def9e580b4c7ba25f279644e3233b63f4f6db0ad98.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE volume SET lease_until = NULL, leased_by = NULL\n WHERE workspace_id = $1 AND name = $2 AND leased_by = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "28df7bbe1f54f69640bc76def9e580b4c7ba25f279644e3233b63f4f6db0ad98"
|
||||
}
|
||||
15
backend/.sqlx/query-2c503e1e8ee0863b3a6274874ef9b9a10b31dbbe2a676a50d1bbfb2e9e0ab7e0.json
generated
Normal file
15
backend/.sqlx/query-2c503e1e8ee0863b3a6274874ef9b9a10b31dbbe2a676a50d1bbfb2e9e0ab7e0.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag, running)\n VALUES ($1, $2, now(), 'flow', false)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2c503e1e8ee0863b3a6274874ef9b9a10b31dbbe2a676a50d1bbfb2e9e0ab7e0"
|
||||
}
|
||||
24
backend/.sqlx/query-2f53576c2ad58abc24617e911e486d7c4b9bdb1e8fb1f7725060990ef8984943.json
generated
Normal file
24
backend/.sqlx/query-2f53576c2ad58abc24617e911e486d7c4b9bdb1e8fb1f7725060990ef8984943.json
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n CASE\n WHEN flow_version.id IS NOT NULL THEN\n flow_version.value -> 'flow_env' -> $3\n ELSE\n root_job.raw_flow -> 'flow_env' -> $3\n END AS \"flow_env: sqlx::types::Json<Box<RawValue>>\"\n FROM\n v2_job current_job\n JOIN\n v2_job root_job ON root_job.id = COALESCE(current_job.root_job, current_job.flow_innermost_root_job, current_job.parent_job, current_job.id)\n AND root_job.workspace_id = current_job.workspace_id\n LEFT JOIN\n flow_version ON flow_version.id = root_job.runnable_id\n AND flow_version.path = root_job.runnable_path\n AND flow_version.workspace_id = root_job.workspace_id\n WHERE\n current_job.id = $1 AND\n current_job.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "flow_env: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "2f53576c2ad58abc24617e911e486d7c4b9bdb1e8fb1f7725060990ef8984943"
|
||||
}
|
||||
14
backend/.sqlx/query-380ca9ebea53d5c016e4e76797cc103178ac4a25fc2842a13ce19b1ec4445c9d.json
generated
Normal file
14
backend/.sqlx/query-380ca9ebea53d5c016e4e76797cc103178ac4a25fc2842a13ce19b1ec4445c9d.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO global_settings (name, value) VALUES ('indexer_settings', $1)\n ON CONFLICT (name) DO UPDATE SET value = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "380ca9ebea53d5c016e4e76797cc103178ac4a25fc2842a13ce19b1ec4445c9d"
|
||||
}
|
||||
18
backend/.sqlx/query-3955e57e216d169c30b1548a2252eb169329116cba57780fa90ecf2bdb910f34.json
generated
Normal file
18
backend/.sqlx/query-3955e57e216d169c30b1548a2252eb169329116cba57780fa90ecf2bdb910f34.json
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE volume\n SET size_bytes = $3, file_count = $4,\n updated_at = now(), updated_by = $5, last_used_at = now(),\n lease_until = NULL, leased_by = NULL\n WHERE workspace_id = $1 AND name = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int4",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3955e57e216d169c30b1548a2252eb169329116cba57780fa90ecf2bdb910f34"
|
||||
}
|
||||
76
backend/.sqlx/query-40d0f6dca30456514cb85e36c6e367b27171894016c714e41497e69115be1468.json
generated
Normal file
76
backend/.sqlx/query-40d0f6dca30456514cb85e36c6e367b27171894016c714e41497e69115be1468.json
generated
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n name as \"name!\",\n size_bytes as \"size_bytes!\",\n file_count as \"file_count!\",\n created_at as \"created_at!\",\n created_by as \"created_by!\",\n updated_at,\n updated_by,\n description as \"description!\",\n last_used_at,\n extra_perms as \"extra_perms!\"\n FROM (\n SELECT\n COALESCE(v.name, a.path) as name,\n COALESCE(v.size_bytes, 0) as size_bytes,\n COALESCE(v.file_count, 0) as file_count,\n COALESCE(v.created_at, a.min_created_at) as created_at,\n COALESCE(v.created_by, 'unknown') as created_by,\n v.updated_at,\n v.updated_by,\n COALESCE(v.description, '') as description,\n v.last_used_at,\n COALESCE(v.extra_perms, '{}'::jsonb) as extra_perms\n FROM (\n SELECT path, MIN(created_at) as min_created_at\n FROM asset\n WHERE workspace_id = $1 AND kind = 'volume'\n GROUP BY path\n ) a\n FULL OUTER JOIN volume v ON v.workspace_id = $1 AND v.name = a.path\n WHERE v.workspace_id = $1 OR a.path IS NOT NULL\n ) combined\n ORDER BY name",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "size_bytes!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "file_count!",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at!",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "created_by!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "updated_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "description!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "last_used_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "extra_perms!",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "40d0f6dca30456514cb85e36c6e367b27171894016c714e41497e69115be1468"
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
23
backend/.sqlx/query-5af44b46a2e2f1a9adeb39013790be7046cf8789d842717b6c793c22a2a05daa.json
generated
Normal file
23
backend/.sqlx/query-5af44b46a2e2f1a9adeb39013790be7046cf8789d842717b6c793c22a2a05daa.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM volume WHERE workspace_id = $1 AND name = $2\n AND (lease_until IS NULL OR lease_until < now())\n RETURNING name",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5af44b46a2e2f1a9adeb39013790be7046cf8789d842717b6c793c22a2a05daa"
|
||||
}
|
||||
29
backend/.sqlx/query-6086849bb08e1b37d6693d2808767cd897dca4722e4f2076308afdb7ee9fc147.json
generated
Normal file
29
backend/.sqlx/query-6086849bb08e1b37d6693d2808767cd897dca4722e4f2076308afdb7ee9fc147.json
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT created_by, extra_perms FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "created_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6086849bb08e1b37d6693d2808767cd897dca4722e4f2076308afdb7ee9fc147"
|
||||
}
|
||||
22
backend/.sqlx/query-712092e5033bc6894025a55ebc58bca8450d09982e582266d215dff521256fa6.json
generated
Normal file
22
backend/.sqlx/query-712092e5033bc6894025a55ebc58bca8450d09982e582266d215dff521256fa6.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT count(*) FROM volume WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "712092e5033bc6894025a55ebc58bca8450d09982e582266d215dff521256fa6"
|
||||
}
|
||||
18
backend/.sqlx/query-75a03e9e4cba350a104e2e3a95de919cd25538c0b433bc29bb052c7a7b8568ca.json
generated
Normal file
18
backend/.sqlx/query-75a03e9e4cba350a104e2e3a95de919cd25538c0b433bc29bb052c7a7b8568ca.json
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE volume\n SET size_bytes = $3, file_count = $4,\n updated_at = now(), last_used_at = now(),\n lease_until = NULL, leased_by = NULL\n WHERE workspace_id = $1 AND name = $2 AND leased_by = $5",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int4",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "75a03e9e4cba350a104e2e3a95de919cd25538c0b433bc29bb052c7a7b8568ca"
|
||||
}
|
||||
16
backend/.sqlx/query-769035629df5a5034f64bf38992e142006825a3911addacdf1a026660b5e2b7f.json
generated
Normal file
16
backend/.sqlx/query-769035629df5a5034f64bf38992e142006825a3911addacdf1a026660b5e2b7f.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE volume SET lease_until = now() + interval '60 seconds'\n WHERE workspace_id = $1 AND name = $2 AND leased_by = $3 AND lease_until > now()",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "769035629df5a5034f64bf38992e142006825a3911addacdf1a026660b5e2b7f"
|
||||
}
|
||||
24
backend/.sqlx/query-78af8bdb6a3ee6396c54f87ff6403b566fc75e16e0b7a81204816fd50b3346a5.json
generated
Normal file
24
backend/.sqlx/query-78af8bdb6a3ee6396c54f87ff6403b566fc75e16e0b7a81204816fd50b3346a5.json
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM volume WHERE workspace_id = $1 AND name = $2 AND lease_until > now() AND leased_by = $3)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "78af8bdb6a3ee6396c54f87ff6403b566fc75e16e0b7a81204816fd50b3346a5"
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH job_result AS (\n SELECT result\n FROM v2_job_completed\n WHERE id = $1\n ),\n updated_queue AS (\n UPDATE v2_job_queue\n SET running = false,\n tag = COALESCE($3, tag),\n scheduled_for = COALESCE($6, scheduled_for)\n WHERE id = $2\n )\n UPDATE v2_job\n SET\n tag = COALESCE($3, tag),\n concurrent_limit = COALESCE($4, concurrent_limit),\n concurrency_time_window_s = COALESCE($5, concurrency_time_window_s),\n args = COALESCE(\n CASE\n WHEN job_result.result IS NULL THEN NULL\n WHEN jsonb_typeof(job_result.result) = 'object'\n THEN job_result.result\n WHEN jsonb_typeof(job_result.result) = 'null'\n THEN NULL\n ELSE jsonb_build_object('value', job_result.result)\n END,\n '{}'::jsonb\n ),\n preprocessed = TRUE\n FROM job_result\n WHERE v2_job.id = $2;\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "79b437ad31ddab94310989b8fb6a1c130b9be1ab4b6a100fffffd687677b9c92"
|
||||
}
|
||||
16
backend/.sqlx/query-7ce06d4f623932fce12352be3a09ba8973a2ef1defa36c6d46d9c1c6406a7c33.json
generated
Normal file
16
backend/.sqlx/query-7ce06d4f623932fce12352be3a09ba8973a2ef1defa36c6d46d9c1c6406a7c33.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE volume SET lease_until = NULL, leased_by = NULL\n WHERE workspace_id = $1 AND name = $2 AND leased_by = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7ce06d4f623932fce12352be3a09ba8973a2ef1defa36c6d46d9c1c6406a7c33"
|
||||
}
|
||||
17
backend/.sqlx/query-7e8e79a7d140be511cedbfe9ff8eea76a8a3079ce80c035087f797cdc410f35b.json
generated
Normal file
17
backend/.sqlx/query-7e8e79a7d140be511cedbfe9ff8eea76a8a3079ce80c035087f797cdc410f35b.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO volume (workspace_id, name, size_bytes, created_by)\n VALUES ($1, $2, $3, $4)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7e8e79a7d140be511cedbfe9ff8eea76a8a3079ce80c035087f797cdc410f35b"
|
||||
}
|
||||
23
backend/.sqlx/query-803abdcd3614437b26c5d2e4f1ad75ca7014b431239ac1b681f2b26380c719c4.json
generated
Normal file
23
backend/.sqlx/query-803abdcd3614437b26c5d2e4f1ad75ca7014b431239ac1b681f2b26380c719c4.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT last_used_at FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "last_used_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "803abdcd3614437b26c5d2e4f1ad75ca7014b431239ac1b681f2b26380c719c4"
|
||||
}
|
||||
16
backend/.sqlx/query-82b3bd95e5d28c4cd4eedcae8cf050ba7b7e4d9eabba03be251ae9a8017b317d.json
generated
Normal file
16
backend/.sqlx/query-82b3bd95e5d28c4cd4eedcae8cf050ba7b7e4d9eabba03be251ae9a8017b317d.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE volume SET extra_perms = extra_perms - $1\n WHERE workspace_id = $2 AND name = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "82b3bd95e5d28c4cd4eedcae8cf050ba7b7e4d9eabba03be251ae9a8017b317d"
|
||||
}
|
||||
23
backend/.sqlx/query-88e25dc24bb06237b3677c947ee53fd6e9c7606231ad3c522e98cb1fcc14361a.json
generated
Normal file
23
backend/.sqlx/query-88e25dc24bb06237b3677c947ee53fd6e9c7606231ad3c522e98cb1fcc14361a.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT leased_by FROM volume WHERE workspace_id = $1 AND name = $2 AND lease_until > now()",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "leased_by",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "88e25dc24bb06237b3677c947ee53fd6e9c7606231ad3c522e98cb1fcc14361a"
|
||||
}
|
||||
23
backend/.sqlx/query-907241c195fea227e4a945ee472425e5f7600e28c728a06235f7ff430a4bd77a.json
generated
Normal file
23
backend/.sqlx/query-907241c195fea227e4a945ee472425e5f7600e28c728a06235f7ff430a4bd77a.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT count(*) FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "907241c195fea227e4a945ee472425e5f7600e28c728a06235f7ff430a4bd77a"
|
||||
}
|
||||
23
backend/.sqlx/query-94d6f598076ad67d68e6f01926c9fc2c73e855790e17abf5461b96ea30fbbdb7.json
generated
Normal file
23
backend/.sqlx/query-94d6f598076ad67d68e6f01926c9fc2c73e855790e17abf5461b96ea30fbbdb7.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT size_bytes FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "size_bytes",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "94d6f598076ad67d68e6f01926c9fc2c73e855790e17abf5461b96ea30fbbdb7"
|
||||
}
|
||||
16
backend/.sqlx/query-9662f1e304124fa52db4aa1e80e03b2601630f2d31458bdaf70c2702b2998d89.json
generated
Normal file
16
backend/.sqlx/query-9662f1e304124fa52db4aa1e80e03b2601630f2d31458bdaf70c2702b2998d89.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE volume SET lease_until = NULL, leased_by = NULL\n WHERE workspace_id = $1 AND name = $2 AND leased_by = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9662f1e304124fa52db4aa1e80e03b2601630f2d31458bdaf70c2702b2998d89"
|
||||
}
|
||||
17
backend/.sqlx/query-9c76a980bf1e3b79ab26c79aee19e5552aa16eb3626618da4dbb44ed18efee60.json
generated
Normal file
17
backend/.sqlx/query-9c76a980bf1e3b79ab26c79aee19e5552aa16eb3626618da4dbb44ed18efee60.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path, args)\n VALUES ($1, 'script', 'deno', 'test-user', 'u/test-user', 'test@windmill.dev', $2, $3, $4)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9c76a980bf1e3b79ab26c79aee19e5552aa16eb3626618da4dbb44ed18efee60"
|
||||
}
|
||||
15
backend/.sqlx/query-9e30b5545a51453205a713a6276156ada29ae320465d9790dce7e1e8a436d4de.json
generated
Normal file
15
backend/.sqlx/query-9e30b5545a51453205a713a6276156ada29ae320465d9790dce7e1e8a436d4de.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9e30b5545a51453205a713a6276156ada29ae320465d9790dce7e1e8a436d4de"
|
||||
}
|
||||
29
backend/.sqlx/query-9f64d6ed0adb609ced1551563062550919fcac56deaf1b3cb36b3e15117936e7.json
generated
Normal file
29
backend/.sqlx/query-9f64d6ed0adb609ced1551563062550919fcac56deaf1b3cb36b3e15117936e7.json
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT extra_perms, created_by FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "created_by",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9f64d6ed0adb609ced1551563062550919fcac56deaf1b3cb36b3e15117936e7"
|
||||
}
|
||||
24
backend/.sqlx/query-a3970c15271a124307301c0dafa263e7168fa325c5ceb44e9dd1595bdb7e7ce6.json
generated
Normal file
24
backend/.sqlx/query-a3970c15271a124307301c0dafa263e7168fa325c5ceb44e9dd1595bdb7e7ce6.json
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO volume (workspace_id, name, size_bytes, created_by)\n VALUES ($1, $2, 0, $3)\n ON CONFLICT (workspace_id, name) DO NOTHING\n RETURNING name",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a3970c15271a124307301c0dafa263e7168fa325c5ceb44e9dd1595bdb7e7ce6"
|
||||
}
|
||||
15
backend/.sqlx/query-a4d973d0f1c293345ad2bfd2472da8d6a3b425ea0590a66f1db6692dd2ddb437.json
generated
Normal file
15
backend/.sqlx/query-a4d973d0f1c293345ad2bfd2472da8d6a3b425ea0590a66f1db6692dd2ddb437.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO token_expiry_notification (token, expiration) VALUES ($1, $2) ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a4d973d0f1c293345ad2bfd2472da8d6a3b425ea0590a66f1db6692dd2ddb437"
|
||||
}
|
||||
12
backend/.sqlx/query-a6b1c8808c892e62ae4ba04171d856a39c89cdc658b09c478050de5145a45ca4.json
generated
Normal file
12
backend/.sqlx/query-a6b1c8808c892e62ae4ba04171d856a39c89cdc658b09c478050de5145a45ca4.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM token_expiry_notification WHERE expiration <= now()",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a6b1c8808c892e62ae4ba04171d856a39c89cdc658b09c478050de5145a45ca4"
|
||||
}
|
||||
17
backend/.sqlx/query-ab8daa93bc66d0142b9e9e8d7fa6719fc41b2ca5cb0b7ac5ad73ab01b650c935.json
generated
Normal file
17
backend/.sqlx/query-ab8daa93bc66d0142b9e9e8d7fa6719fc41b2ca5cb0b7ac5ad73ab01b650c935.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO volume (workspace_id, name, size_bytes, created_by)\n VALUES ($1, $2, $3, $4)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ab8daa93bc66d0142b9e9e8d7fa6719fc41b2ca5cb0b7ac5ad73ab01b650c935"
|
||||
}
|
||||
38
backend/.sqlx/query-bb446cbb20166f274a7ee6e88abaa27e233e60e18b3d35545005eb680701241f.json
generated
Normal file
38
backend/.sqlx/query-bb446cbb20166f274a7ee6e88abaa27e233e60e18b3d35545005eb680701241f.json
generated
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM token WHERE expiration <= now()\n RETURNING substring(token for 10) as token_prefix, label, email, workspace_id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token_prefix",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "label",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bb446cbb20166f274a7ee6e88abaa27e233e60e18b3d35545005eb680701241f"
|
||||
}
|
||||
29
backend/.sqlx/query-bc61ca62d8f71880facb5d701a6e78697414b35618c50f8693f4e804bf1d7dbb.json
generated
Normal file
29
backend/.sqlx/query-bc61ca62d8f71880facb5d701a6e78697414b35618c50f8693f4e804bf1d7dbb.json
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT size_bytes, last_used_at FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "size_bytes",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "last_used_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bc61ca62d8f71880facb5d701a6e78697414b35618c50f8693f4e804bf1d7dbb"
|
||||
}
|
||||
34
backend/.sqlx/query-bcefd1ce47d05f2ce14493f0e7c4d4fea16c0cf71ddc233f6431cf624ecdfe60.json
generated
Normal file
34
backend/.sqlx/query-bcefd1ce47d05f2ce14493f0e7c4d4fea16c0cf71ddc233f6431cf624ecdfe60.json
generated
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, last_locked_at, owner FROM concurrency_locks WHERE id = ANY($1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "last_locked_at",
|
||||
"type_info": "Timestamp"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "owner",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bcefd1ce47d05f2ce14493f0e7c4d4fea16c0cf71ddc233f6431cf624ecdfe60"
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n CASE\n WHEN flow_version.id IS NOT NULL THEN\n (flow_version.value -> 'flow_env' -> $3) #> $4\n ELSE\n (root_job.raw_flow -> 'flow_env' -> $3) #> $4\n END AS \"flow_env: sqlx::types::Json<Box<RawValue>>\"\n FROM\n v2_job current_job\n JOIN\n v2_job root_job ON root_job.id = COALESCE(current_job.root_job, current_job.flow_innermost_root_job, current_job.parent_job, current_job.id)\n AND root_job.workspace_id = current_job.workspace_id\n LEFT JOIN\n flow_version ON flow_version.id = root_job.runnable_id\n AND flow_version.path = root_job.runnable_path\n AND flow_version.workspace_id = root_job.workspace_id\n WHERE\n current_job.id = $1 AND\n current_job.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "flow_env: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "c23bea7db9623a60683596b7d6e689e2c0100c1569436a01b207876aaa470154"
|
||||
}
|
||||
20
backend/.sqlx/query-c2a0605b07f5df8d972bc02cc23fe7def5e1ee8fdf6dfb68576d3b72aa03f666.json
generated
Normal file
20
backend/.sqlx/query-c2a0605b07f5df8d972bc02cc23fe7def5e1ee8fdf6dfb68576d3b72aa03f666.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH job_result AS (\n SELECT result\n FROM v2_job_completed\n WHERE id = $1\n ),\n updated_queue AS (\n UPDATE v2_job_queue\n SET running = false,\n tag = COALESCE($3, tag),\n scheduled_for = COALESCE($6, scheduled_for),\n runnable_settings_handle = COALESCE($7, runnable_settings_handle)\n WHERE id = $2\n )\n UPDATE v2_job\n SET\n tag = COALESCE($3, tag),\n concurrent_limit = COALESCE($4, concurrent_limit),\n concurrency_time_window_s = COALESCE($5, concurrency_time_window_s),\n args = COALESCE(\n CASE\n WHEN job_result.result IS NULL THEN NULL\n WHEN jsonb_typeof(job_result.result) = 'object'\n THEN job_result.result\n WHEN jsonb_typeof(job_result.result) = 'null'\n THEN NULL\n ELSE jsonb_build_object('value', job_result.result)\n END,\n '{}'::jsonb\n ),\n preprocessed = TRUE\n FROM job_result\n WHERE v2_job.id = $2;\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Timestamptz",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c2a0605b07f5df8d972bc02cc23fe7def5e1ee8fdf6dfb68576d3b72aa03f666"
|
||||
}
|
||||
15
backend/.sqlx/query-c31cf6239044615e1cc3743aa1c82cce96e1a23ada28107ffffc8b5546d48101.json
generated
Normal file
15
backend/.sqlx/query-c31cf6239044615e1cc3743aa1c82cce96e1a23ada28107ffffc8b5546d48101.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE v2_job SET args = $2 WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c31cf6239044615e1cc3743aa1c82cce96e1a23ada28107ffffc8b5546d48101"
|
||||
}
|
||||
47
backend/.sqlx/query-d0869a340c8f34ca7a560d3b4c0070c9f117da3dd00ce3247c54a61052a6809c.json
generated
Normal file
47
backend/.sqlx/query-d0869a340c8f34ca7a560d3b4c0070c9f117da3dd00ce3247c54a61052a6809c.json
generated
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace_id, name, size_bytes, created_by, last_used_at\n FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "size_bytes",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "last_used_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d0869a340c8f34ca7a560d3b4c0070c9f117da3dd00ce3247c54a61052a6809c"
|
||||
}
|
||||
23
backend/.sqlx/query-d1ad2baf5e3a6f45f1f079d494e8d6affad03a1f388024806a5de3f9cc939c04.json
generated
Normal file
23
backend/.sqlx/query-d1ad2baf5e3a6f45f1f079d494e8d6affad03a1f388024806a5de3f9cc939c04.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT leased_by FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "leased_by",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d1ad2baf5e3a6f45f1f079d494e8d6affad03a1f388024806a5de3f9cc939c04"
|
||||
}
|
||||
38
backend/.sqlx/query-d7e9b69fef8369117ce057d01d87288b39ea7c802007f112eb3d62230d07abb6.json
generated
Normal file
38
backend/.sqlx/query-d7e9b69fef8369117ce057d01d87288b39ea7c802007f112eb3d62230d07abb6.json
generated
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM token_expiry_notification n\n USING token t\n WHERE n.token = t.token\n AND n.expiration > now()\n AND n.expiration <= now() + interval '7 days'\n RETURNING substring(t.token for 10) as token_prefix, t.label, t.email, t.workspace_id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token_prefix",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "label",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d7e9b69fef8369117ce057d01d87288b39ea7c802007f112eb3d62230d07abb6"
|
||||
}
|
||||
28
backend/.sqlx/query-dc18db954239c4ebdd3b46cfd34f33554794444f0dc4e2d2fec158eca5ebe865.json
generated
Normal file
28
backend/.sqlx/query-dc18db954239c4ebdd3b46cfd34f33554794444f0dc4e2d2fec158eca5ebe865.json
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT name, size_bytes FROM volume WHERE workspace_id = $1 ORDER BY name",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "size_bytes",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "dc18db954239c4ebdd3b46cfd34f33554794444f0dc4e2d2fec158eca5ebe865"
|
||||
}
|
||||
17
backend/.sqlx/query-eb79db2aeac7bf246ad56a5f116511b9d3183cb91b740a86944a77a2a964b57d.json
generated
Normal file
17
backend/.sqlx/query-eb79db2aeac7bf246ad56a5f116511b9d3183cb91b740a86944a77a2a964b57d.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE volume SET extra_perms = jsonb_set(extra_perms, $1, to_jsonb($2::bool), true)\n WHERE workspace_id = $3 AND name = $4",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "eb79db2aeac7bf246ad56a5f116511b9d3183cb91b740a86944a77a2a964b57d"
|
||||
}
|
||||
22
backend/.sqlx/query-eba16eb819e2644284fb073c891706d78a6f24cb0e614d7d81ba1b643805bf06.json
generated
Normal file
22
backend/.sqlx/query-eba16eb819e2644284fb073c891706d78a6f24cb0e614d7d81ba1b643805bf06.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT token as \"token!\"\n FROM token\n WHERE token LIKE concat($1::text, '%')\n LIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token!",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "eba16eb819e2644284fb073c891706d78a6f24cb0e614d7d81ba1b643805bf06"
|
||||
}
|
||||
23
backend/.sqlx/query-f0ac12b66c5d3cca680541aed04359b064baf73b890efdc25426261d4eadfee0.json
generated
Normal file
23
backend/.sqlx/query-f0ac12b66c5d3cca680541aed04359b064baf73b890efdc25426261d4eadfee0.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT permissioned_as FROM v2_job WHERE id = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "permissioned_as",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f0ac12b66c5d3cca680541aed04359b064baf73b890efdc25426261d4eadfee0"
|
||||
}
|
||||
41
backend/.sqlx/query-f7ba87d5804b9bc05e7156c7c18c5a30037abef63efb5b44dc535c5f45d62a06.json
generated
Normal file
41
backend/.sqlx/query-f7ba87d5804b9bc05e7156c7c18c5a30037abef63efb5b44dc535c5f45d62a06.json
generated
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT size_bytes, file_count, leased_by, lease_until\n FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "size_bytes",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "file_count",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "leased_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "lease_until",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "f7ba87d5804b9bc05e7156c7c18c5a30037abef63efb5b44dc535c5f45d62a06"
|
||||
}
|
||||
257
backend/Cargo.lock
generated
257
backend/Cargo.lock
generated
@@ -860,9 +860,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.16.0"
|
||||
version = "1.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9"
|
||||
checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
@@ -870,9 +870,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.37.1"
|
||||
version = "0.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549"
|
||||
checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
@@ -1334,9 +1334,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-xml"
|
||||
version = "0.60.14"
|
||||
version = "0.60.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b53543b4b86ed43f051644f704a98c7291b3618b67adf057ee77a366fa52fcaa"
|
||||
checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3"
|
||||
dependencies = [
|
||||
"xmlparser",
|
||||
]
|
||||
@@ -1900,7 +1900,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro-crate 3.4.0",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@@ -2550,6 +2550,15 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cooked-waker"
|
||||
version = "5.0.0"
|
||||
@@ -6173,20 +6182,20 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"r-efi 6.0.0",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
@@ -7421,9 +7430,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "ipnetwork"
|
||||
@@ -8683,7 +8692,7 @@ dependencies = [
|
||||
"darling 0.20.11",
|
||||
"heck 0.5.0",
|
||||
"num-bigint",
|
||||
"proc-macro-crate 3.4.0",
|
||||
"proc-macro-crate",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -9252,7 +9261,7 @@ version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.4.0",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@@ -10331,16 +10340,6 @@ dependencies = [
|
||||
"elliptic-curve",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.4.0"
|
||||
@@ -10710,9 +10709,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -10723,6 +10722,12 @@ version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
@@ -11088,9 +11093,12 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "relative-path"
|
||||
version = "1.9.3"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
|
||||
checksum = "bca40a312222d8ba74837cb474edef44b37f561da5f773981007a10bbaa992b0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rend"
|
||||
@@ -11384,9 +11392,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rquickjs"
|
||||
version = "0.8.1"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d16661bff09e9ed8e01094a188b463de45ec0693ade55b92ed54027d7ba7c40c"
|
||||
checksum = "c50dc6d6c587c339edb4769cf705867497a2baf0eca8b4645fa6ecd22f02c77a"
|
||||
dependencies = [
|
||||
"rquickjs-core",
|
||||
"rquickjs-macro",
|
||||
@@ -11394,26 +11402,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rquickjs-core"
|
||||
version = "0.8.1"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8db6379e204ef84c0811e90e7cc3e3e4d7688701db68a00d14a6db6849087b"
|
||||
checksum = "b8bf7840285c321c3ab20e752a9afb95548c75cd7f4632a0627cea3507e310c1"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"hashbrown 0.16.0",
|
||||
"relative-path",
|
||||
"rquickjs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rquickjs-macro"
|
||||
version = "0.8.1"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6041104330c019fcd936026ae05e2446f5e8a2abef329d924f25424b7052a2f3"
|
||||
checksum = "7106215ff41a5677b104906a13e1a440b880f4b6362b5dc4f3978c267fad2b80"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"convert_case 0.10.0",
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"indexmap 2.11.1",
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rquickjs-core",
|
||||
@@ -11422,9 +11431,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rquickjs-sys"
|
||||
version = "0.8.1"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bc352c6b663604c3c186c000cfcc6c271f4b50bc135a285dd6d4f2a42f9790a"
|
||||
checksum = "27344601ef27460e82d6a4e1ecb9e7e99f518122095f3c51296da8e9be2b9d83"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
@@ -13850,7 +13859,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.1",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -15732,7 +15741,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -15764,6 +15773,7 @@ dependencies = [
|
||||
"sql-builder",
|
||||
"sqlx",
|
||||
"strum 0.27.2",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"tikv-jemalloc-ctl",
|
||||
"tikv-jemalloc-sys",
|
||||
@@ -15789,14 +15799,16 @@ dependencies = [
|
||||
"windmill-queue",
|
||||
"windmill-runtime-nativets",
|
||||
"windmill-test-utils",
|
||||
"windmill-types",
|
||||
"windmill-worker",
|
||||
"windmill-worker-volumes",
|
||||
"windows-service",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windmill-alerting"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -15809,7 +15821,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -15943,11 +15955,12 @@ dependencies = [
|
||||
"windmill-trigger-websocket",
|
||||
"windmill-types",
|
||||
"windmill-worker",
|
||||
"windmill-worker-volumes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-agent-workers"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -15970,7 +15983,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-assets"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -15983,7 +15996,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-auth"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16009,7 +16022,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
@@ -16019,7 +16032,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-configs"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16036,7 +16049,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-debug"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"base64 0.22.1",
|
||||
@@ -16059,7 +16072,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-embeddings"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16082,7 +16095,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flow-conversations"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16098,7 +16111,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flows"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16118,7 +16131,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-groups"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16138,7 +16151,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-inputs"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16152,7 +16165,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-integration-tests"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -16179,7 +16192,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-jobs"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16204,7 +16217,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-npm-proxy"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"flate2",
|
||||
@@ -16222,7 +16235,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-openapi"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16243,7 +16256,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-schedule"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16263,7 +16276,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-scripts"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16293,7 +16306,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-settings"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16320,7 +16333,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-sse"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"serde",
|
||||
@@ -16332,7 +16345,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-users"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"axum 0.7.9",
|
||||
@@ -16355,7 +16368,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workers"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16369,7 +16382,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workspaces"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16377,6 +16390,7 @@ dependencies = [
|
||||
"http 1.4.0",
|
||||
"hyper 1.8.1",
|
||||
"lazy_static",
|
||||
"magic-crypt",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -16399,7 +16413,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -16413,7 +16427,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16432,7 +16446,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -16531,7 +16545,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-dep-map"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"itertools 0.14.0",
|
||||
@@ -16550,7 +16564,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -16565,7 +16579,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -16589,7 +16603,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-jseval"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -16606,7 +16620,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -16622,7 +16636,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-mcp"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16643,7 +16657,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-native-triggers"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16674,7 +16688,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-oauth"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-oauth2",
|
||||
@@ -16698,7 +16712,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-object-store"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -16732,7 +16746,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-operator"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -16750,7 +16764,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -16759,7 +16773,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16771,7 +16785,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -16783,7 +16797,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -16795,7 +16809,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16807,7 +16821,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -16819,7 +16833,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -16830,7 +16844,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16841,7 +16855,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16854,7 +16868,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16878,7 +16892,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16892,7 +16906,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -16909,7 +16923,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16924,7 +16938,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16943,7 +16957,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -16954,7 +16968,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16991,7 +17005,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-runtime-nativets"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"const_format",
|
||||
@@ -17029,7 +17043,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"wasm-bindgen",
|
||||
@@ -17040,7 +17054,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-store"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -17069,7 +17083,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-test-utils"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -17092,7 +17106,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17125,7 +17139,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-email"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17145,7 +17159,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-gcp"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17179,7 +17193,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-http"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17214,7 +17228,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-kafka"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17237,7 +17251,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-mqtt"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17261,7 +17275,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-nats"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -17285,7 +17299,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-postgres"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17320,7 +17334,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-sqs"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17348,7 +17362,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-websocket"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17371,7 +17385,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.4",
|
||||
@@ -17389,7 +17403,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
@@ -17489,9 +17503,28 @@ dependencies = [
|
||||
"windmill-queue",
|
||||
"windmill-runtime-nativets",
|
||||
"windmill-types",
|
||||
"windmill-worker-volumes",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker-volumes"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
"lazy_static",
|
||||
"md-5 0.10.6",
|
||||
"object_store",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"windmill-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.56.0"
|
||||
@@ -18465,9 +18498,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c745c48e1007337ed136dc99df34128b9faa6ed542d80a1c673cf55a6d7236c8"
|
||||
checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -70,13 +70,14 @@ members = [
|
||||
"./parsers/windmill-parser-py-imports",
|
||||
"./parsers/windmill-sql-datatype-parser-wasm",
|
||||
"./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu",
|
||||
"./windmill-worker-volumes",
|
||||
"./windmill-test-utils",
|
||||
"./windmill-api-integration-tests",
|
||||
]
|
||||
exclude = ["./windmill-duckdb-ffi-internal"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.647.2"
|
||||
version = "1.651.1"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -250,10 +251,13 @@ reqwest.workspace = true
|
||||
windmill-queue = { workspace = true, features = ["failpoints"] }
|
||||
windmill-dep-map.workspace = true
|
||||
windmill-test-utils.workspace = true
|
||||
windmill-worker-volumes.workspace = true
|
||||
windmill-types.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
windmill-api-client.workspace = true
|
||||
tempfile.workspace = true
|
||||
tar.workspace = true
|
||||
windmill-parser-ts.workspace = true
|
||||
rumqttc.workspace = true
|
||||
rdkafka.workspace = true
|
||||
@@ -267,6 +271,7 @@ aws-credential-types.workspace = true
|
||||
windmill-api = { path = "./windmill-api", default-features = false }
|
||||
windmill-queue = { path = "./windmill-queue" }
|
||||
windmill-worker = { path = "./windmill-worker" }
|
||||
windmill-worker-volumes = { path = "./windmill-worker-volumes" }
|
||||
windmill-dep-map = { path = "./windmill-dep-map" }
|
||||
windmill-types = { path = "./windmill-types" }
|
||||
windmill-common = { path = "./windmill-common", default-features = false }
|
||||
@@ -439,6 +444,7 @@ base64 = "^0.22.1"
|
||||
base32 = "^0"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.6"
|
||||
md-5 = "0.10.6"
|
||||
sha1 = "0.10.6"
|
||||
sqlx = { version = "0.8.0", features = [
|
||||
"macros",
|
||||
@@ -512,7 +518,7 @@ nu-parser = { version = "0.101.0", default-features = false }
|
||||
globset = "0.4.16"
|
||||
croner = "2.2.0"
|
||||
rmcp = { version = "=0.15.0", features = ["client", "transport-streamable-http-client", "transport-streamable-http-client-reqwest"] }
|
||||
rquickjs = { version = "0.8", features = ["futures", "parallel", "macro"] }
|
||||
rquickjs = { version = "0.11", features = ["futures", "parallel", "macro"] }
|
||||
process-wrap = { version = "8.2.1", features = ["tokio1"] }
|
||||
|
||||
systemstat = "0.2.4"
|
||||
|
||||
@@ -1 +1 @@
|
||||
8ffae1f43b31dc8136714fa612d22b6301773e27
|
||||
716b350bce1730b302c66ea69df618fa40f2f16b
|
||||
|
||||
1
backend/migrations/20260226000000_add_volumes.down.sql
Normal file
1
backend/migrations/20260226000000_add_volumes.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS volume;
|
||||
22
backend/migrations/20260226000000_add_volumes.up.sql
Normal file
22
backend/migrations/20260226000000_add_volumes.up.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Add 'volume' to the asset_kind enum
|
||||
ALTER TYPE asset_kind ADD VALUE IF NOT EXISTS 'volume';
|
||||
|
||||
-- Volume metadata table
|
||||
CREATE TABLE volume (
|
||||
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
size_bytes BIGINT NOT NULL DEFAULT 0,
|
||||
file_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
created_by VARCHAR(255) NOT NULL,
|
||||
updated_at TIMESTAMPTZ,
|
||||
updated_by VARCHAR(255),
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
lease_until TIMESTAMPTZ,
|
||||
leased_by VARCHAR(255),
|
||||
last_used_at TIMESTAMPTZ,
|
||||
extra_perms JSONB NOT NULL DEFAULT '{}',
|
||||
PRIMARY KEY (workspace_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_volume_last_used ON volume(workspace_id, last_used_at);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS token_expiry_notification;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Tracks pending expiry notifications: row exists = not yet notified.
|
||||
-- Deleted once the notification is sent. Orphaned rows are harmless (filtered out by the join).
|
||||
CREATE TABLE token_expiry_notification (
|
||||
token VARCHAR(255) PRIMARY KEY,
|
||||
expiration TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_token_expiry_notification_expiration ON token_expiry_notification (expiration);
|
||||
@@ -238,7 +238,7 @@ lazy_static::lazy_static! {
|
||||
|
||||
// used for `unsafe` sql interpolation
|
||||
// -- %%name%% (type) = default
|
||||
static ref RE_ARG_SQL_INTERPOLATION: Regex = Regex::new(r#"(?m)^--\s*%%([a-z_][a-z0-9_]*)%%\s*([\s\w\/]+)?(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
|
||||
static ref RE_ARG_SQL_INTERPOLATION: Regex = Regex::new(r#"(?m)^--\s*%%([a-z_][a-z0-9_]*)%%[ \t]*([\w][\w \t\/]*)?(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
|
||||
}
|
||||
|
||||
fn parsed_default(parsed_typ: &Typ, default: String) -> Option<serde_json::Value> {
|
||||
@@ -1547,4 +1547,36 @@ SELECT $1::integer;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_pgsql_safe_interpolated_args() -> anyhow::Result<()> {
|
||||
// There was a bug where enum would be "angrycreative"/"bishop"/"test SELECT x"
|
||||
let code = r#"
|
||||
-- %%table_name%% angrycreative/bishop/test
|
||||
SELECT x
|
||||
"#;
|
||||
assert_eq!(
|
||||
parse_pgsql_sig(code)?,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![Arg {
|
||||
otyp: Some("__sanitized_enum__".to_string()),
|
||||
name: "table_name".to_string(),
|
||||
typ: Typ::Str(Some(vec![
|
||||
"angrycreative".to_string(),
|
||||
"bishop".to_string(),
|
||||
"test".to_string()
|
||||
])),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
},],
|
||||
no_main_func: None,
|
||||
has_preprocessor: None
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ pub enum AssetKind {
|
||||
Resource,
|
||||
Ducklake,
|
||||
DataTable,
|
||||
Volume,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, PartialEq, Clone)]
|
||||
@@ -148,4 +149,5 @@ pub const ASSET_KINDS: &[(&str, AssetKind)] = &[
|
||||
("$res:", AssetKind::Resource),
|
||||
("ducklake://", AssetKind::Ducklake),
|
||||
("datatable://", AssetKind::DataTable),
|
||||
("volume://", AssetKind::Volume),
|
||||
];
|
||||
|
||||
@@ -126,6 +126,7 @@ pub fn json_to_typ(js: &Value, precise_arrays: bool) -> Typ {
|
||||
pub fn to_snake_case(s: &str) -> String {
|
||||
s.with_boundaries(&Boundary::defaults())
|
||||
.without_boundaries(&Boundary::letter_digit())
|
||||
.without_boundaries(&[Boundary::DigitLower])
|
||||
.to_case(Case::Snake)
|
||||
}
|
||||
|
||||
@@ -138,8 +139,8 @@ mod test {
|
||||
assert_eq!("s3", to_snake_case("S3"));
|
||||
assert_eq!("s3", to_snake_case("s3"));
|
||||
assert_eq!("s3_object", to_snake_case("S3Object"));
|
||||
assert_eq!("s3_object", to_snake_case("S3object"));
|
||||
assert_eq!("s3_object", to_snake_case("s3object"));
|
||||
assert_eq!("s3object", to_snake_case("S3object"));
|
||||
assert_eq!("s3object", to_snake_case("s3object"));
|
||||
assert_eq!("abc", to_snake_case("ABC"));
|
||||
assert_eq!("aa_bc", to_snake_case("AaBC"));
|
||||
assert_eq!("a_b_c", to_snake_case("A_B_C"));
|
||||
@@ -181,6 +182,9 @@ mod test {
|
||||
fn test_mixed_case_with_numbers() {
|
||||
assert_eq!(to_snake_case("testCase1"), "test_case1");
|
||||
assert_eq!(to_snake_case("Test123Case"), "test123_case");
|
||||
// digit followed by lowercase should NOT insert underscore (issue #7934)
|
||||
assert_eq!(to_snake_case("Connect2allApi"), "connect2all_api");
|
||||
assert_eq!(to_snake_case("Foo2barApi"), "foo2bar_api");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -38,11 +38,11 @@ use windmill_common::{
|
||||
agent_workers::AgentConfig,
|
||||
global_settings::{
|
||||
APP_WORKSPACED_ROUTE_SETTING, BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING,
|
||||
CRITICAL_ALERTS_ON_DB_OVERSIZE_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING,
|
||||
CRITICAL_ERROR_CHANNELS_SETTING, CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING,
|
||||
DEFAULT_TAGS_WORKSPACES_SETTING, EMAIL_DOMAIN_SETTING, ENV_SETTINGS,
|
||||
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
|
||||
HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INDEXER_SETTING,
|
||||
CRITICAL_ALERTS_ON_DB_OVERSIZE_SETTING, CRITICAL_ALERTS_ON_TOKEN_EXPIRY_SETTING,
|
||||
CRITICAL_ALERT_MUTE_UI_SETTING, CRITICAL_ERROR_CHANNELS_SETTING, CUSTOM_TAGS_SETTING,
|
||||
DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING, EMAIL_DOMAIN_SETTING,
|
||||
ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
|
||||
EXTRA_PIP_INDEX_URL_SETTING, HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INDEXER_SETTING,
|
||||
INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING,
|
||||
JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING,
|
||||
MAVEN_SETTINGS_XML_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NO_DEFAULT_MAVEN_SETTING,
|
||||
@@ -62,7 +62,7 @@ use windmill_common::{
|
||||
},
|
||||
worker::{
|
||||
is_native_mode_from_env, reload_custom_tags_setting, Connection, HUB_CACHE_DIR,
|
||||
HUB_RT_CACHE_DIR, NATIVE_MODE_RESOLVED, TMP_DIR, TMP_LOGS_DIR, WORKER_GROUP,
|
||||
HUB_RT_CACHE_DIR, NATIVE_MODE_RESOLVED, TMP_LOGS_DIR, WINDMILL_DIR, WORKER_GROUP,
|
||||
},
|
||||
KillpillSender, DEFAULT_HUB_BASE_URL, METRICS_ENABLED,
|
||||
};
|
||||
@@ -99,10 +99,10 @@ use crate::monitor::{
|
||||
load_tag_per_workspace_enabled, load_tag_per_workspace_workspaces, monitor_db,
|
||||
reload_app_workspaced_route_setting, reload_base_url_setting,
|
||||
reload_bunfig_install_scopes_setting, reload_critical_alert_mute_ui_setting,
|
||||
reload_critical_error_channels_setting, reload_extra_pip_index_url_setting,
|
||||
reload_hub_api_secret_setting, reload_hub_base_url_setting, reload_job_default_timeout_setting,
|
||||
reload_job_isolation_setting, reload_jwt_secret_setting, reload_license_key,
|
||||
reload_npm_config_registry_setting, reload_otel_tracing_proxy_setting,
|
||||
reload_critical_alerts_on_token_expiry_setting, reload_critical_error_channels_setting,
|
||||
reload_extra_pip_index_url_setting, reload_hub_api_secret_setting, reload_hub_base_url_setting,
|
||||
reload_job_default_timeout_setting, reload_job_isolation_setting, 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_uv_index_strategy_setting, reload_worker_config, MonitorIteration,
|
||||
};
|
||||
@@ -238,8 +238,8 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
|
||||
)
|
||||
})?;
|
||||
|
||||
create_dir_all(HUB_CACHE_DIR)?;
|
||||
create_dir_all(BUN_BUNDLE_CACHE_DIR)?;
|
||||
create_dir_all(&*HUB_CACHE_DIR)?;
|
||||
create_dir_all(&*BUN_BUNDLE_CACHE_DIR)?;
|
||||
|
||||
for path in paths.values() {
|
||||
tracing::info!("Caching hub script at {path}");
|
||||
@@ -249,7 +249,7 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
|
||||
.as_ref()
|
||||
.is_some_and(|x| x == &ScriptLang::Deno)
|
||||
{
|
||||
let job_dir = format!("{}/cache_init/{}", TMP_DIR, Uuid::new_v4());
|
||||
let job_dir = format!("{}/cache_init/{}", *WINDMILL_DIR, Uuid::new_v4());
|
||||
create_dir_all(&job_dir)?;
|
||||
let _ = windmill_worker::generate_deno_lock(
|
||||
&Uuid::nil(),
|
||||
@@ -267,7 +267,7 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
|
||||
tokio::fs::remove_dir_all(job_dir).await?;
|
||||
} else if res.language.as_ref().is_some_and(|x| x == &ScriptLang::Bun) {
|
||||
let job_id = Uuid::new_v4();
|
||||
let job_dir = format!("{}/cache_init/{}", TMP_DIR, job_id);
|
||||
let job_dir = format!("{}/cache_init/{}", *WINDMILL_DIR, job_id);
|
||||
create_dir_all(&job_dir)?;
|
||||
if let Some(lock) = res.lockfile {
|
||||
let _ = windmill_worker::prepare_job_dir(&lock, &job_dir).await?;
|
||||
@@ -384,9 +384,9 @@ async fn cache_hub_resource_types() -> anyhow::Result<()> {
|
||||
|
||||
println!("Fetched {} resource types from hub", resource_types.len());
|
||||
|
||||
create_dir_all(HUB_RT_CACHE_DIR)?;
|
||||
create_dir_all(&*HUB_RT_CACHE_DIR)?;
|
||||
|
||||
let cache_path = format!("{}/{}", HUB_RT_CACHE_DIR, HUB_RT_CACHE_FILE);
|
||||
let cache_path = format!("{}/{}", *HUB_RT_CACHE_DIR, HUB_RT_CACHE_FILE);
|
||||
let content = serde_json::to_string_pretty(&resource_types)
|
||||
.with_context(|| "Failed to serialize resource types")?;
|
||||
|
||||
@@ -398,7 +398,7 @@ async fn cache_hub_resource_types() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
pub async fn sync_cached_resource_types(db: &sqlx::Pool<sqlx::Postgres>) -> anyhow::Result<()> {
|
||||
let cache_path = format!("{}/{}", HUB_RT_CACHE_DIR, HUB_RT_CACHE_FILE);
|
||||
let cache_path = format!("{}/{}", *HUB_RT_CACHE_DIR, HUB_RT_CACHE_FILE);
|
||||
|
||||
if tokio::fs::metadata(&cache_path).await.is_err() {
|
||||
tracing::info!(
|
||||
@@ -969,7 +969,7 @@ Windmill Community Edition {GIT_VERSION}
|
||||
|
||||
DirBuilder::new()
|
||||
.recursive(true)
|
||||
.create("/tmp/windmill")
|
||||
.create(&*WINDMILL_DIR)
|
||||
.expect("could not create initial server dir");
|
||||
|
||||
#[cfg(feature = "tantivy")]
|
||||
@@ -1717,6 +1717,11 @@ async fn process_notify_event(
|
||||
tracing::error!(error = %e, "Could not reload critical alert UI setting");
|
||||
}
|
||||
}
|
||||
CRITICAL_ALERTS_ON_TOKEN_EXPIRY_SETTING => {
|
||||
if let Err(e) = reload_critical_alerts_on_token_expiry_setting(conn).await {
|
||||
tracing::error!(error = %e, "Could not reload critical alerts on token expiry setting");
|
||||
}
|
||||
}
|
||||
"workspace_telemetry_enabled" => {
|
||||
// Read the new value from the database and log it
|
||||
let enabled = sqlx::query_scalar!(
|
||||
@@ -1794,27 +1799,27 @@ pub async fn run_workers(
|
||||
let mut handles = Vec::with_capacity(num_workers as usize);
|
||||
|
||||
for x in [
|
||||
TMP_LOGS_DIR,
|
||||
UV_CACHE_DIR,
|
||||
DENO_CACHE_DIR,
|
||||
DENO_CACHE_DIR_DEPS,
|
||||
DENO_CACHE_DIR_NPM,
|
||||
BUN_CACHE_DIR,
|
||||
PY310_CACHE_DIR,
|
||||
PY311_CACHE_DIR,
|
||||
PY312_CACHE_DIR,
|
||||
PY313_CACHE_DIR,
|
||||
BUN_BUNDLE_CACHE_DIR,
|
||||
GO_CACHE_DIR,
|
||||
GO_BIN_CACHE_DIR,
|
||||
RUST_CACHE_DIR,
|
||||
CSHARP_CACHE_DIR,
|
||||
NU_CACHE_DIR,
|
||||
HUB_CACHE_DIR,
|
||||
POWERSHELL_CACHE_DIR,
|
||||
JAVA_CACHE_DIR,
|
||||
RUBY_CACHE_DIR,
|
||||
TAR_JAVA_CACHE_DIR, // for related places search: ADD_NEW_LANG
|
||||
&*TMP_LOGS_DIR,
|
||||
&*UV_CACHE_DIR,
|
||||
&*DENO_CACHE_DIR,
|
||||
&*DENO_CACHE_DIR_DEPS,
|
||||
&*DENO_CACHE_DIR_NPM,
|
||||
&*BUN_CACHE_DIR,
|
||||
&*PY310_CACHE_DIR,
|
||||
&*PY311_CACHE_DIR,
|
||||
&*PY312_CACHE_DIR,
|
||||
&*PY313_CACHE_DIR,
|
||||
&*BUN_BUNDLE_CACHE_DIR,
|
||||
&*GO_CACHE_DIR,
|
||||
&*GO_BIN_CACHE_DIR,
|
||||
&*RUST_CACHE_DIR,
|
||||
&*CSHARP_CACHE_DIR,
|
||||
&*NU_CACHE_DIR,
|
||||
&*HUB_CACHE_DIR,
|
||||
&*POWERSHELL_CACHE_DIR,
|
||||
&*JAVA_CACHE_DIR,
|
||||
&*RUBY_CACHE_DIR,
|
||||
&*TAR_JAVA_CACHE_DIR, // for related places search: ADD_NEW_LANG
|
||||
] {
|
||||
DirBuilder::new()
|
||||
.recursive(true)
|
||||
|
||||
@@ -44,19 +44,20 @@ use windmill_common::{
|
||||
apps::APP_WORKSPACED_ROUTE,
|
||||
auth::create_token_for_owner,
|
||||
ee_oss::CriticalErrorChannel,
|
||||
email_oss::send_email_if_possible,
|
||||
error,
|
||||
flow_status::{FlowStatus, FlowStatusModule},
|
||||
global_settings::{
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ALERTS_ON_DB_OVERSIZE_SETTING,
|
||||
CRITICAL_ALERT_MUTE_UI_SETTING, CRITICAL_ERROR_CHANNELS_SETTING,
|
||||
DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING,
|
||||
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
|
||||
HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INSTANCE_PYTHON_VERSION_SETTING,
|
||||
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, JWT_SECRET_SETTING,
|
||||
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING,
|
||||
NPMRC_SETTING, NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OTEL_SETTING,
|
||||
OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING,
|
||||
POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
CRITICAL_ALERTS_ON_TOKEN_EXPIRY_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING,
|
||||
CRITICAL_ERROR_CHANNELS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING,
|
||||
DEFAULT_TAGS_WORKSPACES_SETTING, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
|
||||
EXTRA_PIP_INDEX_URL_SETTING, HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING,
|
||||
INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING,
|
||||
JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING,
|
||||
MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPMRC_SETTING, NPM_CONFIG_REGISTRY_SETTING,
|
||||
NUGET_CONFIG_SETTING, OTEL_SETTING, OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_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,
|
||||
UV_INDEX_STRATEGY_SETTING,
|
||||
@@ -73,13 +74,14 @@ use windmill_common::{
|
||||
load_periodic_bash_script_interval_from_env, load_whitelist_env_vars_from_env,
|
||||
load_worker_config, reload_custom_tags_setting, store_pull_query,
|
||||
store_suspended_pull_query, Connection, WorkerConfig, DEFAULT_TAGS_PER_WORKSPACE,
|
||||
DEFAULT_TAGS_WORKSPACES, INDEXER_CONFIG, SCRIPT_TOKEN_EXPIRY, SMTP_CONFIG, TMP_DIR,
|
||||
DEFAULT_TAGS_WORKSPACES, INDEXER_CONFIG, SCRIPT_TOKEN_EXPIRY, SMTP_CONFIG, WINDMILL_DIR,
|
||||
WORKER_CONFIG, WORKER_GROUP,
|
||||
},
|
||||
KillpillSender, BASE_URL, CRITICAL_ALERTS_ON_DB_OVERSIZE, CRITICAL_ALERT_MUTE_UI_ENABLED,
|
||||
CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, JOB_RETENTION_SECS,
|
||||
METRICS_DEBUG_ENABLED, METRICS_ENABLED, MONITOR_LOGS_ON_OBJECT_STORE, OTEL_LOGS_ENABLED,
|
||||
OTEL_METRICS_ENABLED, OTEL_TRACING_ENABLED, SERVICE_LOG_RETENTION_SECS,
|
||||
KillpillSender, BASE_URL, CRITICAL_ALERTS_ON_DB_OVERSIZE, CRITICAL_ALERTS_ON_TOKEN_EXPIRY,
|
||||
CRITICAL_ALERT_MUTE_UI_ENABLED, CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL,
|
||||
HUB_BASE_URL, JOB_RETENTION_SECS, METRICS_DEBUG_ENABLED, METRICS_ENABLED,
|
||||
MONITOR_LOGS_ON_OBJECT_STORE, OTEL_LOGS_ENABLED, OTEL_METRICS_ENABLED, OTEL_TRACING_ENABLED,
|
||||
SERVICE_LOG_RETENTION_SECS,
|
||||
};
|
||||
use windmill_common::{client::AuthedClient, global_settings::APP_WORKSPACED_ROUTE_SETTING};
|
||||
#[cfg(feature = "parquet")]
|
||||
@@ -207,6 +209,10 @@ pub async fn initial_load(
|
||||
tracing::error!("Error loading critical alert mute ui setting: {e:#}");
|
||||
}
|
||||
|
||||
if let Err(e) = reload_critical_alerts_on_token_expiry_setting(conn).await {
|
||||
tracing::error!("Error loading critical alerts on token expiry setting: {e:#}");
|
||||
}
|
||||
|
||||
if let Some(db) = conn.as_sql() {
|
||||
if let Err(e) = load_tag_per_workspace_enabled(db).await {
|
||||
tracing::error!("Error loading default tag per workpsace: {e:#}");
|
||||
@@ -477,6 +483,21 @@ pub async fn reload_critical_alert_mute_ui_setting(conn: &Connection) -> error::
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reload_critical_alerts_on_token_expiry_setting(
|
||||
conn: &Connection,
|
||||
) -> error::Result<()> {
|
||||
if let Ok(Some(serde_json::Value::Bool(t))) = load_value_from_global_settings_with_conn(
|
||||
conn,
|
||||
CRITICAL_ALERTS_ON_TOKEN_EXPIRY_SETTING,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
CRITICAL_ALERTS_ON_TOKEN_EXPIRY.store(t, Ordering::Relaxed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn load_metrics_debug_enabled(conn: &Connection) -> error::Result<()> {
|
||||
let metrics_enabled =
|
||||
load_value_from_global_settings_with_conn(conn, EXPOSE_DEBUG_METRICS_SETTING, true).await;
|
||||
@@ -595,7 +616,7 @@ async fn sleep_until_next_minute_start_plus_one_s() {
|
||||
|
||||
use windmill_common::tracing_init::TMP_WINDMILL_LOGS_SERVICE;
|
||||
async fn find_two_highest_files(hostname: &str) -> (Option<String>, Option<String>) {
|
||||
let log_dir = format!("{}/{}/", TMP_WINDMILL_LOGS_SERVICE, hostname);
|
||||
let log_dir = format!("{}/{}/", *TMP_WINDMILL_LOGS_SERVICE, hostname);
|
||||
let rd_dir = tokio::fs::read_dir(log_dir).await;
|
||||
if let Ok(mut log_files) = rd_dir {
|
||||
let mut highest_file: Option<String> = None;
|
||||
@@ -614,7 +635,8 @@ async fn find_two_highest_files(hostname: &str) -> (Option<String>, Option<Strin
|
||||
(highest_file, second_highest_file)
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Error reading log files: {TMP_WINDMILL_LOGS_SERVICE}, {:#?}",
|
||||
"Error reading log files: {}, {:#?}",
|
||||
*TMP_WINDMILL_LOGS_SERVICE,
|
||||
rd_dir.unwrap_err()
|
||||
);
|
||||
(None, None)
|
||||
@@ -716,7 +738,7 @@ async fn send_log_file_to_object_store(
|
||||
let s3_client = windmill_object_store::get_object_store().await;
|
||||
#[cfg(feature = "parquet")]
|
||||
if let Some(s3_client) = s3_client {
|
||||
let path = std::path::Path::new(TMP_WINDMILL_LOGS_SERVICE)
|
||||
let path = std::path::Path::new(&*TMP_WINDMILL_LOGS_SERVICE)
|
||||
.join(hostname)
|
||||
.join(&highest_file);
|
||||
|
||||
@@ -844,18 +866,82 @@ struct LogFile {
|
||||
hostname: String,
|
||||
}
|
||||
|
||||
struct TokenRow {
|
||||
token_prefix: Option<String>,
|
||||
label: Option<String>,
|
||||
email: Option<String>,
|
||||
workspace_id: Option<String>,
|
||||
}
|
||||
|
||||
fn is_user_token(label: Option<&str>) -> bool {
|
||||
match label {
|
||||
None => true,
|
||||
Some(l) => l != "session" && !l.starts_with("ephemeral") && !l.starts_with("Ephemeral"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn report_token_expiration(db: &DB, token: &TokenRow, expired: bool) {
|
||||
if !is_user_token(token.label.as_deref()) {
|
||||
return;
|
||||
}
|
||||
let prefix = token.token_prefix.as_deref().unwrap_or("??????????");
|
||||
let email_addr = token.email.as_deref().unwrap_or("unknown");
|
||||
let token_desc = match token.label.as_deref() {
|
||||
Some(l) if !l.is_empty() => format!("'{l}' ({prefix}****)"),
|
||||
_ => format!("{prefix}****"),
|
||||
};
|
||||
|
||||
let (alert_message, email_subject, email_body) = if expired {
|
||||
(
|
||||
format!(
|
||||
"API token {token_desc} of '{email_addr}' has expired and been deleted"
|
||||
),
|
||||
"Windmill: Your API token has expired",
|
||||
format!(
|
||||
"Your API token {token_desc} has expired and been deleted.\n\nPlease create a new token if you still need API access."
|
||||
),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
format!("API token {token_desc} of '{email_addr}' is expiring soon"),
|
||||
"Windmill: Your API token is expiring soon",
|
||||
format!(
|
||||
"Your API token {token_desc} is expiring soon.\n\nPlease rotate or renew your token to avoid service disruption."
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
tracing::info!("{}", alert_message);
|
||||
if CRITICAL_ALERTS_ON_TOKEN_EXPIRY.load(Ordering::Relaxed) {
|
||||
report_critical_error(
|
||||
alert_message,
|
||||
db.clone(),
|
||||
token.workspace_id.as_deref(),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if let Some(email) = &token.email {
|
||||
send_email_if_possible(email_subject, &email_body, email);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_expired_items(db: &DB) -> () {
|
||||
let tokens_deleted_r: std::result::Result<Vec<String>, _> = sqlx::query_scalar(
|
||||
let expired_tokens_r = sqlx::query_as!(
|
||||
TokenRow,
|
||||
"DELETE FROM token WHERE expiration <= now()
|
||||
RETURNING concat(substring(token for 10), '*****')",
|
||||
RETURNING substring(token for 10) as token_prefix, label, email, workspace_id",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await;
|
||||
|
||||
match tokens_deleted_r {
|
||||
match expired_tokens_r {
|
||||
Ok(tokens) => {
|
||||
if tokens.len() > 0 {
|
||||
tracing::info!("deleted {} tokens: {:?}", tokens.len(), tokens)
|
||||
if !tokens.is_empty() {
|
||||
tracing::info!("deleted {} expired tokens", tokens.len());
|
||||
for t in &tokens {
|
||||
report_token_expiration(db, t, true).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::error!("Error deleting token: {}", e.to_string()),
|
||||
@@ -935,7 +1021,7 @@ pub async fn delete_expired_items(db: &DB) -> () {
|
||||
.iter()
|
||||
.map(|f| format!("{}/{}", f.hostname, f.file_path))
|
||||
.collect();
|
||||
delete_log_files_from_disk_and_store(paths, TMP_WINDMILL_LOGS_SERVICE, windmill_common::tracing_init::LOGS_SERVICE).await;
|
||||
delete_log_files_from_disk_and_store(paths, &*TMP_WINDMILL_LOGS_SERVICE, windmill_common::tracing_init::LOGS_SERVICE).await;
|
||||
|
||||
}
|
||||
Err(e) => tracing::error!("Error deleting log file: {:?}", e),
|
||||
@@ -1064,6 +1150,41 @@ pub async fn delete_expired_items(db: &DB) -> () {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn check_expiring_tokens(db: &DB) {
|
||||
// Find tokens expiring within 7 days that still have a pending notification row
|
||||
let expiring_tokens_r = sqlx::query_as!(
|
||||
TokenRow,
|
||||
"DELETE FROM token_expiry_notification n
|
||||
USING token t
|
||||
WHERE n.token = t.token
|
||||
AND n.expiration > now()
|
||||
AND n.expiration <= now() + interval '7 days'
|
||||
RETURNING substring(t.token for 10) as token_prefix, t.label, t.email, t.workspace_id",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await;
|
||||
|
||||
match expiring_tokens_r {
|
||||
Ok(tokens) => {
|
||||
for t in &tokens {
|
||||
report_token_expiration(db, t, false).await;
|
||||
}
|
||||
if !tokens.is_empty() {
|
||||
tracing::info!("Sent expiration warnings for {} token(s)", tokens.len());
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::error!("Error checking expiring tokens: {}", e),
|
||||
}
|
||||
|
||||
// Clean up notification rows whose expiration has passed
|
||||
if let Err(e) = sqlx::query!("DELETE FROM token_expiry_notification WHERE expiration <= now()")
|
||||
.execute(db)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error cleaning up expired token notifications: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a batch of expired jobs with LIMIT and SKIP LOCKED for high-scale environments.
|
||||
/// Uses a single transaction per batch to minimize lock duration.
|
||||
/// Returns the number of jobs deleted in this batch.
|
||||
@@ -1140,7 +1261,7 @@ async fn delete_expired_jobs_batch(
|
||||
.filter_map(|opt| opt)
|
||||
.flat_map(|inner_vec| inner_vec.into_iter())
|
||||
.collect();
|
||||
delete_log_files_from_disk_and_store(paths, TMP_DIR, "").await;
|
||||
delete_log_files_from_disk_and_store(paths, &*WINDMILL_DIR, "").await;
|
||||
}
|
||||
Err(e) => tracing::error!("Error deleting job logs: {:?}", e),
|
||||
}
|
||||
@@ -1367,7 +1488,7 @@ pub async fn reload_maven_settings_xml_setting(conn: &Connection) {
|
||||
let settings_xml = MAVEN_SETTINGS_XML.read().await.clone();
|
||||
match settings_xml {
|
||||
Some(ref content) if !content.trim().is_empty() => {
|
||||
let m2_dir = format!("{JAVA_HOME_DIR}/.m2");
|
||||
let m2_dir = format!("{}/.m2", *JAVA_HOME_DIR);
|
||||
if let Err(e) = tokio::fs::create_dir_all(&m2_dir).await {
|
||||
tracing::error!("Failed to create .m2 directory: {e:#}");
|
||||
return;
|
||||
@@ -1378,7 +1499,7 @@ pub async fn reload_maven_settings_xml_setting(conn: &Connection) {
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let settings_path = format!("{JAVA_HOME_DIR}/.m2/settings.xml");
|
||||
let settings_path = format!("{}/.m2/settings.xml", *JAVA_HOME_DIR);
|
||||
let _ = tokio::fs::remove_file(&settings_path).await;
|
||||
}
|
||||
}
|
||||
@@ -2051,6 +2172,16 @@ pub async fn monitor_db(
|
||||
}
|
||||
};
|
||||
|
||||
// Run every hour (10 iterations * 30s = 5 minutes)
|
||||
// Check for tokens expiring within 7 days and send alerts
|
||||
let check_expiring_tokens_f = async {
|
||||
if server_mode && iteration.is_some() && iteration.as_ref().unwrap().should_run(10) {
|
||||
if let Some(db) = conn.as_sql() {
|
||||
check_expiring_tokens(&db).await;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
join!(
|
||||
expired_items_f,
|
||||
zombie_jobs_f,
|
||||
@@ -2072,6 +2203,7 @@ pub async fn monitor_db(
|
||||
cleanup_worker_group_stats_f,
|
||||
native_triggers_sync_f,
|
||||
cleanup_notify_events_f,
|
||||
check_expiring_tokens_f,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -151,6 +151,8 @@ sqs_trigger: path(char), queue_url(char), aws_resource_path(char), message_attri
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
token: token(char), label(char), expiration(ts), workspace_id(char), owner(char), email(char), super_admin(bool), created_at(ts), last_used_at(ts), scopes(text[]), job(uuid)
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
token_expiry_notification: token(char), expiration(ts)
|
||||
INDEX: idx_token_expiry_notification_expiration (expiration)
|
||||
tutorial_progress: email(char), progress(bit64), skipped_all(bool)
|
||||
unique_ext_jwt_token: jwt_hash(bigint), last_used_at(ts)
|
||||
usage: id(char), is_workspace(bool), month_(int), usage(int)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#![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,
|
||||
};
|
||||
use windmill_test_utils::*;
|
||||
|
||||
fn bun_code(code: &str) -> RawCode {
|
||||
RawCode {
|
||||
@@ -18,8 +18,8 @@ fn bun_code(code: &str) -> RawCode {
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
concurrency_settings:
|
||||
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
}
|
||||
}
|
||||
@@ -223,7 +223,10 @@ async fn test_agent_worker_token_and_ping(db: Pool<Postgres>) -> anyhow::Result<
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
|
||||
assert!(worker_count > 0, "worker ping should be recorded in database");
|
||||
assert!(
|
||||
worker_count > 0,
|
||||
"worker ping should be recorded in database"
|
||||
);
|
||||
|
||||
// MainLoop ping updates the existing record
|
||||
let resp = http_client
|
||||
@@ -265,3 +268,319 @@ async fn test_agent_worker_multiple_jobs_sequential(db: Pool<Postgres>) -> anyho
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test the volume HTTP proxy endpoints that agent workers use.
|
||||
///
|
||||
/// Exercises the full volume lifecycle via HTTP:
|
||||
/// 1. Configure workspace S3 storage (FilesystemStorage)
|
||||
/// 2. Pre-populate a volume with a file
|
||||
/// 3. POST /begin — acquire lease, get manifest
|
||||
/// 4. GET /file/* — download existing file
|
||||
/// 5. PUT /file/* — upload a new file
|
||||
/// 6. POST /commit — finalize with stats, release lease
|
||||
/// 7. Verify DB state and storage
|
||||
#[cfg(feature = "parquet")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_agent_worker_volume_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
let (client, _port, _server) = init_client_agent_mode(db.clone()).await;
|
||||
|
||||
// 1. Set up filesystem-based object storage in a temp dir
|
||||
let storage_dir = tempfile::tempdir()?;
|
||||
let storage_root = storage_dir.path().to_string_lossy().to_string();
|
||||
|
||||
let lfs_config = json!({
|
||||
"type": "FilesystemStorage",
|
||||
"root_path": storage_root,
|
||||
"public_resource": null,
|
||||
"advanced_permissions": null
|
||||
});
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET large_file_storage = $1 WHERE workspace_id = $2",
|
||||
lfs_config,
|
||||
"test-workspace"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
// 2. Pre-populate the volume with a file
|
||||
let vol_dir = storage_dir.path().join("volumes").join("test-vol");
|
||||
std::fs::create_dir_all(&vol_dir)?;
|
||||
std::fs::write(vol_dir.join("hello.txt"), b"hello from volume")?;
|
||||
|
||||
let base = client.baseurl();
|
||||
let http = client.client();
|
||||
let vol_base = format!("{base}/w/test-workspace/volumes/test-vol");
|
||||
|
||||
// 3. POST /begin — acquire lease, get manifest + permissions
|
||||
let resp = http
|
||||
.post(format!("{vol_base}/begin"))
|
||||
.json(&json!({
|
||||
"worker_name": "test-worker-1",
|
||||
"permissioned_as": "u/test-user"
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
assert!(
|
||||
resp.status().is_success(),
|
||||
"begin should succeed, got: {}",
|
||||
resp.status()
|
||||
);
|
||||
|
||||
let begin_body: serde_json::Value = resp.json().await?;
|
||||
assert!(
|
||||
begin_body["writable"].as_bool().unwrap(),
|
||||
"should be writable"
|
||||
);
|
||||
let manifest = begin_body["manifest"].as_object().unwrap();
|
||||
assert!(
|
||||
manifest.contains_key("hello.txt"),
|
||||
"manifest should contain hello.txt, got: {manifest:?}"
|
||||
);
|
||||
|
||||
// 4. GET /file/* — download the existing file
|
||||
let resp = http
|
||||
.get(format!("{vol_base}/file/hello.txt"))
|
||||
.send()
|
||||
.await?;
|
||||
assert!(
|
||||
resp.status().is_success(),
|
||||
"file download should succeed, got: {}",
|
||||
resp.status()
|
||||
);
|
||||
let file_bytes = resp.bytes().await?;
|
||||
assert_eq!(
|
||||
file_bytes.as_ref(),
|
||||
b"hello from volume",
|
||||
"downloaded file content should match"
|
||||
);
|
||||
|
||||
// 5. PUT /file/* — upload a new file
|
||||
let resp = http
|
||||
.put(format!("{vol_base}/file/output.txt"))
|
||||
.body(b"written by agent worker".to_vec())
|
||||
.send()
|
||||
.await?;
|
||||
assert!(
|
||||
resp.status().is_success(),
|
||||
"file upload should succeed, got: {}",
|
||||
resp.status()
|
||||
);
|
||||
|
||||
// 6. POST /commit — finalize: report stats, release lease
|
||||
let resp = http
|
||||
.post(format!("{vol_base}/commit"))
|
||||
.json(&json!({
|
||||
"worker_name": "test-worker-1",
|
||||
"deleted_keys": [],
|
||||
"symlinks": {},
|
||||
"file_count": 2,
|
||||
"size_bytes": 39
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
assert!(
|
||||
resp.status().is_success(),
|
||||
"commit should succeed, got: {}",
|
||||
resp.status()
|
||||
);
|
||||
|
||||
// 7. Verify volume DB row was updated
|
||||
let vol_row = sqlx::query!(
|
||||
"SELECT size_bytes, file_count, leased_by, lease_until
|
||||
FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"test-vol"
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
|
||||
let vol_row = vol_row.expect("volume row should exist");
|
||||
assert_eq!(vol_row.file_count, 2, "file_count should be 2");
|
||||
assert_eq!(vol_row.size_bytes, 39, "size_bytes should match");
|
||||
assert!(vol_row.leased_by.is_none(), "lease should be released");
|
||||
assert!(
|
||||
vol_row.lease_until.is_none() || vol_row.lease_until.unwrap() < chrono::Utc::now(),
|
||||
"lease_until should be cleared or in the past"
|
||||
);
|
||||
|
||||
// 8. Verify the uploaded file was persisted in storage
|
||||
let output_path = vol_dir.join("output.txt");
|
||||
assert!(output_path.exists(), "output.txt should be in storage");
|
||||
let output_content = std::fs::read_to_string(&output_path)?;
|
||||
assert_eq!(output_content, "written by agent worker");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Full E2E test: agent worker in HTTP mode runs a Bun script with a volume mount.
|
||||
///
|
||||
/// The worker pulls the job via HTTP, downloads volume files via the server-side
|
||||
/// volume proxy endpoints, executes the script, and syncs changes back.
|
||||
#[cfg(all(feature = "parquet", feature = "enterprise"))]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_agent_worker_volume_http_worker_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
let (_client, port, _server) = init_client_agent_mode(db.clone()).await;
|
||||
|
||||
// 1. Set up filesystem-based object storage in a temp dir
|
||||
let storage_dir = tempfile::tempdir()?;
|
||||
let storage_root = storage_dir.path().to_string_lossy().to_string();
|
||||
|
||||
let lfs_config = json!({
|
||||
"type": "FilesystemStorage",
|
||||
"root_path": storage_root,
|
||||
"public_resource": null,
|
||||
"advanced_permissions": null
|
||||
});
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET large_file_storage = $1 WHERE workspace_id = $2",
|
||||
lfs_config,
|
||||
"test-workspace"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
// 2. Pre-populate the volume with a file
|
||||
let vol_dir = storage_dir.path().join("volumes").join("test-vol");
|
||||
std::fs::create_dir_all(&vol_dir)?;
|
||||
std::fs::write(vol_dir.join("hello.txt"), b"hello from volume")?;
|
||||
|
||||
// 3. Push the job, then run worker with HTTP connection (bun tag)
|
||||
let code = r#"// volume: test-vol /tmp/data
|
||||
import { readFileSync, writeFileSync, existsSync } from "fs";
|
||||
|
||||
export function main() {
|
||||
const content = readFileSync("/tmp/data/hello.txt", "utf-8");
|
||||
writeFileSync("/tmp/data/output.txt", "written by agent worker");
|
||||
return {
|
||||
read_content: content,
|
||||
output_exists: existsSync("/tmp/data/output.txt"),
|
||||
};
|
||||
}"#;
|
||||
|
||||
let uuid = RunJob::from(JobPayload::Code(bun_code(code)))
|
||||
.push(&db)
|
||||
.await;
|
||||
let listener = listen_for_completed_jobs(&db).await;
|
||||
|
||||
let conn = testing_http_connection_with_tags(
|
||||
port,
|
||||
vec!["bun".into(), "flow".into(), "dependency".into()],
|
||||
)
|
||||
.await;
|
||||
|
||||
in_test_worker(conn, listener.find(&uuid), port).await;
|
||||
|
||||
let result = completed_job(uuid, &db).await;
|
||||
|
||||
assert!(result.success, "job should succeed: {:?}", result.result);
|
||||
let json = result.json_result().expect("should have JSON result");
|
||||
assert_eq!(json["read_content"], json!("hello from volume"));
|
||||
assert_eq!(json["output_exists"], json!(true));
|
||||
|
||||
// 4. Verify volume DB row was updated
|
||||
let vol_row = sqlx::query!(
|
||||
"SELECT size_bytes, file_count, leased_by, lease_until
|
||||
FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"test-vol"
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
|
||||
let vol_row = vol_row.expect("volume row should exist");
|
||||
assert!(
|
||||
vol_row.file_count >= 2,
|
||||
"should have at least 2 files (hello.txt + output.txt), got: {}",
|
||||
vol_row.file_count
|
||||
);
|
||||
assert!(vol_row.size_bytes > 0, "size_bytes should be > 0");
|
||||
assert!(vol_row.leased_by.is_none(), "lease should be released");
|
||||
assert!(
|
||||
vol_row.lease_until.is_none() || vol_row.lease_until.unwrap() < chrono::Utc::now(),
|
||||
"lease_until should be cleared or in the past"
|
||||
);
|
||||
|
||||
// 5. Verify the new file was written back to the storage
|
||||
let output_path = vol_dir.join("output.txt");
|
||||
assert!(
|
||||
output_path.exists(),
|
||||
"output.txt should be synced back to storage"
|
||||
);
|
||||
let output_content = std::fs::read_to_string(&output_path)?;
|
||||
assert_eq!(output_content, "written by agent worker");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test the volume release endpoint (error/cancel path).
|
||||
#[cfg(feature = "parquet")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_agent_worker_volume_release(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
let (client, _port, _server) = init_client_agent_mode(db.clone()).await;
|
||||
|
||||
// Set up filesystem storage
|
||||
let storage_dir = tempfile::tempdir()?;
|
||||
let storage_root = storage_dir.path().to_string_lossy().to_string();
|
||||
let lfs_config = json!({
|
||||
"type": "FilesystemStorage",
|
||||
"root_path": storage_root,
|
||||
"public_resource": null,
|
||||
"advanced_permissions": null
|
||||
});
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET large_file_storage = $1 WHERE workspace_id = $2",
|
||||
lfs_config,
|
||||
"test-workspace"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let base = client.baseurl();
|
||||
let http = client.client();
|
||||
let vol_base = format!("{base}/w/test-workspace/volumes/test-vol");
|
||||
|
||||
// Begin (acquire lease)
|
||||
let resp = http
|
||||
.post(format!("{vol_base}/begin"))
|
||||
.json(&json!({
|
||||
"worker_name": "test-worker-2",
|
||||
"permissioned_as": "u/test-user"
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
assert!(resp.status().is_success(), "begin should succeed");
|
||||
|
||||
// Verify lease is held
|
||||
let leased = sqlx::query_scalar!(
|
||||
"SELECT leased_by FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"test-vol"
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.flatten();
|
||||
assert_eq!(leased.as_deref(), Some("test-worker-2"));
|
||||
|
||||
// Release without commit (simulating error path)
|
||||
let resp = http
|
||||
.post(format!("{vol_base}/release"))
|
||||
.json(&json!({ "worker_name": "test-worker-2" }))
|
||||
.send()
|
||||
.await?;
|
||||
assert!(resp.status().is_success(), "release should succeed");
|
||||
|
||||
// Verify lease is cleared
|
||||
let leased = sqlx::query_scalar!(
|
||||
"SELECT leased_by FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"test-vol"
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.flatten();
|
||||
assert!(leased.is_none(), "lease should be released");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use sqlx::postgres::Postgres;
|
||||
use sqlx::Pool;
|
||||
use uuid::Uuid;
|
||||
use windmill_common::jobs::{JobPayload, RawCode};
|
||||
use windmill_common::scripts::ScriptLang;
|
||||
use windmill_test_utils::*;
|
||||
@@ -1448,3 +1449,240 @@ export function main() { return { a, b }; }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Codebase Mode Tests
|
||||
// ============================================================================
|
||||
|
||||
/// Create a TAR archive in memory containing a single `main.js` file.
|
||||
fn create_codebase_tar(main_js_content: &str) -> Vec<u8> {
|
||||
let mut builder = tar::Builder::new(Vec::new());
|
||||
let content = main_js_content.as_bytes();
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_path("main.js").unwrap();
|
||||
header.set_size(content.len() as u64);
|
||||
header.set_mode(0o644);
|
||||
header.set_cksum();
|
||||
builder.append(&header, content).unwrap();
|
||||
builder.into_inner().unwrap()
|
||||
}
|
||||
|
||||
/// Place a TAR codebase at the expected cache path for the given job ID and hash.
|
||||
fn place_codebase_in_cache(job_id: &Uuid, tar_bytes: &[u8], is_esm: bool) {
|
||||
let codebase_id = if is_esm {
|
||||
format!("{}.esm.tar", job_id)
|
||||
} else {
|
||||
format!("{}.tar", job_id)
|
||||
};
|
||||
let bundle_path = format!("script_bundle/test-workspace/{}", codebase_id);
|
||||
let cache_path = format!(
|
||||
"{}/{}.tar",
|
||||
*windmill_common::worker::ROOT_CACHE_NOMOUNT_DIR,
|
||||
bundle_path,
|
||||
);
|
||||
let parent = std::path::Path::new(&cache_path).parent().unwrap();
|
||||
std::fs::create_dir_all(parent).unwrap();
|
||||
std::fs::write(&cache_path, tar_bytes).unwrap();
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_cjs_codebase_tar(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let main_js = r#"
|
||||
module.exports.main = function() {
|
||||
return "cjs codebase ok";
|
||||
};
|
||||
"#;
|
||||
let inner_content = r#"export function main() { return "cjs codebase ok"; }"#;
|
||||
|
||||
let job_id = Uuid::new_v4();
|
||||
let tar_bytes = create_codebase_tar(main_js);
|
||||
place_codebase_in_cache(&job_id, &tar_bytes, false);
|
||||
|
||||
let job = JobPayload::Code(RawCode {
|
||||
hash: Some(-43), // PREVIEW_IS_TAR_CODEBASE_HASH
|
||||
content: inner_content.to_string(),
|
||||
path: None,
|
||||
language: ScriptLang::Bun,
|
||||
lock: None,
|
||||
concurrency_settings: Default::default(),
|
||||
debouncing_settings: Default::default(),
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
});
|
||||
|
||||
let result = RunJob::from(job)
|
||||
.job_id(job_id)
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
.json_result()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result, serde_json::json!("cjs codebase ok"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_esm_codebase_tar(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let main_js = r#"
|
||||
export function main() {
|
||||
return "esm codebase ok";
|
||||
}
|
||||
"#;
|
||||
let inner_content = r#"export function main() { return "esm codebase ok"; }"#;
|
||||
|
||||
let job_id = Uuid::new_v4();
|
||||
let tar_bytes = create_codebase_tar(main_js);
|
||||
place_codebase_in_cache(&job_id, &tar_bytes, true);
|
||||
|
||||
let job = JobPayload::Code(RawCode {
|
||||
hash: Some(-45), // PREVIEW_IS_TAR_ESM_CODEBASE_HASH
|
||||
content: inner_content.to_string(),
|
||||
path: None,
|
||||
language: ScriptLang::Bun,
|
||||
lock: None,
|
||||
concurrency_settings: Default::default(),
|
||||
debouncing_settings: Default::default(),
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
});
|
||||
|
||||
let result = RunJob::from(job)
|
||||
.job_id(job_id)
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
.json_result()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result, serde_json::json!("esm codebase ok"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_cjs_codebase_tar_nsjail(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
if std::process::Command::new("nsjail")
|
||||
.arg("--help")
|
||||
.output()
|
||||
.is_err()
|
||||
{
|
||||
eprintln!("nsjail not found, skipping test");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let main_js = r#"
|
||||
module.exports.main = function() {
|
||||
return "cjs nsjail ok";
|
||||
};
|
||||
"#;
|
||||
let inner_content = r#"export function main() { return "cjs nsjail ok"; }"#;
|
||||
|
||||
let job_id = Uuid::new_v4();
|
||||
let tar_bytes = create_codebase_tar(main_js);
|
||||
place_codebase_in_cache(&job_id, &tar_bytes, false);
|
||||
|
||||
let job = JobPayload::Code(RawCode {
|
||||
hash: Some(-43),
|
||||
content: inner_content.to_string(),
|
||||
path: None,
|
||||
language: ScriptLang::Bun,
|
||||
lock: None,
|
||||
concurrency_settings: Default::default(),
|
||||
debouncing_settings: Default::default(),
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
});
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
windmill_worker::JOB_ISOLATION.store(
|
||||
windmill_worker::JobIsolationLevel::NsjailSandboxing as u8,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
|
||||
let result = RunJob::from(job)
|
||||
.job_id(job_id)
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
|
||||
windmill_worker::JOB_ISOLATION.store(
|
||||
windmill_worker::JobIsolationLevel::Undefined as u8,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
|
||||
let json = result.json_result().unwrap();
|
||||
assert_eq!(json, serde_json::json!("cjs nsjail ok"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_esm_codebase_tar_nsjail(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
if std::process::Command::new("nsjail")
|
||||
.arg("--help")
|
||||
.output()
|
||||
.is_err()
|
||||
{
|
||||
eprintln!("nsjail not found, skipping test");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let main_js = r#"
|
||||
export function main() {
|
||||
return "esm nsjail ok";
|
||||
}
|
||||
"#;
|
||||
let inner_content = r#"export function main() { return "esm nsjail ok"; }"#;
|
||||
|
||||
let job_id = Uuid::new_v4();
|
||||
let tar_bytes = create_codebase_tar(main_js);
|
||||
place_codebase_in_cache(&job_id, &tar_bytes, true);
|
||||
|
||||
let job = JobPayload::Code(RawCode {
|
||||
hash: Some(-45),
|
||||
content: inner_content.to_string(),
|
||||
path: None,
|
||||
language: ScriptLang::Bun,
|
||||
lock: None,
|
||||
concurrency_settings: Default::default(),
|
||||
debouncing_settings: Default::default(),
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
});
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
windmill_worker::JOB_ISOLATION.store(
|
||||
windmill_worker::JobIsolationLevel::NsjailSandboxing as u8,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
|
||||
let result = RunJob::from(job)
|
||||
.job_id(job_id)
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
|
||||
windmill_worker::JOB_ISOLATION.store(
|
||||
windmill_worker::JobIsolationLevel::Undefined as u8,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
|
||||
let json = result.json_result().unwrap();
|
||||
assert_eq!(json, serde_json::json!("esm nsjail ok"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
323
backend/tests/end_user_email.rs
Normal file
323
backend/tests/end_user_email.rs
Normal file
@@ -0,0 +1,323 @@
|
||||
//! Tests for WM_END_USER_EMAIL environment variable.
|
||||
//!
|
||||
//! These tests verify that WM_END_USER_EMAIL is populated with the authenticated
|
||||
//! user's email when executing app components.
|
||||
//!
|
||||
//! TODO: Add tests for scripts and flows once public execution endpoints are identified.
|
||||
//! Currently only apps support non-workspace-member execution via OptAuthed + token lookup.
|
||||
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::worker::Connection;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
const SAME_WS_TOKEN: &str = "SECRET_TOKEN";
|
||||
const OTHER_WS_TOKEN: &str = "OTHER_WS_TOKEN";
|
||||
const NO_WS_TOKEN: &str = "NO_WS_TOKEN";
|
||||
|
||||
const SAME_WS_EMAIL: &str = "test@windmill.dev";
|
||||
const OTHER_WS_EMAIL: &str = "other-ws@windmill.dev";
|
||||
const NO_WS_EMAIL: &str = "no-ws@windmill.dev";
|
||||
|
||||
fn client() -> reqwest::Client {
|
||||
reqwest::Client::new()
|
||||
}
|
||||
|
||||
fn authed(builder: reqwest::RequestBuilder, token: &str) -> reqwest::RequestBuilder {
|
||||
builder.header("Authorization", format!("Bearer {}", token))
|
||||
}
|
||||
|
||||
// TODO: Script tests - need to identify public execution endpoints for non-workspace-members
|
||||
// async fn run_script(port: u16, token: &str) -> anyhow::Result<String> {
|
||||
// let url = format!(
|
||||
// "http://localhost:{}/api/w/test-workspace/jobs/run_wait_result/p/f/test/get_end_user_email",
|
||||
// port
|
||||
// );
|
||||
// let resp = authed(client().post(&url), token)
|
||||
// .json(&json!({}))
|
||||
// .send()
|
||||
// .await?;
|
||||
// if !resp.status().is_success() {
|
||||
// anyhow::bail!("script run failed: {} - {}", resp.status(), resp.text().await?);
|
||||
// }
|
||||
// Ok(resp.json::<serde_json::Value>().await?
|
||||
// .as_str().unwrap_or("").to_string())
|
||||
// }
|
||||
|
||||
// TODO: Flow tests - need to identify public execution endpoints for non-workspace-members
|
||||
// async fn run_flow(port: u16, token: &str) -> anyhow::Result<String> {
|
||||
// let url = format!(
|
||||
// "http://localhost:{}/api/w/test-workspace/jobs/run_wait_result/f/f/test/get_end_user_email_flow",
|
||||
// port
|
||||
// );
|
||||
// let resp = authed(client().post(&url), token)
|
||||
// .json(&json!({}))
|
||||
// .send()
|
||||
// .await?;
|
||||
// if !resp.status().is_success() {
|
||||
// anyhow::bail!("flow run failed: {} - {}", resp.status(), resp.text().await?);
|
||||
// }
|
||||
// Ok(resp.json::<serde_json::Value>().await?
|
||||
// .as_str().unwrap_or("").to_string())
|
||||
// }
|
||||
|
||||
/// Create an app with inline script via API
|
||||
async fn create_app_with_inline_script(port: u16, path: &str) -> anyhow::Result<()> {
|
||||
let url = format!(
|
||||
"http://localhost:{}/api/w/test-workspace/apps/create",
|
||||
port
|
||||
);
|
||||
let resp = authed(client().post(&url), SAME_WS_TOKEN)
|
||||
.json(&json!({
|
||||
"path": path,
|
||||
"summary": "Test app for WM_END_USER_EMAIL",
|
||||
"value": {
|
||||
"type": "app",
|
||||
"grid": [],
|
||||
"subgrids": {},
|
||||
"hiddenInlineScripts": [{
|
||||
"name": "get_email",
|
||||
"language": "deno",
|
||||
"content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }",
|
||||
"path": "f/test/email_app/get_email"
|
||||
}]
|
||||
},
|
||||
"policy": {
|
||||
"execution_mode": "anonymous",
|
||||
"on_behalf_of": null,
|
||||
"on_behalf_of_email": null,
|
||||
"triggerables_v2": {
|
||||
"get_email": {
|
||||
"static_inputs": {},
|
||||
"one_of_inputs": {}
|
||||
},
|
||||
// SHA256 hash of raw_code content for anonymous execution
|
||||
"rawscript/6428aba5aa2d3ea8e1215bfdccbedd3718b18da7a239e3778a9787bb9a0ea606": {
|
||||
"static_inputs": {},
|
||||
"one_of_inputs": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("create app failed: {} - {}", resp.status(), resp.text().await?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a raw app with inline script via API (uses regular app endpoint with rawapp type)
|
||||
async fn create_raw_app_with_inline_script(port: u16, path: &str) -> anyhow::Result<()> {
|
||||
let url = format!(
|
||||
"http://localhost:{}/api/w/test-workspace/apps/create",
|
||||
port
|
||||
);
|
||||
let resp = authed(client().post(&url), SAME_WS_TOKEN)
|
||||
.json(&json!({
|
||||
"path": path,
|
||||
"summary": "Test raw app for WM_END_USER_EMAIL",
|
||||
"value": {
|
||||
"type": "rawapp",
|
||||
"css": "",
|
||||
"inlineScripts": [{
|
||||
"name": "get_email",
|
||||
"language": "deno",
|
||||
"content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }"
|
||||
}]
|
||||
},
|
||||
"policy": {
|
||||
"execution_mode": "anonymous",
|
||||
"on_behalf_of": null,
|
||||
"on_behalf_of_email": null,
|
||||
"triggerables_v2": {
|
||||
"get_email": {
|
||||
"static_inputs": {},
|
||||
"one_of_inputs": {}
|
||||
},
|
||||
// SHA256 hash of raw_code content for anonymous execution
|
||||
"rawscript/6428aba5aa2d3ea8e1215bfdccbedd3718b18da7a239e3778a9787bb9a0ea606": {
|
||||
"static_inputs": {},
|
||||
"one_of_inputs": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("create raw app failed: {} - {}", resp.status(), resp.text().await?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_app_inline_script(port: u16, token: &str, app_path: &str, force_viewer: bool) -> anyhow::Result<String> {
|
||||
let url = format!(
|
||||
"http://localhost:{}/api/w/test-workspace/apps_u/execute_component/{}",
|
||||
port, app_path
|
||||
);
|
||||
let mut payload = json!({
|
||||
"args": {},
|
||||
"component": "get_email",
|
||||
"raw_code": {
|
||||
"language": "deno",
|
||||
"content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }",
|
||||
"path": format!("{}/get_email", app_path)
|
||||
}
|
||||
});
|
||||
if force_viewer {
|
||||
payload["force_viewer_static_fields"] = json!({});
|
||||
}
|
||||
let resp = authed(client().post(&url), token)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("app inline script run failed: {} - {}", resp.status(), resp.text().await?);
|
||||
}
|
||||
let job_id = resp.text().await?;
|
||||
wait_for_job_result(port, token, &job_id).await
|
||||
}
|
||||
|
||||
async fn run_raw_app_inline_script(port: u16, token: &str, app_path: &str, force_viewer: bool) -> anyhow::Result<String> {
|
||||
let url = format!(
|
||||
"http://localhost:{}/api/w/test-workspace/apps_u/execute_component/{}",
|
||||
port, app_path
|
||||
);
|
||||
let mut payload = json!({
|
||||
"args": {},
|
||||
"component": "get_email",
|
||||
"raw_code": {
|
||||
"language": "deno",
|
||||
"content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }"
|
||||
}
|
||||
});
|
||||
if force_viewer {
|
||||
payload["force_viewer_static_fields"] = json!({});
|
||||
}
|
||||
let resp = authed(client().post(&url), token)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("raw app inline script run failed: {} - {}", resp.status(), resp.text().await?);
|
||||
}
|
||||
let job_id = resp.text().await?;
|
||||
wait_for_job_result(port, token, &job_id).await
|
||||
}
|
||||
|
||||
async fn wait_for_job_result(port: u16, token: &str, job_id: &str) -> anyhow::Result<String> {
|
||||
let url = format!(
|
||||
"http://localhost:{}/api/w/test-workspace/jobs_u/completed/get_result/{}",
|
||||
port, job_id
|
||||
);
|
||||
for _ in 0..100 {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
let resp = authed(client().get(&url), token).send().await?;
|
||||
if resp.status().is_success() {
|
||||
return Ok(resp.json::<serde_json::Value>().await?
|
||||
.as_str().unwrap_or("").to_string());
|
||||
}
|
||||
}
|
||||
anyhow::bail!("timeout waiting for job result")
|
||||
}
|
||||
|
||||
// TODO: Script tests - need to identify public execution endpoints for non-workspace-members
|
||||
// #[cfg(feature = "deno_core")]
|
||||
// #[sqlx::test(fixtures("base", "end_user_email"))]
|
||||
// async fn test_script_wm_end_user_email(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
// initialize_tracing().await;
|
||||
// set_jwt_secret().await;
|
||||
// let server = ApiServer::start(db.clone()).await?;
|
||||
// let port = server.addr.port();
|
||||
//
|
||||
// in_test_worker(Connection::Sql(db.clone()), async move {
|
||||
// let result = run_script(port, SAME_WS_TOKEN).await?;
|
||||
// assert_eq!(result, SAME_WS_EMAIL, "same workspace user should get their email");
|
||||
// Ok::<(), anyhow::Error>(())
|
||||
// }, port).await?;
|
||||
//
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
// TODO: Flow tests - need to identify public execution endpoints for non-workspace-members
|
||||
// #[cfg(feature = "deno_core")]
|
||||
// #[sqlx::test(fixtures("base", "end_user_email"))]
|
||||
// async fn test_flow_wm_end_user_email(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
// initialize_tracing().await;
|
||||
// set_jwt_secret().await;
|
||||
// let server = ApiServer::start(db.clone()).await?;
|
||||
// let port = server.addr.port();
|
||||
//
|
||||
// in_test_worker(Connection::Sql(db.clone()), async move {
|
||||
// let result = run_flow(port, SAME_WS_TOKEN).await?;
|
||||
// assert_eq!(result, SAME_WS_EMAIL, "same workspace user should get their email");
|
||||
// Ok::<(), anyhow::Error>(())
|
||||
// }, port).await?;
|
||||
//
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base", "end_user_email"))]
|
||||
async fn test_app_wm_end_user_email(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
set_jwt_secret().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let app_path = "f/test/email_app";
|
||||
|
||||
in_test_worker(Connection::Sql(db.clone()), async move {
|
||||
// Create the app with inline script first
|
||||
create_app_with_inline_script(port, app_path).await?;
|
||||
|
||||
// Same workspace user (force_viewer mode works for workspace members)
|
||||
let result = run_app_inline_script(port, SAME_WS_TOKEN, app_path, true).await?;
|
||||
assert_eq!(result, SAME_WS_EMAIL, "same workspace user should get their email");
|
||||
|
||||
// Other workspace user (uses app's anonymous policy + token lookup)
|
||||
let result = run_app_inline_script(port, OTHER_WS_TOKEN, app_path, false).await?;
|
||||
assert_eq!(result, OTHER_WS_EMAIL, "other workspace user should get their email");
|
||||
|
||||
// No workspace user (uses app's anonymous policy + token lookup)
|
||||
let result = run_app_inline_script(port, NO_WS_TOKEN, app_path, false).await?;
|
||||
assert_eq!(result, NO_WS_EMAIL, "no workspace user should get their email");
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
}, port).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base", "end_user_email"))]
|
||||
async fn test_raw_app_wm_end_user_email(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
set_jwt_secret().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let app_path = "f/test/email_raw_app";
|
||||
|
||||
in_test_worker(Connection::Sql(db.clone()), async move {
|
||||
// Create the raw app with inline script first
|
||||
create_raw_app_with_inline_script(port, app_path).await?;
|
||||
|
||||
// Same workspace user (force_viewer mode works for workspace members)
|
||||
let result = run_raw_app_inline_script(port, SAME_WS_TOKEN, app_path, true).await?;
|
||||
assert_eq!(result, SAME_WS_EMAIL, "same workspace user should get their email");
|
||||
|
||||
// Other workspace user (uses app's anonymous policy + token lookup)
|
||||
let result = run_raw_app_inline_script(port, OTHER_WS_TOKEN, app_path, false).await?;
|
||||
assert_eq!(result, OTHER_WS_EMAIL, "other workspace user should get their email");
|
||||
|
||||
// No workspace user (uses app's anonymous policy + token lookup)
|
||||
let result = run_raw_app_inline_script(port, NO_WS_TOKEN, app_path, false).await?;
|
||||
assert_eq!(result, NO_WS_EMAIL, "no workspace user should get their email");
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
}, port).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
63
backend/tests/fixtures/end_user_email.sql
vendored
Normal file
63
backend/tests/fixtures/end_user_email.sql
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
-- Fixture for WM_END_USER_EMAIL tests
|
||||
-- Sets up 3 users with different workspace memberships:
|
||||
-- 1. test@windmill.dev - in test-workspace (from base.sql)
|
||||
-- 2. other-ws@windmill.dev - in other-workspace only
|
||||
-- 3. no-ws@windmill.dev - not in any workspace
|
||||
|
||||
-- Second workspace for cross-workspace user
|
||||
INSERT INTO workspace (id, name, owner)
|
||||
VALUES ('other-workspace', 'other-workspace', 'other-ws-user');
|
||||
|
||||
INSERT INTO workspace_key(workspace_id, kind, key)
|
||||
VALUES ('other-workspace', 'cloud', 'other-key');
|
||||
|
||||
INSERT INTO workspace_settings (workspace_id)
|
||||
VALUES ('other-workspace');
|
||||
|
||||
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
|
||||
VALUES ('other-workspace', 'all', 'All users', '{}');
|
||||
|
||||
-- User in other-workspace only (not in test-workspace)
|
||||
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name)
|
||||
VALUES ('other-ws@windmill.dev', 'hash', 'password', false, true, 'Other WS User');
|
||||
|
||||
INSERT INTO usr(workspace_id, email, username, is_admin, role)
|
||||
VALUES ('other-workspace', 'other-ws@windmill.dev', 'other-ws-user', true, 'Admin');
|
||||
|
||||
INSERT INTO token(token, email, label, super_admin)
|
||||
VALUES ('OTHER_WS_TOKEN', 'other-ws@windmill.dev', 'other ws token', false);
|
||||
|
||||
-- User not in any workspace
|
||||
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name)
|
||||
VALUES ('no-ws@windmill.dev', 'hash', 'password', false, true, 'No WS User');
|
||||
|
||||
INSERT INTO token(token, email, label, super_admin)
|
||||
VALUES ('NO_WS_TOKEN', 'no-ws@windmill.dev', 'no ws token', false);
|
||||
|
||||
-- Script that returns WM_END_USER_EMAIL (public via extra_perms)
|
||||
INSERT INTO script (workspace_id, created_by, content, schema, summary, description, path, hash, language, lock, kind, extra_perms)
|
||||
VALUES (
|
||||
'test-workspace', 'test-user',
|
||||
'export function main() { return Deno.env.get("WM_END_USER_EMAIL") || ""; }',
|
||||
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
|
||||
'Returns WM_END_USER_EMAIL', '', 'f/test/get_end_user_email', 900001, 'deno', '', 'script',
|
||||
'{"g/all": true}'
|
||||
);
|
||||
|
||||
-- Flow that returns WM_END_USER_EMAIL (public via extra_perms)
|
||||
INSERT INTO flow (workspace_id, summary, description, path, versions, schema, value, edited_by, extra_perms)
|
||||
VALUES (
|
||||
'test-workspace', 'Returns WM_END_USER_EMAIL', '', 'f/test/get_end_user_email_flow', '{900002}',
|
||||
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
|
||||
'{"modules": [{"id": "a", "value": {"type": "rawscript", "language": "deno", "content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }", "input_transforms": {}}}]}',
|
||||
'test-user',
|
||||
'{"g/all": true}'
|
||||
);
|
||||
|
||||
INSERT INTO flow_version (id, workspace_id, path, schema, value, created_by)
|
||||
VALUES (
|
||||
900002, 'test-workspace', 'f/test/get_end_user_email_flow',
|
||||
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
|
||||
'{"modules": [{"id": "a", "value": {"type": "rawscript", "language": "deno", "content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }", "input_transforms": {}}}]}',
|
||||
'test-user'
|
||||
);
|
||||
@@ -206,7 +206,7 @@ fn spawn_workers(
|
||||
|
||||
std::fs::DirBuilder::new()
|
||||
.recursive(true)
|
||||
.create(windmill_worker::GO_BIN_CACHE_DIR)
|
||||
.create(&*windmill_worker::GO_BIN_CACHE_DIR)
|
||||
.expect("could not create initial worker dir");
|
||||
|
||||
let (tx, _) = KillpillSender::new(n + 1);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use windmill_test_utils::*;
|
||||
use sqlx::postgres::Postgres;
|
||||
use sqlx::Pool;
|
||||
use windmill_common::scripts::ScriptLang;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
#[sqlx::test(fixtures("base", "lockfile_python"))]
|
||||
@@ -188,7 +188,8 @@ def main():
|
||||
path: None,
|
||||
language: ScriptLang::Python3,
|
||||
lock: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
@@ -207,14 +208,14 @@ def main():
|
||||
#[cfg(feature = "python")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_python_global_site_packages(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
use windmill_common::{cache::concatcp, worker::ROOT_CACHE_DIR};
|
||||
use windmill_common::worker::ROOT_CACHE_DIR;
|
||||
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
// Shared for all 3.12.*
|
||||
let path = concatcp!(ROOT_CACHE_DIR, "python_3_12/global-site-packages").to_owned();
|
||||
let path = format!("{}python_3_12/global-site-packages", *ROOT_CACHE_DIR);
|
||||
std::fs::create_dir_all(&path).unwrap();
|
||||
std::fs::write(path + "/my_global_site_package_3_12_any.py", "").unwrap();
|
||||
|
||||
@@ -237,7 +238,9 @@ def main():
|
||||
path: None,
|
||||
language: ScriptLang::Python3,
|
||||
lock: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
)
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
@@ -271,7 +274,9 @@ def main():
|
||||
path: None,
|
||||
language: ScriptLang::Python3,
|
||||
lock: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
)
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
@@ -310,7 +315,8 @@ def main():
|
||||
path: None,
|
||||
language: ScriptLang::Python3,
|
||||
lock: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
@@ -347,7 +353,8 @@ def main():
|
||||
path: None,
|
||||
language: ScriptLang::Python3,
|
||||
lock: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
|
||||
102
backend/tests/scripts/test_volume_with_claude.ts
Normal file
102
backend/tests/scripts/test_volume_with_claude.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
// volume: agent-memory .claude
|
||||
// sandbox
|
||||
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
type Anthropic = {
|
||||
api_key: string;
|
||||
model?: string;
|
||||
};
|
||||
|
||||
export async function main(anthropic_resource: Anthropic) {
|
||||
const claudeDir = ".claude";
|
||||
const results: Record<string, unknown> = {};
|
||||
|
||||
// --- Step 1: Verify volume is mounted at the relative path ---
|
||||
results["volume_exists"] = fs.existsSync(claudeDir);
|
||||
if (!results["volume_exists"]) {
|
||||
fs.mkdirSync(claudeDir, { recursive: true });
|
||||
}
|
||||
|
||||
const testFile = path.join(claudeDir, "mount-check.txt");
|
||||
fs.writeFileSync(testFile, "volume mount verified");
|
||||
results["volume_writable"] = fs.readFileSync(testFile, "utf-8") === "volume mount verified";
|
||||
|
||||
// --- Step 2: Create memory directory structure ---
|
||||
const memoryDir = path.join(claudeDir, "memory");
|
||||
fs.mkdirSync(memoryDir, { recursive: true });
|
||||
|
||||
const memoryFile = path.join(memoryDir, "MEMORY.md");
|
||||
fs.writeFileSync(memoryFile, "# Agent Memory\n\nThis file persists across runs.\n");
|
||||
results["memory_file_created"] = fs.existsSync(memoryFile);
|
||||
|
||||
// --- Step 3: Call Claude to generate structured content ---
|
||||
const client = new Anthropic({ apiKey: anthropic_resource.api_key });
|
||||
const model = anthropic_resource.model ?? "claude-sonnet-4-20250514";
|
||||
|
||||
const response = await client.messages.create({
|
||||
model,
|
||||
max_tokens: 256,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
'Return a JSON object with exactly these keys: "greeting" (a short hello), "timestamp" (current ISO date you estimate), "items" (array of 3 random fruit names). Only return the JSON, no markdown.',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const assistantText =
|
||||
response.content[0].type === "text" ? response.content[0].text : "";
|
||||
results["claude_responded"] = assistantText.length > 0;
|
||||
results["claude_model"] = response.model;
|
||||
results["claude_stop_reason"] = response.stop_reason;
|
||||
|
||||
let parsed: Record<string, unknown> = {};
|
||||
try {
|
||||
parsed = JSON.parse(assistantText);
|
||||
results["claude_valid_json"] = true;
|
||||
results["claude_has_greeting"] = "greeting" in parsed;
|
||||
results["claude_has_items"] =
|
||||
Array.isArray(parsed.items) && parsed.items.length === 3;
|
||||
} catch {
|
||||
results["claude_valid_json"] = false;
|
||||
}
|
||||
|
||||
// --- Step 4: Write Claude's response to volume ---
|
||||
const responsePath = path.join(claudeDir, "claude-response.json");
|
||||
fs.writeFileSync(responsePath, JSON.stringify(parsed, null, 2));
|
||||
results["response_written"] = fs.existsSync(responsePath);
|
||||
|
||||
// --- Step 5: Read back and verify ---
|
||||
const readBack = fs.readFileSync(responsePath, "utf-8");
|
||||
const readParsed = JSON.parse(readBack);
|
||||
results["readback_matches"] =
|
||||
JSON.stringify(readParsed) === JSON.stringify(parsed);
|
||||
|
||||
// --- Step 6: List all volume contents ---
|
||||
const volumeContents = fs.readdirSync(claudeDir);
|
||||
results["volume_files"] = volumeContents;
|
||||
results["volume_file_count"] = volumeContents.length;
|
||||
|
||||
// --- Step 7: Verify memory file persists ---
|
||||
const memoryContent = fs.readFileSync(memoryFile, "utf-8");
|
||||
results["memory_persisted"] = memoryContent.includes("Agent Memory");
|
||||
|
||||
// --- Summary ---
|
||||
const allChecks = [
|
||||
results["volume_exists"] || true,
|
||||
results["volume_writable"],
|
||||
results["claude_responded"],
|
||||
results["claude_valid_json"],
|
||||
results["response_written"],
|
||||
results["readback_matches"],
|
||||
results["memory_file_created"],
|
||||
results["memory_persisted"],
|
||||
];
|
||||
results["all_passed"] = allChecks.every(Boolean);
|
||||
|
||||
return results;
|
||||
}
|
||||
637
backend/tests/volume_tests.rs
Normal file
637
backend/tests/volume_tests.rs
Normal file
@@ -0,0 +1,637 @@
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::jobs::{JobPayload, RawCode};
|
||||
use windmill_common::scripts::ScriptLang;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_volume_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO volume (workspace_id, name, size_bytes, created_by)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
"test-workspace",
|
||||
"test-volume",
|
||||
1024_i64,
|
||||
"test-user"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let row = sqlx::query!(
|
||||
"SELECT workspace_id, name, size_bytes, created_by, last_used_at
|
||||
FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"test-volume"
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
|
||||
assert_eq!(row.workspace_id, "test-workspace");
|
||||
assert_eq!(row.name, "test-volume");
|
||||
assert_eq!(row.size_bytes, 1024);
|
||||
assert_eq!(row.created_by, "test-user");
|
||||
assert!(row.last_used_at.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_volume_upsert_size(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO volume (workspace_id, name, size_bytes, created_by, last_used_at)
|
||||
VALUES ($1, $2, $3, $4, now())
|
||||
ON CONFLICT (workspace_id, name) DO UPDATE
|
||||
SET size_bytes = $3, last_used_at = now()",
|
||||
"test-workspace",
|
||||
"upsert-vol",
|
||||
500_i64,
|
||||
"test-user"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let row = sqlx::query!(
|
||||
"SELECT size_bytes FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"upsert-vol"
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert_eq!(row.size_bytes, 500);
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO volume (workspace_id, name, size_bytes, created_by, last_used_at)
|
||||
VALUES ($1, $2, $3, $4, now())
|
||||
ON CONFLICT (workspace_id, name) DO UPDATE
|
||||
SET size_bytes = $3, last_used_at = now()",
|
||||
"test-workspace",
|
||||
"upsert-vol",
|
||||
2048_i64,
|
||||
"test-user"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let row = sqlx::query!(
|
||||
"SELECT size_bytes, last_used_at FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"upsert-vol"
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert_eq!(row.size_bytes, 2048);
|
||||
assert!(row.last_used_at.is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_volume_update_last_used(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO volume (workspace_id, name, size_bytes, created_by)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
"test-workspace",
|
||||
"used-vol",
|
||||
100_i64,
|
||||
"test-user"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let row = sqlx::query!(
|
||||
"SELECT last_used_at FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"used-vol"
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert!(row.last_used_at.is_none());
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE volume SET last_used_at = now() WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"used-vol"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let row = sqlx::query!(
|
||||
"SELECT last_used_at FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"used-vol"
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert!(row.last_used_at.is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_volume_update_nonexistent_noop(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"UPDATE volume SET last_used_at = now() WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"nonexistent-vol"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
assert_eq!(result.rows_affected(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_volume_list_multiple(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
for i in 0..5 {
|
||||
sqlx::query!(
|
||||
"INSERT INTO volume (workspace_id, name, size_bytes, created_by)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
"test-workspace",
|
||||
format!("vol-{}", i),
|
||||
(i * 100) as i64,
|
||||
"test-user"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let rows = sqlx::query!(
|
||||
"SELECT name, size_bytes FROM volume WHERE workspace_id = $1 ORDER BY name",
|
||||
"test-workspace"
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
|
||||
assert_eq!(rows.len(), 5);
|
||||
assert_eq!(rows[0].name, "vol-0");
|
||||
assert_eq!(rows[0].size_bytes, 0);
|
||||
assert_eq!(rows[4].name, "vol-4");
|
||||
assert_eq!(rows[4].size_bytes, 400);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_volume_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO volume (workspace_id, name, size_bytes, created_by)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
"test-workspace",
|
||||
"deleteme",
|
||||
100_i64,
|
||||
"test-user"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let count = sqlx::query_scalar!(
|
||||
"SELECT count(*) FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"deleteme"
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert_eq!(count, Some(1));
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"deleteme"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let count = sqlx::query_scalar!(
|
||||
"SELECT count(*) FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"deleteme"
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert_eq!(count, Some(0));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_volume_workspace_fk_constraint(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"INSERT INTO volume (workspace_id, name, size_bytes, created_by)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
"nonexistent-workspace",
|
||||
"vol",
|
||||
100_i64,
|
||||
"test-user"
|
||||
)
|
||||
.execute(&db)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
err.contains("foreign key"),
|
||||
"Expected foreign key violation, got: {}",
|
||||
err
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_volume_primary_key_uniqueness(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO volume (workspace_id, name, size_bytes, created_by)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
"test-workspace",
|
||||
"unique-vol",
|
||||
100_i64,
|
||||
"test-user"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"INSERT INTO volume (workspace_id, name, size_bytes, created_by)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
"test-workspace",
|
||||
"unique-vol",
|
||||
200_i64,
|
||||
"another-user"
|
||||
)
|
||||
.execute(&db)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
err.contains("duplicate key") || err.contains("unique"),
|
||||
"Expected unique violation, got: {}",
|
||||
err
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_volume_extra_perms(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
// Insert volume with default (empty) extra_perms
|
||||
sqlx::query!(
|
||||
"INSERT INTO volume (workspace_id, name, size_bytes, created_by)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
"test-workspace",
|
||||
"perms-vol",
|
||||
100_i64,
|
||||
"test-user"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
// Default extra_perms should be empty object
|
||||
let row = sqlx::query!(
|
||||
"SELECT extra_perms FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"perms-vol"
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert_eq!(row.extra_perms, serde_json::json!({}));
|
||||
|
||||
// Set extra_perms via jsonb_set (same pattern as granular_acls.rs)
|
||||
sqlx::query!(
|
||||
"UPDATE volume SET extra_perms = jsonb_set(extra_perms, $1, to_jsonb($2::bool), true)
|
||||
WHERE workspace_id = $3 AND name = $4",
|
||||
&vec!["u/alice".to_string()],
|
||||
true,
|
||||
"test-workspace",
|
||||
"perms-vol"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let row = sqlx::query!(
|
||||
"SELECT extra_perms FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"perms-vol"
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
let perms = row.extra_perms.as_object().unwrap();
|
||||
assert_eq!(perms.get("u/alice").and_then(|v| v.as_bool()), Some(true));
|
||||
|
||||
// Remove a permission entry
|
||||
sqlx::query!(
|
||||
"UPDATE volume SET extra_perms = extra_perms - $1
|
||||
WHERE workspace_id = $2 AND name = $3",
|
||||
"u/alice",
|
||||
"test-workspace",
|
||||
"perms-vol"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let row = sqlx::query!(
|
||||
"SELECT extra_perms FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"perms-vol"
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert_eq!(row.extra_perms, serde_json::json!({}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_volume_annotations_python() {
|
||||
use windmill_worker_volumes::parse_volume_annotations;
|
||||
|
||||
let content = r#"# sandbox
|
||||
# volume: training-data /tmp/training
|
||||
# volume: models /opt/models
|
||||
|
||||
def main():
|
||||
pass
|
||||
"#;
|
||||
let volumes = parse_volume_annotations(content, "#");
|
||||
assert_eq!(volumes.len(), 2);
|
||||
assert_eq!(volumes[0].name, "training-data");
|
||||
assert_eq!(volumes[0].target, "/tmp/training");
|
||||
assert_eq!(volumes[1].name, "models");
|
||||
assert_eq!(volumes[1].target, "/opt/models");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_volume_annotations_typescript() {
|
||||
use windmill_worker_volumes::parse_volume_annotations;
|
||||
|
||||
let content = r#"// sandbox
|
||||
// volume: datasets /tmp/datasets
|
||||
|
||||
export async function main() {
|
||||
return "hello";
|
||||
}
|
||||
"#;
|
||||
let volumes = parse_volume_annotations(content, "//");
|
||||
assert_eq!(volumes.len(), 1);
|
||||
assert_eq!(volumes[0].name, "datasets");
|
||||
assert_eq!(volumes[0].target, "/tmp/datasets");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_volume_annotations_no_prefix_match() {
|
||||
use windmill_worker_volumes::parse_volume_annotations;
|
||||
|
||||
let content = "def main():\n pass";
|
||||
let volumes = parse_volume_annotations(content, "#");
|
||||
assert!(volumes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_volume_annotations_empty_script() {
|
||||
use windmill_worker_volumes::parse_volume_annotations;
|
||||
|
||||
let volumes = parse_volume_annotations("", "#");
|
||||
assert!(volumes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sandbox_annotation_python() {
|
||||
use windmill_common::worker::PythonAnnotations;
|
||||
|
||||
let content = "# sandbox\n# volume: data /tmp/data\ndef main():\n pass";
|
||||
let annotations = PythonAnnotations::parse(content);
|
||||
assert!(annotations.sandbox);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sandbox_annotation_typescript() {
|
||||
use windmill_common::worker::TypeScriptAnnotations;
|
||||
|
||||
let content = "// sandbox\n// volume: data /tmp/data\nexport function main() {}";
|
||||
let annotations = TypeScriptAnnotations::parse(content);
|
||||
assert!(annotations.sandbox);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_volume_comment_prefix_selection() {
|
||||
use windmill_common::scripts::ScriptLang;
|
||||
|
||||
let get_prefix = |lang: &ScriptLang| -> &str {
|
||||
match lang {
|
||||
ScriptLang::Python3
|
||||
| ScriptLang::Bash
|
||||
| ScriptLang::Powershell
|
||||
| ScriptLang::Ansible
|
||||
| ScriptLang::Ruby => "#",
|
||||
ScriptLang::Deno
|
||||
| ScriptLang::Bun
|
||||
| ScriptLang::Bunnative
|
||||
| ScriptLang::Nativets
|
||||
| ScriptLang::Go => "//",
|
||||
_ => "",
|
||||
}
|
||||
};
|
||||
|
||||
assert_eq!(get_prefix(&ScriptLang::Python3), "#");
|
||||
assert_eq!(get_prefix(&ScriptLang::Bash), "#");
|
||||
assert_eq!(get_prefix(&ScriptLang::Powershell), "#");
|
||||
assert_eq!(get_prefix(&ScriptLang::Ansible), "#");
|
||||
assert_eq!(get_prefix(&ScriptLang::Ruby), "#");
|
||||
assert_eq!(get_prefix(&ScriptLang::Deno), "//");
|
||||
assert_eq!(get_prefix(&ScriptLang::Bun), "//");
|
||||
assert_eq!(get_prefix(&ScriptLang::Bunnative), "//");
|
||||
assert_eq!(get_prefix(&ScriptLang::Nativets), "//");
|
||||
assert_eq!(get_prefix(&ScriptLang::Go), "//");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_volume_mount_struct() {
|
||||
use windmill_worker_volumes::VolumeMount;
|
||||
|
||||
let mount = VolumeMount { name: "test-vol".to_string(), target: "/mnt/data".to_string() };
|
||||
assert_eq!(mount.name, "test-vol");
|
||||
assert_eq!(mount.target, "/mnt/data");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_volume_relative_path() {
|
||||
use windmill_worker_volumes::parse_volume_annotations;
|
||||
|
||||
let content = "// volume: agent-memory .claude\nexport function main() {}";
|
||||
let volumes = parse_volume_annotations(content, "//");
|
||||
assert_eq!(volumes.len(), 1);
|
||||
assert_eq!(volumes[0].name, "agent-memory");
|
||||
assert_eq!(volumes[0].target, ".claude");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_volume_relative_nested_path() {
|
||||
use windmill_worker_volumes::parse_volume_annotations;
|
||||
|
||||
let content = "# volume: data data/models\ndef main():\n pass";
|
||||
let volumes = parse_volume_annotations(content, "#");
|
||||
assert_eq!(volumes.len(), 1);
|
||||
assert_eq!(volumes[0].name, "data");
|
||||
assert_eq!(volumes[0].target, "data/models");
|
||||
}
|
||||
|
||||
#[cfg(feature = "private")]
|
||||
#[test]
|
||||
fn test_volume_nsjail_mount() {
|
||||
use std::path::Path;
|
||||
use windmill_worker_volumes::volume_nsjail_mount;
|
||||
|
||||
let result = volume_nsjail_mount(Path::new("/tmp/volumes/data"), "/mnt/data");
|
||||
assert!(result.contains("src: \"/tmp/volumes/data\""));
|
||||
assert!(result.contains("dst: \"/mnt/data\""));
|
||||
assert!(result.contains("is_bind: true"));
|
||||
assert!(result.contains("rw: true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_stats_default() {
|
||||
use windmill_worker_volumes::SyncStats;
|
||||
|
||||
let stats = SyncStats { new_size_bytes: 0, file_count: 0, uploaded: 0, skipped: 0 };
|
||||
assert_eq!(stats.new_size_bytes, 0);
|
||||
assert_eq!(stats.file_count, 0);
|
||||
assert_eq!(stats.uploaded, 0);
|
||||
assert_eq!(stats.skipped, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_asset_kind_volume_variant() {
|
||||
use windmill_types::assets::AssetKind;
|
||||
|
||||
let kind = AssetKind::Volume;
|
||||
let serialized = serde_json::to_string(&kind).unwrap();
|
||||
assert_eq!(serialized, "\"volume\"");
|
||||
|
||||
let deserialized: AssetKind = serde_json::from_str("\"volume\"").unwrap();
|
||||
assert!(matches!(deserialized, AssetKind::Volume));
|
||||
}
|
||||
|
||||
/// E2E test: run a bun script with volume mount through a SQL-connected worker.
|
||||
/// Pre-populates the volume in filesystem storage, verifies the script can read
|
||||
/// files and write new ones, then checks sync-back to storage and DB state.
|
||||
#[cfg(feature = "parquet")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_volume_sql_worker_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
// 1. Set up filesystem-based object storage in a temp dir
|
||||
let storage_dir = tempfile::tempdir()?;
|
||||
let storage_root = storage_dir.path().to_string_lossy().to_string();
|
||||
|
||||
let lfs_config = json!({
|
||||
"type": "FilesystemStorage",
|
||||
"root_path": storage_root,
|
||||
"public_resource": null,
|
||||
"advanced_permissions": null,
|
||||
"volume_storage": "primary"
|
||||
});
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET large_file_storage = $1 WHERE workspace_id = $2",
|
||||
lfs_config,
|
||||
"test-workspace"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
// 2. Pre-populate the volume with a file (workspace-namespaced path)
|
||||
let vol_dir = storage_dir
|
||||
.path()
|
||||
.join("volumes")
|
||||
.join("test-workspace")
|
||||
.join("test-vol");
|
||||
std::fs::create_dir_all(&vol_dir)?;
|
||||
std::fs::write(vol_dir.join("hello.txt"), b"hello from volume")?;
|
||||
|
||||
// 3. Push the job and run with SQL-connected worker
|
||||
let code = r#"// volume: test-vol /tmp/data
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync } from "fs";
|
||||
|
||||
export function main() {
|
||||
const content = readFileSync("/tmp/data/hello.txt", "utf-8");
|
||||
writeFileSync("/tmp/data/output.txt", "written by sql worker");
|
||||
return {
|
||||
read_content: content,
|
||||
output_exists: existsSync("/tmp/data/output.txt"),
|
||||
};
|
||||
}"#;
|
||||
|
||||
let job = JobPayload::Code(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(),
|
||||
});
|
||||
|
||||
let result = run_job_in_new_worker_until_complete(&db, false, job, port).await;
|
||||
|
||||
assert!(result.success, "job should succeed: {:?}", result.result);
|
||||
let json = result.json_result().expect("should have JSON result");
|
||||
assert_eq!(json["read_content"], json!("hello from volume"));
|
||||
assert_eq!(json["output_exists"], json!(true));
|
||||
|
||||
// 4. Verify volume DB row was updated
|
||||
let vol_row = sqlx::query!(
|
||||
"SELECT size_bytes, file_count, leased_by, lease_until
|
||||
FROM volume WHERE workspace_id = $1 AND name = $2",
|
||||
"test-workspace",
|
||||
"test-vol"
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
|
||||
let vol_row = vol_row.expect("volume row should exist");
|
||||
assert!(
|
||||
vol_row.file_count >= 2,
|
||||
"should have at least 2 files (hello.txt + output.txt), got: {}",
|
||||
vol_row.file_count
|
||||
);
|
||||
assert!(vol_row.size_bytes > 0, "size_bytes should be > 0");
|
||||
assert!(vol_row.leased_by.is_none(), "lease should be released");
|
||||
|
||||
// 5. Verify the new file was written back to storage
|
||||
let output_path = vol_dir.join("output.txt");
|
||||
assert!(
|
||||
output_path.exists(),
|
||||
"output.txt should be synced back to storage"
|
||||
);
|
||||
let output_content = std::fs::read_to_string(&output_path)?;
|
||||
assert_eq!(output_content, "written by sql worker");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3548,3 +3548,170 @@ async fn test_flow_substep_tag_availability_check(db: Pool<Postgres>) -> anyhow:
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "quickjs", feature = "python"))]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_stop_after_all_iters_if_bad_expr_parallel_branchall(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
let port = 123;
|
||||
let flow: FlowValue = serde_json::from_value(serde_json::json!({
|
||||
"modules": [
|
||||
{
|
||||
"id": "a",
|
||||
"value": {
|
||||
"branches": [
|
||||
{"modules": [{
|
||||
"id": "b",
|
||||
"value": {
|
||||
"input_transforms": { "n": { "type": "javascript", "expr": "flow_input.n" } },
|
||||
"type": "rawscript",
|
||||
"language": "python3",
|
||||
"content": "def main(n): return n",
|
||||
},
|
||||
}]}
|
||||
],
|
||||
"type": "branchall",
|
||||
"parallel": true,
|
||||
},
|
||||
"stop_after_all_iters_if": {
|
||||
"expr": "invalid!!!syntax",
|
||||
"skip_if_stopped": false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
.unwrap();
|
||||
let job = JobPayload::RawFlow { value: flow, path: None, restarted_from: None };
|
||||
|
||||
let cjob = RunJob::from(job)
|
||||
.arg("n", json!(42))
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
!cjob.success,
|
||||
"flow should fail when stop_after_all_iters_if has bad expression"
|
||||
);
|
||||
|
||||
let result = cjob.json_result().unwrap();
|
||||
let error_msg = result["error"]["message"].as_str().unwrap_or("");
|
||||
assert!(
|
||||
error_msg.contains("stop_after_all_iters_if"),
|
||||
"error should mention stop_after_all_iters_if, got: {error_msg}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "quickjs", feature = "python"))]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_stop_after_all_iters_if_bad_expr_parallel_forloop(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
let port = 123;
|
||||
let flow: FlowValue = serde_json::from_value(serde_json::json!({
|
||||
"modules": [
|
||||
{
|
||||
"id": "a",
|
||||
"value": {
|
||||
"type": "forloopflow",
|
||||
"iterator": { "type": "javascript", "expr": "result.items" },
|
||||
"skip_failures": false,
|
||||
"parallel": true,
|
||||
"modules": [{
|
||||
"value": {
|
||||
"input_transforms": {
|
||||
"n": { "type": "javascript", "expr": "flow_input.iter.value" },
|
||||
},
|
||||
"type": "rawscript",
|
||||
"language": "python3",
|
||||
"content": "def main(n): return n",
|
||||
},
|
||||
}],
|
||||
},
|
||||
"stop_after_all_iters_if": {
|
||||
"expr": "invalid!!!syntax",
|
||||
"skip_if_stopped": false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
.unwrap();
|
||||
let job = JobPayload::RawFlow { value: flow, path: None, restarted_from: None };
|
||||
|
||||
let cjob = RunJob::from(job)
|
||||
.arg("items", json!([1, 2, 3]))
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
!cjob.success,
|
||||
"flow should fail when stop_after_all_iters_if has bad expression"
|
||||
);
|
||||
|
||||
let result = cjob.json_result().unwrap();
|
||||
let error_msg = result["error"]["message"].as_str().unwrap_or("");
|
||||
assert!(
|
||||
error_msg.contains("stop_after_all_iters_if"),
|
||||
"error should mention stop_after_all_iters_if, got: {error_msg}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "quickjs", feature = "python"))]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_results_length_in_input_transform(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
// Step a returns a list, step b accesses results.a.length via input transform.
|
||||
// This tests that the handle_full_regex fast path falls through to QuickJS
|
||||
// when the SQL JSON path operator can't resolve JS properties like .length.
|
||||
let flow: FlowValue = serde_json::from_value(json!({
|
||||
"modules": [
|
||||
{
|
||||
"id": "a",
|
||||
"value": {
|
||||
"type": "rawscript",
|
||||
"language": "python3",
|
||||
"content": "def main(): return [10, 20, 30]",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
"value": {
|
||||
"input_transforms": {
|
||||
"v": { "type": "javascript", "expr": "results.a.length" },
|
||||
},
|
||||
"type": "rawscript",
|
||||
"language": "python3",
|
||||
"content": "def main(v): return v",
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let result =
|
||||
RunJob::from(JobPayload::RawFlow { value: flow, path: None, restarted_from: None })
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
.json_result()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
json!(3),
|
||||
"results.a.length should resolve to 3, not null"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -51,4 +51,12 @@ impl AgentCache {
|
||||
pub fn new() -> Self {
|
||||
AgentCache {}
|
||||
}
|
||||
|
||||
pub async fn extract_worker_name(
|
||||
&self,
|
||||
_token: &str,
|
||||
_db: &windmill_common::DB,
|
||||
) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,45 @@ use windmill_common::{
|
||||
lazy_static::lazy_static! {
|
||||
// Global auth cache accessible from main.rs for direct invalidation
|
||||
pub static ref AUTH_CACHE: Cache<(String, String), ExpiringAuthCache> = Cache::new(300);
|
||||
// Cache for token -> email lookups (for non-workspace-member authenticated users)
|
||||
static ref TOKEN_EMAIL_CACHE: Cache<String, Option<String>> = Cache::new(500);
|
||||
}
|
||||
|
||||
/// Get email from a valid token, with caching.
|
||||
/// Used for WM_END_USER_EMAIL when user is authenticated but not a workspace member.
|
||||
async fn get_email_from_token(db: &DB, token: &str) -> Option<String> {
|
||||
if let Some(cached) = TOKEN_EMAIL_CACHE.get(token) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
let email = sqlx::query_scalar!(
|
||||
"SELECT email FROM token WHERE token = $1 AND (expiration > NOW() OR expiration IS NULL)",
|
||||
token
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.flatten(); // email column is nullable, so we get Option<Option<String>>
|
||||
|
||||
TOKEN_EMAIL_CACHE.insert(token.to_string(), email.clone());
|
||||
email
|
||||
}
|
||||
|
||||
/// Get end user email from authenticated user or token.
|
||||
/// Returns email if user is authenticated (workspace member) or has valid instance token.
|
||||
pub async fn get_end_user_email(
|
||||
db: &DB,
|
||||
opt_authed: Option<&ApiAuthed>,
|
||||
token: Option<&str>,
|
||||
) -> Option<String> {
|
||||
if let Some(authed) = opt_authed {
|
||||
return Some(authed.email.clone());
|
||||
}
|
||||
if let Some(token) = token {
|
||||
return get_email_from_token(db, token).await;
|
||||
}
|
||||
None
|
||||
}
|
||||
// Global function to invalidate a specific token from cache
|
||||
pub fn invalidate_token_from_cache(token: &str) {
|
||||
|
||||
@@ -29,8 +29,8 @@ use scopes::ScopeDefinition;
|
||||
|
||||
// Re-export key auth types and functions
|
||||
pub use auth::{
|
||||
invalidate_token_from_cache, AuthCache, ExpiringAuthCache, OptTokened, Tokened,
|
||||
TruncatedTokenWithEmail, AUTH_CACHE,
|
||||
get_end_user_email, invalidate_token_from_cache, AuthCache, ExpiringAuthCache, OptTokened,
|
||||
Tokened, TruncatedTokenWithEmail, AUTH_CACHE,
|
||||
};
|
||||
|
||||
// ------------ ApiAuthed & OptJobAuthed types ------------
|
||||
@@ -557,6 +557,14 @@ pub async fn create_token_internal(
|
||||
));
|
||||
}
|
||||
|
||||
register_token_expiry_notification(
|
||||
&mut *tx,
|
||||
&token,
|
||||
token_config.label.as_deref(),
|
||||
token_config.expiration,
|
||||
)
|
||||
.await;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
authed,
|
||||
@@ -572,6 +580,31 @@ pub async fn create_token_internal(
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
/// Insert a pending expiry notification row for user tokens that have an expiration.
|
||||
pub async fn register_token_expiry_notification(
|
||||
tx: &mut sqlx::PgConnection,
|
||||
token: &str,
|
||||
label: Option<&str>,
|
||||
expiration: Option<chrono::DateTime<chrono::Utc>>,
|
||||
) {
|
||||
let Some(expiration) = expiration else { return };
|
||||
if label == Some("session")
|
||||
|| label.is_some_and(|l| l.starts_with("ephemeral") || l.starts_with("Ephemeral"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Err(e) = sqlx::query!(
|
||||
"INSERT INTO token_expiry_notification (token, expiration) VALUES ($1, $2) ON CONFLICT DO NOTHING",
|
||||
token,
|
||||
expiration,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to register token expiry notification: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------ Permission helpers ------------
|
||||
|
||||
pub fn get_perm_in_extra_perms_for_authed(
|
||||
|
||||
@@ -24,7 +24,7 @@ use windmill_common::{
|
||||
utils::{not_found_if_none, StripPath},
|
||||
};
|
||||
|
||||
const KINDS: [&str; 18] = [
|
||||
const KINDS: [&str; 19] = [
|
||||
"script",
|
||||
"group_",
|
||||
"resource",
|
||||
@@ -43,6 +43,7 @@ const KINDS: [&str; 18] = [
|
||||
"gcp_trigger",
|
||||
"sqs_trigger",
|
||||
"email_trigger",
|
||||
"volume",
|
||||
];
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
@@ -77,7 +78,7 @@ async fn add_granular_acl(
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let identifier = if kind == "group_" || kind == "folder" {
|
||||
let identifier = if kind == "group_" || kind == "folder" || kind == "volume" {
|
||||
"name"
|
||||
} else {
|
||||
"path"
|
||||
@@ -89,6 +90,22 @@ async fn add_granular_acl(
|
||||
} else if kind == "group_" {
|
||||
crate::groups::require_is_owner(path, &authed.username, &authed.groups, &w_id, &db)
|
||||
.await?;
|
||||
} else if kind == "volume" {
|
||||
let created_by = sqlx::query_scalar!(
|
||||
"SELECT created_by FROM volume WHERE name = $1 AND workspace_id = $2",
|
||||
path,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.ok_or_else(|| Error::NotFound(format!("volume '{path}' not found")))?;
|
||||
// created_by is stored with u/ prefix (from job.permissioned_as)
|
||||
let owner_username = created_by.strip_prefix("u/").unwrap_or(&created_by);
|
||||
if owner_username != authed.username {
|
||||
return Err(Error::NotAuthorized(
|
||||
"Only the volume owner or an admin can modify permissions".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
require_owner_of_path(&authed, path)?;
|
||||
}
|
||||
@@ -243,6 +260,22 @@ async fn remove_granular_acl(
|
||||
} else if kind == "group_" {
|
||||
crate::groups::require_is_owner(path, &authed.username, &authed.groups, &w_id, &db)
|
||||
.await?;
|
||||
} else if kind == "volume" {
|
||||
let created_by = sqlx::query_scalar!(
|
||||
"SELECT created_by FROM volume WHERE name = $1 AND workspace_id = $2",
|
||||
path,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.ok_or_else(|| Error::NotFound(format!("volume '{path}' not found")))?;
|
||||
// created_by is stored with u/ prefix (from job.permissioned_as)
|
||||
let owner_username = created_by.strip_prefix("u/").unwrap_or(&created_by);
|
||||
if owner_username != authed.username {
|
||||
return Err(Error::NotAuthorized(
|
||||
"Only the volume owner or an admin can modify permissions".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
require_owner_of_path(&authed, path)?;
|
||||
}
|
||||
@@ -250,7 +283,7 @@ async fn remove_granular_acl(
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let identifier = if kind == "group_" || kind == "folder" {
|
||||
let identifier = if kind == "group_" || kind == "folder" || kind == "volume" {
|
||||
"name"
|
||||
} else {
|
||||
"path"
|
||||
@@ -380,7 +413,11 @@ async fn get_granular_acls(
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let identifier = if kind == "group_" { "name" } else { "path" };
|
||||
let identifier = if kind == "group_" || kind == "folder" || kind == "volume" {
|
||||
"name"
|
||||
} else {
|
||||
"path"
|
||||
};
|
||||
let obj_o = sqlx::query_scalar::<_, serde_json::Value>(&format!(
|
||||
"SELECT extra_perms from {kind} WHERE {identifier} = $1 AND workspace_id = $2"
|
||||
))
|
||||
|
||||
@@ -146,15 +146,24 @@ async fn get_input_history(
|
||||
"AND parent_job IS NULL"
|
||||
};
|
||||
|
||||
let sql = &format!(
|
||||
"select id, v2_job_completed.completed_at, created_by, 'null'::jsonb as args, status = 'success' as success from v2_job JOIN v2_job_completed USING (id) \
|
||||
where v2_job.workspace_id = $3 and {} = $1 and kind = any($2) {args_query} AND v2_job_completed.status != 'skipped' {include_non_root} \
|
||||
order by v2_job_completed.completed_at desc limit $4 offset $5",
|
||||
r.runnable_type.column_name(),
|
||||
// Two-step approach: first fetch 2*(per_page+offset) rows using created_at ordering
|
||||
// (which leverages the ix_job_root_job_index_by_path_2 index on v2_job), then sort
|
||||
// the small result set by completed_at. This works because created_at and completed_at
|
||||
// are highly correlated.
|
||||
let inner_limit = 2 * (per_page + offset);
|
||||
|
||||
let sql = &format!(
|
||||
"SELECT id, completed_at, created_by, args, success FROM (\
|
||||
SELECT id, v2_job_completed.completed_at, created_by, 'null'::jsonb as args, \
|
||||
status = 'success' as success \
|
||||
FROM v2_job JOIN v2_job_completed USING (id) \
|
||||
WHERE v2_job.workspace_id = $3 AND {} = $1 AND kind = any($2) \
|
||||
{args_query} AND v2_job_completed.status != 'skipped' {include_non_root} \
|
||||
ORDER BY v2_job.created_at DESC LIMIT $4\
|
||||
) t ORDER BY completed_at DESC LIMIT $5 OFFSET $6",
|
||||
r.runnable_type.column_name(),
|
||||
);
|
||||
|
||||
// tracing::info!("sql: {}", sql);
|
||||
let query = sqlx::query_as::<_, CompletedJobMini>(sql);
|
||||
|
||||
let query = match r.runnable_type {
|
||||
@@ -175,6 +184,7 @@ async fn get_input_history(
|
||||
let rows = query
|
||||
.bind(job_kinds)
|
||||
.bind(&w_id)
|
||||
.bind(inner_limit as i32)
|
||||
.bind(per_page as i32)
|
||||
.bind(offset as i32)
|
||||
.fetch_all(&mut *tx)
|
||||
|
||||
@@ -43,8 +43,8 @@ use windmill_common::{
|
||||
get_database_url,
|
||||
global_settings::{
|
||||
APP_WORKSPACED_ROUTE_SETTING, AUTOMATE_USERNAME_CREATION_SETTING,
|
||||
CRITICAL_ALERT_MUTE_UI_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING, EMAIL_DOMAIN_SETTING,
|
||||
ENV_SETTINGS, HUB_ACCESSIBLE_URL_SETTING, HUB_BASE_URL_SETTING,
|
||||
CRITICAL_ALERT_MUTE_UI_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING, DISABLE_HUB_SETTING,
|
||||
EMAIL_DOMAIN_SETTING, ENV_SETTINGS, HUB_ACCESSIBLE_URL_SETTING, HUB_BASE_URL_SETTING,
|
||||
},
|
||||
instance_config::{self, ApplyMode, InstanceConfig},
|
||||
server::Smtp,
|
||||
@@ -519,6 +519,7 @@ pub async fn get_global_setting(
|
||||
&& key != DEFAULT_TAGS_WORKSPACES_SETTING
|
||||
&& key != HUB_BASE_URL_SETTING
|
||||
&& key != HUB_ACCESSIBLE_URL_SETTING
|
||||
&& key != DISABLE_HUB_SETTING
|
||||
&& key != EMAIL_DOMAIN_SETTING
|
||||
&& key != APP_WORKSPACED_ROUTE_SETTING
|
||||
{
|
||||
@@ -1085,7 +1086,7 @@ async fn sync_cached_resource_types(
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
|
||||
use windmill_common::worker::HUB_RT_CACHE_DIR;
|
||||
let cache_path = format!("{}/resource_types.json", HUB_RT_CACHE_DIR);
|
||||
let cache_path = format!("{}/resource_types.json", *HUB_RT_CACHE_DIR);
|
||||
|
||||
let content = tokio::fs::read_to_string(&cache_path).await.map_err(|e| {
|
||||
error::Error::NotFound(format!(
|
||||
|
||||
@@ -1850,6 +1850,14 @@ async fn impersonate(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
windmill_api_auth::register_token_expiry_notification(
|
||||
&mut *tx,
|
||||
&token,
|
||||
new_token.label.as_deref(),
|
||||
new_token.expiration,
|
||||
)
|
||||
.await;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
|
||||
@@ -29,6 +29,7 @@ windmill-dep-map.workspace = true
|
||||
axum.workspace = true
|
||||
chrono.workspace = true
|
||||
hex.workspace = true
|
||||
magic-crypt.workspace = true
|
||||
http.workspace = true
|
||||
hyper.workspace = true
|
||||
lazy_static.workspace = true
|
||||
|
||||
@@ -31,7 +31,9 @@ use windmill_audit::audit_oss::{audit_log, AuditAuthorable};
|
||||
use windmill_audit::ActionKind;
|
||||
use windmill_common::db::UserDB;
|
||||
use windmill_common::users::username_to_permissioned_as;
|
||||
use windmill_common::variables::{build_crypt, decrypt, encrypt, WORKSPACE_CRYPT_CACHE};
|
||||
use windmill_common::variables::{
|
||||
build_crypt, decrypt, encrypt, SECRET_SALT, WORKSPACE_CRYPT_CACHE,
|
||||
};
|
||||
use windmill_common::worker::{to_raw_value, CLOUD_HOSTED};
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::workspaces::GitRepositorySettings;
|
||||
@@ -300,6 +302,8 @@ struct LargeFileStorageWithSecondary {
|
||||
large_file_storage: LargeFileStorage,
|
||||
#[serde(default)]
|
||||
secondary_storage: HashMap<String, LargeFileStorage>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
volume_storage: Option<String>,
|
||||
}
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct EditLargeFileStorageConfig {
|
||||
@@ -2418,20 +2422,28 @@ async fn set_encryption_key(
|
||||
));
|
||||
}
|
||||
|
||||
// Build the previous cipher before the transaction (reads from cache/pool)
|
||||
let previous_encryption_key = build_crypt(&db, w_id.as_str()).await?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_key SET key = $1 WHERE workspace_id = $2",
|
||||
request.new_key.clone(),
|
||||
w_id
|
||||
)
|
||||
.execute(&db)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
WORKSPACE_CRYPT_CACHE.remove(w_id.as_str());
|
||||
|
||||
if !request.skip_reencrypt.unwrap_or(false) {
|
||||
let new_encryption_key = build_crypt(&db, w_id.as_str()).await?;
|
||||
// Build the new cipher directly from the key string, since the transaction
|
||||
// hasn't committed yet and build_crypt() would read the old key from the pool.
|
||||
let crypt_key = if let Some(ref salt) = SECRET_SALT.as_ref() {
|
||||
format!("{}{}", request.new_key, salt)
|
||||
} else {
|
||||
request.new_key.clone()
|
||||
};
|
||||
let new_encryption_key = magic_crypt::new_magic_crypt!(crypt_key, 256);
|
||||
|
||||
let mut truncated_new_key = request.new_key.clone();
|
||||
truncated_new_key.truncate(8);
|
||||
@@ -2445,7 +2457,7 @@ async fn set_encryption_key(
|
||||
"SELECT path, value, is_secret FROM variable WHERE workspace_id = $1",
|
||||
w_id
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
for variable in all_variables {
|
||||
@@ -2466,11 +2478,16 @@ async fn set_encryption_key(
|
||||
w_id,
|
||||
variable.path
|
||||
)
|
||||
.execute(&db)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
// Invalidate the cache only after the transaction has committed
|
||||
WORKSPACE_CRYPT_CACHE.remove(w_id.as_str());
|
||||
|
||||
// Trigger git sync for encryption key changes
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
|
||||
@@ -70,6 +70,7 @@ windmill-git-sync.workspace = true
|
||||
windmill-indexer = { workspace = true, optional = true }
|
||||
windmill-autoscaling = { workspace = true, optional = true }
|
||||
windmill-worker = { workspace = true, optional = true }
|
||||
windmill-worker-volumes.workspace = true
|
||||
windmill-dep-map.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-stream.workspace = true
|
||||
|
||||
@@ -8857,9 +8857,8 @@ paths:
|
||||
type: boolean
|
||||
flow_env:
|
||||
type: object
|
||||
description: Environment variables available to all steps
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: "Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource)."
|
||||
additionalProperties: {}
|
||||
priority:
|
||||
type: number
|
||||
description: Execution priority (higher numbers run first)
|
||||
@@ -14644,9 +14643,8 @@ paths:
|
||||
type: boolean
|
||||
flow_env:
|
||||
type: object
|
||||
description: Environment variables available to all steps
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: "Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource)."
|
||||
additionalProperties: {}
|
||||
priority:
|
||||
type: number
|
||||
description: Execution priority (higher numbers run first)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.647.2
|
||||
version: 1.651.1
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -15198,6 +15198,7 @@ paths:
|
||||
gcp_trigger,
|
||||
sqs_trigger,
|
||||
email_trigger,
|
||||
volume,
|
||||
]
|
||||
responses:
|
||||
"200":
|
||||
@@ -15243,6 +15244,7 @@ paths:
|
||||
gcp_trigger,
|
||||
sqs_trigger,
|
||||
email_trigger,
|
||||
volume,
|
||||
]
|
||||
requestBody:
|
||||
description: acl to add
|
||||
@@ -15299,6 +15301,7 @@ paths:
|
||||
gcp_trigger,
|
||||
sqs_trigger,
|
||||
email_trigger,
|
||||
volume,
|
||||
]
|
||||
requestBody:
|
||||
description: acl to add
|
||||
@@ -16973,9 +16976,9 @@ paths:
|
||||
description: count of log lines that matched the query per hostname
|
||||
type: object
|
||||
|
||||
/srch/index/delete/{idx_name}:
|
||||
/indexer/delete/{idx_name}:
|
||||
delete:
|
||||
summary: Restart container and delete the index to recreate it.
|
||||
summary: Clear an index and restart the indexer.
|
||||
operationId: clearIndex
|
||||
tags:
|
||||
- indexSearch
|
||||
@@ -16990,12 +16993,102 @@ paths:
|
||||
- ServiceLogIndex
|
||||
responses:
|
||||
"200":
|
||||
description: idx to be deleted and container restarting
|
||||
description: idx to be deleted and indexer restarting
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/indexer/storage:
|
||||
get:
|
||||
summary: Get index storage sizes (disk and S3).
|
||||
operationId: getIndexStorageSizes
|
||||
tags:
|
||||
- indexSearch
|
||||
responses:
|
||||
"200":
|
||||
description: storage sizes for each index
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
job_index:
|
||||
type: object
|
||||
properties:
|
||||
disk_size_bytes:
|
||||
type: integer
|
||||
nullable: true
|
||||
s3_size_bytes:
|
||||
type: integer
|
||||
nullable: true
|
||||
service_log_index:
|
||||
type: object
|
||||
properties:
|
||||
disk_size_bytes:
|
||||
type: integer
|
||||
nullable: true
|
||||
s3_size_bytes:
|
||||
type: integer
|
||||
nullable: true
|
||||
|
||||
/indexer/status:
|
||||
get:
|
||||
summary: Get indexer status including liveness and storage sizes.
|
||||
operationId: getIndexerStatus
|
||||
tags:
|
||||
- indexSearch
|
||||
responses:
|
||||
"200":
|
||||
description: indexer status for each index
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
job_indexer:
|
||||
type: object
|
||||
properties:
|
||||
is_alive:
|
||||
type: boolean
|
||||
last_locked_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
owner:
|
||||
type: string
|
||||
nullable: true
|
||||
storage:
|
||||
type: object
|
||||
properties:
|
||||
disk_size_bytes:
|
||||
type: integer
|
||||
nullable: true
|
||||
s3_size_bytes:
|
||||
type: integer
|
||||
nullable: true
|
||||
log_indexer:
|
||||
type: object
|
||||
properties:
|
||||
is_alive:
|
||||
type: boolean
|
||||
last_locked_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
owner:
|
||||
type: string
|
||||
nullable: true
|
||||
storage:
|
||||
type: object
|
||||
properties:
|
||||
disk_size_bytes:
|
||||
type: integer
|
||||
nullable: true
|
||||
s3_size_bytes:
|
||||
type: integer
|
||||
nullable: true
|
||||
|
||||
/w/{workspace}/assets/list:
|
||||
get:
|
||||
summary: List all assets in the workspace with cursor pagination
|
||||
@@ -17192,7 +17285,90 @@ paths:
|
||||
path:
|
||||
type: string
|
||||
description: The asset path
|
||||
|
||||
|
||||
|
||||
/w/{workspace}/volumes/list:
|
||||
get:
|
||||
summary: List all volumes in the workspace
|
||||
operationId: listVolumes
|
||||
tags:
|
||||
- volume
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
responses:
|
||||
"200":
|
||||
description: list of volumes
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Volume"
|
||||
|
||||
/w/{workspace}/volumes/storage:
|
||||
get:
|
||||
summary: Get the volume storage name (secondary storage) or null for primary
|
||||
operationId: getVolumeStorage
|
||||
tags:
|
||||
- volume
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
responses:
|
||||
"200":
|
||||
description: volume storage name or null
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
|
||||
/w/{workspace}/volumes/create:
|
||||
post:
|
||||
summary: Create a new volume
|
||||
operationId: createVolume
|
||||
tags:
|
||||
- volume
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: volume created
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/volumes/delete/{name}:
|
||||
delete:
|
||||
summary: Delete a volume (admin only)
|
||||
operationId: deleteVolume
|
||||
tags:
|
||||
- volume
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: volume deleted
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/mcp/w/{workspace}/list_tools:
|
||||
get:
|
||||
@@ -23907,6 +24083,7 @@ components:
|
||||
- resource
|
||||
- ducklake
|
||||
- datatable
|
||||
- volume
|
||||
Asset:
|
||||
type: object
|
||||
properties:
|
||||
@@ -23915,6 +24092,38 @@ components:
|
||||
kind:
|
||||
$ref: "#/components/schemas/AssetKind"
|
||||
required: [path, kind]
|
||||
Volume:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- size_bytes
|
||||
- file_count
|
||||
- created_at
|
||||
- created_by
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
size_bytes:
|
||||
type: integer
|
||||
format: int64
|
||||
file_count:
|
||||
type: integer
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
created_by:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
last_used_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
extra_perms:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
ProtectionRuleset:
|
||||
type: object
|
||||
description: A workspace protection rule defining restrictions and bypass permissions
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::{collections::HashMap, sync::Arc};
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
use crate::{
|
||||
auth::OptTokened,
|
||||
auth::{get_end_user_email, OptTokened},
|
||||
db::{ApiAuthed, DB},
|
||||
jobs::RunJobQuery,
|
||||
users::{require_owner_of_path, OptAuthed},
|
||||
@@ -993,9 +993,18 @@ macro_rules! process_app_multipart {
|
||||
let mut uploaded_js = false;
|
||||
|
||||
let mut multipart = $multipart;
|
||||
while let Some(field) = multipart.next_field().await.unwrap() {
|
||||
let name = field.name().unwrap().to_string();
|
||||
let data = field.bytes().await.unwrap();
|
||||
while let Some(field) = multipart
|
||||
.next_field()
|
||||
.await
|
||||
.map_err(|e| Error::BadRequest(format!("failed to read multipart field: {e}")))?
|
||||
{
|
||||
let name = field
|
||||
.name()
|
||||
.ok_or_else(|| Error::BadRequest("multipart field missing name".to_string()))?
|
||||
.to_string();
|
||||
let data = field.bytes().await.map_err(|e| {
|
||||
Error::BadRequest(format!("failed to read multipart stream: {e}"))
|
||||
})?;
|
||||
if name == "app" {
|
||||
let app = serde_json::from_slice(&data).map_err(to_anyhow)?;
|
||||
let (ntx, npath, nid) = $internal_fn(
|
||||
@@ -1868,6 +1877,15 @@ pub struct ExecuteApp {
|
||||
pub run_query_params: Option<RunJobQuery>,
|
||||
}
|
||||
|
||||
fn maybe_replace_internal_db_script(mut raw_code: RawCode) -> RawCode {
|
||||
if let Some(replaced) =
|
||||
crate::db_studio_scripts::maybe_replace_internal_script(&raw_code.content)
|
||||
{
|
||||
raw_code.content = replaced;
|
||||
}
|
||||
raw_code
|
||||
}
|
||||
|
||||
fn digest(code: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(code);
|
||||
@@ -2129,13 +2147,30 @@ async fn execute_component(
|
||||
// flow or script:
|
||||
(Some(path), None, None) => get_payload_tag_from_prefixed_path(&path, &db, &w_id).await?,
|
||||
// inline script: in "preview" mode or without entry in the `app_script` table.
|
||||
(None, Some(raw_code), None) => (JobPayload::Code(raw_code), None, None),
|
||||
(None, Some(raw_code), None) => {
|
||||
let raw_code = maybe_replace_internal_db_script(raw_code);
|
||||
(JobPayload::Code(raw_code), None, None)
|
||||
}
|
||||
// inline script: in "run" mode and with an entry in the `app_script` table.
|
||||
(None, Some(RawCode { language, path, cache_ttl, .. }), Some(id)) => (
|
||||
JobPayload::AppScript { id: AppScriptId(id), cache_ttl, language, path },
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(None, Some(raw_code), Some(id)) => {
|
||||
// Check if this is an internal DB script marker — if so, replace content and
|
||||
// execute as Code (preview-style) since the content is server-controlled.
|
||||
if raw_code
|
||||
.content
|
||||
.trim_start()
|
||||
.starts_with(crate::db_studio_scripts::WM_INTERNAL_PREFIX)
|
||||
{
|
||||
let raw_code = maybe_replace_internal_db_script(raw_code);
|
||||
(JobPayload::Code(raw_code), None, None)
|
||||
} else {
|
||||
let RawCode { language, path, cache_ttl, .. } = raw_code;
|
||||
(
|
||||
JobPayload::AppScript { id: AppScriptId(id), cache_ttl, language, path },
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let tx = PushIsolationLevel::IsolatedRoot(db.clone());
|
||||
@@ -2149,7 +2184,8 @@ async fn execute_component(
|
||||
(email.as_str(), permissioned_as)
|
||||
};
|
||||
|
||||
let end_user_email = opt_authed.as_ref().map(|a| a.email.clone());
|
||||
let end_user_email =
|
||||
get_end_user_email(&db, opt_authed.as_ref(), tokened.token.as_deref()).await;
|
||||
|
||||
let (uuid, mut tx) = push(
|
||||
&db,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub use windmill_api_auth::auth::{
|
||||
invalidate_token_from_cache, list_tokens_internal, transform_old_scope_to_new_scope, AuthCache,
|
||||
ExpiringAuthCache, OptTokened, Tokened, TruncatedTokenWithEmail,
|
||||
get_end_user_email, invalidate_token_from_cache, list_tokens_internal,
|
||||
transform_old_scope_to_new_scope, AuthCache, ExpiringAuthCache, OptTokened, Tokened,
|
||||
TruncatedTokenWithEmail,
|
||||
};
|
||||
|
||||
@@ -284,6 +284,9 @@ pub async fn migrate(
|
||||
20260207000004,
|
||||
];
|
||||
for m in migrator.migrations.iter() {
|
||||
if m.migration_type.is_down_migration() {
|
||||
continue;
|
||||
}
|
||||
if potentially_stale.contains(&m.version) {
|
||||
if let Err(err) =
|
||||
sqlx::query("DELETE FROM _sqlx_migrations WHERE version = $1 AND checksum != $2")
|
||||
|
||||
1109
backend/windmill-api/src/db_studio_scripts.rs
Normal file
1109
backend/windmill-api/src/db_studio_scripts.rs
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user