Merge remote-tracking branch 'origin/main' into feat/frontend/save-flow-builder-layout

# Conflicts:
#	frontend/src/lib/components/FlowBuilder.svelte
#	frontend/src/lib/components/common/tabs/Tabs.svelte
#	frontend/src/lib/components/flows/FlowEditor.svelte
#	frontend/src/lib/components/flows/content/FlowBranchesAllWrapper.svelte
#	frontend/src/lib/components/flows/content/FlowBranchesOneWrapper.svelte
#	frontend/src/lib/components/flows/content/FlowLoop.svelte
#	frontend/src/lib/components/flows/content/FlowModuleComponent.svelte
#	frontend/src/lib/components/flows/content/FlowWhileLoop.svelte
#	frontend/src/routes/(root)/(logged)/flows/add/+page.svelte
#	frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte
This commit is contained in:
Guilhem
2025-06-18 11:39:12 +01:00
833 changed files with 39929 additions and 22279 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ jobs:
timeout-minutes: 16
run: |
mkdir -p fake_frontend_build
FRONTEND_BUILD_DIR=$(pwd)/fake_frontend_build SQLX_OFFLINE=true cargo check --all-features
FRONTEND_BUILD_DIR=$(pwd)/fake_frontend_build SQLX_OFFLINE=true cargo check --features $(./all_features_oss.sh)
check_ee:
runs-on: ubicloud-standard-8
+2 -2
View File
@@ -64,7 +64,7 @@ jobs:
platforms: linux/amd64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages,deno_core,mcp
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages,deno_core,mcp,private
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
@@ -81,7 +81,7 @@ jobs:
platforms: linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages,deno_core,mcp
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages,deno_core,mcp,private
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
$env:OPENSSL_DIR="${Env:VCPKG_INSTALLATION_ROOT}\installed\x64-windows-static"
mkdir frontend/build && cd backend
New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,license,http_trigger,zip,oauth2,kafka,nats,sqs_trigger,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages,mcp
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,license,http_trigger,zip,oauth2,kafka,nats,sqs_trigger,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages_windows,mcp,private
- name: Rename binary with corresponding architecture
run: |
Rename-Item -Path ".\backend\target\release\windmill.exe" -NewName "windmill-ee.exe"
+2 -2
View File
@@ -12,12 +12,12 @@ jobs:
- name: Check organization membership
id: check-membership
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ORG_ACCESS_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }}
COMMENTER: ${{ github.event.comment.user.login }}
run: |
ORG="windmill-labs"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token $GH_TOKEN" \
-H "Authorization: token $ORG_ACCESS_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/orgs/$ORG/members/$COMMENTER")
@@ -17,8 +17,11 @@ jobs:
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
PR_STATUS: "opened"
PR_NUMBER: ${{ github.event.pull_request.number }}
DISCORD_CHANNEL_ID: "1372204995868491786"
DISCORD_GUILD_ID: "930051556043276338"
secrets:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_PR_REVIEWS_WEBHOOK }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}
merge_success_emoji:
if: github.event.pull_request.merged == true
+2 -2
View File
@@ -92,7 +92,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=embedding,parquet,openidconnect,jemalloc,license,http_trigger,zip,oauth2,dind,postgres_trigger,mqtt_trigger,websocket,smtp,static_frontend,agent_worker_server,all_languages,deno_core,mcp
features=embedding,parquet,openidconnect,jemalloc,license,http_trigger,zip,oauth2,dind,postgres_trigger,mqtt_trigger,websocket,smtp,static_frontend,agent_worker_server,all_languages,deno_core,mcp,private
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEV_SHA }}
${{ steps.meta-public.outputs.tags }}
@@ -154,7 +154,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,agent_worker_server,tantivy,license,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,otel,dind,postgres_trigger,mqtt_trigger,gcp_trigger,websocket,smtp,static_frontend,all_languages,deno_core,mcp
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,agent_worker_server,tantivy,license,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,otel,dind,postgres_trigger,mqtt_trigger,gcp_trigger,websocket,smtp,static_frontend,all_languages,private,deno_core,mcp
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:${{ env.DEV_SHA }}
${{ steps.meta-ee-public.outputs.tags }}
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
uses: actions/checkout@v3
with:
repository: windmill-labs/windmill-helm-charts
token: ${{ secrets.DOCS_TOKEN }}
token: ${{ secrets.HELM_CHART_TOKEN }}
- name: Get version
id: get_version
@@ -57,7 +57,7 @@ jobs:
- name: Create PR
env:
GH_TOKEN: ${{ secrets.DOCS_TOKEN }}
GH_TOKEN: ${{ secrets.HELM_CHART_TOKEN }}
run: |
gh pr create \
--title "helm: bump version to ${{ env.VERSION }}" \
+1 -1
View File
@@ -53,7 +53,7 @@ jobs:
$env:OPENSSL_DIR="${Env:VCPKG_INSTALLATION_ROOT}\installed\x64-windows-static"
mkdir frontend/build && cd backend
New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,license,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,mqtt_trigger,gcp_trigger,websocket,smtp,static_frontend,all_languages,mcp
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,license,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,mqtt_trigger,gcp_trigger,websocket,smtp,static_frontend,all_languages_windows,mcp,private
- name: Rename binary with corresponding architecture
run: |
Rename-Item -Path ".\backend\target\release\windmill.exe" -NewName "windmill-ee.exe"
+19
View File
@@ -0,0 +1,19 @@
name: Publish rust-client to crates.io on release
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
build_rust_and_publish_to_crates_io:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v20
with:
extra_nix_config: |
experimental-features = nix-command flakes
- run: cd rust-client && nix develop ../ --command ./dev.nu --check --publish
env:
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
@@ -38,24 +38,45 @@ jobs:
- name: Send Discord notification and start a thread
env:
WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
CHANNEL_ID: ${{ inputs.DISCORD_CHANNEL_ID }}
GUILD_ID: ${{ inputs.DISCORD_GUILD_ID }}
PR_TITLE: ${{ inputs.PR_TITLE }}
PR_NUMBER: ${{ inputs.PR_NUMBER }}
PR_URL: ${{ inputs.PR_URL }}
PR_AUTHOR: ${{ inputs.PR_AUTHOR }}
run: |
payload=$(jq -n \
--arg content "${PR_URL}" \
--arg thread "#${PR_NUMBER}: $PR_TITLE by \`${PR_AUTHOR}\`" \
'{
content: $content,
thread_name: $thread,
auto_archive_duration: 10080
}'
)
curl -H "Content-Type: application/json" \
-X POST \
-d "$payload" \
"$WEBHOOK_URL"
# Check if thread already exists
thread_exists=false
if threads=$(curl -s -H "Authorization: Bot $BOT_TOKEN" "https://discord.com/api/v10/guilds/${GUILD_ID}/threads/active"); then
if thread_id=$(echo "$threads" | jq -r --arg cid "$CHANNEL_ID" --arg pref "#${PR_NUMBER}:" '.threads[] | select(.parent_id == $cid and (.name | startswith($pref))) | .id' 2>/dev/null); then
if [ -n "$thread_id" ]; then
thread_exists=true
echo "Thread already exists, skipping creation"
fi
fi
else
echo "Failed to check for existing threads, will create new thread"
fi
# Create thread if it doesn't exist or if check failed
if [ "$thread_exists" = false ]; then
echo "Creating new thread"
THREAD_TITLE="#${PR_NUMBER}: ${PR_TITLE} by \`${PR_AUTHOR}\`"
payload=$(jq -n \
--arg content "${PR_URL}" \
--arg thread "${THREAD_TITLE:0:99}" \
'{
content: $content,
thread_name: $thread,
auto_archive_duration: 10080
}'
)
curl -H "Content-Type: application/json" \
-X POST \
-d "$payload" \
"$WEBHOOK_URL"
fi
merge_success_emoji:
runs-on: ubuntu-latest
+106
View File
@@ -1,5 +1,111 @@
# Changelog
## [1.498.0](https://github.com/windmill-labs/windmill/compare/v1.497.2...v1.498.0) (2025-06-17)
### Features
* use provider api to list available AI models in workspace settings ([#5947](https://github.com/windmill-labs/windmill/issues/5947)) ([7490e88](https://github.com/windmill-labs/windmill/commit/7490e883d747a7f65b2fefd3ec14b1cfc3d9bbd4))
* windmill http triggers and webhooks to openapi spec ([#5918](https://github.com/windmill-labs/windmill/issues/5918)) ([aba8c01](https://github.com/windmill-labs/windmill/commit/aba8c01d7f44ba4be369a3c711be9e156d6bf215))
## [1.497.2](https://github.com/windmill-labs/windmill/compare/v1.497.1...v1.497.2) (2025-06-17)
### Bug Fixes
* always rm containers in docker mode ([38eb71b](https://github.com/windmill-labs/windmill/commit/38eb71bdf55ee2f606d1d2ad2e987d5af16d88c0))
* flow steps use their tags if any specific when used as subflow ([26bec05](https://github.com/windmill-labs/windmill/commit/26bec054a3447a91c5d5f56d8b98717c06496087))
## [1.497.1](https://github.com/windmill-labs/windmill/compare/v1.497.0...v1.497.1) (2025-06-16)
### Bug Fixes
* fix mcp server initialization ([1c6a7c8](https://github.com/windmill-labs/windmill/commit/1c6a7c8cd0bd8396f158e3cb0583b927ce957f12))
## [1.497.0](https://github.com/windmill-labs/windmill/compare/v1.496.3...v1.497.0) (2025-06-16)
### Features
* add api tools to ai chat ([#5921](https://github.com/windmill-labs/windmill/issues/5921)) ([f7a83c0](https://github.com/windmill-labs/windmill/commit/f7a83c03c12b8ae70179fb228e0e2391b6ea2858))
* **backend:** use streamable http in favor of sse for MCP ([#5910](https://github.com/windmill-labs/windmill/issues/5910)) ([d47c078](https://github.com/windmill-labs/windmill/commit/d47c078bb5ab86d82d9cbbce3c55c89c0c20d809))
* better graph layout algorithm + migrate to svelte 5 almost everywhere + xyflow 1.0 ([23920ae](https://github.com/windmill-labs/windmill/commit/23920aee84fdca4a557a34ff2d66a0bb7bdca605))
* fill runnable inputs with AI chat ([#5887](https://github.com/windmill-labs/windmill/issues/5887)) ([b4a6a7e](https://github.com/windmill-labs/windmill/commit/b4a6a7e72429617d420af85a9de35bb13adfc6fb))
* **go:** local go.mod ([#5929](https://github.com/windmill-labs/windmill/issues/5929)) ([0b89260](https://github.com/windmill-labs/windmill/commit/0b89260540b307c6d614ca4275dd038fbfdac33c))
* multiple azure models support ([#5920](https://github.com/windmill-labs/windmill/issues/5920)) ([f412ede](https://github.com/windmill-labs/windmill/commit/f412ede6ed48e9a492f39582ac70a5584477529e))
* **rust:** add rust sdk ([#5909](https://github.com/windmill-labs/windmill/issues/5909)) ([332f66e](https://github.com/windmill-labs/windmill/commit/332f66e3483abbeacd4e7c1b74c94c5265314882))
### Bug Fixes
* ai chat tooltip + user settings autocomplete issue ([#5917](https://github.com/windmill-labs/windmill/issues/5917)) ([6f907c7](https://github.com/windmill-labs/windmill/commit/6f907c79b4cf6279bd52e35a3ee96e0d021422f5))
* audit logs for token refresh + consider refresh for active users ([#5930](https://github.com/windmill-labs/windmill/issues/5930)) ([cf2d09e](https://github.com/windmill-labs/windmill/commit/cf2d09e7a8c5d2472af0d483689c3fcfa2976117))
* fix input with wrong height on first render ([#5935](https://github.com/windmill-labs/windmill/issues/5935)) ([1a6283b](https://github.com/windmill-labs/windmill/commit/1a6283b42a6a514ab2e05160855cdc0f70b61d0e))
* flow step missing input warnings ([#5916](https://github.com/windmill-labs/windmill/issues/5916)) ([f077849](https://github.com/windmill-labs/windmill/commit/f077849b8f7c1916fd420e85b4844a5c5e93a139))
* **frontend:** use correct kind for flow insert module btn ([#5938](https://github.com/windmill-labs/windmill/issues/5938)) ([17c8c8a](https://github.com/windmill-labs/windmill/commit/17c8c8a5616ab8656799cea3fc5bc7cfaedc4995))
## [1.496.3](https://github.com/windmill-labs/windmill/compare/v1.496.2...v1.496.3) (2025-06-09)
### Bug Fixes
* improve concurrent job parallelism performance ([e8836a3](https://github.com/windmill-labs/windmill/commit/e8836a393a872bb91e68ba0037681caf24149470))
* Prioritize diff contexts in script mode for ai chat ([#5888](https://github.com/windmill-labs/windmill/issues/5888)) ([a47939d](https://github.com/windmill-labs/windmill/commit/a47939d13c30e2d4b41efd539f845959174d4fb1))
## [1.496.2](https://github.com/windmill-labs/windmill/compare/v1.496.1...v1.496.2) (2025-06-07)
### Bug Fixes
* add clearable by default for select ([#5900](https://github.com/windmill-labs/windmill/issues/5900)) ([b44b9c1](https://github.com/windmill-labs/windmill/commit/b44b9c1b82116ad5487af95d1f78226d56c75179))
## [1.496.1](https://github.com/windmill-labs/windmill/compare/v1.496.0...v1.496.1) (2025-06-07)
### Bug Fixes
* never consider minor version for global site packages ([#5893](https://github.com/windmill-labs/windmill/issues/5893)) ([22b2f49](https://github.com/windmill-labs/windmill/commit/22b2f4988db9314f2403508933d0aa932187c668))
## [1.496.0](https://github.com/windmill-labs/windmill/compare/v1.495.1...v1.496.0) (2025-06-06)
### Features
* generate http route triggers from openapi spec ([#5857](https://github.com/windmill-labs/windmill/issues/5857)) ([5713483](https://github.com/windmill-labs/windmill/commit/571348377b73d54b4d2a1c5775ab00b247b01910))
### Bug Fixes
* allow fileupload drag and drop in edit mode on full component without triggering file picker ([#5889](https://github.com/windmill-labs/windmill/issues/5889)) ([9ae3212](https://github.com/windmill-labs/windmill/commit/9ae3212a1e0f88a8297bf41ab53e3c1be4bcc56c))
* **python:** account instance version when cli deploy and local lockfile ([#5894](https://github.com/windmill-labs/windmill/issues/5894)) ([ec552d5](https://github.com/windmill-labs/windmill/commit/ec552d5ef6fdb5e824e453f196f9cf16629ee2ea))
* use full client side js library for route gen from openapi ([#5891](https://github.com/windmill-labs/windmill/issues/5891)) ([3c3fdbd](https://github.com/windmill-labs/windmill/commit/3c3fdbdf26a9581b815210839b91ebdedb924093))
## [1.495.0](https://github.com/windmill-labs/windmill/compare/v1.494.0...v1.495.0) (2025-06-05)
### Features
* Add ask mode to AI chat ([#5878](https://github.com/windmill-labs/windmill/issues/5878)) ([67ab469](https://github.com/windmill-labs/windmill/commit/67ab46990ad0c9fad810a64c54297419c6151c79))
* add navigator mode to AIChat and unify UI ([#5859](https://github.com/windmill-labs/windmill/issues/5859)) ([cbba829](https://github.com/windmill-labs/windmill/commit/cbba8297cd4c1caa21b96a8422bbbd5c306b8398))
* ai flow chat ([#5842](https://github.com/windmill-labs/windmill/issues/5842)) ([68ebf66](https://github.com/windmill-labs/windmill/commit/68ebf667d5c0bc306329d0b55a3cc59e5b4862cb))
* ai prompts improvements + o3/o4 support ([#5862](https://github.com/windmill-labs/windmill/issues/5862)) ([825422c](https://github.com/windmill-labs/windmill/commit/825422c48456b2c9b230e1a35914b3fbf7d1e836))
* connect fix btn in flow editor to ai chat ([#5863](https://github.com/windmill-labs/windmill/issues/5863)) ([6247d15](https://github.com/windmill-labs/windmill/commit/6247d159ce25ae13f6fbc5c105df88305ce29451))
* fix backward compatibility pg 14 for postgres trigger ([#5851](https://github.com/windmill-labs/windmill/issues/5851)) ([4cbcbdb](https://github.com/windmill-labs/windmill/commit/4cbcbdb960b469acf773d3943128b6c7d0dcb0b8))
* ssh repl like direct to workers hosts machine ([#5809](https://github.com/windmill-labs/windmill/issues/5809)) ([f252657](https://github.com/windmill-labs/windmill/commit/f2526571a3614156b2b1e5cc91b15d0c57565d99))
* use rust-postgres client instead of sqlx for postgres trigger ([#5853](https://github.com/windmill-labs/windmill/issues/5853)) ([39dbd64](https://github.com/windmill-labs/windmill/commit/39dbd646b9683e0ad8de047cca786ae468759e77))
### Bug Fixes
* broken event dispatch for simpleditor ([#5879](https://github.com/windmill-labs/windmill/issues/5879)) ([df4992a](https://github.com/windmill-labs/windmill/commit/df4992a9295ed188c2a2cb0a5dfd3e33ae2e2dcb))
* cannot parse INSTANCE_PYTHON_VERSION ([#5874](https://github.com/windmill-labs/windmill/issues/5874)) ([a0b302d](https://github.com/windmill-labs/windmill/commit/a0b302d2c58d4245260376cf280bc866be91717c))
* fix regex that extract workspaces from custom tags ([#5876](https://github.com/windmill-labs/windmill/issues/5876)) ([1551dc8](https://github.com/windmill-labs/windmill/commit/1551dc8af22f6ea41f68290ace4c58f936c47745))
* nit ai flow prompt ([#5867](https://github.com/windmill-labs/windmill/issues/5867)) ([3e769f0](https://github.com/windmill-labs/windmill/commit/3e769f0c591b80138b3a356d147228675756452f))
* **python:** assign PATCH version to python runtime only when needed ([#5866](https://github.com/windmill-labs/windmill/issues/5866)) ([50a5c1f](https://github.com/windmill-labs/windmill/commit/50a5c1f56a7e45882fa0095203de709571e149bb))
* remove duplicate tools from script ai chat ([#5880](https://github.com/windmill-labs/windmill/issues/5880)) ([fe4a767](https://github.com/windmill-labs/windmill/commit/fe4a767df0e6f46fd0c0fd21b4116c7375978bf9))
* replace crypto.randomUUID with generateRandomString for HTTP compatibility ([#5849](https://github.com/windmill-labs/windmill/issues/5849)) ([64f35d0](https://github.com/windmill-labs/windmill/commit/64f35d050fb0d1008ce7142fd62d500845e62c4a)), closes [#5847](https://github.com/windmill-labs/windmill/issues/5847)
## [1.494.0](https://github.com/windmill-labs/windmill/compare/v1.493.4...v1.494.0) (2025-05-31)
+1 -1
View File
@@ -1,3 +1,3 @@
To have an overview of what this app does, see @.cursor/rules/windmill-overview.mdc
For backend modifications, follow the rules mentioned here @.cursor/rules/rust-best-practices.mdc
For backend modifications, follow the rules mentioned here @.cursor/rules/rust-best-practices.mdc. You also have access to a summarized version of the database schema here @backend/summarized_schema.txt
For frontend modifications, follow the rules mentioned here @.cursor/rules/svelte5-best-practices.mdc
+2 -1
View File
@@ -367,10 +367,11 @@ you to have it being synced automatically everyday.
## Run a local dev setup
Using [Nix](./frontend/README_DEV.md#nix) (Recommended).
See the [./frontend/README_DEV.md](./frontend/README_DEV.md) file for all
running options.
Using [Nix](./frontend/README_DEV.md#nix).
### only Frontend
+2 -1
View File
@@ -6,4 +6,5 @@ tracing.folded
heaptrack*
index/
windmill-api/openapi-*.*
.duckdb/*
.duckdb/*
*ee.rs
+1
View File
@@ -0,0 +1 @@
!*ee.rs
@@ -61,7 +61,8 @@
"csharp",
"oracledb",
"nu",
"java"
"java",
"duckdb"
]
}
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n path\n FROM\n flow\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"TextArray",
"Text"
]
},
"nullable": [
false
]
},
"hash": "0de53d764bbcf44d76cd8e47f7b2cd49e4632f03f16ee6f34d9a2c0842890f05"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "CREATE INDEX CONCURRENTLY IF NOT EXISTS v2_job_queue_suspend ON v2_job_queue (workspace_id, suspend) WHERE suspend > 0;",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "19f0ccadd3ee44719a781ea0d73ea4e45f5b2c3d5c0aa5dbecf9ea9838881b74"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, TRUE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), $2) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int4"
]
},
"nullable": []
},
"hash": "1d87f41fd1abb9361d795a899120e6b77e24bf5a9044fdc5284d0d7f1e14eafa"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, FALSE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), $2) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int4"
]
},
"nullable": []
},
"hash": "2bf5f7f2cf9d85a5d23e5db2f7616fb41fece9b3d46fde2d546d70b46f9008e3"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usage (id, is_workspace, month_, usage)\n VALUES ($1, FALSE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)\n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "usage",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Varchar"
]
},
"nullable": [
false
]
},
"hash": "2e9b3e718440f3c5269e9217a13076c565f3add98b6768b5476bd3afed11ea31"
}
@@ -0,0 +1,170 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n script_path, \n summary,\n description,\n is_flow, \n http_method as \"http_method: _\", \n edited_by, \n email, \n edited_at, \n extra_perms, \n is_async, \n authentication_method as \"authentication_method: _\", \n static_asset_config as \"static_asset_config: _\", \n is_static_website,\n authentication_resource_path,\n wrap_body,\n raw_string\n FROM \n http_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "route_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "route_path_key",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "workspaced_route",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 8,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 9,
"name": "http_method: _",
"type_info": {
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
}
},
{
"ordinal": 10,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 12,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 13,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 14,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 15,
"name": "authentication_method: _",
"type_info": {
"Custom": {
"name": "authentication_method",
"kind": {
"Enum": [
"none",
"windmill",
"api_key",
"basic_http",
"custom_script",
"signature"
]
}
}
}
},
{
"ordinal": 16,
"name": "static_asset_config: _",
"type_info": "Jsonb"
},
{
"ordinal": 17,
"name": "is_static_website",
"type_info": "Bool"
},
{
"ordinal": 18,
"name": "authentication_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 19,
"name": "wrap_body",
"type_info": "Bool"
},
{
"ordinal": 20,
"name": "raw_string",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
true,
true,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
true,
false,
false
]
},
"hash": "39401cb0db8d367b5beb2be0c13aa7595adae0eac4e4e3a888cb12b972d1a7ce"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n path\n FROM\n script\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"TextArray",
"Text"
]
},
"nullable": [
false
]
},
"hash": "3aaa6b6e362b10f2f3546b8595c60ab725295e0f20cb5f85d5fddc14d503a119"
}
@@ -1,22 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT tag FROM v2_job WHERE id = $1",
"query": "SELECT path FROM script WHERE workspace_id = $1 AND archived = false",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "tag",
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
"Text"
]
},
"nullable": [
false
]
},
"hash": "faf2c77242e0ab39b33886edf3b742531bf1351d0be1c3631bde0adfe375497a"
"hash": "3e244a5057d4f1b4a18c0edac52cdf695c7e7aa0468d2686255de3d83719e6d0"
}
@@ -0,0 +1,60 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n http_trigger \n SET \n route_path = $1, \n route_path_key = $2, \n workspaced_route = $3,\n wrap_body = $4,\n raw_string = $5,\n authentication_resource_path = $6,\n script_path = $7, \n path = $8, \n is_flow = $9, \n http_method = $10, \n static_asset_config = $11, \n edited_by = $12, \n email = $13, \n is_async = $14, \n authentication_method = $15, \n summary = $16,\n description = $17,\n edited_at = now(), \n is_static_website = $18\n WHERE \n workspace_id = $19 AND \n path = $20\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Bool",
"Bool",
"Bool",
"Varchar",
"Varchar",
"Varchar",
"Bool",
{
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
},
"Jsonb",
"Varchar",
"Varchar",
"Bool",
{
"Custom": {
"name": "authentication_method",
"kind": {
"Enum": [
"none",
"windmill",
"api_key",
"basic_http",
"custom_script",
"signature"
]
}
}
},
"Varchar",
"Text",
"Bool",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "3f05e6186050a7ce6d8efb41067d3c5282319fe7e041f114e02fb22b91716637"
}
@@ -0,0 +1,169 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n authentication_resource_path,\n script_path, \n is_flow, \n summary,\n description,\n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n authentication_method AS \"authentication_method: _\", \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string\n FROM http_trigger\n WHERE workspace_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "workspaced_route",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "route_path",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "route_path_key",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "authentication_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 8,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 10,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 13,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 14,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 15,
"name": "authentication_method: _",
"type_info": {
"Custom": {
"name": "authentication_method",
"kind": {
"Enum": [
"none",
"windmill",
"api_key",
"basic_http",
"custom_script",
"signature"
]
}
}
}
},
{
"ordinal": 16,
"name": "http_method: _",
"type_info": {
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
}
},
{
"ordinal": 17,
"name": "static_asset_config: _",
"type_info": "Jsonb"
},
{
"ordinal": 18,
"name": "is_static_website",
"type_info": "Bool"
},
{
"ordinal": 19,
"name": "wrap_body",
"type_info": "Bool"
},
{
"ordinal": 20,
"name": "raw_string",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
true,
false,
false,
true,
true,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false
]
},
"hash": "4228b098883408323bd8413ee094454b95962047458a6927d19ac0d3e7b3f0fa"
}
@@ -70,7 +70,8 @@
"csharp",
"oracledb",
"nu",
"java"
"java",
"duckdb"
]
}
}
@@ -137,7 +137,8 @@
"csharp",
"oracledb",
"nu",
"java"
"java",
"duckdb"
]
}
}
@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n path,\n summary,\n description\n FROM\n flow\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "summary",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "description",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"TextArray",
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "4dc21dda58758a84bbc1b7f9328efb9806223d1684c53907ff825bc0228ec18d"
}
@@ -0,0 +1,58 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n tag, \n script_lang AS \"script_lang!: _\"\n FROM \n v2_job\n WHERE \n id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "script_lang!: _",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb"
]
}
}
}
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
true
]
},
"hash": "4e5273b9ce05f6ee2dfd5f14c8574a0cf43682480452f7dbe23012320fe7fe25"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC, devops DESC\n LIMIT $1 OFFSET $2",
"query": "WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC, devops DESC\n LIMIT $1 OFFSET $2",
"describe": {
"columns": [
{
@@ -67,5 +67,5 @@
true
]
},
"hash": "3895cee539a24b4c6ea89fa7a835fc62bc93b0530efba09fc3c32a8f93eaabb1"
"hash": "5430f7728c1e9b539cc8aad29ca9e6733943278998d3df62a9486607827e59ec"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_queue q SET suspend = 0\n FROM v2_job j, v2_job_status f\n WHERE parent_job = $1\n AND f.id = j.id AND q.id = j.id\n AND suspend = $2 AND (f.flow_status->'step')::int = 0",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Int4"
]
},
"nullable": []
},
"hash": "553108ba3c0b8d579800bc8b5a4f887d79fb4c13b60b19c4913a8db18521958c"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usage (id, is_workspace, month_, usage)\n VALUES ($1, FALSE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)\n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "usage",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Varchar"
]
},
"nullable": [
false
]
},
"hash": "56b2326015fde12b1a4efa226518566101dd27a0f3363884781071d417f8b7e7"
}
@@ -61,7 +61,8 @@
"csharp",
"oracledb",
"nu",
"java"
"java",
"duckdb"
]
}
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usage (id, is_workspace, month_, usage)\n VALUES ($1, TRUE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)\n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "usage",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Varchar"
]
},
"nullable": [
false
]
},
"hash": "621e9a2a53187dac3ebed62f0d645b692815f1594bf302dbebd5f80d5d22b98e"
}
@@ -0,0 +1,55 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n route_path,\n http_method AS \"http_method: _\",\n is_async,\n workspaced_route\n FROM\n http_trigger\n WHERE\n path ~ ANY($1) AND\n route_path ~ ANY($2) AND\n workspace_id = $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "route_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "http_method: _",
"type_info": {
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
}
},
{
"ordinal": 2,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "workspaced_route",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"TextArray",
"TextArray",
"Text"
]
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "6b6aca712df657f81e74a438f6753bc72e787a3f768040c8c73ea86313badb71"
}
@@ -147,7 +147,8 @@
"csharp",
"oracledb",
"nu",
"java"
"java",
"duckdb"
]
}
}
@@ -0,0 +1,93 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n route_path,\n http_method AS \"http_method: _\",\n is_async,\n workspaced_route,\n summary,\n description,\n authentication_method AS \"authentication_method: _\",\n authentication_resource_path\n FROM\n http_trigger\n WHERE\n path ~ ANY($1) AND\n route_path ~ ANY($2) AND\n workspace_id = $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "route_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "http_method: _",
"type_info": {
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
}
},
{
"ordinal": 2,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "workspaced_route",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "authentication_method: _",
"type_info": {
"Custom": {
"name": "authentication_method",
"kind": {
"Enum": [
"none",
"windmill",
"api_key",
"basic_http",
"custom_script",
"signature"
]
}
}
}
},
{
"ordinal": 7,
"name": "authentication_resource_path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"TextArray",
"TextArray",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
true,
true,
false,
true
]
},
"hash": "714fb0f66ceb536aee8cb9ae0144757b999d25870fda37fe904e09dd5c742015"
}
@@ -34,7 +34,8 @@
"csharp",
"oracledb",
"nu",
"java"
"java",
"duckdb"
]
}
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT distinct(path) FROM script WHERE workspace_id = $1 AND archived = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "8373b2649ab46310860adbdd7b717261771ac61d46d82d42d085ffebeb18be06"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usage (id, is_workspace, month_, usage)\n VALUES ($1, TRUE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)\n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "usage",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Varchar"
]
},
"nullable": [
false
]
},
"hash": "83f64dd93b1ddc03b84681d65d9be69959987cbac1d83b64225fd1bf9ab047c9"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE worker_ping SET\nping_at = now(),\njobs_executed = 1,\ncurrent_job_id = $1,\ncurrent_job_workspace_id = 'admins'\nWHERE worker = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "997586ac14384db2c0eeee1bb3382cc6ae013695d0cda6da9ab848ca1b9a9606"
}
@@ -36,7 +36,8 @@
"csharp",
"oracledb",
"nu",
"java"
"java",
"duckdb"
]
}
}
@@ -41,7 +41,8 @@
"csharp",
"oracledb",
"nu",
"java"
"java",
"duckdb"
]
}
}
@@ -65,7 +65,8 @@
"csharp",
"oracledb",
"nu",
"java"
"java",
"duckdb"
]
}
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE worker_ping SET \nping_at = now(), \njobs_executed = 1, \ncurrent_job_id = $1, \ncurrent_job_workspace_id = 'admins' \nWHERE worker = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "c3025cdb6e421e1225d420e8b1efd18d1dd3bb2fac53c1f2df648b61fb7488aa"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, TRUE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), $2) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int4"
]
},
"nullable": []
},
"hash": "ca3ba808e020c8c7a35eaef842b20cfeee64fd47ded72fce55cc75e0bbb291a8"
}
@@ -0,0 +1,38 @@
{
"db_name": "PostgreSQL",
"query": "WITH active_users as (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),\n active_authors as (SELECT distinct email FROM usr WHERE usr.operator IS false AND email IN (SELECT email FROM active_users)),\n active_authors_agg as (SELECT array_agg(email) as authors FROM active_authors),\n active_ops_agg as (SELECT array_agg(email) as operators from active_users WHERE email NOT IN (SELECT email FROM active_authors))\n SELECT active_authors_agg.authors, active_ops_agg.operators, array_length(active_authors_agg.authors, 1) as author_count, array_length(active_ops_agg.operators, 1) as operator_count FROM active_authors_agg, active_ops_agg",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "authors",
"type_info": "VarcharArray"
},
{
"ordinal": 1,
"name": "operators",
"type_info": "VarcharArray"
},
{
"ordinal": 2,
"name": "author_count",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "operator_count",
"type_info": "Int4"
}
],
"parameters": {
"Left": []
},
"nullable": [
null,
null,
null,
null
]
},
"hash": "cb3862634f18160207ee2621ddfca43f00456a27fda32583846497116f92f96c"
}
@@ -70,7 +70,8 @@
"csharp",
"oracledb",
"nu",
"java"
"java",
"duckdb"
]
}
}
@@ -1,38 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "WITH active_users as (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),\n active_authors as (SELECT distinct email FROM usr WHERE usr.operator IS false AND email IN (SELECT email FROM active_users)),\n active_authors_agg as (SELECT array_agg(email) as authors FROM active_authors),\n active_ops_agg as (SELECT array_agg(email) as operators from active_users WHERE email NOT IN (SELECT email FROM active_authors))\n SELECT active_authors_agg.authors, active_ops_agg.operators, array_length(active_authors_agg.authors, 1) as author_count, array_length(active_ops_agg.operators, 1) as operator_count FROM active_authors_agg, active_ops_agg",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "authors",
"type_info": "VarcharArray"
},
{
"ordinal": 1,
"name": "operators",
"type_info": "VarcharArray"
},
{
"ordinal": 2,
"name": "author_count",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "operator_count",
"type_info": "Int4"
}
],
"parameters": {
"Left": []
},
"nullable": [
null,
null,
null,
null
]
},
"hash": "cce991f582bc9d2ba28a5b2b41c679366bb07bc6a100727721a787160ac6910c"
}
@@ -42,7 +42,8 @@
"csharp",
"oracledb",
"nu",
"java"
"java",
"duckdb"
]
}
}
@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n path,\n summary,\n description\n FROM\n script\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "summary",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "description",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"TextArray",
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "d41df114e78873111e409e1182bccc0024acf17ce740f70d2243e5e2a2b25163"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, FALSE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), $2) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int4"
]
},
"nullable": []
},
"hash": "e38240e6d50bfe60e1c2b649588eb41dcef121ed161db04b2568ac2d990aed7c"
}
@@ -0,0 +1,59 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO http_trigger (\n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n authentication_resource_path,\n wrap_body,\n raw_string,\n script_path, \n summary,\n description,\n is_flow, \n is_async, \n authentication_method, \n http_method, \n static_asset_config, \n edited_by, \n email, \n edited_at, \n is_static_website\n ) \n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), $19\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Bool",
"Bool",
"Varchar",
"Varchar",
"Text",
"Bool",
"Bool",
{
"Custom": {
"name": "authentication_method",
"kind": {
"Enum": [
"none",
"windmill",
"api_key",
"basic_http",
"custom_script",
"signature"
]
}
}
},
{
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
},
"Jsonb",
"Varchar",
"Varchar",
"Bool"
]
},
"nullable": []
},
"hash": "ed99d4d088d0fd0c01f29803b12e99ae0a53d0b1feaa67737da409c51c1b6751"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_queue q SET suspend = 0\n FROM v2_job j, v2_job_status f\n WHERE q.workspace_id = $1 AND q.suspend = $3 AND j.parent_job = $2\n AND f.id = j.id AND q.id = j.id\n AND (f.flow_status->'step')::int = 0",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid",
"Int4"
]
},
"nullable": []
},
"hash": "f1dbcb6e6d82d17c19eb88c0e67dc1cb8baf5bd40b75a2a9cd3ebac440fda632"
}
@@ -147,7 +147,8 @@
"csharp",
"oracledb",
"nu",
"java"
"java",
"duckdb"
]
}
}
@@ -65,7 +65,8 @@
"csharp",
"oracledb",
"nu",
"java"
"java",
"duckdb"
]
}
}
+2 -1
View File
@@ -12,5 +12,6 @@
"conventionalCommits.scopes": [
"restructring triggers, decoding trigger message on work"
],
"rust-analyzer.cargo.features": ["postgres_trigger"]
"files.exclude": { "**/*ee.rs": false },
"search.exclude": { "**/*ee.rs": false }
}
+500 -608
View File
File diff suppressed because it is too large Load Diff
+10 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.494.0"
version = "1.498.0"
authors.workspace = true
edition.workspace = true
@@ -32,7 +32,7 @@ members = [
]
[workspace.package]
version = "1.494.0"
version = "1.498.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -49,6 +49,7 @@ lto = "thin"
[features]
default = []
private = ["windmill-api/private", "windmill-autoscaling/private", "windmill-common/private", "windmill-git-sync/private", "windmill-indexer/private", "windmill-queue/private", "windmill-worker/private"]
agent_worker_server = ["windmill-api/agent_worker_server"]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise"]
enterprise_saml = ["windmill-api/enterprise_saml", "oauth2"]
@@ -94,8 +95,10 @@ php = ["windmill-worker/php"]
csharp = ["windmill-worker/csharp"]
nu = ["windmill-worker/nu"]
java = ["windmill-worker/java"]
all_languages = [ "python", "deno_core", "rust", "mysql", "oracledb", "duckdb", "mssql", "bigquery", "csharp", "nu", "php", "java"]
all_languages = ["python", "deno_core", "rust", "mysql", "oracledb", "duckdb", "mssql", "bigquery", "csharp", "nu", "php", "java"]
# For windows we have another set of languages enabled
# NOTE: DuckDB is ignored because of compilation problems
all_languages_windows = ["python", "deno_core", "rust", "mysql", "oracledb", "mssql", "bigquery", "csharp", "nu", "php", "java"]
[patch.crates-io]
object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7" }
@@ -127,6 +130,7 @@ prometheus = { workspace = true, optional = true }
uuid.workspace = true
gethostname.workspace = true
serde_json.workspace = true
serde_yml.workspace = true
serde.workspace = true
deno_core = { workspace = true, optional = true }
object_store = { workspace = true, optional = true }
@@ -198,6 +202,7 @@ tower-http = { version = "^0.6", features = ["trace", "cors"] }
tower-cookies = "^0.10"
serde = "^1"
serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
serde_yml = "0.0.12"
uuid = { version = "^1", features = ["serde", "v4"] }
thiserror = "^2"
anyhow = "^1"
@@ -227,7 +232,7 @@ php-parser-rs = { git = "https://github.com/php-rust-tools/parser", rev = "ec4cb
cron = "^0"
mail-send = { version = "0.4.0", features = ["builder"], default-features=false }
urlencoding = "^2"
url = "^2"
url = { version = "^2" , features = ["serde"]}
async-oauth2 = "^0"
reqwest = { version = "^0.12", features = ["json", "stream", "gzip"] }
time = "^0"
+20
View File
@@ -0,0 +1,20 @@
# This script outputs all features except private. Usage :
# > cargo build --features $(./all_features_oss.sh)
#!/bin/bash
# Path to the Cargo.toml file
CARGO_TOML_PATH="./Cargo.toml"
# Extract features from Cargo.toml and output them separated by commas
if [[ -f "$CARGO_TOML_PATH" ]]; then
grep -A 100 '\[features\]' "$CARGO_TOML_PATH" | \
sed -n '/\[features\]/,/^\[/p' | \
grep -E '^[a-zA-Z0-9_-]+' | \
grep -v 'private' | \
cut -d' ' -f1 | \
paste -sd ',' -
else
echo "Cargo.toml not found at $CARGO_TOML_PATH"
exit 1
fi
+1 -1
View File
@@ -1 +1 @@
8a2506e86b923c00522cb83b052586f705f7aa8e
67e727c618cf673850a0887931c803241abfcfe8
@@ -4,4 +4,4 @@ DROP TYPE http_method;
ALTER TABLE script DROP COLUMN has_preprocessor;
DROP FUNCTION prevent_route_path_change();
DROP FUNCTION prevent_route_path_change();
@@ -0,0 +1,4 @@
-- Remove token invalidation notification trigger
DROP TRIGGER IF EXISTS token_invalidation_trigger ON token;
DROP FUNCTION IF EXISTS notify_token_invalidation();
@@ -0,0 +1,17 @@
-- Add token invalidation notification trigger
CREATE OR REPLACE FUNCTION notify_token_invalidation()
RETURNS TRIGGER AS $$
BEGIN
-- Only notify for session token deletions when the invalidation settings are enabled
IF OLD.label = 'session' AND OLD.email IS NOT NULL THEN
PERFORM pg_notify('notify_token_invalidation', OLD.token);
END IF;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER token_invalidation_trigger
AFTER DELETE ON token
FOR EACH ROW
EXECUTE FUNCTION notify_token_invalidation();
@@ -0,0 +1,4 @@
-- Add down migration script here
ALTER TABLE http_trigger
DROP COLUMN summary,
DROP COLUMN description;
@@ -0,0 +1,5 @@
-- Add up migration script here
ALTER TABLE http_trigger
ADD COLUMN summary VARCHAR(512) NULL,
ADD COLUMN description TEXT NULL;
+6 -1
View File
@@ -1,8 +1,13 @@
#[cfg(feature = "private")]
#[allow(unused)]
pub use crate::ee::*;
#[cfg(not(feature = "private"))]
pub async fn set_license_key(_license_key: String) -> () {
// Implementation is not open source
}
#[cfg(feature = "enterprise")]
#[cfg(all(feature = "enterprise", not(feature = "private")))]
pub async fn verify_license_key() -> () {
// Implementation is not open source
}
+38 -24
View File
@@ -28,7 +28,9 @@ use uuid::Uuid;
use windmill_api::HTTP_CLIENT;
#[cfg(feature = "enterprise")]
use windmill_common::ee::{maybe_renew_license_key_on_start, LICENSE_KEY_ID, LICENSE_KEY_VALID};
use windmill_common::ee_oss::{
maybe_renew_license_key_on_start, LICENSE_KEY_ID, LICENSE_KEY_VALID,
};
use windmill_common::{
agent_workers::build_agent_http_client,
@@ -49,9 +51,12 @@ use windmill_common::{
TIMEOUT_WAIT_RESULT_SETTING,
},
scripts::ScriptLang,
stats_ee::schedule_stats,
stats_oss::schedule_stats,
triggers::TriggerKind,
utils::{hostname, rd_string, Mode, GIT_VERSION, MODE_AND_ADDONS},
utils::{
create_default_worker_suffix, create_ssh_agent_worker_suffix, worker_name_with_suffix,
Mode, GIT_VERSION, HOSTNAME, MODE_AND_ADDONS,
},
worker::{
reload_custom_tags_setting, Connection, HUB_CACHE_DIR, TMP_DIR, TMP_LOGS_DIR, WORKER_GROUP,
},
@@ -75,8 +80,7 @@ use windmill_worker::{
get_hub_script_content_and_requirements, BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR, CSHARP_CACHE_DIR,
DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, DENO_CACHE_DIR_NPM, GO_BIN_CACHE_DIR, GO_CACHE_DIR,
JAVA_CACHE_DIR, NU_CACHE_DIR, POWERSHELL_CACHE_DIR, PY310_CACHE_DIR, PY311_CACHE_DIR,
PY312_CACHE_DIR, PY313_CACHE_DIR, RUST_CACHE_DIR, TAR_JAVA_CACHE_DIR, TAR_PY310_CACHE_DIR,
TAR_PY311_CACHE_DIR, TAR_PY312_CACHE_DIR, TAR_PY313_CACHE_DIR, UV_CACHE_DIR,
PY312_CACHE_DIR, PY313_CACHE_DIR, RUST_CACHE_DIR, TAR_JAVA_CACHE_DIR, UV_CACHE_DIR,
};
use crate::monitor::{
@@ -98,7 +102,9 @@ const DEFAULT_NUM_WORKERS: usize = 1;
const DEFAULT_PORT: u16 = 8000;
const DEFAULT_SERVER_BIND_ADDR: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
mod ee;
#[cfg(feature = "private")]
pub mod ee;
mod ee_oss;
mod monitor;
pub fn setup_deno_runtime() -> anyhow::Result<()> {
@@ -153,6 +159,7 @@ lazy_static::lazy_static! {
.ok()
.and_then(|x| x.parse::<u64>().ok())
.unwrap_or(3600 * 12);
}
pub fn main() -> anyhow::Result<()> {
@@ -261,7 +268,7 @@ async fn windmill_main() -> anyhow::Result<()> {
tracing::error!("Failed to install rustls crypto provider");
}
let hostname = hostname();
let hostname = HOSTNAME.to_owned();
let mode_and_addons = MODE_AND_ADDONS.clone();
let mode = mode_and_addons.mode;
@@ -340,7 +347,7 @@ async fn windmill_main() -> anyhow::Result<()> {
"Creating http client for cluster using base internal url {}",
std::env::var("BASE_INTERNAL_URL").unwrap_or_default()
);
let suffix = windmill_common::utils::worker_suffix(&hostname, &rd_string(5));
let suffix = create_ssh_agent_worker_suffix(&hostname);
(
Connection::Http(build_agent_http_client(&suffix)),
Some(suffix),
@@ -552,7 +559,7 @@ Windmill Community Edition {GIT_VERSION}
_ = indexer_rx.recv() => {
tracing::info!("Received killpill, aborting index initialization");
},
res = windmill_indexer::completed_runs_ee::init_index(&db) => {
res = windmill_indexer::completed_runs_oss::init_index(&db) => {
let res = res?;
reader = Some(res.0);
writer = Some(res.1);
@@ -574,7 +581,7 @@ Windmill Community Edition {GIT_VERSION}
async {
if let Some(db) = conn.as_sql() {
if let Some(index_writer) = index_writer2 {
windmill_indexer::completed_runs_ee::run_indexer(
windmill_indexer::completed_runs_oss::run_indexer(
db.clone(),
index_writer,
indexer_rx,
@@ -596,7 +603,7 @@ Windmill Community Edition {GIT_VERSION}
_ = indexer_rx.recv() => {
tracing::info!("Received killpill, aborting index initialization");
},
res = windmill_indexer::service_logs_ee::init_index(&db, killpill_tx.clone()) => {
res = windmill_indexer::service_logs_oss::init_index(&db, killpill_tx.clone()) => {
let res = res?;
reader = Some(res.0);
writer = Some(res.1);
@@ -618,7 +625,7 @@ Windmill Community Edition {GIT_VERSION}
async {
if let Some(db) = conn.as_sql() {
if let Some(log_index_writer) = log_index_writer2 {
windmill_indexer::service_logs_ee::run_indexer(
windmill_indexer::service_logs_oss::run_indexer(
db.clone(),
log_index_writer,
log_indexer_rx,
@@ -675,19 +682,21 @@ Windmill Community Edition {GIT_VERSION}
let base_internal_url = base_internal_rx.await?;
if worker_mode {
let mut workers = vec![];
for i in 0..num_workers {
let suffix: String = if i == 0 && first_suffix.as_ref().is_some() {
let suffix = if i == 0 && first_suffix.is_some() {
first_suffix.as_ref().unwrap().clone()
} else {
windmill_common::utils::worker_suffix(&hostname, &rd_string(5))
create_default_worker_suffix(&hostname)
};
let worker_conn = WorkerConn {
conn: if i == 0 || mode != Mode::Agent {
conn.clone()
} else {
Connection::Http(build_agent_http_client(&suffix))
},
worker_name: windmill_common::utils::worker_name_with_suffix(
worker_name: worker_name_with_suffix(
mode == Mode::Agent,
WORKER_GROUP.as_str(),
&suffix,
@@ -852,6 +861,11 @@ Windmill Community Edition {GIT_VERSION}
}
};
},
"notify_token_invalidation" => {
let token = n.payload();
tracing::info!("Token invalidation detected for token: {}...", &token[..token.len().min(8)]);
windmill_api::auth::invalidate_token_from_cache(token);
},
"notify_global_setting_change" => {
tracing::info!("Global setting change detected: {}", n.payload());
match n.payload() {
@@ -884,7 +898,7 @@ Windmill Community Edition {GIT_VERSION}
if let Err(e) = load_tag_per_workspace_workspaces(&db).await {
tracing::error!("Error loading default tag per workspace workspaces: {e:#}");
}
}
},
SMTP_SETTING => {
reload_smtp_config(&db).await;
},
@@ -1001,7 +1015,6 @@ Windmill Community Edition {GIT_VERSION}
tracing::error!(error = %e, "Could not reload critical alert UI setting");
}
},
a @_ => {
tracing::info!("Unrecognized Global Setting Change Payload: {:?}", a);
}
@@ -1053,7 +1066,9 @@ Windmill Community Edition {GIT_VERSION}
}
if server_mode {
tracing::info!("monitor task started");
if !*windmill_common::QUIET_LOGS {
tracing::info!("monitor task started");
}
}
monitor_db(
&conn,
@@ -1065,7 +1080,9 @@ Windmill Community Edition {GIT_VERSION}
)
.await;
if server_mode {
tracing::info!("monitor task finished");
if !*windmill_common::QUIET_LOGS {
tracing::info!("monitor task finished");
}
}
},
}
@@ -1086,7 +1103,7 @@ Windmill Community Edition {GIT_VERSION}
tracing::info!("Reloading config after 12 hours");
initial_load(&conn, tx.clone(), worker_mode, server_mode, #[cfg(feature = "parquet")] disable_s3_store).await;
#[cfg(feature = "enterprise")]
ee::verify_license_key().await;
ee_oss::verify_license_key().await;
}
}
},
@@ -1174,6 +1191,7 @@ async fn listen_pg(url: &str) -> Option<PgListener> {
"notify_webhook_change",
"notify_workspace_envs_change",
"notify_runnable_version_change",
"notify_token_invalidation",
];
#[cfg(feature = "http_trigger")]
@@ -1267,10 +1285,6 @@ pub async fn run_workers(
PY311_CACHE_DIR,
PY312_CACHE_DIR,
PY313_CACHE_DIR,
TAR_PY310_CACHE_DIR,
TAR_PY311_CACHE_DIR,
TAR_PY312_CACHE_DIR,
TAR_PY313_CACHE_DIR,
BUN_BUNDLE_CACHE_DIR,
GO_CACHE_DIR,
GO_BIN_CACHE_DIR,
+13 -15
View File
@@ -29,9 +29,9 @@ use windmill_api::{
};
#[cfg(feature = "enterprise")]
use windmill_common::ee::low_disk_alerts;
use windmill_common::ee_oss::low_disk_alerts;
#[cfg(feature = "enterprise")]
use windmill_common::ee::{jobs_waiting_alerts, worker_groups_alerts};
use windmill_common::ee_oss::{jobs_waiting_alerts, worker_groups_alerts};
use windmill_common::client::AuthedClient;
#[cfg(feature = "oauth2")]
@@ -41,7 +41,7 @@ use windmill_common::s3_helpers::reload_object_store_setting;
use windmill_common::{
agent_workers::DECODED_AGENT_TOKEN,
auth::create_token_for_owner,
ee::CriticalErrorChannel,
ee_oss::CriticalErrorChannel,
error,
flow_status::{FlowStatus, FlowStatusModule},
global_settings::{
@@ -87,9 +87,9 @@ use windmill_worker::{
use windmill_common::s3_helpers::ObjectStoreReload;
#[cfg(feature = "enterprise")]
use crate::ee::verify_license_key;
use crate::ee_oss::verify_license_key;
use crate::ee::set_license_key;
use crate::ee_oss::set_license_key;
#[cfg(feature = "prometheus")]
lazy_static::lazy_static! {
@@ -1324,7 +1324,6 @@ pub async fn monitor_db(
initial_load: bool,
_killpill_tx: KillpillSender,
) {
tracing::info!("Starting periodic monitor task");
let zombie_jobs_f = async {
if server_mode && !initial_load && !*DISABLE_ZOMBIE_JOBS_MONITORING {
if let Some(db) = conn.as_sql() {
@@ -1422,7 +1421,6 @@ pub async fn monitor_db(
apply_autoscaling_f,
update_min_worker_version_f,
);
tracing::info!("Periodic monitor task completed");
}
pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
@@ -1609,7 +1607,7 @@ pub async fn reload_base_url_setting(conn: &Connection) -> error::Result<()> {
if let Some(q) = q_oauth {
if let Ok(v) = serde_json::from_value::<
Option<HashMap<String, windmill_api::oauth2_ee::OAuthClient>>,
Option<HashMap<String, windmill_api::oauth2_oss::OAuthClient>>,
>(q.clone())
{
v
@@ -1630,7 +1628,7 @@ pub async fn reload_base_url_setting(conn: &Connection) -> error::Result<()> {
{
if let Some(db) = conn.as_sql() {
let mut l = windmill_api::OAUTH_CLIENTS.write().await;
*l = windmill_api::oauth2_ee::build_oauth_clients(&base_url, oauths, db).await
*l = windmill_api::oauth2_oss::build_oauth_clients(&base_url, oauths, db).await
.map_err(|e| tracing::error!("Error building oauth clients (is the oauth.json mounted and in correct format? Use '{}' as minimal oauth.json): {}", "{}", e))
.unwrap();
}
@@ -1914,12 +1912,12 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
.await
.expect("could not create job token");
let client = AuthedClient {
base_internal_url: base_internal_url.to_string(),
let client = AuthedClient::new(
base_internal_url.to_string(),
job.workspace_id.to_string(),
token,
workspace: job.workspace_id.to_string(),
force_client: None,
};
None,
);
let last_ping = job.last_ping.clone();
let error_message = format!(
@@ -1938,7 +1936,7 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
None,
error::Error::ExecutionErr(error_message),
true,
same_worker_tx_never_used,
Some(&same_worker_tx_never_used),
"",
worker_name,
send_result_never_used,
+31 -27
View File
@@ -4,8 +4,8 @@ script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
root_dirpath="$(cd "${script_dirpath}/.." && pwd)"
REVERT="NO"
REVERT_PREVIOUS="NO"
COPY="NO"
MOVE_NEW_FILES="NO"
EE_CODE_DIR="../windmill-ee-private/"
while [[ $# -gt 0 ]]; do
@@ -16,13 +16,7 @@ while [[ $# -gt 0 ]]; do
# this to work (commit hooks should prevent this from happening, as well as the fact
# that we're using symlinks by default).
REVERT="YES"
shift
;;
--revert-previous)
# This is a special case of --revert that will revert to the previous commit.
REVERT="YES"
REVERT_PREVIOUS="YES"
echo "Reverting to previous commit"
MOVE_NEW_FILES="YES"
shift
;;
-c|--copy)
@@ -33,6 +27,11 @@ while [[ $# -gt 0 ]]; do
COPY="YES"
shift # past argument
;;
-m|--move-new-files)
# This moves all new EE files from the public repository to the private repository.
MOVE_NEW_FILES="YES"
shift # past argument
;;
-d|--dir)
# Path to the local directory of the windmill-ee-private repository. By defaults, it
# assumes it is cloned next to the Windmill OSS repo.
@@ -70,29 +69,34 @@ if [ "$REVERT" == "YES" ]; then
for ee_file in $(find ${EE_CODE_DIR} -name "*ee.rs"); do
ce_file="${ee_file/${EE_CODE_DIR}/}"
ce_file="${root_dirpath}/backend/${ce_file}"
if [ "$REVERT_PREVIOUS" == "YES" ]; then
git checkout HEAD@{3} ${ce_file} || true
else
git restore --staged ${ce_file} || true
git restore ${ce_file} || true
fi
rm ${ce_file} || true
done
else
elif [ "$MOVE_NEW_FILES" == "NO" ]; then
# This replaces all files in current repo with alternative EE files in windmill-ee-private
for ee_file in $(find "${EE_CODE_DIR}" -name "*ee.rs"); do
ce_file="${ee_file/${EE_CODE_DIR}/}"
ce_file="${root_dirpath}/backend/${ce_file}"
if [[ -f "${ce_file}" ]]; then
rm "${ce_file}"
if [ "$COPY" == "YES" ]; then
cp "${ee_file}" "${ce_file}"
echo "File copied '${ee_file}' -->> '${ce_file}'"
else
ln -s "${ee_file}" "${ce_file}"
echo "Symlink created '${ee_file}' -->> '${ce_file}'"
fi
ce_file="${ee_file/${EE_CODE_DIR}/}"
ce_file="${root_dirpath}/backend/${ce_file}"
if [ "$COPY" == "YES" ]; then
cp "${ee_file}" "${ce_file}"
echo "File copied '${ee_file}' -->> '${ce_file}'"
else
echo "File ${ce_file} is not a file, ignoring"
ln -s "${ee_file}" "${ce_file}" || true
echo "Symlink created '${ee_file}' -->> '${ce_file}'"
fi
done
fi
if [ "$MOVE_NEW_FILES" == "YES" ]; then
for ce_file in $(find "${root_dirpath}"/backend/windmill-*/src/ -name "*ee.rs"); do
backend_dirpath="${root_dirpath}/backend/"
ee_file="${ce_file/${backend_dirpath}/}"
ee_file="${EE_CODE_DIR}${ee_file}"
if [ ! -f "${ee_file}" ]; then
mv "${ce_file}" "${ee_file}"
if [ ! "$REVERT" == "YES" ]; then
ln -s "${ee_file}" "${ce_file}"
fi
echo "File moved '${ce_file}' -->> '${ee_file}'"
fi
done
fi
+154
View File
@@ -0,0 +1,154 @@
# This script is used to summarize the database schema.
# You can use pg_dump to dump the schema to a file.
# pg_dump --file "schema.sql" --host "localhost" --port "5432" --username "postgres" --no-password --format=c --large-objects --schema-only --no-owner --no-privileges --no-tablespaces --no-unlogged-table-data --no-comments --no-publications --no-subscriptions --no-security-labels --no-toast-compression --no-table-access-method --verbose --schema "public" "windmill"
# Then you can run python summarize_schema.py schema.sql to get the summarized schema.
import re
import sys
from collections import defaultdict
def summarize_schema(file_path):
"""
Parses a PostgreSQL dump file and extracts a summarized schema.
"""
tables = defaultdict(lambda: {'columns': [], 'pks': set(), 'fks': [], 'indexes': []})
enums = defaultdict(list)
# Use state variables to parse multi-line definitions
current_table = None
current_enum = None
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
# --- State Resets ---
if line.startswith(');'):
current_table = None
current_enum = None
continue
# --- Parse ENUM definitions ---
match_enum = re.match(r"CREATE TYPE public\.(\w+) AS ENUM \($", line)
if match_enum:
current_enum = match_enum.group(1)
continue
if current_enum:
# Extract enum values, which are typically like 'value',
value = line.strip("',")
if value and not value.startswith('--'):
enums[current_enum].append(value)
continue
# --- Parse TABLE definitions ---
match_table = re.match(r"CREATE TABLE public\.(\w+) \($", line)
if match_table:
current_table = match_table.group(1)
continue
if current_table:
# Parse columns within a CREATE TABLE block
# e.g., "column_name type NOT NULL,"
# e.g., "id bigint NOT NULL,"
match_column = re.match(r'^"?(\w+)"?\s+([\w\d\.\[\]\(\)]+)', line)
if match_column:
col_name = match_column.group(1)
col_type = match_column.group(2)
tables[current_table]['columns'].append(f"{col_name} ({col_type})")
# Parse PRIMARY KEY defined inside the table
match_pk = re.search(r"CONSTRAINT \w+ PRIMARY KEY \((.+)\)", line)
if match_pk:
# Handle multiple PK columns: "col1, col2, col3"
pk_cols = [p.strip().strip('"') for p in match_pk.group(1).split(',')]
tables[current_table]['pks'].update(pk_cols)
continue
# --- Parse Foreign Keys (defined outside CREATE TABLE) ---
match_fk = re.match(r"ALTER TABLE ONLY public\.(\w+)\s+ADD CONSTRAINT \w+ FOREIGN KEY \(([\w,\s\"]+)\) REFERENCES public\.(\w+)\(([\w,\s\"]+)\);", line)
if match_fk:
from_table, from_cols, to_table, to_cols = match_fk.groups()
# Clean up column names
from_cols_clean = ', '.join([c.strip().strip('"') for c in from_cols.split(',')])
to_cols_clean = ', '.join([c.strip().strip('"') for c in to_cols.split(',')])
fk_string = f"({from_cols_clean}) -> {to_table}({to_cols_clean})"
tables[from_table]['fks'].append(fk_string)
# --- Parse Index definitions ---
match_index = re.match(r"CREATE (UNIQUE )?INDEX (\w+) ON public\.(\w+) USING (\w+) \((.+)\);", line)
if match_index:
is_unique = match_index.group(1) is not None
index_name = match_index.group(2)
table_name = match_index.group(3)
index_type = match_index.group(4)
columns = match_index.group(5)
# Clean up column expressions
columns_clean = columns.replace('"', '')
unique_str = "UNIQUE " if is_unique else ""
index_string = f"{unique_str}INDEX {index_name} ({index_type}) ON ({columns_clean})"
tables[table_name]['indexes'].append(index_string)
return enums, tables
def format_output(enums, tables):
"""
Formats the parsed schema data into a clean, readable string.
"""
output = []
output.append("### Simplified Database Schema ###")
output.append("\n--- Custom Data Types (ENUMs) ---\n")
if not enums:
output.append("No custom ENUM types found.")
else:
for name, values in sorted(enums.items()):
output.append(f"{name}:")
for v in values:
output.append(f" - {v}")
output.append("")
output.append("\n--- Tables and Relationships ---\n")
if not tables:
output.append("No tables found.")
else:
for name, data in sorted(tables.items()):
output.append(f"TABLE: {name}")
for col in data['columns']:
col_name = col.split(' ')[0]
marker = " (PK)" if col_name in data['pks'] else ""
output.append(f" - {col}{marker}")
if data['fks']:
output.append(" Relationships:")
for fk in data['fks']:
output.append(f" - {fk}")
if data['indexes']:
output.append(" Indexes:")
for idx in data['indexes']:
output.append(f" - {idx}")
output.append("-" * 20)
return "\n".join(output)
if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"Usage: python {sys.argv[0]} <path_to_dump.sql>")
sys.exit(1)
input_file = sys.argv[1]
try:
enums_data, tables_data = summarize_schema(input_file)
formatted_summary = format_output(enums_data, tables_data)
print(formatted_summary)
except FileNotFoundError:
print(f"Error: The file '{input_file}' was not found.")
sys.exit(1)
except Exception as e:
print(f"An unexpected error occurred: {e}")
sys.exit(1)
File diff suppressed because it is too large Load Diff
+4 -8
View File
@@ -3969,8 +3969,7 @@ async fn assert_lockfile(
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_requirements_python(db: Pool<Postgres>) {
let content = r#"
# py: 3.11.11
let content = r#"# py: ==3.11.11
# requirements:
# tiny==0.1.3
@@ -3997,8 +3996,7 @@ def main():
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_extra_requirements_python(db: Pool<Postgres>) {
{
let content = r#"
# py: ==3.11.11
let content = r#"# py: ==3.11.11
# extra_requirements:
# tiny
@@ -4025,8 +4023,7 @@ def main():
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_extra_requirements_python2(db: Pool<Postgres>) {
let content = r#"
# py: ==3.11.11
let content = r#"# py: ==3.11.11
# extra_requirements:
# tiny==0.1.3
@@ -4048,8 +4045,7 @@ def main():
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_pins_python(db: Pool<Postgres>) {
let content = r#"
# py: ==3.11.11
let content = r#"# py: ==3.11.11
# extra_requirements:
# tiny==0.1.3
# bottle==0.13.2
-1
View File
@@ -10,7 +10,6 @@ if [[ "$(uname)" == "Darwin" ]]; then
fi
cargo sqlx prepare --workspace -- --all-targets --all-features
./substitute_ee_code.sh -r --dir ../windmill-ee-private
# Undo the samael changes on macOS
if [[ "$(uname)" == "Darwin" ]]; then
+6 -3
View File
@@ -10,13 +10,14 @@ path = "src/lib.rs"
[features]
default = []
private = ["windmill-audit/private"]
enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "windmill-worker/enterprise"]
stripe = []
agent_worker_server = []
enterprise_saml = ["dep:samael", "dep:libxml"]
benchmark = []
embedding = ["dep:tinyvector", "dep:hf-hub", "dep:tokenizers", "dep:candle-core", "dep:candle-transformers", "dep:candle-nn"]
parquet = ["dep:datafusion", "dep:object_store", "dep:url", "windmill-common/parquet", "windmill-worker/parquet"]
parquet = ["dep:datafusion", "dep:object_store", "windmill-common/parquet", "windmill-worker/parquet"]
prometheus = ["windmill-common/prometheus", "windmill-queue/prometheus", "dep:prometheus", "windmill-worker/prometheus"]
openidconnect = ["dep:openidconnect", "windmill-common/openidconnect"]
tantivy = ["dep:windmill-indexer"]
@@ -39,7 +40,7 @@ mcp = ["dep:rmcp"]
python = []
[dependencies]
rmcp = { git = "https://github.com/windmill-labs/rust-sdk", features = ["transport-sse-server"], optional = true }
rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", features=["transport-streamable-http-server", "transport-streamable-http-server-session", "transport-worker"], optional = true }
windmill-queue.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-audit.workspace = true
@@ -75,6 +76,7 @@ hex.workspace = true
base64.workspace = true
base32.workspace = true
serde_urlencoded.workspace = true
serde_yml.workspace = true
cron.workspace = true
mime_guess.workspace = true
rust-embed = { workspace = true, optional = true }
@@ -101,6 +103,7 @@ prometheus = { workspace = true, optional = true }
async_zip = { workspace = true, optional = true }
regex.workspace = true
bytes.workspace = true
url.workspace = true
samael = { workspace = true, optional = true }
libxml = { workspace = true, optional = true }
async-recursion.workspace = true
@@ -115,7 +118,6 @@ candle-nn = { workspace = true, optional = true}
datafusion = { workspace = true, optional = true}
object_store = { workspace = true, optional = true}
openidconnect = { workspace = true, optional = true}
url = { workspace = true, optional = true}
jsonwebtoken = { workspace = true }
matchit = { workspace = true, optional = true }
tokio-tungstenite = { workspace = true, optional = true}
@@ -125,6 +127,7 @@ nkeys = { workspace = true, optional = true }
const_format.workspace = true
pin-project.workspace = true
http.workspace = true
indexmap.workspace = true
async-stream.workspace = true
ulid.workspace = true
rust-postgres = { workspace = true, optional = true }
+207 -24
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.494.0
version: 1.498.0
title: Windmill API
contact:
@@ -4501,6 +4501,13 @@ paths:
in: query
schema:
type: boolean
- name: languages
in: query
description: |
Filter to only include scripts written in the given languages.
Accepts multiple values as a comma-separated list.
schema:
type: string
responses:
"200":
description: All scripts
@@ -8444,6 +8451,76 @@ paths:
"201":
description: default error handler set
/w/{workspace}/openapi/generate:
post:
summary: generate openapi spec from http routes/webhook
operationId: generateOpenapiSpec
tags:
- openapi
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: openapi spec info and url
content:
application/json:
schema:
$ref: "#/components/schemas/GenerateOpenapiSpec"
responses:
"200":
description: openapi spec
content:
text/plain:
schema:
type: string
/w/{workspace}/openapi/download:
post:
summary: Download the OpenAPI v3.1 spec as a file
operationId: DownloadOpenapiSpec
tags:
- openapi
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: openapi spec info and url
content:
application/json:
schema:
$ref: "#/components/schemas/GenerateOpenapiSpec"
responses:
"200":
description: Downloaded OpenAPI spec
content:
application/octet-stream:
schema:
type: string
format: binary
/w/{workspace}/http_triggers/create_many:
post:
summary: create many HTTP triggers
operationId: createHttpTriggers
tags:
- http_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: new http trigger
required: true
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/NewHttpTrigger"
responses:
"201":
description: http trigger created
content:
text/plain:
schema:
type: string
/w/{workspace}/http_triggers/create:
post:
summary: create http trigger
@@ -8595,8 +8672,7 @@ paths:
route_path:
type: string
http_method:
type: string
enum: ["get", "post", "put", "delete", "patch"]
$ref: "#/components/schemas/HttpMethod"
trigger_path:
type: string
workspaced_route:
@@ -13185,6 +13261,8 @@ components:
# NOTE: Not so many generators and validators support this format:
# $ref: "../../openflow.openapi.yaml#/components/schemas"
# This is why it is better to inline each of schemas for better compat
# Do not change next line. It is used by python-client for pre-processing
# -- INLINE START --
OpenFlow:
$ref: "../../openflow.openapi.yaml#/components/schemas/OpenFlow"
FlowValue:
@@ -13223,6 +13301,8 @@ components:
$ref: "../../openflow.openapi.yaml#/components/schemas/FlowStatus"
FlowStatusModule:
$ref: "../../openflow.openapi.yaml#/components/schemas/FlowStatusModule"
# -- INLINE END --
# Do not change line above
AIProvider:
type: string
@@ -14719,6 +14799,115 @@ components:
- custom_script
- signature
RunnableKind:
type: string
enum:
- script
- flow
OpenapiSpecFormat:
type: string
enum:
- yaml
- json
OpenapiHttpRouteFilters:
type: object
properties:
folder_regex:
type: string
path_regex:
type: string
route_path_regex:
type: string
required:
- folder_regex
- path_regex
- route_path_regex
WebhookFilters:
type: object
properties:
user_or_folder_regex:
type: string
enum:
- "*"
- u
- f
user_or_folder_regex_value:
type: string
path:
type: string
runnable_kind:
$ref: "#/components/schemas/RunnableKind"
required:
- user_or_folder_regex
- user_or_folder_regex_value
- path
- runnable_kind
OpenapiV3Info:
type: object
properties:
title:
type: string
version:
type: string
description:
type: string
terms_of_service:
type: string
contact:
type: object
properties:
name:
type: string
url:
type: string
email:
type: string
license:
type: object
properties:
name:
type: string
identifier:
type: string
url:
type: string
required:
- name
required:
- title
- version
GenerateOpenapiSpec:
type: object
properties:
info:
$ref: "#/components/schemas/OpenapiV3Info"
url:
type: string
openapi_spec_format:
$ref: "#/components/schemas/OpenapiSpecFormat"
http_route_filters:
type: array
items:
$ref: "#/components/schemas/OpenapiHttpRouteFilters"
webhook_filters:
type: array
items:
$ref: "#/components/schemas/WebhookFilters"
HttpMethod:
type: string
enum:
- get
- post
- put
- delete
- patch
HttpTrigger:
allOf:
- $ref: "#/components/schemas/TriggerExtraProperty"
@@ -14738,15 +14927,13 @@ components:
required:
- s3
http_method:
type: string
enum:
- get
- post
- put
- delete
- patch
$ref: "#/components/schemas/HttpMethod"
authentication_resource_path:
type: string
summary:
type: string
description:
type: string
is_async:
type: boolean
authentication_method:
@@ -14781,6 +14968,10 @@ components:
type: string
workspaced_route:
type: boolean
summary:
type: string
description:
type: string
static_asset_config:
type: object
properties:
@@ -14795,13 +14986,7 @@ components:
is_flow:
type: boolean
http_method:
type: string
enum:
- get
- post
- put
- delete
- patch
$ref: "#/components/schemas/HttpMethod"
authentication_resource_path:
type: string
is_async:
@@ -14834,6 +15019,10 @@ components:
type: string
route_path:
type: string
summary:
type: string
description:
type: string
workspaced_route:
type: boolean
static_asset_config:
@@ -14852,13 +15041,7 @@ components:
is_flow:
type: boolean
http_method:
type: string
enum:
- get
- post
- put
- delete
- patch
$ref: "#/components/schemas/HttpMethod"
is_async:
type: boolean
authentication_method:
@@ -1,3 +1,7 @@
#[cfg(feature = "private")]
#[allow(unused)]
pub use crate::agent_workers_ee::*;
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
@@ -6,16 +10,21 @@
* LICENSE-AGPL for a copy of the license.
*/
#[cfg(not(feature = "private"))]
use crate::db::DB;
#[cfg(not(feature = "private"))]
use axum::Router;
#[cfg(not(feature = "private"))]
use serde::{Deserialize, Serialize};
#[cfg(not(feature = "private"))]
pub fn global_service() -> Router {
Router::new()
}
#[cfg(not(feature = "private"))]
pub fn workspaced_service(
db: DB,
_base_internal_url: String,
@@ -36,6 +45,7 @@ pub fn workspaced_service(
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg(not(feature = "private"))]
pub struct AgentAuth {
pub worker_group: String,
pub suffix: Option<String>,
@@ -43,8 +53,10 @@ pub struct AgentAuth {
pub exp: Option<usize>,
}
#[cfg(not(feature = "private"))]
pub struct AgentCache {}
#[cfg(not(feature = "private"))]
impl AgentCache {
pub fn new() -> Self {
AgentCache {}
+58 -15
View File
@@ -4,13 +4,13 @@ use crate::{
};
use axum::{body::Bytes, extract::Path, response::IntoResponse, routing::post, Extension, Router};
use http::HeaderMap;
use http::{HeaderMap, Method};
use quick_cache::sync::Cache;
use reqwest::{Client, RequestBuilder};
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use std::collections::HashMap;
use windmill_audit::{audit_ee::audit_log, ActionKind};
use windmill_audit::{audit_oss::audit_log, ActionKind};
use windmill_common::error::{to_anyhow, Error, Result};
lazy_static::lazy_static! {
@@ -24,7 +24,7 @@ lazy_static::lazy_static! {
pub static ref AI_REQUEST_CACHE: Cache<(String, AIProvider), ExpiringAIRequestConfig> = Cache::new(500);
}
const AZURE_API_VERSION: &str = "2024-10-21";
const AZURE_API_VERSION: &str = "2025-04-01-preview";
const OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
#[derive(Deserialize, Debug)]
@@ -141,23 +141,49 @@ impl AIRequestConfig {
self,
provider: &AIProvider,
path: &str,
method: Method,
headers: HeaderMap,
body: Bytes,
) -> Result<RequestBuilder> {
let url = format!("{}/{}", self.base_url, path);
let body = if let Some(user) = self.user {
Self::add_user_to_body(body, user)?
} else {
body
};
let is_azure = matches!(provider, AIProvider::OpenAI) && self.base_url != OPENAI_BASE_URL
let base_url = self.base_url.trim_end_matches('/');
let is_azure = matches!(provider, AIProvider::OpenAI) && base_url != OPENAI_BASE_URL
|| matches!(provider, AIProvider::AzureOpenAI);
let is_anthropic = matches!(provider, AIProvider::Anthropic);
let url = if is_azure && method != Method::GET {
if base_url.ends_with("/deployments") {
let model = Self::get_azure_model(&body)?;
format!("{}/{}/{}", base_url, model, path)
} else if base_url.ends_with("/openai") {
let model = Self::get_azure_model(&body)?;
format!("{}/deployments/{}/{}", base_url, model, path)
} else {
format!("{}/{}", base_url, path)
}
} else {
format!("{}/{}", base_url, path)
};
tracing::debug!("AI request URL: {}", url);
let mut request = HTTP_CLIENT
.post(url)
.header("content-type", "application/json")
.body(body);
.request(method, url)
.header("content-type", "application/json");
for (header_name, header_value) in headers.iter() {
if header_name.to_string().starts_with("anthropic-") {
request = request.header(header_name, header_value);
}
}
request = request.body(body);
if is_azure {
request = request.query(&[("api-version", AZURE_API_VERSION)])
@@ -165,9 +191,12 @@ impl AIRequestConfig {
if let Some(api_key) = self.api_key {
if is_azure {
request = request.header("api-key", api_key)
request = request.header("api-key", api_key.clone())
} else {
request = request.header("authorization", format!("Bearer {}", api_key))
request = request.header("authorization", format!("Bearer {}", api_key.clone()))
}
if is_anthropic {
request = request.header("X-API-Key", api_key);
}
}
@@ -199,6 +228,18 @@ impl AIRequestConfig {
.map_err(|e| Error::internal_err(format!("Failed to reserialize request body: {}", e)))?
.into())
}
fn get_azure_model(body: &Bytes) -> Result<String> {
#[derive(Deserialize, Debug)]
struct AzureModel {
model: String,
}
let azure_model: AzureModel = serde_json::from_slice(body)
.map_err(|e| Error::internal_err(format!("Failed to parse request body: {}", e)))?;
Ok(azure_model.model)
}
}
#[derive(Clone, Debug)]
@@ -311,17 +352,18 @@ pub struct AIConfig {
}
pub fn global_service() -> Router {
Router::new().route("/proxy/*ai", post(global_proxy))
Router::new().route("/proxy/*ai", post(global_proxy).get(global_proxy))
}
pub fn workspaced_service() -> Router {
Router::new().route("/proxy/*ai", post(proxy))
Router::new().route("/proxy/*ai", post(proxy).get(proxy))
}
async fn global_proxy(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(ai_path): Path<String>,
method: Method,
headers: HeaderMap,
body: Bytes,
) -> impl IntoResponse {
@@ -346,7 +388,7 @@ async fn global_proxy(
let url = format!("{}/{}", base_url, ai_path);
let request = HTTP_CLIENT
.post(url)
.request(method, url)
.header("content-type", "application/json")
.header("Authorization", format!("Bearer {}", api_key))
.body(body);
@@ -382,6 +424,7 @@ async fn proxy(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path((w_id, ai_path)): Path<(String, String)>,
method: Method,
headers: HeaderMap,
body: Bytes,
) -> impl IntoResponse {
@@ -464,7 +507,7 @@ async fn proxy(
}
};
let request = request_config.prepare_request(&provider, &ai_path, body)?;
let request = request_config.prepare_request(&provider, &ai_path, method, headers, body)?;
let response = request.send().await.map_err(to_anyhow)?;
+4 -4
View File
@@ -12,13 +12,13 @@ use crate::{
db::{ApiAuthed, DB},
resources::get_resource_value_interpolated_internal,
users::{require_owner_of_path, OptAuthed},
utils::{RunnableKind, WithStarredInfoQuery},
utils::WithStarredInfoQuery,
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
#[cfg(feature = "parquet")]
use crate::{
job_helpers_ee::{
job_helpers_oss::{
download_s3_file_internal, get_random_file_name, get_s3_resource,
get_workspace_s3_resource, upload_file_from_req, DownloadFileQuery,
},
@@ -48,7 +48,7 @@ use sha2::{Digest, Sha256};
use sql_builder::{bind::Bind, SqlBuilder};
use sqlx::{types::Uuid, FromRow};
use std::str;
use windmill_audit::audit_ee::audit_log;
use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_common::{
apps::{AppScriptId, ListAppQuery},
@@ -59,7 +59,7 @@ use windmill_common::{
users::username_to_permissioned_as,
utils::{
http_get_from_hub, not_found_if_none, paginate, query_elems_from_hub, require_admin,
Pagination, StripPath,
Pagination, RunnableKind, StripPath,
},
variables::{build_crypt, build_crypt_with_key_suffix, encrypt},
worker::{to_raw_value, CLOUD_HOSTED},
-5
View File
@@ -1,5 +0,0 @@
use axum::Router;
pub fn global_unauthed_service() -> Router {
Router::new()
}
+11
View File
@@ -0,0 +1,11 @@
#[cfg(feature = "private")]
#[allow(unused)]
pub use crate::apps_ee::*;
#[cfg(not(feature = "private"))]
use axum::Router;
#[cfg(not(feature = "private"))]
pub fn global_unauthed_service() -> Router {
Router::new()
}
+1 -1
View File
@@ -85,7 +85,7 @@ impl RawWebhookArgs {
db: &DB,
w_id: &str,
) -> Result<HashMap<String, Box<RawValue>>, Error> {
use crate::job_helpers_ee::{
use crate::job_helpers_oss::{
get_random_file_name, get_workspace_s3_resource, upload_file_internal,
};
use futures::TryStreamExt;
+2 -2
View File
@@ -28,7 +28,7 @@ async fn get_audit(
Path((w_id, id)): Path<(String, i32)>,
) -> JsonResult<AuditLog> {
let tx = user_db.begin(&authed).await?;
let audit = windmill_audit::audit_ee::get_audit(tx, id, &w_id).await?;
let audit = windmill_audit::audit_oss::get_audit(tx, id, &w_id).await?;
Ok(Json(audit))
}
async fn list_audit(
@@ -39,6 +39,6 @@ async fn list_audit(
Query(lq): Query<ListAuditLogQuery>,
) -> JsonResult<Vec<AuditLog>> {
let tx = user_db.begin(&authed).await?;
let rows = windmill_audit::audit_ee::list_audit(tx, w_id, pagination, lq).await?;
let rows = windmill_audit::audit_oss::list_audit(tx, w_id, pagination, lq).await?;
Ok(Json(rows))
}
+92 -81
View File
@@ -1,5 +1,5 @@
#[cfg(feature = "enterprise")]
use crate::ee::ExternalJwks;
use crate::ee_oss::ExternalJwks;
use axum::{
async_trait,
extract::{FromRequestParts, OriginalUri, Query},
@@ -26,6 +26,21 @@ use windmill_common::{
users::{COOKIE_NAME, SUPERADMIN_SECRET_EMAIL},
};
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);
}
// Global function to invalidate a specific token from cache
pub fn invalidate_token_from_cache(token: &str) {
// Remove all cache entries for this token (across all workspaces)
AUTH_CACHE.retain(|(_workspace_id, cached_token), _cached_value| cached_token != token);
tracing::info!(
"Invalidated token from auth cache: {}...",
&token[..token.len().min(8)]
);
}
#[derive(Clone)]
pub struct ExpiringAuthCache {
pub authed: ApiAuthed,
@@ -33,7 +48,6 @@ pub struct ExpiringAuthCache {
}
pub struct AuthCache {
cache: Cache<(String, String), ExpiringAuthCache>,
db: DB,
superadmin_secret: Option<String>,
#[cfg(feature = "enterprise")]
@@ -47,7 +61,6 @@ impl AuthCache {
#[cfg(feature = "enterprise")] ext_jwks: Option<Arc<RwLock<ExternalJwks>>>,
) -> Self {
AuthCache {
cache: Cache::new(300),
db,
superadmin_secret,
#[cfg(feature = "enterprise")]
@@ -56,7 +69,7 @@ impl AuthCache {
}
pub async fn invalidate(&self, w_id: &str, token: String) {
self.cache.remove(&(w_id.to_string(), token));
AUTH_CACHE.remove(&(w_id.to_string(), token));
}
pub async fn get_authed(&self, w_id: Option<String>, token: &str) -> Option<ApiAuthed> {
@@ -64,14 +77,14 @@ impl AuthCache {
w_id.as_ref().unwrap_or(&"".to_string()).to_string(),
token.to_string(),
);
let s = self.cache.get(&key).map(|c| c.to_owned());
let s = AUTH_CACHE.get(&key).map(|c| c.to_owned());
match s {
Some(ExpiringAuthCache { authed, expiry }) if expiry > chrono::Utc::now() => {
Some(authed)
}
#[cfg(feature = "enterprise")]
_ if token.starts_with("jwt_ext_") => {
let authed_and_exp = match crate::ee::jwt_ext_auth(
let authed_and_exp = match crate::ee_oss::jwt_ext_auth(
w_id.as_ref(),
token.trim_start_matches("jwt_ext_"),
self.ext_jwks.clone(),
@@ -86,7 +99,7 @@ impl AuthCache {
};
if let Some((authed, exp)) = authed_and_exp.clone() {
self.cache.insert(
AUTH_CACHE.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
@@ -123,7 +136,7 @@ impl AuthCache {
username_override,
};
self.cache.insert(
AUTH_CACHE.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
@@ -317,7 +330,7 @@ impl AuthCache {
}
};
if let Some(authed) = authed_o.as_ref() {
self.cache.insert(
AUTH_CACHE.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
@@ -470,6 +483,27 @@ where
}
}
fn maybe_get_workspace_id_from_path(path_vec: &[&str]) -> Option<String> {
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
Some(path_vec[3].to_owned())
} else if path_vec.len() >= 5
&& path_vec[0] == ""
&& path_vec[1] == "api"
&& path_vec[2] == "mcp"
&& path_vec[3] == "w"
{
Some(path_vec[4].to_owned())
} else {
if path_vec.len() >= 5 && path_vec[0] == "" && path_vec[2] == "srch" && path_vec[3] == "w" {
Some(path_vec[4].to_owned())
} else {
None
}
};
workspace_id
}
#[async_trait]
impl<S> FromRequestParts<S> for ApiAuthed
where
@@ -482,85 +516,62 @@ where
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
if parts.method == http::Method::OPTIONS {
return Ok(ApiAuthed {
email: "".to_owned(),
username: "".to_owned(),
is_admin: false,
is_operator: false,
groups: Vec::new(),
folders: Vec::new(),
scopes: None,
username_override: None,
});
return Ok(ApiAuthed::default());
};
let already_authed = parts.extensions.get::<ApiAuthed>();
if let Some(authed) = already_authed {
Ok(authed.clone())
} else {
let already_tokened = parts.extensions.get::<Tokened>();
let token_o = if let Some(token) = already_tokened {
Some(token.token.clone())
} else {
extract_token(parts, state).await
};
let original_uri = OriginalUri::from_request_parts(parts, state)
.await
.ok()
.map(|x| x.0)
.unwrap_or_default();
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
Some(path_vec[3].to_owned())
} else if path_vec.len() >= 5
&& path_vec[0] == ""
&& path_vec[1] == "api"
&& path_vec[2] == "mcp"
&& path_vec[3] == "w"
{
Some(path_vec[4].to_string())
} else {
if path_vec.len() >= 5
&& path_vec[0] == ""
&& path_vec[2] == "srch"
&& path_vec[3] == "w"
{
Some(path_vec[4].to_string())
} else {
None
}
};
if let Some(token) = token_o {
if let Ok(Extension(cache)) =
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
{
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
parts.extensions.insert(authed.clone());
if authed.scopes.as_ref().is_some_and(|scopes| {
scopes
.iter()
.any(|s| s.starts_with("jobs:") || s.starts_with("run:"))
}) && (path_vec.len() < 3
|| (path_vec[4] != "jobs" && path_vec[4] != "jobs_u"))
{
BRUTE_FORCE_COUNTER.increment().await;
return Err((
StatusCode::UNAUTHORIZED,
format!("Unauthorized scoped token: {:?}", authed.scopes),
));
}
Span::current().record("username", &authed.username.as_str());
Span::current().record("email", &authed.email);
if let Some(workspace_id) = workspace_id {
Span::current().record("workspace_id", &workspace_id);
}
return Ok(authed);
if let Some(authed) = already_authed {
return Ok(authed.clone());
}
let already_tokened = parts.extensions.get::<Tokened>();
let token_o = if let Some(token) = already_tokened {
Some(token.token.clone())
} else {
extract_token(parts, state).await
};
if let Some(token) = token_o {
if let Ok(Extension(cache)) =
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
{
let original_uri = OriginalUri::from_request_parts(parts, state)
.await
.ok()
.map(|x| x.0)
.unwrap_or_default();
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = maybe_get_workspace_id_from_path(&path_vec);
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
if authed.scopes.as_ref().is_some_and(|scopes| {
scopes
.iter()
.any(|s| s.starts_with("jobs:") || s.starts_with("run:"))
}) && (path_vec.len() < 3
|| (path_vec[4] != "jobs" && path_vec[4] != "jobs_u"))
{
BRUTE_FORCE_COUNTER.increment().await;
return Err((
StatusCode::UNAUTHORIZED,
format!("Unauthorized scoped token: {:?}", authed.scopes),
));
}
parts.extensions.insert(authed.clone());
Span::current().record("username", &authed.username.as_str());
Span::current().record("email", &authed.email);
if let Some(workspace_id) = workspace_id {
Span::current().record("workspace_id", &workspace_id);
}
return Ok(authed);
}
}
BRUTE_FORCE_COUNTER.increment().await;
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
}
BRUTE_FORCE_COUNTER.increment().await;
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
}
}
+20 -18
View File
@@ -15,7 +15,7 @@ use {
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
use {
crate::gcp_triggers_ee::{
crate::gcp_triggers_oss::{
manage_google_subscription, process_google_push_request, validate_jwt_token,
CreateUpdateConfig, SubscriptionMode,
},
@@ -46,28 +46,24 @@ use serde::de::DeserializeOwned;
use windmill_common::error::Error;
#[cfg(all(feature = "enterprise", feature = "kafka"))]
use crate::kafka_triggers_ee::KafkaTriggerConfigConnection;
use crate::kafka_triggers_oss::KafkaTriggerConfigConnection;
#[cfg(feature = "mqtt_trigger")]
use crate::mqtt_triggers::{MqttClientVersion, MqttV3Config, MqttV5Config, SubscribeTopic};
#[cfg(all(feature = "enterprise", feature = "nats"))]
use crate::nats_triggers_ee::NatsTriggerConfigConnection;
use crate::nats_triggers_oss::NatsTriggerConfigConnection;
#[cfg(feature = "postgres_trigger")]
use {
crate::postgres_triggers::{
create_logical_replication_slot, create_pg_publication, generate_random_string,
get_pg_connection, PublicationData,
},
sqlx::Connection,
use crate::postgres_triggers::{
create_logical_replication_slot, create_pg_publication, generate_random_string,
get_default_pg_connection, PublicationData,
};
use crate::{
args::RawWebhookArgs,
db::{ApiAuthed, DB},
users::fetch_api_authed,
utils::RunnableKind,
};
use axum::{
@@ -85,7 +81,7 @@ use windmill_common::{
db::UserDB,
error::{JsonResult, Result},
triggers::{RunnableFormat, RunnableFormatVersion, TriggerKind},
utils::{not_found_if_none, paginate, Pagination, StripPath},
utils::{not_found_if_none, paginate, Pagination, RunnableKind, StripPath},
worker::{to_raw_value, CLOUD_HOSTED},
};
@@ -304,13 +300,15 @@ async fn set_postgres_trigger_config(
user_db: UserDB,
mut capture_config: NewCaptureConfig,
) -> Result<NewCaptureConfig> {
use windmill_common::error::to_anyhow;
let Some(TriggerConfig::Postgres(postgres_config)) = capture_config.trigger_config.as_mut()
else {
return Err(Error::BadRequest("Invalid postgres config".to_string()));
};
if postgres_config.basic_mode.unwrap_or(false) {
let mut pg_connection = get_pg_connection(
let mut pg_connection = get_default_pg_connection(
authed,
Some(user_db),
&db,
@@ -319,22 +317,26 @@ async fn set_postgres_trigger_config(
)
.await?;
let mut tx = pg_connection.begin().await?;
let tx = pg_connection.transaction().await.map_err(to_anyhow)?;
let publication_name = format!("windmill_capture_{}", generate_random_string());
let replication_slot_name = publication_name.clone();
create_logical_replication_slot(&mut tx, &replication_slot_name).await?;
create_logical_replication_slot(tx.client(), &replication_slot_name)
.await
.map_err(to_anyhow)?;
create_pg_publication(
&mut tx,
tx.client(),
&publication_name,
postgres_config.publication.table_to_track.as_deref(),
&postgres_config.publication.transaction_to_track,
)
.await?;
.await
.map_err(to_anyhow)?;
tx.commit().await.map_err(to_anyhow)?;
tx.commit().await?;
postgres_config.publication_name = Some(publication_name);
postgres_config.replication_slot_name = Some(replication_slot_name);
} else {
@@ -905,7 +907,7 @@ async fn gcp_payload(
headers: HeaderMap,
request: Request,
) -> Result<StatusCode> {
use crate::{gcp_triggers_ee::GcpTrigger, trigger_helpers::TriggerJobArgs};
use crate::{gcp_triggers_oss::GcpTrigger, trigger_helpers::TriggerJobArgs};
let is_flow = matches!(runnable_kind, RunnableKind::Flow);
let (gcp_trigger_config, owner, email): (GcpTriggerConfig, _, _) =
+1 -1
View File
@@ -14,7 +14,7 @@ use axum::{
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use windmill_audit::audit_ee::audit_log;
use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_common::{
error::{self},
+10 -2
View File
@@ -16,7 +16,7 @@ use sqlx::{
};
use tokio::task::JoinHandle;
use windmill_audit::audit_ee::{AuditAuthor, AuditAuthorable};
use windmill_audit::audit_oss::{AuditAuthor, AuditAuthorable};
use windmill_common::{
db::{Authable, Authed},
error::Error,
@@ -804,10 +804,18 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> {
.execute(db)
.await?;
});
run_windmill_migration!("v2_job_queue_suspend", db, |tx| {
sqlx::query!(
"CREATE INDEX CONCURRENTLY IF NOT EXISTS v2_job_queue_suspend ON v2_job_queue (workspace_id, suspend) WHERE suspend > 0;"
)
.execute(db)
.await?;
});
Ok(())
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
#[derive(Clone, Debug, Default, Hash, Eq, PartialEq)]
pub struct ApiAuthed {
pub email: String,
pub username: String,
@@ -1,15 +1,21 @@
#[cfg(feature = "private")]
#[allow(unused)]
pub use crate::ee::*;
#[cfg(not(feature = "private"))]
use anyhow::anyhow;
#[cfg(feature = "enterprise")]
#[cfg(all(feature = "enterprise", not(feature = "private")))]
use std::sync::Arc;
#[cfg(feature = "enterprise")]
#[cfg(all(feature = "enterprise", not(feature = "private")))]
use tokio::sync::RwLock;
#[cfg(not(feature = "private"))]
pub async fn validate_license_key(_license_key: String) -> anyhow::Result<(String, bool)> {
// Implementation is not open source
Err(anyhow!("License can't be validated in Windmill CE"))
}
#[cfg(feature = "enterprise")]
#[cfg(all(feature = "enterprise", not(feature = "private")))]
pub async fn jwt_ext_auth(
_w_id: Option<&String>,
_token: &str,
@@ -20,10 +26,10 @@ pub async fn jwt_ext_auth(
Err(anyhow!("External JWT auth is not open source"))
}
#[cfg(feature = "enterprise")]
#[cfg(all(feature = "enterprise", not(feature = "private")))]
pub struct ExternalJwks;
#[cfg(feature = "enterprise")]
#[cfg(all(feature = "enterprise", not(feature = "private")))]
impl ExternalJwks {
pub async fn load() -> Option<Arc<RwLock<Self>>> {
// Implementation is not open source
+4 -4
View File
@@ -12,7 +12,7 @@ use crate::db::ApiAuthed;
use crate::triggers::{
get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail,
};
use crate::utils::{RunnableKind, WithStarredInfoQuery};
use crate::utils::WithStarredInfoQuery;
use crate::{
db::DB,
schedule::clear_schedule,
@@ -31,7 +31,7 @@ use hyper::StatusCode;
use serde::{Deserialize, Serialize};
use sql_builder::prelude::*;
use sqlx::{FromRow, Postgres, Transaction};
use windmill_audit::audit_ee::audit_log;
use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_common::utils::query_elems_from_hub;
use windmill_common::worker::to_raw_value;
@@ -43,7 +43,7 @@ use windmill_common::{
jobs::JobPayload,
schedule::Schedule,
scripts::Schema,
utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, StripPath},
utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, RunnableKind, StripPath},
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_queue::{push, schedule::push_scheduled_job, PushIsolationLevel};
@@ -477,7 +477,7 @@ async fn create_flow(
false,
None,
true,
nf.tag,
None,
None,
None,
None,
+1 -1
View File
@@ -23,7 +23,7 @@ use axum::{
};
use lazy_static::lazy_static;
use regex::Regex;
use windmill_audit::audit_ee::audit_log;
use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_common::{
db::UserDB,
@@ -1,29 +1,38 @@
use crate::db::{ApiAuthed, DB};
use crate::trigger_helpers::TriggerJobArgs;
use axum::{extract::Request, Router};
use http::HeaderMap;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use sqlx::prelude::FromRow;
use sqlx::types::Json as SqlxJson;
use std::collections::HashMap;
use windmill_common::db::UserDB;
use windmill_common::worker::to_raw_value;
use windmill_common::{
error::{Error as WindmillError, Result as WindmillResult},
triggers::TriggerKind,
utils::empty_as_none,
#[cfg(feature = "private")]
#[allow(unused)]
pub use crate::gcp_triggers_ee::*;
#[cfg(not(feature = "private"))]
use {
crate::db::{ApiAuthed, DB},
crate::trigger_helpers::TriggerJobArgs,
axum::{extract::Request, Router},
http::HeaderMap,
serde::{Deserialize, Serialize},
serde_json::value::RawValue,
sqlx::prelude::FromRow,
sqlx::types::Json as SqlxJson,
std::collections::HashMap,
windmill_common::db::UserDB,
windmill_common::worker::to_raw_value,
windmill_common::{
error::{Error as WindmillError, Result as WindmillResult},
triggers::TriggerKind,
utils::empty_as_none,
},
};
#[derive(sqlx::Type, Debug, Deserialize, Serialize)]
#[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))]
#[sqlx(type_name = "DELIVERY_MODE", rename_all = "lowercase")]
#[allow(unused)]
#[cfg(not(feature = "private"))]
pub enum DeliveryType {
Pull,
Push,
}
#[cfg(not(feature = "private"))]
impl Default for DeliveryType {
fn default() -> Self {
Self::Pull
@@ -32,6 +41,7 @@ impl Default for DeliveryType {
#[derive(FromRow, Deserialize, Serialize, Debug)]
#[allow(unused)]
#[cfg(not(feature = "private"))]
pub struct PushConfig {
#[serde(deserialize_with = "empty_as_none")]
route_path: Option<String>,
@@ -42,6 +52,7 @@ pub struct PushConfig {
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[allow(unused)]
#[cfg(not(feature = "private"))]
pub struct CreateUpdateConfig {
pub delivery_type: DeliveryType,
#[serde(default, deserialize_with = "empty_as_none")]
@@ -50,6 +61,7 @@ pub struct CreateUpdateConfig {
}
#[derive(Debug, Deserialize, Serialize)]
#[cfg(not(feature = "private"))]
pub struct ExistingGcpSubscription {
pub subscription_id: String,
pub base_endpoint: String,
@@ -58,15 +70,18 @@ pub struct ExistingGcpSubscription {
#[derive(Debug, Deserialize, Serialize, sqlx::Type)]
#[serde(rename_all = "snake_case")]
#[sqlx(type_name = "GCP_SUBSCRIPTION_MODE", rename_all = "snake_case")]
#[cfg(not(feature = "private"))]
pub enum SubscriptionMode {
Existing,
CreateUpdate,
}
#[cfg(not(feature = "private"))]
pub fn workspaced_service() -> Router {
Router::new()
}
#[cfg(not(feature = "private"))]
pub fn start_consuming_gcp_pubsub_event(
_db: DB,
mut _killpill_rx: tokio::sync::broadcast::Receiver<()>,
@@ -74,6 +89,7 @@ pub fn start_consuming_gcp_pubsub_event(
// implementation is not open source
}
#[cfg(not(feature = "private"))]
pub async fn manage_google_subscription(
_authed: ApiAuthed,
_db: &DB,
@@ -91,6 +107,7 @@ pub async fn manage_google_subscription(
Ok(CreateUpdateConfig::default())
}
#[cfg(not(feature = "private"))]
pub async fn process_google_push_request(
_headers: HeaderMap,
_request: Request,
@@ -98,6 +115,7 @@ pub async fn process_google_push_request(
Ok((String::new(), HashMap::new()))
}
#[cfg(not(feature = "private"))]
pub async fn validate_jwt_token(
_db: &DB,
_user_db: UserDB,
@@ -110,11 +128,13 @@ pub async fn validate_jwt_token(
Ok(())
}
#[cfg(not(feature = "private"))]
pub fn gcp_push_route_handler() -> Router {
Router::new()
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
#[cfg(not(feature = "private"))]
pub struct GcpTrigger {
pub gcp_resource_path: String,
pub subscription_id: String,
@@ -135,7 +155,7 @@ pub struct GcpTrigger {
pub last_server_ping: Option<chrono::DateTime<chrono::Utc>>,
pub enabled: bool,
}
#[cfg(not(feature = "private"))]
impl TriggerJobArgs<String> for GcpTrigger {
fn v1_payload_fn(payload: String) -> HashMap<String, Box<RawValue>> {
HashMap::from([("payload".to_string(), to_raw_value(&payload))])
-9
View File
@@ -1,9 +0,0 @@
use axum::routing::Router;
pub fn workspaced_service() -> Router {
Router::new()
}
pub fn global_service() -> Router {
Router::new()
}
+16
View File
@@ -0,0 +1,16 @@
#[cfg(feature = "private")]
#[allow(unused)]
pub use crate::git_sync_ee::*;
#[cfg(not(feature = "private"))]
use axum::routing::Router;
#[cfg(not(feature = "private"))]
pub fn workspaced_service() -> Router {
Router::new()
}
#[cfg(not(feature = "private"))]
pub fn global_service() -> Router {
Router::new()
}
+1 -1
View File
@@ -14,7 +14,7 @@ use axum::{
routing::{delete, get, post},
Json, Router,
};
use windmill_audit::audit_ee::audit_log;
use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::{
@@ -21,7 +21,7 @@ use crate::{
pub struct RawHttpTriggerArgs(pub RawWebhookArgs);
#[derive(Serialize, Deserialize, sqlx::Type, Debug, Clone, Hash, Eq, PartialEq)]
#[derive(Serialize, Deserialize, sqlx::Type, Debug, Clone, Copy, Hash, Eq, PartialEq)]
#[sqlx(type_name = "HTTP_METHOD", rename_all = "lowercase")]
#[serde(rename_all = "lowercase")]
pub enum HttpMethod {
@@ -441,8 +441,8 @@ pub struct BasicAuthAuthentication {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ApiKeyAuthentication {
api_key_header: String,
api_key_secret: String,
pub api_key_header: String,
pub api_key_secret: String,
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize)]
+242 -95
View File
@@ -1,7 +1,7 @@
#[cfg(feature = "http_trigger")]
use crate::http_trigger_args::{HttpMethod, RawHttpTriggerArgs};
#[cfg(feature = "parquet")]
use crate::job_helpers_ee::get_workspace_s3_resource;
use crate::job_helpers_oss::get_workspace_s3_resource;
use crate::resources::try_get_resource_from_db_as;
use crate::trigger_helpers::{get_runnable_format, RunnableId};
use crate::utils::{non_empty_str, ExpiringCacheEntry};
@@ -14,6 +14,7 @@ use crate::{
},
users::fetch_api_authed,
};
use anyhow::anyhow;
use axum::response::Response;
use axum::{
extract::{Path, Query},
@@ -28,13 +29,14 @@ use quick_cache::sync::Cache;
use serde::{Deserialize, Serialize};
use sql_builder::{bind::Bind, SqlBuilder};
use sqlx::prelude::FromRow;
use sqlx::PgTransaction;
use sqlx::PgConnection;
use std::borrow::Cow;
use std::collections::HashSet;
use std::{collections::HashMap, sync::Arc};
use tokio::sync::{RwLock, RwLockReadGuard};
use tower_http::cors::CorsLayer;
use windmill_audit::{audit_ee::audit_log, ActionKind};
use windmill_common::error::Error;
use windmill_audit::{audit_oss::audit_log, ActionKind};
use windmill_common::error::{Error, Result as WindmillResult};
#[cfg(feature = "parquet")]
use windmill_common::s3_helpers::build_object_store_client;
use windmill_common::{
@@ -42,7 +44,7 @@ use windmill_common::{
error::{self, JsonResult},
s3_helpers::S3Object,
triggers::TriggerKind,
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
utils::{empty_as_none, not_found_if_none, paginate, require_admin, Pagination, StripPath},
worker::CLOUD_HOSTED,
};
use windmill_git_sync::handle_deployment_metadata;
@@ -79,6 +81,7 @@ pub fn routes_global_service() -> Router {
pub fn workspaced_service() -> Router {
Router::new()
.route("/create", post(create_trigger))
.route("/create_many", post(create_many_http_trigger))
.route("/list", get(list_triggers))
.route("/get/*path", get(get_trigger))
.route("/update/*path", post(update_trigger))
@@ -111,6 +114,8 @@ struct NewTrigger {
static_asset_config: Option<sqlx::types::Json<S3Object>>,
http_method: HttpMethod,
workspaced_route: Option<bool>,
summary: Option<String>,
description: Option<String>,
is_static_website: bool,
wrap_body: Option<bool>,
raw_string: Option<bool>,
@@ -131,6 +136,8 @@ pub struct HttpTrigger {
pub is_async: bool,
pub authentication_method: AuthenticationMethod,
pub http_method: HttpMethod,
pub summary: Option<String>,
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub static_asset_config: Option<sqlx::types::Json<S3Object>>,
pub is_static_website: bool,
@@ -150,6 +157,8 @@ struct EditTrigger {
authentication_method: AuthenticationMethod,
#[serde(deserialize_with = "non_empty_str")]
authentication_resource_path: Option<String>,
summary: Option<String>,
description: Option<String>,
http_method: HttpMethod,
static_asset_config: Option<sqlx::types::Json<S3Object>>,
workspaced_route: Option<bool>,
@@ -164,6 +173,7 @@ pub struct ListTriggerQuery {
pub per_page: Option<usize>,
pub path: Option<String>,
pub is_flow: Option<bool>,
#[serde(default, deserialize_with = "empty_as_none")]
pub path_start: Option<String>,
}
@@ -185,6 +195,8 @@ async fn list_triggers(
"wrap_body",
"raw_string",
"script_path",
"summary",
"description",
"is_flow",
"http_method",
"edited_by",
@@ -239,6 +251,8 @@ async fn get_trigger(
route_path_key,
workspaced_route,
script_path,
summary,
description,
is_flow,
http_method as "http_method: _",
edited_by,
@@ -273,7 +287,7 @@ async fn get_trigger(
fn validate_authentication_method(
authentication_method: AuthenticationMethod,
raw_string: Option<bool>,
) -> error::Result<()> {
) -> WindmillResult<()> {
match (authentication_method, raw_string) {
(AuthenticationMethod::CustomScript, raw) if !raw.unwrap_or(false) == true => {
return Err(Error::BadRequest(
@@ -287,57 +301,21 @@ fn validate_authentication_method(
Ok(())
}
async fn increase_trigger_version_and_commit(mut tx: PgTransaction<'_>) -> error::Result<()> {
async fn increase_trigger_version(tx: &mut PgConnection) -> WindmillResult<()> {
sqlx::query!("SELECT nextval('http_trigger_version_seq')",)
.fetch_one(&mut *tx)
.fetch_one(tx)
.await?;
tx.commit().await?;
Ok(())
}
async fn create_trigger(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(ct): Json<NewTrigger>,
) -> error::Result<(StatusCode, String)> {
require_admin(authed.is_admin, &authed.username)?;
if !VALID_ROUTE_PATH_RE.is_match(&ct.route_path) {
return Err(error::Error::BadRequest("Invalid route path".to_string()));
}
validate_authentication_method(ct.authentication_method, ct.raw_string)?;
// route path key is extracted from the route path to check for uniqueness
// it replaces /?:{key} with :key
// it will also remove the leading / if present, not an issue as we only allow : after slashes
let route_path_key = ROUTE_PATH_KEY_RE.replace_all(&ct.route_path, ":key");
let exists = route_path_key_exists(
&route_path_key,
&ct.http_method,
&w_id,
None,
ct.workspaced_route,
&db,
)
.await?;
if exists {
return Err(error::Error::BadRequest(
"A route already exists with this path".to_string(),
));
}
if *CLOUD_HOSTED && (ct.is_static_website || ct.static_asset_config.is_some()) {
return Err(error::Error::BadRequest(
"Static website and static asset are not supported on cloud".to_string(),
));
}
let mut tx = user_db.begin(&authed).await?;
async fn create_trigger_inner(
tx: &mut PgConnection,
w_id: &str,
authed: &ApiAuthed,
new_http_trigger: &NewTrigger,
route_path_key: &str,
) -> WindmillResult<()> {
sqlx::query!(
r#"
INSERT INTO http_trigger (
@@ -350,6 +328,8 @@ async fn create_trigger(
wrap_body,
raw_string,
script_path,
summary,
description,
is_flow,
is_async,
authentication_method,
@@ -361,55 +341,209 @@ async fn create_trigger(
is_static_website
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, now(), $17
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), $19
)
"#,
w_id,
ct.path,
ct.route_path,
new_http_trigger.path,
new_http_trigger.route_path,
&route_path_key,
ct.workspaced_route,
ct.authentication_resource_path,
ct.wrap_body.unwrap_or(false),
ct.raw_string.unwrap_or(false),
ct.script_path,
ct.is_flow,
ct.is_async,
ct.authentication_method as _,
ct.http_method as _,
ct.static_asset_config as _,
new_http_trigger.workspaced_route,
new_http_trigger.authentication_resource_path,
new_http_trigger.wrap_body.unwrap_or(false),
new_http_trigger.raw_string.unwrap_or(false),
new_http_trigger.script_path,
new_http_trigger.summary,
new_http_trigger.description,
new_http_trigger.is_flow,
new_http_trigger.is_async,
new_http_trigger.authentication_method as _,
new_http_trigger.http_method as _,
new_http_trigger.static_asset_config as _,
&authed.username,
&authed.email,
ct.is_static_website
new_http_trigger.is_static_website
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed,
authed,
"http_triggers.create",
ActionKind::Create,
&w_id,
Some(ct.path.as_str()),
Some(new_http_trigger.path.as_str()),
None,
)
.await?;
increase_trigger_version_and_commit(tx).await?;
increase_trigger_version(tx).await?;
Ok(())
}
fn check_no_duplicates<'trigger>(
new_http_triggers: &[NewTrigger],
route_path_key: &[Cow<'trigger, str>],
) -> Result<(), Error> {
let mut seen = HashSet::with_capacity(new_http_triggers.len());
for (i, trigger) in new_http_triggers.iter().enumerate() {
if !seen.insert((
&route_path_key[i],
trigger.http_method,
trigger.workspaced_route,
)) {
return Err(Error::BadRequest(format!(
"Duplicate HTTP route detected: '{}'. Each HTTP route must have a unique 'route_path'.",
&trigger.route_path
)));
}
}
Ok(())
}
async fn create_many_http_trigger(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(new_http_triggers): Json<Vec<NewTrigger>>,
) -> WindmillResult<(StatusCode, String)> {
require_admin(authed.is_admin, &authed.username)?;
let error_wrapper = |path: &str, error: Error| -> Error {
anyhow!(
"Error occurred for HTTP route at route path: {}, error: {}",
path,
error
)
.into()
};
let mut route_path_keys = Vec::with_capacity(new_http_triggers.len());
for new_http_trigger in new_http_triggers.iter() {
let route_path_key = validate_http_trigger(&db, &w_id, new_http_trigger)
.await
.map_err(|err| error_wrapper(&new_http_trigger.route_path, err))?;
route_path_keys.push(route_path_key);
}
check_no_duplicates(&new_http_triggers, &route_path_keys)?;
let mut tx = user_db.begin(&authed).await?;
for (i, new_http_trigger) in new_http_triggers.iter().enumerate() {
create_trigger_inner(
&mut tx,
&w_id,
&authed,
new_http_trigger,
&route_path_keys[i],
)
.await
.map_err(|err| error_wrapper(&new_http_trigger.route_path, err))?;
}
tx.commit().await?;
for http_trigger in new_http_triggers.into_iter() {
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
windmill_git_sync::DeployedObject::HttpTrigger { path: http_trigger.path.clone() },
Some(format!("HTTP route '{}' created", http_trigger.path)),
true,
)
.await?;
}
Ok((StatusCode::CREATED, format!("Created all HTTP routes")))
}
async fn validate_http_trigger<'trigger>(
db: &DB,
w_id: &str,
new_http_trigger: &'trigger NewTrigger,
) -> WindmillResult<Cow<'trigger, str>> {
if !VALID_ROUTE_PATH_RE.is_match(&new_http_trigger.route_path) {
return Err(error::Error::BadRequest("Invalid route path".to_string()));
}
validate_authentication_method(
new_http_trigger.authentication_method,
new_http_trigger.raw_string,
)?;
// route path key is extracted from the route path to check for uniqueness
// it replaces /?:{key} with :key
// it will also remove the leading / if present, not an issue as we only allow : after slashes
let route_path_key = ROUTE_PATH_KEY_RE.replace_all(&new_http_trigger.route_path, ":key");
let exists = route_path_key_exists(
&route_path_key,
&new_http_trigger.http_method,
&w_id,
None,
new_http_trigger.workspaced_route,
db,
)
.await?;
if exists {
return Err(error::Error::BadRequest(
"A route already exists with this path".to_string(),
));
}
if *CLOUD_HOSTED
&& (new_http_trigger.is_static_website || new_http_trigger.static_asset_config.is_some())
{
return Err(error::Error::BadRequest(
"Static website and static asset are not supported on cloud".to_string(),
));
}
Ok(route_path_key)
}
async fn create_trigger(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(new_http_trigger): Json<NewTrigger>,
) -> WindmillResult<(StatusCode, String)> {
require_admin(authed.is_admin, &authed.username)?;
let route_path_key = validate_http_trigger(&db, &w_id, &new_http_trigger).await?;
let mut tx = user_db.begin(&authed).await?;
let http_trigger_path = new_http_trigger.path.clone();
create_trigger_inner(&mut tx, &w_id, &authed, &new_http_trigger, &route_path_key).await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
windmill_git_sync::DeployedObject::HttpTrigger { path: ct.path.clone() },
Some(format!("HTTP trigger '{}' created", ct.path)),
windmill_git_sync::DeployedObject::HttpTrigger { path: new_http_trigger.path.clone() },
Some(format!("HTTP route '{}' created", new_http_trigger.path)),
true,
)
.await?;
Ok((StatusCode::CREATED, format!("{}", ct.path)))
Ok((StatusCode::CREATED, format!("{}", http_trigger_path)))
}
async fn update_trigger(
@@ -418,8 +552,9 @@ async fn update_trigger(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ct): Json<EditTrigger>,
) -> error::Result<String> {
) -> WindmillResult<String> {
let path = path.to_path();
if *CLOUD_HOSTED && (ct.is_static_website || ct.static_asset_config.is_some()) {
return Err(error::Error::BadRequest(
"Static website and static asset are not supported on cloud".to_string(),
@@ -478,11 +613,13 @@ async fn update_trigger(
email = $13,
is_async = $14,
authentication_method = $15,
summary = $16,
description = $17,
edited_at = now(),
is_static_website = $16
is_static_website = $18
WHERE
workspace_id = $17 AND
path = $18
workspace_id = $19 AND
path = $20
"#,
route_path,
&route_path_key,
@@ -499,6 +636,8 @@ async fn update_trigger(
&authed.email,
ct.is_async,
ct.authentication_method as _,
ct.summary,
ct.description,
ct.is_static_website,
w_id,
path,
@@ -563,7 +702,9 @@ async fn update_trigger(
)
.await?;
increase_trigger_version_and_commit(tx).await?;
increase_trigger_version(&mut tx).await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
@@ -571,7 +712,7 @@ async fn update_trigger(
&db,
&w_id,
windmill_git_sync::DeployedObject::HttpTrigger { path: ct.path.clone() },
Some(format!("HTTP trigger '{}' updated", ct.path)),
Some(format!("HTTP route '{}' updated", ct.path)),
true,
)
.await?;
@@ -584,7 +725,7 @@ async fn delete_trigger(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> error::Result<String> {
) -> WindmillResult<String> {
require_admin(authed.is_admin, &authed.username)?;
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
@@ -609,7 +750,9 @@ async fn delete_trigger(
)
.await?;
increase_trigger_version_and_commit(tx).await?;
increase_trigger_version(&mut tx).await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
@@ -617,12 +760,12 @@ async fn delete_trigger(
&db,
&w_id,
windmill_git_sync::DeployedObject::HttpTrigger { path: path.to_string() },
Some(format!("HTTP trigger '{}' deleted", path)),
Some(format!("HTTP route '{}' deleted", path)),
true,
)
.await?;
Ok(format!("HTTP trigger {path} deleted"))
Ok(format!("HTTP route {path} deleted"))
}
async fn exists_trigger(
@@ -660,7 +803,7 @@ async fn route_path_key_exists(
trigger_path: Option<&str>,
workspaced_route: Option<bool>,
db: &DB,
) -> error::Result<bool> {
) -> WindmillResult<bool> {
let exists = if *CLOUD_HOSTED {
sqlx::query_scalar!(
r#"
@@ -861,7 +1004,7 @@ pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, Route
.insert(format!("{}/*wm_subpath", full_path), trigger.clone())
.unwrap_or_else(|e| {
tracing::warn!(
"Failed to consider http trigger route {}/*wm_subpath: {:?}",
"Failed to consider HTTP route {}/*wm_subpath: {:?}",
full_path,
e,
);
@@ -870,11 +1013,7 @@ pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, Route
router
.insert(full_path.clone(), trigger.clone())
.unwrap_or_else(|e| {
tracing::warn!(
"Failed to consider http trigger route {}: {:?}",
full_path,
e,
);
tracing::warn!("Failed to consider HTTP route {}: {:?}", full_path, e,);
});
}
@@ -898,7 +1037,7 @@ async fn get_http_route_trigger(
db: &DB,
user_db: UserDB,
method: &http::Method,
) -> error::Result<(TriggerRoute, String, HashMap<String, String>, ApiAuthed)> {
) -> WindmillResult<(TriggerRoute, String, HashMap<String, String>, ApiAuthed)> {
let http_method: HttpMethod = method.try_into()?;
let requested_path = format!("/{}", route_path);
@@ -947,11 +1086,11 @@ async fn get_http_route_trigger(
);
let exists = match HTTP_ACCESS_CACHE.get(&cache_key) {
Some(cache_entry) if cache_entry.expiry > std::time::Instant::now() => {
tracing::debug!("HTTP access cache hit for trigger {}", trigger.path);
tracing::debug!("HTTP access cache hit for route {}", trigger.path);
true
}
_ => {
tracing::debug!("HTTP access cache miss for trigger {}", trigger.path);
tracing::debug!("HTTP access cache miss for route {}", trigger.path);
let mut tx = user_db.begin(&authed).await?;
let exists = sqlx::query_scalar!(
r#"
@@ -1002,7 +1141,7 @@ async fn get_http_route_trigger(
trigger.email.clone(),
&trigger.workspace_id,
&db,
Some(username_override.unwrap_or(format!("http-{}", trigger.path))),
Some(username_override.unwrap_or(format!("HTTP-{}", trigger.path))),
)
.await?;
@@ -1030,6 +1169,14 @@ async fn route_job(
.await
.map_err(|e| e.into_response())?;
if trigger.script_path.is_empty() && trigger.static_asset_config.is_none() {
return Err(Error::NotFound(format!(
"Runnable path of HTTP route at path: {}",
trigger.path
))
.into_response());
}
let args = args
.process_args(
&authed,
@@ -1066,15 +1213,15 @@ async fn route_job(
let authentication_method = match HTTP_AUTH_CACHE.get(&cache_key) {
Some(cache_entry) if cache_entry.expiry > std::time::Instant::now() => {
tracing::debug!("HTTP auth method cache hit for trigger {}", trigger.path);
tracing::debug!("HTTP auth method cache hit for route {}", trigger.path);
cache_entry.value
}
_ => {
tracing::debug!("HTTP auth method cache miss for trigger {}", trigger.path);
tracing::debug!("HTTP auth method cache miss for route {}", trigger.path);
let auth_method = try_get_resource_from_db_as::<
crate::http_trigger_auth::AuthenticationMethod,
>(
authed.clone(),
&authed,
Some(user_db.clone()),
&db,
&resource_path,
-9
View File
@@ -1,9 +0,0 @@
use axum::Router;
pub fn workspaced_service() -> Router {
Router::new()
}
pub fn global_service() -> Router {
Router::new()
}
+16
View File
@@ -0,0 +1,16 @@
#[cfg(feature = "private")]
#[allow(unused)]
pub use crate::indexer_ee::*;
#[cfg(not(feature = "private"))]
use axum::Router;
#[cfg(not(feature = "private"))]
pub fn workspaced_service() -> Router {
Router::new()
}
#[cfg(not(feature = "private"))]
pub fn global_service() -> Router {
Router::new()
}

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