mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 00:00:46 +00:00
Merge remote-tracking branch 'origin/main' into di/data-tables
This commit is contained in:
@@ -67,13 +67,23 @@ jobs:
|
||||
- name: Substitute EE code (EE logic is behind feature flag)
|
||||
run: |
|
||||
./substitute_ee_code.sh --copy --dir ./windmill-ee-private
|
||||
- name: Cache DuckDB FFI module build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./backend/windmill-duckdb-ffi-internal/target
|
||||
key: ${{ runner.os }}-duckdb-ffi-${{ hashFiles('./backend/windmill-duckdb-ffi-internal/src/**/*.rs', './backend/windmill-duckdb-ffi-internal/Cargo.toml', './backend/windmill-duckdb-ffi-internal/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-duckdb-ffi-
|
||||
- name: cargo test
|
||||
timeout-minutes: 16
|
||||
run: deno --version && bun -v && go version && python3 --version &&
|
||||
SQLX_OFFLINE=true
|
||||
DATABASE_URL=postgres://postgres:changeme@localhost:5432/windmill
|
||||
DISABLE_EMBEDDING=true RUST_LOG=info RUST_LOG_STYLE=never
|
||||
DENO_PATH=$(which deno) BUN_PATH=$(which bun) GO_PATH=$(which go)
|
||||
UV_PATH=$(which uv) cargo test --features
|
||||
enterprise,deno_core,license,python,rust,scoped_cache,private --all --
|
||||
--nocapture
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
DATABASE_URL: postgres://postgres:changeme@localhost:5432/windmill
|
||||
DISABLE_EMBEDDING: true
|
||||
RUST_LOG: info
|
||||
RUST_LOG_STYLE: never
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: true
|
||||
run: |
|
||||
deno --version && bun -v && go version && python3 --version
|
||||
cd windmill-duckdb-ffi-internal && ./build_dev.sh && cd ..
|
||||
DENO_PATH=$(which deno) BUN_PATH=$(which bun) GO_PATH=$(which go) UV_PATH=$(which uv) cargo test --features enterprise,deno_core,duckdb,license,python,rust,scoped_cache,private --all -- --nocapture
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
name: Build and publish windmill for RHEL8
|
||||
on: workflow_dispatch
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
build_ee:
|
||||
runs-on: ubicloud
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Read EE repo commit hash
|
||||
run: |
|
||||
echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_ENV"
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: windmill-labs/windmill-ee-private
|
||||
path: ./windmill-ee-private
|
||||
ref: ${{ env.ee_repo_ref }}
|
||||
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
||||
fetch-depth: 0
|
||||
|
||||
# - name: Set up Docker Buildx
|
||||
# uses: docker/setup-buildx-action@v2
|
||||
- uses: depot/setup-action@v1
|
||||
|
||||
- name: Docker meta
|
||||
id: meta-ee-public
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee-rhel8
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=sha
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Substitute EE code
|
||||
run: |
|
||||
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
|
||||
|
||||
- name: Copy RHEL8 Dockerfile
|
||||
run: |
|
||||
cp ./docker/RHEL8/Dockerfile ./Dockerfile
|
||||
|
||||
- name: Build and push publicly ee amd64
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
context: .
|
||||
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,private
|
||||
secrets: |
|
||||
rh_username=${{ secrets.RH_USERNAME }}
|
||||
rh_password=${{ secrets.RH_PASSWORD }}
|
||||
tags: |
|
||||
${{ steps.meta-ee-public.outputs.tags }}-amd64
|
||||
labels: |
|
||||
${{ steps.meta-ee-public.outputs.labels }}-amd64
|
||||
org.opencontainers.image.licenses=Windmill-Enterprise-License
|
||||
|
||||
- name: Build and push publicly ee arm64
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
context: .
|
||||
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,private
|
||||
secrets: |
|
||||
rh_username=${{ secrets.RH_USERNAME }}
|
||||
rh_password=${{ secrets.RH_PASSWORD }}
|
||||
tags: |
|
||||
${{ steps.meta-ee-public.outputs.tags }}-arm64
|
||||
labels: |
|
||||
${{ steps.meta-ee-public.outputs.labels }}-arm64
|
||||
org.opencontainers.image.licenses=Windmill-Enterprise-License
|
||||
|
||||
- uses: shrink/actions-docker-extract@v3
|
||||
id: extract-ee-amd64
|
||||
with:
|
||||
image: ${{ steps.meta-ee-public.outputs.tags}}-amd64
|
||||
path: "/windmill/target/release/windmill"
|
||||
|
||||
- uses: shrink/actions-docker-extract@v3
|
||||
id: extract-duckdb-ffi-internal
|
||||
with:
|
||||
image: ${{ steps.meta-ee-public.outputs.tags}}-amd64
|
||||
path: "/usr/src/app/libwindmill_duckdb_ffi_internal.so"
|
||||
|
||||
# - uses: shrink/actions-docker-extract@v3
|
||||
# id: extract-ee-arm64
|
||||
# with:
|
||||
# image: ${{ steps.meta-ee-public.outputs.tags}}-arm64
|
||||
# path: "/windmill/target/release/windmill"
|
||||
|
||||
- name: Rename binary with corresponding architecture
|
||||
run: |
|
||||
mv "${{ steps.extract-ee-amd64.outputs.destination }}/windmill" "${{ steps.extract-ee-amd64.outputs.destination }}/windmill-ee-amd64-rhel8"
|
||||
# mv "${{ steps.extract-ee-arm64.outputs.destination }}/windmill" "${{ steps.extract-ee-arm64.outputs.destination }}/windmill-ee-arm64-rhel8"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: RHEL8-amd64 build
|
||||
path: ${{ steps.extract-ee-amd64.outputs.destination }}/windmill-ee-amd64-rhel8
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: RHEL8-amd64 dynamic libraries build
|
||||
path: ${{ steps.extract-duckdb-ffi-internal.outputs.destination }}/libwindmill_duckdb_ffi_internal.so
|
||||
|
||||
# - uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: RHEL8-arm64 build
|
||||
# path:
|
||||
# ${{ steps.extract-ee-arm64.outputs.destination
|
||||
# }}/windmill-ee-arm64-rhel8
|
||||
|
||||
# - name: Attach binary to release
|
||||
# uses: softprops/action-gh-release@v2
|
||||
# if: startsWith(github.ref, 'refs/tags/')
|
||||
# with:
|
||||
# files: |
|
||||
# ${{ steps.extract-ee-arm64.outputs.destination }}/windmill-ee-arm64-rhel8
|
||||
# ${{ steps.extract-ee-amd64.outputs.destination }}/windmill-ee-amd64-rhel8
|
||||
+144
@@ -1,5 +1,149 @@
|
||||
# Changelog
|
||||
|
||||
## [1.582.2](https://github.com/windmill-labs/windmill/compare/v1.582.1...v1.582.2) (2025-11-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix aws oidc refresh ([98bdb68](https://github.com/windmill-labs/windmill/commit/98bdb6825a1b85c973ddec3e6b933e4a3d6d6972))
|
||||
|
||||
## [1.582.1](https://github.com/windmill-labs/windmill/compare/v1.582.0...v1.582.1) (2025-11-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix aws oidc refresh ([a3b4cfc](https://github.com/windmill-labs/windmill/commit/a3b4cfcb8f11db326b0ebf1777ad7e6479425125))
|
||||
|
||||
## [1.582.0](https://github.com/windmill-labs/windmill/compare/v1.581.1...v1.582.0) (2025-11-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **aichat:** handle duckdb scripts ([#7187](https://github.com/windmill-labs/windmill/issues/7187)) ([ce5a318](https://github.com/windmill-labs/windmill/commit/ce5a31865cf6965ec28c449c2a832b93572a8eb6))
|
||||
* **ee:** support iamrds ([e9691c9](https://github.com/windmill-labs/windmill/commit/e9691c9eb080236849850a1ea6f3237ae39a2c4c))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **aichat:** fallback to completion if responses fails ([#7190](https://github.com/windmill-labs/windmill/issues/7190)) ([b56e611](https://github.com/windmill-labs/windmill/commit/b56e611700f06844dda4f30d02a1119e714d73a4))
|
||||
* **frontend:** show code/lock in flow steps on runs page ([#7191](https://github.com/windmill-labs/windmill/issues/7191)) ([338fd8a](https://github.com/windmill-labs/windmill/commit/338fd8a38cb035de298006ed1b96b6513eab9769))
|
||||
|
||||
## [1.581.1](https://github.com/windmill-labs/windmill/compare/v1.581.0...v1.581.1) (2025-11-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** missing node Result id migration ([#7182](https://github.com/windmill-labs/windmill/issues/7182)) ([054aeb3](https://github.com/windmill-labs/windmill/commit/054aeb33271288dc9458b012881164c3c4597280))
|
||||
|
||||
## [1.581.0](https://github.com/windmill-labs/windmill/compare/v1.580.0...v1.581.0) (2025-11-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** add notes to flow ([#6628](https://github.com/windmill-labs/windmill/issues/6628)) ([cfeb294](https://github.com/windmill-labs/windmill/commit/cfeb294308ba85763025f3628cbb85144d7f0778))
|
||||
|
||||
## [1.580.0](https://github.com/windmill-labs/windmill/compare/v1.579.2...v1.580.0) (2025-11-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **aichat:** use responses api for openai models ([#7163](https://github.com/windmill-labs/windmill/issues/7163)) ([5c79a35](https://github.com/windmill-labs/windmill/commit/5c79a35306855143428d0725519578aea0a746fd))
|
||||
* disabling/enabling email triggers ([#7171](https://github.com/windmill-labs/windmill/issues/7171)) ([8ae266b](https://github.com/windmill-labs/windmill/commit/8ae266b6a9ced16e1b7416cfc8bea5fe7a7af042))
|
||||
* **security:** unshare pid of worker job process ([#7106](https://github.com/windmill-labs/windmill/issues/7106)) ([5aa251a](https://github.com/windmill-labs/windmill/commit/5aa251a2d276cc9d27bf104f8e4f724ea6a28231))
|
||||
* support secondary promotion repos in git sync settings ([#7173](https://github.com/windmill-labs/windmill/issues/7173)) ([5548221](https://github.com/windmill-labs/windmill/commit/55482210921fe2eb0fd158abd4f7369495f2dfd7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* change uv tool dir from /root to /usr/local/uv ([c3e59fe](https://github.com/windmill-labs/windmill/commit/c3e59fe064fc3b9d4c05958eea54601ff3410899))
|
||||
* improve delete to handle ai chat ([f371fbe](https://github.com/windmill-labs/windmill/commit/f371fbeb9bb0946bd29a6413ee7ede75dedda5d9))
|
||||
* support IRSA for duckdb s3 proxy ([2058f27](https://github.com/windmill-labs/windmill/commit/2058f27e03468d45813f340b8563f935ca2142f4))
|
||||
|
||||
## [1.579.2](https://github.com/windmill-labs/windmill/compare/v1.579.1...v1.579.2) (2025-11-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ducklake manager table explorer issue ([d08c091](https://github.com/windmill-labs/windmill/commit/d08c0916f72a67f01e0c4475f03f9d1d33c10905))
|
||||
|
||||
## [1.579.1](https://github.com/windmill-labs/windmill/compare/v1.579.0...v1.579.1) (2025-11-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix s3 object download frontend freezes ([09a6e1f](https://github.com/windmill-labs/windmill/commit/09a6e1feaa79ce3f8548f8090fddbf46abb08b18))
|
||||
|
||||
## [1.579.0](https://github.com/windmill-labs/windmill/compare/v1.578.0...v1.579.0) (2025-11-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ai:** handle aws bedrock as provider ([#7155](https://github.com/windmill-labs/windmill/issues/7155)) ([79ac631](https://github.com/windmill-labs/windmill/commit/79ac6312e87afa3646bddc0f7e66fc4367dbff7c))
|
||||
* **mcp:** granular token scopes for scripts, flows, and endpoints ([#7130](https://github.com/windmill-labs/windmill/issues/7130)) ([88d04b9](https://github.com/windmill-labs/windmill/commit/88d04b9cbeee98f3256b78e9d34beb930cd729ec))
|
||||
* rhel8 + fix rhel9 ([#7165](https://github.com/windmill-labs/windmill/issues/7165)) ([499d7d4](https://github.com/windmill-labs/windmill/commit/499d7d4098758726a8cb2bf3e4837927b8fd70a4))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** worker count in latest worker usage ([#7160](https://github.com/windmill-labs/windmill/issues/7160)) ([b87d2cc](https://github.com/windmill-labs/windmill/commit/b87d2cc64cb54b602ee599fcde7f0fd3c8931550))
|
||||
* fix custom email triggers enabled ([#7164](https://github.com/windmill-labs/windmill/issues/7164)) ([90b5569](https://github.com/windmill-labs/windmill/commit/90b5569c911f9025b0e6b5318f57705efbd9bd17))
|
||||
|
||||
## [1.578.0](https://github.com/windmill-labs/windmill/compare/v1.577.0...v1.578.0) (2025-11-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* support to run windows binary as service ([#7153](https://github.com/windmill-labs/windmill/issues/7153)) ([ceeff5f](https://github.com/windmill-labs/windmill/commit/ceeff5f76c69d98319bb3fb7f7779b6046478d6b))
|
||||
|
||||
## [1.577.0](https://github.com/windmill-labs/windmill/compare/v1.576.3...v1.577.0) (2025-11-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for validateset in pwsh ([#7158](https://github.com/windmill-labs/windmill/issues/7158)) ([b66e038](https://github.com/windmill-labs/windmill/commit/b66e038a0f8b6bffe157a83671c8e692c1441f23))
|
||||
* allow http trigger to be disabled ([#6976](https://github.com/windmill-labs/windmill/issues/6976)) ([09082de](https://github.com/windmill-labs/windmill/commit/09082de53971d0d2f2a6308bc8ee573458a3b913))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* create app_themes/groups/components only when needed ([cf5d58e](https://github.com/windmill-labs/windmill/commit/cf5d58ea43cef6add3da2aa1e24efc83be6df3b9))
|
||||
* fix parse_postgres_uri not decoding password ([#7157](https://github.com/windmill-labs/windmill/issues/7157)) ([2cae72c](https://github.com/windmill-labs/windmill/commit/2cae72c9db6bd08689e1672be6dda32f6af831fb))
|
||||
|
||||
## [1.576.3](https://github.com/windmill-labs/windmill/compare/v1.576.2...v1.576.3) (2025-11-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* handle better alias types in duckdb ([2c04e04](https://github.com/windmill-labs/windmill/commit/2c04e04bf0e3272c89f321392158888d02a1191b))
|
||||
|
||||
## [1.576.2](https://github.com/windmill-labs/windmill/compare/v1.576.1...v1.576.2) (2025-11-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* temporary fix for duckdb type_aliases causing issues ([#7148](https://github.com/windmill-labs/windmill/issues/7148)) ([6426ebf](https://github.com/windmill-labs/windmill/commit/6426ebf8cb713443904065064b6a07eb1db0761a))
|
||||
|
||||
## [1.576.1](https://github.com/windmill-labs/windmill/compare/v1.576.0...v1.576.1) (2025-11-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* DuckDB FFI crash fix ([#7145](https://github.com/windmill-labs/windmill/issues/7145)) ([d3fc459](https://github.com/windmill-labs/windmill/commit/d3fc459b407682bf588236236916363d94f3e1ff))
|
||||
|
||||
## [1.576.0](https://github.com/windmill-labs/windmill/compare/v1.575.4...v1.576.0) (2025-11-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for switch and attributes in pwsh params ([#7143](https://github.com/windmill-labs/windmill/issues/7143)) ([c16bef8](https://github.com/windmill-labs/windmill/commit/c16bef8f296645ff873f9d8d28e3dcb50a65e304))
|
||||
* **ai:** handle aws bedrock as provider ([#7131](https://github.com/windmill-labs/windmill/issues/7131)) ([30eb9aa](https://github.com/windmill-labs/windmill/commit/30eb9aae25eeb563ad119ef93f3ff1ab17c66d75))
|
||||
* webhook by flow version ([#7062](https://github.com/windmill-labs/windmill/issues/7062)) ([09cdfb4](https://github.com/windmill-labs/windmill/commit/09cdfb4556748903dc5bbf53ef3356ac97c57d90))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use proper TLS connector for DuckLake instance catalog setup ([#7138](https://github.com/windmill-labs/windmill/issues/7138)) ([cf36fe3](https://github.com/windmill-labs/windmill/commit/cf36fe3bb1beec80fa84dc342a8a38cc7369bc4d))
|
||||
|
||||
## [1.575.4](https://github.com/windmill-labs/windmill/compare/v1.575.3...v1.575.4) (2025-11-13)
|
||||
|
||||
|
||||
|
||||
@@ -114,7 +114,10 @@ ARG WITH_GIT=true
|
||||
ARG LATEST_STABLE_PY=3.11.10
|
||||
ENV UV_PYTHON_INSTALL_DIR=/tmp/windmill/cache/py_runtime
|
||||
ENV UV_PYTHON_PREFERENCE=only-managed
|
||||
|
||||
RUN mkdir -p /usr/local/uv
|
||||
ENV UV_TOOL_BIN_DIR=/usr/local/bin
|
||||
ENV UV_TOOL_DIR=/usr/local/uv
|
||||
|
||||
ENV PATH /usr/local/bin:/root/.local/bin:$PATH
|
||||
|
||||
|
||||
@@ -13,4 +13,7 @@ rustflags = [
|
||||
"-C", "link-arg=-undefined",
|
||||
"-C", "link-arg=dynamic_lookup",
|
||||
"-C", "link-args=-Wl,-rpath,$ORIGIN/"
|
||||
]
|
||||
]
|
||||
|
||||
[net]
|
||||
git-fetch-with-cli = true
|
||||
-14
@@ -1,14 +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",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "045b26db0cefe6eaac0e572661d984ff5ce7086ac511e8647e2024d9dbe0af56"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM flow_conversation WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "08574e8e5dc165041750880fb02e7ffea83ae94a670b598b6dada0b3d0914629"
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE\n flow\n SET\n path = $1,\n summary = $2,\n description = $3,\n dependency_job = NULL,\n lock_error_logs = '',\n draft_only = NULL,\n tag = $4,\n dedicated_worker = $5,\n visible_to_runner_only = $6,\n on_behalf_of_email = $7,\n value = $8,\n schema = $9::text::json,\n edited_by = $10,\n edited_at = now()\n WHERE\n path = $11 AND workspace_id = $12",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "207a0721b6f0b8b6ddd4120343eba524a2bc1e9047bdde5f568af4d993dbb74c"
|
||||
}
|
||||
-14
@@ -1,14 +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",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "42baae3c69367bbb21771a9711c311afe67d339ca022ba61c2767c004b038ef0"
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow \n (workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at) \n SELECT workspace_id, $1, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at\n FROM flow\n WHERE path = $2 AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "544a00afb5c72d2aa24ab4ccfe68d1e383aeda9a5a71183b7fbaa41deca4e333"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM ai_agent_memory WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "56d3dccce81c652d6ab3d4ba69de89c4a5cb1fbc9a8b73e6b8ba4d6e74088352"
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n script_path, \n is_flow, \n workspace_id, \n edited_by, \n email, \n path, \n error_handler_path as \"error_handler_path: _\", \n error_handler_args as \"error_handler_args: _\", \n retry as \"retry: _\" \n FROM email_trigger \n WHERE local_part = $1 \n AND workspaced_local_part = FALSE\n ",
|
||||
"query": "\n SELECT \n script_path, \n is_flow, \n workspace_id, \n edited_by, \n email, \n path, \n error_handler_path as \"error_handler_path: _\", \n error_handler_args as \"error_handler_args: _\", \n retry as \"retry: _\" \n FROM email_trigger \n WHERE local_part = $1 \n AND workspaced_local_part = FALSE\n AND enabled IS TRUE\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -66,5 +66,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "668edc2f84eccf5db6b8daa3ed97e25e46a37c50a9d1a6eca5a277fb58a2e7b4"
|
||||
"hash": "628d303956d55d22e5ac64496b66010d4476d5c222184f09e5a189975a37b941"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"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",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6525e65ffe66643fed4db83aa81b525fc3ec54fdbca5d12647c1da88c9983b10"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow\n (workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at)\n SELECT workspace_id, $1, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at\n FROM flow\n WHERE path = $2 AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "676c758d9c4492dada50edd3ad06dc628e2ba56eab5e1a50c99481da9793759e"
|
||||
}
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO email_trigger (\n workspace_id,\n path,\n script_path,\n is_flow,\n local_part,\n workspaced_local_part,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, now(), $9, $10, $11\n )\n ",
|
||||
"query": "\n INSERT INTO email_trigger (\n workspace_id,\n path,\n script_path,\n is_flow,\n local_part,\n workspaced_local_part,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry,\n enabled\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, now(), $9, $10, $11, $12\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -15,10 +15,11 @@
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
"Jsonb",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d328d00e5393b0e8d2c4b3674221fabbae580fc404e0d4481fd8d7fb51272c94"
|
||||
"hash": "6c568509908c1833d9e6f58f739b8a186fc91f94d4fcc40eeb9914084e4ef60c"
|
||||
}
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE worker_ping SET ping_at = now(), current_job_id = $1, current_job_workspace_id = $2, memory_usage = $3, wm_memory_usage = $4,\n occupancy_rate = $6, occupancy_rate_15s = $7, occupancy_rate_5m = $8, occupancy_rate_30m = $9 WHERE worker = $5",
|
||||
"query": "UPDATE worker_ping SET ping_at = now(), current_job_id = $1, current_job_workspace_id = $2, memory_usage = $3, wm_memory_usage = $4,\n occupancy_rate = $6, occupancy_rate_15s = $7, occupancy_rate_5m = $8, occupancy_rate_30m = $9, job_isolation = $10 WHERE worker = $5",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -13,10 +13,11 @@
|
||||
"Float4",
|
||||
"Float4",
|
||||
"Float4",
|
||||
"Float4"
|
||||
"Float4",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "506066203c49424e9944eb3948dc1657d3d796e6233e9f0ec925879c705d4773"
|
||||
"hash": "76a7ad0588afcb4e9f0b876c50e203c36be56b2b2b08ca787417a9822ef56f64"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed,\n CASE WHEN $4 IS TRUE THEN current_job_id ELSE NULL END as last_job_id, CASE WHEN $4 IS TRUE THEN current_job_workspace_id ELSE NULL END as last_job_workspace_id, \n custom_tags, worker_group, wm_version, occupancy_rate, occupancy_rate_15s, occupancy_rate_5m, occupancy_rate_30m, memory, vcpus, memory_usage, wm_memory_usage\n FROM worker_ping\n WHERE ($1::integer IS NULL AND ping_at > now() - interval '5 minute') OR (ping_at > now() - ($1 || ' seconds')::interval)\n ORDER BY ping_at desc LIMIT $2 OFFSET $3",
|
||||
"query": "SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed,\n CASE WHEN $4 IS TRUE THEN current_job_id ELSE NULL END as last_job_id, CASE WHEN $4 IS TRUE THEN current_job_workspace_id ELSE NULL END as last_job_workspace_id,\n custom_tags, worker_group, wm_version, occupancy_rate, occupancy_rate_15s, occupancy_rate_5m, occupancy_rate_30m, memory, vcpus, memory_usage, wm_memory_usage, job_isolation\n FROM worker_ping\n WHERE ($1::integer IS NULL AND ping_at > now() - interval '5 minute') OR (ping_at > now() - ($1 || ' seconds')::interval)\n ORDER BY ping_at desc LIMIT $2 OFFSET $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -97,6 +97,11 @@
|
||||
"ordinal": 18,
|
||||
"name": "wm_memory_usage",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "job_isolation",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -126,8 +131,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "6a497334c98bfaf70be44fced572a1cc0dde4141aa4c5002765a95432d0101ab"
|
||||
"hash": "771a858a4b7ca41b6787e61f5a4a5c9c4d48fd213852e2f997cd4b2420580d30"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO unique_ext_jwt_token (jwt_hash, last_used_at)\n VALUES ($1, NOW())\n ON CONFLICT (jwt_hash)\n DO UPDATE SET last_used_at = NOW()",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "778ab8ceb2a84978919ceb07f399468e01c4bee4cd755322eb2a83353a279a2b"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"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",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "88d529fd26d6ecba7640c74daccb5cb0c7146c11adae8c287c0c9e71ac68f7a7"
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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 request_type,\n authentication_method,\n http_method,\n static_asset_config,\n edited_by,\n email,\n edited_at,\n is_static_website,\n error_handler_path,\n error_handler_args,\n retry\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), $19, $20, $21, $22\n )\n ",
|
||||
"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 enabled,\n request_type,\n authentication_method,\n http_method,\n static_asset_config,\n edited_by,\n email,\n edited_at,\n is_static_website,\n error_handler_path,\n error_handler_args,\n retry\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, now(), $20, $21, $22, $23\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -17,6 +17,7 @@
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "request_type",
|
||||
@@ -69,5 +70,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1f6b773ce34fe51d03d6f9a2345481629c62453eebbb08f82dd2da23389bc117"
|
||||
"hash": "94e2e899fc0a5134c29217feda1f0530d03548cc5c884c83d7b85d068e738b8e"
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE email_trigger \n SET \n script_path = $1,\n path = $2,\n is_flow = $3,\n edited_by = $4,\n email = $5,\n edited_at = now(),\n error_handler_path = $6,\n error_handler_args = $7,\n retry = $8\n WHERE \n workspace_id = $9 AND path = $10\n ",
|
||||
"query": "\n UPDATE email_trigger \n SET \n script_path = $1,\n path = $2,\n is_flow = $3,\n edited_by = $4,\n email = $5,\n edited_at = now(),\n error_handler_path = $6,\n error_handler_args = $7,\n retry = $8,\n enabled = $9\n WHERE \n workspace_id = $10 AND path = $11\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -13,11 +13,12 @@
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "acb094aef60bba9083087264d65034fce38417099f15e8312be72a386f10bc1f"
|
||||
"hash": "a704283ff62ac1cd6db489ca3f84d6994d75c6b9e5f61daf54d298a2fdcfd9af"
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE\n http_trigger\n SET\n wrap_body = $1,\n raw_string = $2,\n authentication_resource_path = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n http_method = $7,\n static_asset_config = $8,\n edited_by = $9,\n email = $10,\n request_type = $11,\n authentication_method = $12,\n summary = $13,\n description = $14,\n edited_at = now(),\n is_static_website = $15,\n error_handler_path = $16,\n error_handler_args = $17,\n retry = $18\n WHERE\n workspace_id = $19 AND\n path = $20\n ",
|
||||
"query": "\n UPDATE\n http_trigger\n SET\n wrap_body = $1,\n raw_string = $2,\n authentication_resource_path = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n enabled = $7,\n http_method = $8,\n static_asset_config = $9,\n edited_by = $10,\n email = $11,\n request_type = $12,\n authentication_method = $13,\n summary = $14,\n description = $15,\n edited_at = now(),\n is_static_website = $16,\n error_handler_path = $17,\n error_handler_args = $18,\n retry = $19\n WHERE\n workspace_id = $20 AND\n path = $21\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -11,6 +11,7 @@
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
@@ -67,5 +68,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4c64bd0e364f536597db83161b5a27ff58b5ec7148bc94807423593bcfa27b90"
|
||||
"hash": "ada1a14b4f25e41742df153b07e8bdf5b5feac9c6fa9de38bd24104bac8539d2"
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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 request_type = $14,\n authentication_method = $15,\n summary = $16,\n description = $17,\n edited_at = now(),\n is_static_website = $18,\n error_handler_path = $19,\n error_handler_args = $20,\n retry = $21\n WHERE\n workspace_id = $22 AND\n path = $23\n ",
|
||||
"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 enabled = $10,\n http_method = $11,\n static_asset_config = $12,\n edited_by = $13,\n email = $14,\n request_type = $15,\n authentication_method = $16,\n summary = $17,\n description = $18,\n edited_at = now(),\n is_static_website = $19,\n error_handler_path = $20,\n error_handler_args = $21,\n retry = $22\n WHERE\n workspace_id = $23 AND\n path = $24\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -14,6 +14,7 @@
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
@@ -70,5 +71,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "465144ea7e2930203618d9814a3e20c77b4363cf9e7c655d395f3fe40c247f61"
|
||||
"hash": "b8bcf1fef244395dad802174dfc9bff45ff6f9cd1298eae5de40218cbab2eb96"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n path,\n script_path,\n is_flow,\n route_path,\n authentication_resource_path,\n workspace_id,\n request_type AS \"request_type: _\",\n authentication_method AS \"authentication_method: _\",\n edited_by,\n email,\n static_asset_config AS \"static_asset_config: _\",\n wrap_body,\n raw_string,\n workspaced_route,\n is_static_website,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM\n http_trigger\n WHERE\n http_method = $1\n ",
|
||||
"query": "\n SELECT\n path,\n script_path,\n is_flow,\n route_path,\n authentication_resource_path,\n workspace_id,\n request_type AS \"request_type: _\",\n authentication_method AS \"authentication_method: _\",\n edited_by,\n email,\n static_asset_config AS \"static_asset_config: _\",\n wrap_body,\n raw_string,\n workspaced_route,\n is_static_website,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM\n http_trigger\n WHERE\n http_method = $1 AND\n enabled is TRUE\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -158,5 +158,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1301f873a829db137573b8b39449f6160f2adf44f864f26a99b8eab5818fbd50"
|
||||
"hash": "bbc96ae911d4ca0330582340e65c10e008e507610f095ff3936865101c9ba346"
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO usage (id, is_workspace, month_, usage)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $4",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Int4",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cabc79429825b5f4861e4ba2001a5f235f39558dee88cbb486d7ac988d0a44d1"
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO resource (workspace_id, path, value, description, resource_type, created_by, edited_at) VALUES ($1, 'f/app_themes/theme_0', '{\"name\": \"Default Theme\", \"value\": \"\"}', 'The default app theme', 'app_theme', $2, now()) ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cccf9e216d84dfbb801b92d697496f44d93b344190a26c8393867b3a268b9aba"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM unique_ext_jwt_token WHERE last_used_at > NOW() - INTERVAL '30 days'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "d2732640f09ec029025ebdd4de502c8adee995eac05b5051e480f0a20fa6b7bb"
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n flow \n SET\n path = $1,\n summary = $2,\n description = $3,\n dependency_job = NULL,\n lock_error_logs = '',\n draft_only = NULL,\n tag = $4,\n dedicated_worker = $5,\n visible_to_runner_only = $6,\n on_behalf_of_email = $7,\n value = $8,\n schema = $9::text::json,\n edited_by = $10,\n edited_at = now()\n WHERE \n path = $11 AND workspace_id = $12",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d2acc380c45df31f741bbd420c2f4a5b962f7803def9ecd6e33d2aec6abce772"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n script_path, \n is_flow, \n workspace_id, \n edited_by, \n email, \n path, \n error_handler_path as \"error_handler_path: _\", \n error_handler_args as \"error_handler_args: _\", \n retry as \"retry: _\" \n FROM email_trigger \n WHERE workspace_id = $1 \n AND local_part = $2 \n AND (workspaced_local_part = TRUE OR $3 IS TRUE)\n ",
|
||||
"query": "\n SELECT \n script_path, \n is_flow, \n workspace_id, \n edited_by, \n email, \n path, \n error_handler_path as \"error_handler_path: _\", \n error_handler_args as \"error_handler_args: _\", \n retry as \"retry: _\" \n FROM email_trigger \n WHERE workspace_id = $1 \n AND local_part = $2 \n AND (workspaced_local_part = TRUE OR $3 IS TRUE)\n AND enabled IS TRUE\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -68,5 +68,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bbd51f4f0a8bb2db5d6b634f2d32f5f7f7f57390a0e4b6e7cad45c9147fd6356"
|
||||
"hash": "df6972fb16a2364f10379fe37e125ae228697602e3769c479582470dc0b9488b"
|
||||
}
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO worker_ping (worker_instance, worker, ip, custom_tags, worker_group, dedicated_worker, wm_version, vcpus, memory) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (worker) \n DO UPDATE set ip = EXCLUDED.ip, custom_tags = EXCLUDED.custom_tags, worker_group = EXCLUDED.worker_group",
|
||||
"query": "INSERT INTO worker_ping (worker_instance, worker, ip, custom_tags, worker_group, dedicated_worker, wm_version, vcpus, memory, job_isolation) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (worker)\n DO UPDATE set ip = EXCLUDED.ip, custom_tags = EXCLUDED.custom_tags, worker_group = EXCLUDED.worker_group",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -13,10 +13,11 @@
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Int8"
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e5f8830450e90f678494ae2b0f86b0692a02dec4c8415fd7d364b0cc088905c2"
|
||||
"hash": "ed90b9fcc57f530bab2d2d7426795bb358c91ebdd9dab50d7e3d2fdce63b947c"
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE email_trigger \n SET \n script_path = $1,\n path = $2,\n is_flow = $3,\n local_part = $4,\n workspaced_local_part = $5,\n edited_by = $6,\n email = $7,\n edited_at = now(),\n error_handler_path = $8,\n error_handler_args = $9,\n retry = $10\n WHERE \n workspace_id = $11 AND path = $12\n ",
|
||||
"query": "\n UPDATE email_trigger \n SET \n script_path = $1,\n path = $2,\n is_flow = $3,\n local_part = $4,\n workspaced_local_part = $5,\n edited_by = $6,\n email = $7,\n edited_at = now(),\n error_handler_path = $8,\n error_handler_args = $9,\n retry = $10,\n enabled = $11\n WHERE \n workspace_id = $12 AND path = $13\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -15,11 +15,12 @@
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6fafc23924eded970689040bd4a94d4d23ebee4f2b7d37bb54c47edb7720be00"
|
||||
"hash": "fe12006498b9e7aece6b104fc5d9a80eeac12c6ca07a0e8950941ba796b0ba79"
|
||||
}
|
||||
@@ -10,3 +10,16 @@
|
||||
|
||||
1. Update database schema with migration if necessary
|
||||
2. Update backend/windmill-api/openapi.yaml after modifying API endpoints
|
||||
|
||||
## Querying the Database
|
||||
|
||||
To query the database directly, use psql with the following connection string:
|
||||
|
||||
```bash
|
||||
psql postgres://postgres:changeme@localhost:5432/windmill
|
||||
```
|
||||
|
||||
This can be helpful for:
|
||||
- Inspecting database state during development
|
||||
- Testing queries before implementing them in Rust
|
||||
- Debugging data-related issues
|
||||
|
||||
Generated
+190
-102
@@ -199,22 +199,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.4"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.10"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -788,9 +788,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-credential-types"
|
||||
version = "1.2.9"
|
||||
version = "1.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86590e57ea40121d47d3f2e131bfd873dea15d78dc2f4604f4734537ad9e56c4"
|
||||
checksum = "b01c9521fa01558f750d183c8c68c81b0155b9d193a4ba7f84c36bd1b6d04a06"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api",
|
||||
@@ -823,13 +823,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-runtime"
|
||||
version = "1.5.10"
|
||||
version = "1.5.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c034a1bc1d70e16e7f4e4caf7e9f7693e4c9c24cd91cf17c2a0b21abaebc7c8b"
|
||||
checksum = "7ce527fb7e53ba9626fc47824f25e256250556c40d8f81d27dd92aa38239d632"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-sigv4",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
@@ -845,6 +846,31 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-bedrockruntime"
|
||||
version = "1.113.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5d2b8f081b9e8ff455b8dd7387b6b02263c3dac73172d188d2b523ff1e775e9"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-sigv4",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"bytes",
|
||||
"fastrand",
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.32",
|
||||
"regex-lite",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-config"
|
||||
version = "1.68.0"
|
||||
@@ -868,6 +894,31 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-rds"
|
||||
version = "1.117.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56b116d0229c41d5b72050dda07aa2cc7d1c571de8f7870c8b87fa297a724982"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-sigv4",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-query",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"aws-smithy-xml",
|
||||
"aws-types",
|
||||
"fastrand",
|
||||
"http 0.2.12",
|
||||
"regex-lite",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sqs"
|
||||
version = "1.77.0"
|
||||
@@ -964,6 +1015,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c35452ec3f001e1f2f6db107b6373f1f48f05ec63ba2c5c9fa91f07dad32af11"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
@@ -990,12 +1042,24 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-eventstream"
|
||||
version = "0.60.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e29a304f8319781a39808847efb39561351b1bb76e933da7aa90232673638658"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
"crc32fast",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http"
|
||||
version = "0.62.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "445d5d720c99eed0b4aa674ed00d835d9b1427dd73e04adaf2f94c6b2d6f9fca"
|
||||
dependencies = [
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
@@ -1013,9 +1077,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http-client"
|
||||
version = "1.0.6"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f108f1ca850f3feef3009bdcc977be201bca9a91058864d9de0684e64514bee0"
|
||||
checksum = "623254723e8dfd535f566ee7b2381645f8981da086b5c4aa26c0c41582bb1d2c"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api",
|
||||
@@ -1026,16 +1090,17 @@ dependencies = [
|
||||
"http 1.3.1",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.32",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-rustls 0.24.2",
|
||||
"hyper-rustls 0.27.7",
|
||||
"hyper-util",
|
||||
"pin-project-lite",
|
||||
"rustls 0.21.12",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.35",
|
||||
"rustls-native-certs 0.8.2",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tower 0.5.2",
|
||||
"tracing",
|
||||
]
|
||||
@@ -1070,9 +1135,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime"
|
||||
version = "1.8.6"
|
||||
version = "1.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e107ce0783019dbff59b3a244aa0c114e4a8c9d93498af9162608cd5474e796"
|
||||
checksum = "0bbe9d018d646b96c7be063dd07987849862b0e6d07c778aad7d93d1be6c1ef0"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
@@ -1182,7 +1247,7 @@ dependencies = [
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
@@ -1569,7 +1634,7 @@ dependencies = [
|
||||
"hex",
|
||||
"http 1.3.1",
|
||||
"http-body-util",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-named-pipe",
|
||||
"hyper-util",
|
||||
"hyperlocal",
|
||||
@@ -1799,9 +1864,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -1945,9 +2010,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.45"
|
||||
version = "1.2.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe"
|
||||
checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -2054,9 +2119,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.51"
|
||||
version = "4.5.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
|
||||
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -2064,9 +2129,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.51"
|
||||
version = "4.5.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
|
||||
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -3625,7 +3690,7 @@ dependencies = [
|
||||
"hickory-resolver",
|
||||
"http 1.3.1",
|
||||
"http-body-util",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-rustls 0.27.7",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
@@ -3713,7 +3778,7 @@ dependencies = [
|
||||
"http 1.3.1",
|
||||
"httparse",
|
||||
"hyper 0.14.32",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-util",
|
||||
"itertools 0.10.5",
|
||||
"memmem",
|
||||
@@ -3906,7 +3971,7 @@ dependencies = [
|
||||
"hkdf",
|
||||
"http 1.3.1",
|
||||
"http-body-util",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-util",
|
||||
"idna",
|
||||
"indexmap 2.11.1",
|
||||
@@ -4177,7 +4242,7 @@ dependencies = [
|
||||
"http 1.3.1",
|
||||
"http-body-util",
|
||||
"hyper 0.14.32",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-util",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -4231,7 +4296,7 @@ dependencies = [
|
||||
"deno_error",
|
||||
"deno_tls",
|
||||
"http-body-util",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-rustls 0.27.7",
|
||||
"hyper-util",
|
||||
"log",
|
||||
@@ -4266,7 +4331,7 @@ dependencies = [
|
||||
"deno_core",
|
||||
"deno_error",
|
||||
"deno_native_certs",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.35",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"rustls-tokio-stream",
|
||||
"rustls-webpki 0.102.8",
|
||||
@@ -4361,7 +4426,7 @@ dependencies = [
|
||||
"h2 0.4.12",
|
||||
"http 1.3.1",
|
||||
"http-body-util",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-util",
|
||||
"once_cell",
|
||||
"rustls-tokio-stream",
|
||||
@@ -5225,7 +5290,7 @@ dependencies = [
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-util",
|
||||
"pin-project",
|
||||
"rand 0.8.5",
|
||||
@@ -5292,9 +5357,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
|
||||
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
@@ -6262,9 +6327,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.0"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "hashify"
|
||||
@@ -6604,9 +6669,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.8.0"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f"
|
||||
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -6635,7 +6700,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"headers",
|
||||
"http 1.3.1",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-rustls 0.27.7",
|
||||
"hyper-util",
|
||||
"pin-project-lite",
|
||||
@@ -6652,7 +6717,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
@@ -6683,10 +6748,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||
dependencies = [
|
||||
"http 1.3.1",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-util",
|
||||
"log",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.35",
|
||||
"rustls-native-certs 0.8.2",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
@@ -6701,7 +6766,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
|
||||
dependencies = [
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
@@ -6729,7 +6794,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
@@ -6750,7 +6815,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
@@ -6771,7 +6836,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"http-body-util",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
@@ -7419,7 +7484,7 @@ dependencies = [
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-http-proxy",
|
||||
"hyper-rustls 0.27.7",
|
||||
"hyper-timeout",
|
||||
@@ -7428,7 +7493,7 @@ dependencies = [
|
||||
"k8s-openapi",
|
||||
"kube-core",
|
||||
"pem 3.0.5",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.35",
|
||||
"secrecy",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -7913,7 +7978,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"gethostname",
|
||||
"mail-builder",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.35",
|
||||
"rustls-pki-types",
|
||||
"smtp-proto",
|
||||
"tokio",
|
||||
@@ -8946,7 +9011,7 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"httparse",
|
||||
"humantime",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"itertools 0.14.0",
|
||||
"md-5 0.10.6",
|
||||
"parking_lot 0.12.5",
|
||||
@@ -10246,7 +10311,7 @@ checksum = "7ada44a88ef953a3294f6eb55d2007ba44646015e18613d2f213016379203ef3"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"equivalent",
|
||||
"hashbrown 0.16.0",
|
||||
"hashbrown 0.16.1",
|
||||
"parking_lot 0.12.5",
|
||||
]
|
||||
|
||||
@@ -10262,7 +10327,7 @@ dependencies = [
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.35",
|
||||
"socket2 0.6.1",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
@@ -10282,7 +10347,7 @@ dependencies = [
|
||||
"rand 0.9.0",
|
||||
"ring 0.17.14",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.35",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.17",
|
||||
@@ -10712,7 +10777,7 @@ dependencies = [
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-rustls 0.27.7",
|
||||
"hyper-tls 0.6.0",
|
||||
"hyper-util",
|
||||
@@ -10724,7 +10789,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.35",
|
||||
"rustls-native-certs 0.8.2",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
@@ -10772,7 +10837,7 @@ dependencies = [
|
||||
"futures",
|
||||
"getrandom 0.2.16",
|
||||
"http 1.3.1",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"parking_lot 0.11.2",
|
||||
"reqwest 0.12.24",
|
||||
"reqwest-middleware",
|
||||
@@ -10785,9 +10850,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "resolv-conf"
|
||||
version = "0.7.5"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799"
|
||||
checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"
|
||||
|
||||
[[package]]
|
||||
name = "retry-policies"
|
||||
@@ -11146,9 +11211,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.29"
|
||||
version = "0.23.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1"
|
||||
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
@@ -11232,7 +11297,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22557157d7395bc30727745b365d923f1ecc230c4c80b176545f3f4f08c46e33"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.35",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
]
|
||||
@@ -12297,7 +12362,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.35",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
@@ -13771,7 +13836,7 @@ version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.35",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -13945,7 +14010,7 @@ dependencies = [
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-timeout",
|
||||
"hyper-util",
|
||||
"percent-encoding",
|
||||
@@ -14603,7 +14668,7 @@ dependencies = [
|
||||
"log",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.35",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -15148,7 +15213,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"aws-sdk-config",
|
||||
@@ -15177,7 +15242,7 @@ dependencies = [
|
||||
"quote",
|
||||
"rand 0.9.0",
|
||||
"reqwest 0.12.24",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.35",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yml",
|
||||
@@ -15204,11 +15269,13 @@ dependencies = [
|
||||
"windmill-indexer",
|
||||
"windmill-queue",
|
||||
"windmill-worker",
|
||||
"windows-service",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -15251,7 +15318,7 @@ dependencies = [
|
||||
"hf-hub",
|
||||
"hmac",
|
||||
"http 1.3.1",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"indexmap 2.11.1",
|
||||
"itertools 0.14.0",
|
||||
"jsonwebtoken 8.3.0",
|
||||
@@ -15281,7 +15348,7 @@ dependencies = [
|
||||
"rumqttc",
|
||||
"rust-embed",
|
||||
"rust_decimal",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.35",
|
||||
"samael",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -15329,7 +15396,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -15344,7 +15411,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -15358,7 +15425,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -15377,14 +15444,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"aws-config",
|
||||
"aws-credential-types",
|
||||
"aws-sdk-rds",
|
||||
"aws-sdk-sts",
|
||||
"aws-smithy-types",
|
||||
"aws-smithy-types-convert",
|
||||
"axum",
|
||||
"backon",
|
||||
@@ -15405,7 +15475,7 @@ dependencies = [
|
||||
"globset",
|
||||
"hex",
|
||||
"hmac",
|
||||
"hyper 1.8.0",
|
||||
"hyper 1.8.1",
|
||||
"indexmap 2.11.1",
|
||||
"itertools 0.14.0",
|
||||
"jsonwebtoken 8.3.0",
|
||||
@@ -15452,6 +15522,7 @@ dependencies = [
|
||||
"tracing-opentelemetry",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"windmill-macros",
|
||||
"windmill-parser",
|
||||
@@ -15462,7 +15533,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -15477,7 +15548,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -15501,7 +15572,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -15513,7 +15584,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -15522,7 +15593,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15534,7 +15605,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15546,7 +15617,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -15558,7 +15629,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15570,7 +15641,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15582,7 +15653,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -15593,7 +15664,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15604,7 +15675,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15616,7 +15687,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15639,7 +15710,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15653,7 +15724,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -15670,7 +15741,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15684,7 +15755,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15702,7 +15773,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -15713,7 +15784,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15722,12 +15793,14 @@ dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"cron",
|
||||
"dashmap 6.1.0",
|
||||
"futures",
|
||||
"futures-core",
|
||||
"hex",
|
||||
"hmac",
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
"once_cell",
|
||||
"prometheus",
|
||||
"quick_cache",
|
||||
"regex",
|
||||
@@ -15748,7 +15821,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -15758,13 +15831,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
"async-recursion",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"aws-config",
|
||||
"aws-credential-types",
|
||||
"aws-sdk-bedrockruntime",
|
||||
"aws-smithy-types",
|
||||
"backon",
|
||||
"base64 0.22.1",
|
||||
"bit-vec 0.6.3",
|
||||
@@ -16088,6 +16165,17 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-service"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"widestring",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.2"
|
||||
@@ -16540,18 +16628,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.27"
|
||||
version = "0.8.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
|
||||
checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.27"
|
||||
version = "0.8.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
|
||||
checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
+12
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -33,7 +33,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.575.4"
|
||||
version = "1.582.2"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -161,6 +161,10 @@ nom.workspace = true
|
||||
globset.workspace = true
|
||||
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-service = "0.7"
|
||||
windows-sys = { version = "0.52", features = ["Win32_System_Services", "Win32_System_Console", "Win32_Foundation"] }
|
||||
|
||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
tikv-jemallocator = { optional = true, workspace = true }
|
||||
tikv-jemalloc-sys = { optional = true, workspace = true }
|
||||
@@ -264,6 +268,7 @@ regex = "^1"
|
||||
semver = "^1"
|
||||
aws-sigv4 = "^1.3.4"
|
||||
aws-sdk-config = "=1.68.0"
|
||||
aws-sdk-rds = "^1"
|
||||
async-trait = "0.1.88"
|
||||
|
||||
|
||||
@@ -328,6 +333,7 @@ dyn-iter = "0.2.0"
|
||||
rsa = "^0"
|
||||
async_zip = { version = "0.0.17", features = ["tokio", "tokio-fs", "deflate", "chrono"] }
|
||||
once_cell = "1.17.1"
|
||||
dashmap = "6.1.0"
|
||||
gosyn = "0.2.6"
|
||||
bytes = "1.4.0"
|
||||
gethostname = "0.4.3"
|
||||
@@ -380,11 +386,14 @@ datafusion = "47.0.0"
|
||||
object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7", features = ["aws", "azure", "gcp"] }
|
||||
openidconnect = { version = "4.0.0-rc.1" }
|
||||
aws-config = "^1"
|
||||
aws-sdk-bedrockruntime = "=1.113.0"
|
||||
aws-credential-types = "^1"
|
||||
aws-smithy-types = "^1"
|
||||
aws-sdk-sqs = "=1.77.0"
|
||||
aws-sdk-sts = "=1.79.0"
|
||||
aws-sdk-sso = "=1.77.0"
|
||||
aws-sdk-ssooidc = "=1.78.0"
|
||||
rustls = "=0.23.29"
|
||||
rustls = "=0.23.35"
|
||||
async-once-cell = "0.5.4"
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
31ee9d3449f05cd0328c0fcf43e2b161dee767b9
|
||||
c90bfc11c7c643b2c8c5111f092e7ae142318e9d
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE http_trigger DROP COLUMN enabled;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE http_trigger ADD COLUMN enabled BOOLEAN DEFAULT TRUE NOT NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Rollback: Remove job_isolation column from worker_ping table
|
||||
ALTER TABLE worker_ping DROP COLUMN IF EXISTS job_isolation;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Add job_isolation column to worker_ping table
|
||||
-- This tracks which job isolation method the worker is using: 'nsjail', 'unshare', or 'none'
|
||||
-- Nullable for backwards compatibility - old workers will report NULL
|
||||
ALTER TABLE worker_ping ADD COLUMN job_isolation TEXT;
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add up migration script here
|
||||
|
||||
ALTER TABLE email_trigger ADD COLUMN enabled BOOLEAN DEFAULT TRUE NOT NULL;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add down migration script here
|
||||
|
||||
DROP TABLE IF EXISTS unique_ext_jwt_token;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Add up migration script here
|
||||
|
||||
CREATE TABLE IF NOT EXISTS unique_ext_jwt_token (
|
||||
jwt_hash BIGINT PRIMARY KEY NOT NULL,
|
||||
last_used_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_unique_ext_jwt_token_last_used_at ON unique_ext_jwt_token(last_used_at);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run the command repeatedly until it fails (exits with non-zero code)
|
||||
cd windmill-duckdb-ffi-internal && ./build_dev.sh && cd ..
|
||||
while true; do
|
||||
DISABLE_EMBEDDING=true \
|
||||
RUST_LOG=info \
|
||||
@@ -9,7 +10,7 @@ while true; do
|
||||
GO_PATH=$(which go) \
|
||||
UV_PATH=$(which uv) \
|
||||
CARGO_PATH=$(which cargo) \
|
||||
cargo test --features enterprise,deno_core,license,python,rust,scoped_cache \
|
||||
cargo test --features enterprise,deno_core,license,python,duckdb,rust,scoped_cache \
|
||||
-- --nocapture --test-threads=8 | tee /tmp/test.log
|
||||
|
||||
# Capture the exit code of the cargo test command (not tee)
|
||||
|
||||
+28
-5
@@ -113,6 +113,10 @@ pub mod ee;
|
||||
mod ee_oss;
|
||||
mod monitor;
|
||||
|
||||
// Windows service support - EE feature
|
||||
#[cfg(all(windows, feature = "enterprise", feature = "private"))]
|
||||
mod windows_service_ee;
|
||||
|
||||
pub fn setup_deno_runtime() -> anyhow::Result<()> {
|
||||
// https://github.com/denoland/deno/blob/main/cli/main.rs#L477
|
||||
#[cfg(feature = "deno_core")]
|
||||
@@ -207,6 +211,17 @@ lazy_static::lazy_static! {
|
||||
}
|
||||
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
// On Windows with enterprise feature, check if running as a service
|
||||
#[cfg(all(windows, feature = "enterprise", feature = "private"))]
|
||||
{
|
||||
if windows_service_ee::is_running_as_service() {
|
||||
// Run as Windows service with SCM handlers
|
||||
return windows_service_ee::run_as_windows_service()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to run as Windows service: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// Normal execution (console/foreground mode)
|
||||
setup_deno_runtime()?;
|
||||
create_and_run_current_thread_inner(windmill_main())
|
||||
}
|
||||
@@ -496,7 +511,14 @@ async fn windmill_main() -> anyhow::Result<()> {
|
||||
conn
|
||||
} else {
|
||||
// This time we use a pool of connections
|
||||
let db = windmill_common::connect_db(server_mode, indexer_mode, worker_mode).await?;
|
||||
let db = windmill_common::connect_db(
|
||||
server_mode,
|
||||
indexer_mode,
|
||||
worker_mode,
|
||||
#[cfg(feature = "private")]
|
||||
killpill_rx.resubscribe(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// NOTE: Variable/resource cache initialization moved to API server in windmill-api
|
||||
|
||||
@@ -817,10 +839,10 @@ Windmill Community Edition {GIT_VERSION}
|
||||
match conn {
|
||||
Connection::Sql(ref db) => {
|
||||
let base_internal_url = base_internal_url.to_string();
|
||||
let db_url: String = get_database_url().await?;
|
||||
let db_url = get_database_url().await?;
|
||||
let db = db.clone();
|
||||
let h = tokio::spawn(async move {
|
||||
let mut listener = retry_listen_pg(&db_url).await;
|
||||
let mut listener = retry_listen_pg(&db_url.as_str().await).await;
|
||||
let mut last_listener_refresh = Instant::now();
|
||||
let mut monitor_iteration: u64 = 0;
|
||||
let rd_shift: u8 = rand::rng().random_range(0..200);
|
||||
@@ -1154,13 +1176,14 @@ Windmill Community Edition {GIT_VERSION}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Could not receive notification, attempting to reconnect listener");
|
||||
let db_url = db_url.clone();
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = monitor_killpill_rx.recv() => {
|
||||
tracing::info!("received killpill for monitor job");
|
||||
break;
|
||||
},
|
||||
new_listener = retry_listen_pg(&db_url) => {
|
||||
new_listener = async move { retry_listen_pg(&db_url.as_str().await).await } => {
|
||||
listener = new_listener;
|
||||
continue;
|
||||
}
|
||||
@@ -1174,7 +1197,7 @@ Windmill Community Edition {GIT_VERSION}
|
||||
if let Err(e) = listener.unlisten_all().await {
|
||||
tracing::error!(error = %e, "Could not unlisten to database");
|
||||
}
|
||||
listener = retry_listen_pg(&db_url).await;
|
||||
listener = retry_listen_pg(&db_url.as_str().await).await;
|
||||
initial_load(
|
||||
&conn,
|
||||
tx.clone(),
|
||||
|
||||
@@ -2944,3 +2944,33 @@ async fn test_workflow_as_code(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "duckdb")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_duckdb_ffi(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
|
||||
let content = "-- result_collection=last_statement_first_row_scalar\nSELECT 'Hello world!';";
|
||||
|
||||
let flow: FlowValue = serde_json::from_value(serde_json::json!({
|
||||
"modules": [{
|
||||
"value": {
|
||||
"type": "rawscript",
|
||||
"language": "duckdb",
|
||||
"content": content,
|
||||
},
|
||||
}],
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let result =
|
||||
RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None, restarted_from: None })
|
||||
.run_until_complete(&db, false, server.addr.port())
|
||||
.await
|
||||
.json_result()
|
||||
.unwrap();
|
||||
assert_eq!(result, serde_json::json!("Hello world!"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.575.4
|
||||
version: 1.582.2
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -572,12 +572,12 @@ paths:
|
||||
use_case:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: Onboarding data submitted successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
type: string
|
||||
|
||||
/w/{workspace}/users/delete/{username}:
|
||||
delete:
|
||||
@@ -10163,6 +10163,34 @@ paths:
|
||||
schema:
|
||||
type: boolean
|
||||
|
||||
/w/{workspace}/http_triggers/setenabled/{path}:
|
||||
post:
|
||||
summary: enable/disable http trigger
|
||||
operationId: setHttpTriggerEnabled
|
||||
tags:
|
||||
- http_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
required:
|
||||
- enabled
|
||||
responses:
|
||||
"200":
|
||||
description: http trigger enable/disable
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/websocket_triggers/create:
|
||||
post:
|
||||
summary: create websocket trigger
|
||||
@@ -11970,6 +11998,33 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: boolean
|
||||
/w/{workspace}/email_triggers/setenabled/{path}:
|
||||
post:
|
||||
summary: enable/disable email trigger
|
||||
operationId: setEmailTriggerEnabled
|
||||
tags:
|
||||
- email_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
required:
|
||||
- enabled
|
||||
responses:
|
||||
"200":
|
||||
description: email trigger enable/disable
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/groups/list:
|
||||
get:
|
||||
@@ -15085,7 +15140,7 @@ components:
|
||||
schema:
|
||||
type: integer
|
||||
JobTriggerKind:
|
||||
name: trigger_kind
|
||||
name: trigger_kind
|
||||
description: trigger kind (schedule, http, websocket...)
|
||||
in: query
|
||||
schema:
|
||||
@@ -15266,8 +15321,7 @@ components:
|
||||
|
||||
CreatedAfterQueue:
|
||||
name: created_after_queue
|
||||
description:
|
||||
filter on jobs created after X for jobs in the queue only
|
||||
description: filter on jobs created after X for jobs in the queue only
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
@@ -15275,8 +15329,7 @@ components:
|
||||
|
||||
CreatedBeforeQueue:
|
||||
name: created_before_queue
|
||||
description:
|
||||
filter on jobs created before X for jobs in the queue only
|
||||
description: filter on jobs created before X for jobs in the queue only
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
@@ -15457,6 +15510,8 @@ components:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/FlowStatus"
|
||||
FlowStatusModule:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/FlowStatusModule"
|
||||
FlowNote:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/FlowNote"
|
||||
# -- INLINE END --
|
||||
# Do not change line above
|
||||
|
||||
@@ -15571,6 +15626,7 @@ components:
|
||||
groq,
|
||||
openrouter,
|
||||
togetherai,
|
||||
aws_bedrock,
|
||||
customai,
|
||||
]
|
||||
|
||||
@@ -16722,30 +16778,30 @@ components:
|
||||
ScriptLang:
|
||||
type: string
|
||||
enum: [
|
||||
python3,
|
||||
deno,
|
||||
go,
|
||||
bash,
|
||||
powershell,
|
||||
postgresql,
|
||||
mysql,
|
||||
bigquery,
|
||||
snowflake,
|
||||
mssql,
|
||||
oracledb,
|
||||
graphql,
|
||||
nativets,
|
||||
bun,
|
||||
php,
|
||||
rust,
|
||||
ansible,
|
||||
csharp,
|
||||
nu,
|
||||
java,
|
||||
ruby,
|
||||
duckdb,
|
||||
# for related places search: ADD_NEW_LANG
|
||||
]
|
||||
python3,
|
||||
deno,
|
||||
go,
|
||||
bash,
|
||||
powershell,
|
||||
postgresql,
|
||||
mysql,
|
||||
bigquery,
|
||||
snowflake,
|
||||
mssql,
|
||||
oracledb,
|
||||
graphql,
|
||||
nativets,
|
||||
bun,
|
||||
php,
|
||||
rust,
|
||||
ansible,
|
||||
csharp,
|
||||
nu,
|
||||
java,
|
||||
ruby,
|
||||
duckdb,
|
||||
# for related places search: ADD_NEW_LANG
|
||||
]
|
||||
|
||||
Preview:
|
||||
type: object
|
||||
@@ -17222,6 +17278,8 @@ components:
|
||||
format: date-time
|
||||
is_flow:
|
||||
type: boolean
|
||||
enabled:
|
||||
type: boolean
|
||||
required:
|
||||
- path
|
||||
- script_path
|
||||
@@ -17231,6 +17289,7 @@ components:
|
||||
- edited_by
|
||||
- edited_at
|
||||
- is_flow
|
||||
- enabled
|
||||
|
||||
AuthenticationMethod:
|
||||
type: string
|
||||
@@ -17456,6 +17515,8 @@ components:
|
||||
type: boolean
|
||||
wrap_body:
|
||||
type: boolean
|
||||
enabled:
|
||||
type: boolean
|
||||
raw_string:
|
||||
type: boolean
|
||||
error_handler_path:
|
||||
@@ -17581,8 +17642,6 @@ components:
|
||||
format: date-time
|
||||
error:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
filters:
|
||||
type: array
|
||||
items:
|
||||
@@ -17613,7 +17672,6 @@ components:
|
||||
|
||||
required:
|
||||
- url
|
||||
- enabled
|
||||
- filters
|
||||
- can_return_message
|
||||
- can_return_error_result
|
||||
@@ -17802,17 +17860,13 @@ components:
|
||||
format: date-time
|
||||
error:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
error_handler_path:
|
||||
type: string
|
||||
error_handler_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
retry:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
|
||||
|
||||
required:
|
||||
- enabled
|
||||
- subscribe_topics
|
||||
- mqtt_resource_path
|
||||
|
||||
@@ -17934,8 +17988,6 @@ components:
|
||||
format: date-time
|
||||
error:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
error_handler_path:
|
||||
type: string
|
||||
error_handler_args:
|
||||
@@ -17946,7 +17998,6 @@ components:
|
||||
- gcp_resource_path
|
||||
- topic_id
|
||||
- subscription_id
|
||||
- enabled
|
||||
- delivery_type
|
||||
- subscription_mode
|
||||
|
||||
@@ -18048,8 +18099,6 @@ components:
|
||||
format: date-time
|
||||
error:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
error_handler_path:
|
||||
type: string
|
||||
error_handler_args:
|
||||
@@ -18060,7 +18109,6 @@ components:
|
||||
required:
|
||||
- queue_url
|
||||
- aws_resource_path
|
||||
- enabled
|
||||
- aws_auth_resource_type
|
||||
|
||||
LoggedWizardStatus:
|
||||
@@ -18257,8 +18305,6 @@ components:
|
||||
- $ref: "#/components/schemas/TriggerExtraProperty"
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
postgres_resource_path:
|
||||
type: string
|
||||
publication_name:
|
||||
@@ -18279,7 +18325,6 @@ components:
|
||||
retry:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
|
||||
required:
|
||||
- enabled
|
||||
- postgres_resource_path
|
||||
- replication_slot_name
|
||||
- publication_name
|
||||
@@ -18370,8 +18415,6 @@ components:
|
||||
format: date-time
|
||||
error:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
error_handler_path:
|
||||
type: string
|
||||
error_handler_args:
|
||||
@@ -18383,7 +18426,6 @@ components:
|
||||
- kafka_resource_path
|
||||
- group_id
|
||||
- topics
|
||||
- enabled
|
||||
|
||||
NewKafkaTrigger:
|
||||
type: object
|
||||
@@ -18475,8 +18517,6 @@ components:
|
||||
format: date-time
|
||||
error:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
error_handler_path:
|
||||
type: string
|
||||
error_handler_args:
|
||||
@@ -18488,7 +18528,6 @@ components:
|
||||
- nats_resource_path
|
||||
- use_jetstream
|
||||
- subjects
|
||||
- enabled
|
||||
|
||||
NewNatsTrigger:
|
||||
type: object
|
||||
@@ -18602,7 +18641,8 @@ components:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
retry:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
|
||||
|
||||
enabled:
|
||||
type: boolean
|
||||
required:
|
||||
- path
|
||||
- script_path
|
||||
@@ -18764,6 +18804,8 @@ components:
|
||||
type: number
|
||||
wm_memory_usage:
|
||||
type: number
|
||||
job_isolation:
|
||||
type: string
|
||||
required:
|
||||
- worker
|
||||
- worker_instance
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::bedrock;
|
||||
use crate::db::{ApiAuthed, DB};
|
||||
|
||||
use axum::{body::Bytes, extract::Path, response::IntoResponse, routing::post, Extension, Router};
|
||||
@@ -6,12 +7,12 @@ use quick_cache::sync::Cache;
|
||||
use reqwest::{Client, RequestBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use windmill_common::variables::get_variable_or_self;
|
||||
use std::collections::HashMap;
|
||||
use windmill_audit::{audit_oss::audit_log, ActionKind};
|
||||
use windmill_common::ai_providers::{AIProvider, ProviderConfig, ProviderModel, AZURE_API_VERSION};
|
||||
use windmill_common::ai_providers::{AIProvider, ProviderConfig, ProviderModel};
|
||||
use windmill_common::error::{to_anyhow, Error, Result};
|
||||
use windmill_common::utils::configure_client;
|
||||
use windmill_common::variables::get_variable_or_self;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref HTTP_CLIENT: Client = configure_client(reqwest::ClientBuilder::new()
|
||||
@@ -66,6 +67,7 @@ struct AIStandardResource {
|
||||
#[serde(alias = "apiKey")]
|
||||
api_key: Option<String>,
|
||||
organization_id: Option<String>,
|
||||
region: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -98,7 +100,9 @@ impl AIRequestConfig {
|
||||
) -> Result<Self> {
|
||||
let (api_key, access_token, organization_id, base_url, user) = match resource {
|
||||
AIResource::Standard(resource) => {
|
||||
let base_url = provider.get_base_url(resource.base_url, db).await?;
|
||||
let base_url = provider
|
||||
.get_base_url(resource.base_url, resource.region, db)
|
||||
.await?;
|
||||
let api_key = if let Some(api_key) = resource.api_key {
|
||||
Some(get_variable_or_self(api_key, db, w_id).await?)
|
||||
} else {
|
||||
@@ -119,7 +123,7 @@ impl AIRequestConfig {
|
||||
None
|
||||
};
|
||||
let token = Self::get_token_using_oauth(resource, db, w_id).await?;
|
||||
let base_url = provider.get_base_url(None, db).await?;
|
||||
let base_url = provider.get_base_url(None, None, db).await?;
|
||||
|
||||
(None, Some(token), None, base_url, user)
|
||||
}
|
||||
@@ -180,15 +184,34 @@ impl AIRequestConfig {
|
||||
let is_azure = provider.is_azure_openai(base_url);
|
||||
let is_anthropic = matches!(provider, AIProvider::Anthropic);
|
||||
let is_anthropic_sdk = headers.get("X-Anthropic-SDK").is_some();
|
||||
let is_bedrock = matches!(provider, AIProvider::AWSBedrock);
|
||||
|
||||
let url = if is_azure && method != Method::GET {
|
||||
let model = AIProvider::extract_model_from_body(&body)?;
|
||||
AIProvider::build_azure_openai_url(base_url, &model, path)
|
||||
// Handle AWS Bedrock transformation
|
||||
let (url, body) = if is_bedrock && method != Method::GET {
|
||||
let (model, transformed_body, is_streaming) =
|
||||
bedrock::transform_openai_to_bedrock(&body)?;
|
||||
let endpoint = if is_streaming {
|
||||
"converse-stream"
|
||||
} else {
|
||||
"converse"
|
||||
};
|
||||
let bedrock_url = format!("{}/model/{}/{}", base_url, model, endpoint);
|
||||
(bedrock_url, transformed_body)
|
||||
} else if is_bedrock && (path == "foundation-models" || path == "inference-profiles") {
|
||||
// AWS Bedrock foundation-models and inference-profiles endpoints use different base URL (without -runtime)
|
||||
let bedrock_base_url = base_url.replace("bedrock-runtime.", "bedrock.");
|
||||
let bedrock_url = format!("{}/{}", bedrock_base_url, path);
|
||||
(bedrock_url, body)
|
||||
} else if is_azure {
|
||||
let azure_url = AIProvider::build_azure_openai_url(base_url, path);
|
||||
(azure_url, body)
|
||||
} else if is_anthropic_sdk {
|
||||
let truncated_base_url = base_url.trim_end_matches("/v1");
|
||||
format!("{}/{}", truncated_base_url, path)
|
||||
let anthropic_url = format!("{}/{}", truncated_base_url, path);
|
||||
(anthropic_url, body)
|
||||
} else {
|
||||
format!("{}/{}", base_url, path)
|
||||
let default_url = format!("{}/{}", base_url, path);
|
||||
(default_url, body)
|
||||
};
|
||||
|
||||
tracing::debug!("AI request URL: {}", url);
|
||||
@@ -205,10 +228,6 @@ impl AIRequestConfig {
|
||||
|
||||
request = request.body(body);
|
||||
|
||||
if is_azure {
|
||||
request = request.query(&[("api-version", AZURE_API_VERSION)])
|
||||
}
|
||||
|
||||
if let Some(api_key) = self.api_key {
|
||||
if is_azure {
|
||||
request = request.header("api-key", api_key.clone())
|
||||
@@ -316,7 +335,7 @@ async fn global_proxy(
|
||||
return Err(Error::BadRequest("API key is required".to_string()));
|
||||
};
|
||||
|
||||
let base_url = provider.get_base_url(None, &db).await?;
|
||||
let base_url = provider.get_base_url(None, None, &db).await?;
|
||||
|
||||
let url = format!("{}/{}", base_url, ai_path);
|
||||
|
||||
@@ -446,6 +465,21 @@ async fn proxy(
|
||||
}
|
||||
};
|
||||
|
||||
// Extract model and streaming flag for Bedrock transformation (only for POST requests)
|
||||
let (model, is_streaming) =
|
||||
if matches!(provider, AIProvider::AWSBedrock) && method == Method::POST {
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct BedrockRequest {
|
||||
model: String,
|
||||
stream: bool,
|
||||
}
|
||||
let parsed: BedrockRequest = serde_json::from_slice(&body)
|
||||
.map_err(|e| Error::internal_err(format!("Failed to parse request body: {}", e)))?;
|
||||
(Some(parsed.model), parsed.stream)
|
||||
} else {
|
||||
(None, false)
|
||||
};
|
||||
|
||||
let request = request_config.prepare_request(&provider, &ai_path, method, headers, body)?;
|
||||
|
||||
let response = request.send().await.map_err(to_anyhow)?;
|
||||
@@ -469,8 +503,45 @@ async fn proxy(
|
||||
return Err(Error::AIError(err_msg));
|
||||
}
|
||||
|
||||
let status_code = response.status();
|
||||
let headers = response.headers().clone();
|
||||
let stream = response.bytes_stream();
|
||||
Ok((status_code, headers, axum::body::Body::from_stream(stream)))
|
||||
// Transform Bedrock responses back to OpenAI format
|
||||
if matches!(provider, AIProvider::AWSBedrock) && model.is_some() {
|
||||
if is_streaming {
|
||||
// Transform streaming response
|
||||
use http::StatusCode;
|
||||
|
||||
let mut response_headers = HeaderMap::new();
|
||||
response_headers.insert("content-type", "text/event-stream".parse().unwrap());
|
||||
response_headers.insert("cache-control", "no-cache".parse().unwrap());
|
||||
response_headers.insert("connection", "keep-alive".parse().unwrap());
|
||||
|
||||
let stream = response.bytes_stream();
|
||||
let transformed_stream =
|
||||
bedrock::transform_bedrock_stream_to_openai(stream, model.unwrap());
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
response_headers,
|
||||
axum::body::Body::from_stream(transformed_stream),
|
||||
))
|
||||
} else {
|
||||
// Transform non-streaming response
|
||||
let transformed_body =
|
||||
bedrock::transform_bedrock_to_openai(response, model.unwrap()).await?;
|
||||
|
||||
let mut response_headers = HeaderMap::new();
|
||||
response_headers.insert("content-type", "application/json".parse().unwrap());
|
||||
|
||||
Ok((
|
||||
http::StatusCode::OK,
|
||||
response_headers,
|
||||
axum::body::Body::from(transformed_body),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
// Pass through for other providers
|
||||
let status_code = response.status();
|
||||
let headers = response.headers().clone();
|
||||
let stream = response.bytes_stream();
|
||||
Ok((status_code, headers, axum::body::Body::from_stream(stream)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ impl AuthCache {
|
||||
w_id.as_ref(),
|
||||
token.trim_start_matches("jwt_ext_"),
|
||||
self.ext_jwks.clone(),
|
||||
&self.db,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -0,0 +1,602 @@
|
||||
use axum::body::Bytes;
|
||||
use bytes;
|
||||
use futures;
|
||||
use uuid;
|
||||
use windmill_common::error::{Error, Result};
|
||||
|
||||
/// Transform OpenAI format request to AWS Bedrock Converse format
|
||||
/// Returns: (model_id, transformed_body, is_streaming)
|
||||
pub fn transform_openai_to_bedrock(body: &[u8]) -> Result<(String, Bytes, bool)> {
|
||||
use serde_json::Value;
|
||||
|
||||
// Parse the OpenAI request
|
||||
let openai_req: Value = serde_json::from_slice(body)
|
||||
.map_err(|e| Error::internal_err(format!("Failed to parse OpenAI request: {}", e)))?;
|
||||
|
||||
// Extract model and streaming flag
|
||||
let model = openai_req["model"]
|
||||
.as_str()
|
||||
.ok_or_else(|| Error::BadRequest("Missing 'model' field in request".to_string()))?
|
||||
.to_string();
|
||||
|
||||
let is_streaming = openai_req["stream"].as_bool().unwrap_or(false);
|
||||
|
||||
// Build Bedrock request
|
||||
let mut bedrock_req = serde_json::json!({});
|
||||
|
||||
// Transform messages
|
||||
if let Some(messages) = openai_req["messages"].as_array() {
|
||||
let mut system_messages = Vec::new();
|
||||
let mut conversation_messages = Vec::new();
|
||||
|
||||
for msg in messages {
|
||||
let role = msg["role"].as_str().unwrap_or("");
|
||||
|
||||
match role {
|
||||
"system" => {
|
||||
// Extract system messages to separate array
|
||||
if let Some(content) = msg["content"].as_str() {
|
||||
system_messages.push(serde_json::json!({"text": content}));
|
||||
}
|
||||
}
|
||||
"user" | "assistant" => {
|
||||
// Normalize content to array format
|
||||
let mut content = if let Some(text) = msg["content"].as_str() {
|
||||
// Simple string → array of content blocks
|
||||
vec![serde_json::json!({"text": text})]
|
||||
} else if let Some(content_array) = msg["content"].as_array() {
|
||||
// Already an array - transform each item
|
||||
content_array
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
if let Some(text) = item["text"].as_str() {
|
||||
Some(serde_json::json!({"text": text}))
|
||||
} else if item["type"].as_str() == Some("text") {
|
||||
Some(serde_json::json!({"text": item["text"]}))
|
||||
} else if item["type"].as_str() == Some("image_url") {
|
||||
// Transform image_url format if needed
|
||||
// For now, pass through - may need more sophisticated handling
|
||||
Some(item.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// Handle tool_calls for assistant messages (OpenAI → Bedrock toolUse)
|
||||
if role == "assistant" {
|
||||
if let Some(tool_calls) = msg["tool_calls"].as_array() {
|
||||
for tool_call in tool_calls {
|
||||
if tool_call["type"].as_str() == Some("function") {
|
||||
let tool_use_id = tool_call["id"].as_str().unwrap_or("");
|
||||
let function_name =
|
||||
tool_call["function"]["name"].as_str().unwrap_or("");
|
||||
let arguments_str =
|
||||
tool_call["function"]["arguments"].as_str().unwrap_or("{}");
|
||||
|
||||
// Parse arguments JSON string to object
|
||||
let input = serde_json::from_str::<Value>(arguments_str)
|
||||
.map_err(|e| {
|
||||
Error::internal_err(format!(
|
||||
"Failed to parse tool call arguments: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
content.push(serde_json::json!({
|
||||
"toolUse": {
|
||||
"toolUseId": tool_use_id,
|
||||
"name": function_name,
|
||||
"input": input
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only add message if it has content
|
||||
if !content.is_empty() {
|
||||
conversation_messages.push(serde_json::json!({
|
||||
"role": role,
|
||||
"content": content
|
||||
}));
|
||||
}
|
||||
}
|
||||
"tool" => {
|
||||
// Transform tool response to Bedrock format
|
||||
let tool_call_id = msg["tool_call_id"].as_str().unwrap_or("");
|
||||
let content = msg["content"].as_str().unwrap_or("");
|
||||
|
||||
// Try to parse content as JSON
|
||||
// Bedrock requires json field to be an object, not a primitive or array
|
||||
let tool_result_content =
|
||||
if let Ok(json_content) = serde_json::from_str::<Value>(content) {
|
||||
if json_content.is_object() {
|
||||
vec![serde_json::json!({"json": json_content})]
|
||||
} else {
|
||||
// Wrap primitives and arrays in an object
|
||||
vec![serde_json::json!({"json": {"result": json_content}})]
|
||||
}
|
||||
} else {
|
||||
vec![serde_json::json!({"text": content})]
|
||||
};
|
||||
|
||||
conversation_messages.push(serde_json::json!({
|
||||
"role": "user",
|
||||
"content": [{
|
||||
"toolResult": {
|
||||
"toolUseId": tool_call_id,
|
||||
"content": tool_result_content
|
||||
}
|
||||
}]
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !system_messages.is_empty() {
|
||||
bedrock_req["system"] = Value::Array(system_messages);
|
||||
}
|
||||
bedrock_req["messages"] = Value::Array(conversation_messages);
|
||||
}
|
||||
|
||||
// Transform inference parameters
|
||||
let mut inference_config = serde_json::json!({});
|
||||
if let Some(max_tokens) = openai_req["max_tokens"].as_i64() {
|
||||
inference_config["maxTokens"] = Value::Number(max_tokens.into());
|
||||
}
|
||||
if let Some(temperature) = openai_req["temperature"].as_f64() {
|
||||
inference_config["temperature"] = serde_json::json!(temperature);
|
||||
}
|
||||
if let Some(top_p) = openai_req["top_p"].as_f64() {
|
||||
inference_config["topP"] = serde_json::json!(top_p);
|
||||
}
|
||||
if let Some(stop) = openai_req["stop"].as_array() {
|
||||
let stop_sequences: Vec<String> = stop
|
||||
.iter()
|
||||
.filter_map(|s| s.as_str().map(|s| s.to_string()))
|
||||
.collect();
|
||||
if !stop_sequences.is_empty() {
|
||||
inference_config["stopSequences"] =
|
||||
Value::Array(stop_sequences.into_iter().map(Value::String).collect());
|
||||
}
|
||||
}
|
||||
if !inference_config.as_object().unwrap().is_empty() {
|
||||
bedrock_req["inferenceConfig"] = inference_config;
|
||||
}
|
||||
|
||||
// Transform tools if present
|
||||
if let Some(tools) = openai_req["tools"].as_array() {
|
||||
let mut bedrock_tools = Vec::new();
|
||||
|
||||
for tool in tools {
|
||||
if tool["type"].as_str() == Some("function") {
|
||||
if let Some(function) = tool["function"].as_object() {
|
||||
bedrock_tools.push(serde_json::json!({
|
||||
"toolSpec": {
|
||||
"name": function.get("name"),
|
||||
"description": function.get("description")
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or("Tool function"),
|
||||
"inputSchema": {
|
||||
"json": function.get("parameters")
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !bedrock_tools.is_empty() {
|
||||
let mut tool_config = serde_json::json!({
|
||||
"tools": bedrock_tools
|
||||
});
|
||||
|
||||
// Transform tool_choice
|
||||
if let Some(tool_choice) = openai_req.get("tool_choice") {
|
||||
if tool_choice == "auto" {
|
||||
tool_config["toolChoice"] = serde_json::json!({"auto": {}});
|
||||
} else if tool_choice == "required" {
|
||||
tool_config["toolChoice"] = serde_json::json!({"any": {}});
|
||||
} else if let Some(obj) = tool_choice.as_object() {
|
||||
if obj.get("type").and_then(|v| v.as_str()) == Some("function") {
|
||||
if let Some(function) = obj.get("function").and_then(|v| v.as_object()) {
|
||||
if let Some(name) = function.get("name").and_then(|v| v.as_str()) {
|
||||
tool_config["toolChoice"] = serde_json::json!({
|
||||
"tool": {"name": name}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bedrock_req["toolConfig"] = tool_config;
|
||||
}
|
||||
}
|
||||
|
||||
let transformed_body = serde_json::to_vec(&bedrock_req)
|
||||
.map_err(|e| Error::internal_err(format!("Failed to serialize Bedrock request: {}", e)))?
|
||||
.into();
|
||||
|
||||
Ok((model, transformed_body, is_streaming))
|
||||
}
|
||||
|
||||
/// Transform AWS Bedrock Converse response to OpenAI format
|
||||
pub async fn transform_bedrock_to_openai(
|
||||
response: reqwest::Response,
|
||||
model: String,
|
||||
) -> Result<Bytes> {
|
||||
use serde_json::Value;
|
||||
|
||||
let bedrock_resp: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| Error::internal_err(format!("Failed to parse Bedrock response: {}", e)))?;
|
||||
|
||||
// Generate unique ID and timestamp
|
||||
let id = format!("chatcmpl-{}", uuid::Uuid::new_v4().simple());
|
||||
let created = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
// Extract stop reason and map to finish_reason
|
||||
let stop_reason = bedrock_resp["stopReason"].as_str().unwrap_or("end_turn");
|
||||
let finish_reason = match stop_reason {
|
||||
"end_turn" => "stop",
|
||||
"max_tokens" => "length",
|
||||
"tool_use" => "tool_calls",
|
||||
"stop_sequence" => "stop",
|
||||
"guardrail_intervened" | "content_filtered" => "content_filter",
|
||||
_ => "stop",
|
||||
};
|
||||
|
||||
// Extract message content
|
||||
let message_content = &bedrock_resp["output"]["message"]["content"];
|
||||
let mut text_content = String::new();
|
||||
let mut tool_calls = Vec::new();
|
||||
|
||||
if let Some(content_array) = message_content.as_array() {
|
||||
for (_index, block) in content_array.iter().enumerate() {
|
||||
if let Some(text) = block["text"].as_str() {
|
||||
text_content.push_str(text);
|
||||
} else if let Some(tool_use) = block.get("toolUse") {
|
||||
// Transform tool use to OpenAI tool_calls format
|
||||
let tool_call_id = tool_use["toolUseId"].as_str().unwrap_or("");
|
||||
let name = tool_use["name"].as_str().unwrap_or("");
|
||||
let input = &tool_use["input"];
|
||||
|
||||
tool_calls.push(serde_json::json!({
|
||||
"id": tool_call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": name,
|
||||
"arguments": serde_json::to_string(input).unwrap_or_default()
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the message
|
||||
let message = if !tool_calls.is_empty() {
|
||||
serde_json::json!({
|
||||
"role": "assistant",
|
||||
"content": if text_content.is_empty() { Value::Null } else { Value::String(text_content) },
|
||||
"tool_calls": tool_calls
|
||||
})
|
||||
} else {
|
||||
serde_json::json!({
|
||||
"role": "assistant",
|
||||
"content": text_content
|
||||
})
|
||||
};
|
||||
|
||||
// Extract usage information
|
||||
let usage = if let Some(usage_data) = bedrock_resp.get("usage") {
|
||||
serde_json::json!({
|
||||
"prompt_tokens": usage_data["inputTokens"].as_i64().unwrap_or(0),
|
||||
"completion_tokens": usage_data["outputTokens"].as_i64().unwrap_or(0),
|
||||
"total_tokens": usage_data["totalTokens"].as_i64().unwrap_or(0)
|
||||
})
|
||||
} else {
|
||||
serde_json::json!({
|
||||
"prompt_tokens": 0,
|
||||
"completion_tokens": 0,
|
||||
"total_tokens": 0
|
||||
})
|
||||
};
|
||||
|
||||
// Build OpenAI-format response
|
||||
let openai_resp = serde_json::json!({
|
||||
"id": id,
|
||||
"object": "chat.completion",
|
||||
"created": created,
|
||||
"model": model,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": message,
|
||||
"finish_reason": finish_reason
|
||||
}],
|
||||
"usage": usage
|
||||
});
|
||||
|
||||
let response_body = serde_json::to_vec(&openai_resp)
|
||||
.map_err(|e| Error::internal_err(format!("Failed to serialize OpenAI response: {}", e)))?
|
||||
.into();
|
||||
|
||||
Ok(response_body)
|
||||
}
|
||||
|
||||
/// Transform AWS Bedrock streaming response to OpenAI SSE format
|
||||
/// Bedrock uses AWS event stream binary format, not SSE
|
||||
pub fn transform_bedrock_stream_to_openai(
|
||||
stream: impl futures::Stream<Item = std::result::Result<bytes::Bytes, reqwest::Error>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
model: String,
|
||||
) -> impl futures::Stream<Item = std::result::Result<bytes::Bytes, std::io::Error>> + Send {
|
||||
use futures::stream::StreamExt;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
let id = format!("chatcmpl-{}", uuid::Uuid::new_v4().simple());
|
||||
let created = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
// State to track partial tool calls and binary buffer
|
||||
struct StreamState {
|
||||
id: String,
|
||||
model: String,
|
||||
created: u64,
|
||||
tool_calls: HashMap<usize, (String, String, String)>, // index -> (id, name, args)
|
||||
buffer: Vec<u8>, // Binary buffer for AWS event stream
|
||||
}
|
||||
|
||||
let state = std::sync::Arc::new(tokio::sync::Mutex::new(StreamState {
|
||||
id: id.clone(),
|
||||
model: model.clone(),
|
||||
created,
|
||||
tool_calls: HashMap::new(),
|
||||
buffer: Vec::new(),
|
||||
}));
|
||||
|
||||
stream
|
||||
.then(move |chunk_result| {
|
||||
let state = state.clone();
|
||||
async move {
|
||||
match chunk_result {
|
||||
Ok(chunk) => {
|
||||
let mut state = state.lock().await;
|
||||
state.buffer.extend_from_slice(&chunk);
|
||||
|
||||
let mut events = Vec::new();
|
||||
|
||||
// Parse AWS event stream messages from buffer
|
||||
loop {
|
||||
// Need at least 12 bytes for prelude (8) + prelude CRC (4)
|
||||
if state.buffer.len() < 12 {
|
||||
break;
|
||||
}
|
||||
|
||||
// Read prelude: total_length (4 bytes) + headers_length (4 bytes)
|
||||
let total_length = u32::from_be_bytes([
|
||||
state.buffer[0],
|
||||
state.buffer[1],
|
||||
state.buffer[2],
|
||||
state.buffer[3],
|
||||
]) as usize;
|
||||
|
||||
// Check if we have the complete message
|
||||
if state.buffer.len() < total_length {
|
||||
break;
|
||||
}
|
||||
|
||||
let headers_length = u32::from_be_bytes([
|
||||
state.buffer[4],
|
||||
state.buffer[5],
|
||||
state.buffer[6],
|
||||
state.buffer[7],
|
||||
]) as usize;
|
||||
|
||||
// Skip prelude CRC (4 bytes after prelude)
|
||||
let headers_start = 12;
|
||||
let payload_start = headers_start + headers_length;
|
||||
let payload_end = total_length - 4; // Exclude message CRC
|
||||
|
||||
// Parse headers to extract event type
|
||||
let mut event_type = None;
|
||||
let mut pos = headers_start;
|
||||
while pos < payload_start {
|
||||
if pos + 1 > state.buffer.len() {
|
||||
break;
|
||||
}
|
||||
let name_len = state.buffer[pos] as usize;
|
||||
pos += 1;
|
||||
|
||||
if pos + name_len > state.buffer.len() {
|
||||
break;
|
||||
}
|
||||
let name = String::from_utf8_lossy(&state.buffer[pos..pos + name_len]).to_string();
|
||||
pos += name_len;
|
||||
|
||||
if pos + 3 > state.buffer.len() {
|
||||
break;
|
||||
}
|
||||
let value_type = state.buffer[pos];
|
||||
pos += 1;
|
||||
let value_len = u16::from_be_bytes([state.buffer[pos], state.buffer[pos + 1]]) as usize;
|
||||
pos += 2;
|
||||
|
||||
if pos + value_len > state.buffer.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
if value_type == 7 && name == ":event-type" {
|
||||
event_type = Some(String::from_utf8_lossy(&state.buffer[pos..pos + value_len]).to_string());
|
||||
}
|
||||
pos += value_len;
|
||||
}
|
||||
|
||||
// Extract JSON payload (copy to avoid borrow issues)
|
||||
let payload = state.buffer[payload_start..payload_end].to_vec();
|
||||
|
||||
// Remove processed message from buffer
|
||||
state.buffer.drain(0..total_length);
|
||||
|
||||
// Process the event
|
||||
if let Some(evt_type) = event_type {
|
||||
if let Ok(payload_str) = std::str::from_utf8(&payload) {
|
||||
if let Ok(parsed_data) = serde_json::from_str::<Value>(payload_str) {
|
||||
// Transform based on event type
|
||||
match evt_type.as_str() {
|
||||
"messageStart" => {
|
||||
// No output for messageStart
|
||||
}
|
||||
"contentBlockStart" => {
|
||||
let index = parsed_data["contentBlockIndex"].as_u64().unwrap_or(0) as usize;
|
||||
|
||||
if let Some(tool_use) = parsed_data["start"].get("toolUse") {
|
||||
let tool_id = tool_use["toolUseId"].as_str().unwrap_or("").to_string();
|
||||
let name = tool_use["name"].as_str().unwrap_or("").to_string();
|
||||
|
||||
state.tool_calls.insert(index, (tool_id.clone(), name.clone(), String::new()));
|
||||
|
||||
// Send initial tool call chunk
|
||||
let chunk = serde_json::json!({
|
||||
"id": state.id,
|
||||
"object": "chat.completion.chunk",
|
||||
"created": state.created,
|
||||
"model": state.model,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"tool_calls": [{
|
||||
"index": index,
|
||||
"id": tool_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": name,
|
||||
"arguments": ""
|
||||
}
|
||||
}]
|
||||
},
|
||||
"finish_reason": Value::Null
|
||||
}]
|
||||
});
|
||||
|
||||
events.push(Ok(bytes::Bytes::from(format!("data: {}\n\n", chunk))));
|
||||
}
|
||||
}
|
||||
"contentBlockDelta" => {
|
||||
let index = parsed_data["contentBlockIndex"].as_u64().unwrap_or(0) as usize;
|
||||
|
||||
if let Some(text) = parsed_data["delta"]["text"].as_str() {
|
||||
// Text content delta
|
||||
let chunk = serde_json::json!({
|
||||
"id": state.id,
|
||||
"object": "chat.completion.chunk",
|
||||
"created": state.created,
|
||||
"model": state.model,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"content": text
|
||||
},
|
||||
"finish_reason": Value::Null
|
||||
}]
|
||||
});
|
||||
|
||||
events.push(Ok(bytes::Bytes::from(format!("data: {}\n\n", chunk))));
|
||||
} else if let Some(tool_use_input) = parsed_data["delta"]["toolUse"]["input"].as_str() {
|
||||
// Tool use arguments delta
|
||||
if let Some((_tool_id, _name, ref mut args)) = state.tool_calls.get_mut(&index) {
|
||||
args.push_str(tool_use_input);
|
||||
|
||||
let chunk = serde_json::json!({
|
||||
"id": state.id,
|
||||
"object": "chat.completion.chunk",
|
||||
"created": state.created,
|
||||
"model": state.model,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"tool_calls": [{
|
||||
"index": index,
|
||||
"function": {
|
||||
"arguments": tool_use_input
|
||||
}
|
||||
}]
|
||||
},
|
||||
"finish_reason": Value::Null
|
||||
}]
|
||||
});
|
||||
|
||||
events.push(Ok(bytes::Bytes::from(format!("data: {}\n\n", chunk))));
|
||||
}
|
||||
}
|
||||
}
|
||||
"contentBlockStop" => {
|
||||
// No output needed
|
||||
}
|
||||
"messageStop" => {
|
||||
let stop_reason = parsed_data["stopReason"].as_str().unwrap_or("end_turn");
|
||||
let finish_reason = match stop_reason {
|
||||
"end_turn" => "stop",
|
||||
"max_tokens" => "length",
|
||||
"tool_use" => "tool_calls",
|
||||
"stop_sequence" => "stop",
|
||||
"guardrail_intervened" | "content_filtered" => "content_filter",
|
||||
_ => "stop",
|
||||
};
|
||||
|
||||
let chunk = serde_json::json!({
|
||||
"id": state.id,
|
||||
"object": "chat.completion.chunk",
|
||||
"created": state.created,
|
||||
"model": state.model,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": {},
|
||||
"finish_reason": finish_reason
|
||||
}]
|
||||
});
|
||||
|
||||
events.push(Ok(bytes::Bytes::from(format!("data: {}\n\n", chunk))));
|
||||
}
|
||||
"metadata" => {
|
||||
// Could include usage info here if needed
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // end loop
|
||||
|
||||
events
|
||||
}
|
||||
Err(e) => {
|
||||
vec![Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
e.to_string(),
|
||||
))]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.flat_map(|events| futures::stream::iter(events))
|
||||
.chain(futures::stream::iter(vec![
|
||||
// Send [DONE] at the end
|
||||
Ok(bytes::Bytes::from("data: [DONE]\n\n"))
|
||||
]))
|
||||
}
|
||||
@@ -53,6 +53,9 @@ lazy_static::lazy_static! {
|
||||
).to_string()),
|
||||
(20221105003256, "DELETE FROM workspace_invite WHERE workspace_id = 'demo' AND email = 'ruben@windmill.dev';".to_string()),
|
||||
(20221123151919, "".to_string()),
|
||||
(20251105100125, include_str!(
|
||||
"../../migrations/20251105100125_legacy_sql_result_flag.up.sql"
|
||||
).replace("✅", "")),
|
||||
].into_iter().collect();
|
||||
}
|
||||
|
||||
@@ -166,6 +169,7 @@ impl Migrate for CustomMigrator {
|
||||
|
||||
if let Some(migration_sql) = OVERRIDDEN_MIGRATIONS.get(&migration.version) {
|
||||
tracing::info!("Using custom migration for version {}", migration.version);
|
||||
// tracing::info!("Migration SQL: {}", migration_sql);
|
||||
|
||||
self.inner
|
||||
.execute(&**migration_sql)
|
||||
|
||||
@@ -26,6 +26,7 @@ pub async fn jwt_ext_auth(
|
||||
_w_id: Option<&String>,
|
||||
_token: &str,
|
||||
_external_jwks: Option<Arc<RwLock<ExternalJwks>>>,
|
||||
_db: &crate::db::DB,
|
||||
) -> anyhow::Result<(crate::db::ApiAuthed, usize)> {
|
||||
// Implementation is not open source
|
||||
|
||||
|
||||
@@ -783,8 +783,8 @@ async fn update_flow(
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE
|
||||
flow
|
||||
UPDATE
|
||||
flow
|
||||
SET
|
||||
path = $1,
|
||||
summary = $2,
|
||||
@@ -800,7 +800,7 @@ async fn update_flow(
|
||||
schema = $9::text::json,
|
||||
edited_by = $10,
|
||||
edited_at = now()
|
||||
WHERE
|
||||
WHERE
|
||||
path = $11 AND workspace_id = $12",
|
||||
if is_new_path { flow_path } else { &nf.path },
|
||||
nf.summary,
|
||||
@@ -824,8 +824,8 @@ async fn update_flow(
|
||||
if is_new_path {
|
||||
// if new path, must clone flow to new path and delete old flow for flow_version foreign key constraint
|
||||
sqlx::query!(
|
||||
"INSERT INTO flow
|
||||
(workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at)
|
||||
"INSERT INTO flow
|
||||
(workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at)
|
||||
SELECT workspace_id, $1, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at
|
||||
FROM flow
|
||||
WHERE path = $2 AND workspace_id = $3",
|
||||
@@ -893,6 +893,8 @@ async fn update_flow(
|
||||
.warn_after_seconds(10)
|
||||
.await??;
|
||||
|
||||
// tracing::error!("Updating flow: {:?}", nf.value.get());
|
||||
|
||||
// This will lock anyone who is trying to iterate on flow_versions with given path and parameters.
|
||||
let version = sqlx::query_scalar!(
|
||||
"INSERT INTO flow_version (workspace_id, path, value, schema, created_by) VALUES ($1, $2, $3, $4::text::json, $5) RETURNING id",
|
||||
@@ -1143,11 +1145,11 @@ async fn get_flow_by_path(
|
||||
favorite.path IS NOT NULL AS starred
|
||||
FROM flow
|
||||
LEFT JOIN favorite
|
||||
ON favorite.favorite_kind = 'flow'
|
||||
AND favorite.workspace_id = flow.workspace_id
|
||||
AND favorite.path = flow.path
|
||||
ON favorite.favorite_kind = 'flow'
|
||||
AND favorite.workspace_id = flow.workspace_id
|
||||
AND favorite.path = flow.path
|
||||
AND favorite.usr = $3
|
||||
LEFT JOIN flow_version
|
||||
LEFT JOIN flow_version
|
||||
ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
|
||||
WHERE flow.path = $1 AND flow.workspace_id = $2
|
||||
"#,
|
||||
@@ -1182,7 +1184,7 @@ async fn get_flow_by_path(
|
||||
flow_version.created_by AS edited_by,
|
||||
NULL AS starred
|
||||
FROM flow
|
||||
LEFT JOIN flow_version
|
||||
LEFT JOIN flow_version
|
||||
ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
|
||||
WHERE flow.path = $1 AND flow.workspace_id = $2
|
||||
"#,
|
||||
|
||||
@@ -80,6 +80,7 @@ pub mod args;
|
||||
mod assets;
|
||||
mod audit;
|
||||
pub mod auth;
|
||||
mod bedrock;
|
||||
mod capture;
|
||||
mod concurrency_groups;
|
||||
mod configs;
|
||||
@@ -154,6 +155,7 @@ mod smtp_server_oss;
|
||||
pub mod teams_approvals_ee;
|
||||
mod teams_approvals_oss;
|
||||
|
||||
mod public_app_layer;
|
||||
mod static_assets;
|
||||
#[cfg(all(feature = "stripe", feature = "enterprise", feature = "private"))]
|
||||
pub mod stripe_ee;
|
||||
@@ -183,7 +185,6 @@ pub mod workspaces_ee;
|
||||
mod workspaces_export;
|
||||
mod workspaces_extra;
|
||||
mod workspaces_oss;
|
||||
mod public_app_layer;
|
||||
|
||||
#[cfg(feature = "mcp")]
|
||||
mod mcp;
|
||||
@@ -324,6 +325,11 @@ pub async fn run_server(
|
||||
#[cfg(feature = "embedding")]
|
||||
load_embeddings_db(&db);
|
||||
|
||||
#[cfg(feature = "cloud")]
|
||||
if *CLOUD_HOSTED {
|
||||
windmill_queue::init_usage_buffer(db.clone());
|
||||
}
|
||||
|
||||
let mut start_smtp_server = false;
|
||||
if let Some(smtp_settings) =
|
||||
load_value_from_global_settings(&db, EMAIL_DOMAIN_SETTING).await?
|
||||
|
||||
@@ -39,6 +39,7 @@ use super::utils::{
|
||||
FlowInfo, ResourceInfo, ResourceType, SchemaType, ScriptInfo, ToolableItem, WorkspaceId,
|
||||
},
|
||||
schema::transform_schema_for_resources,
|
||||
scope_matcher::{is_resource_allowed, parse_mcp_scopes},
|
||||
transform::{reverse_transform, reverse_transform_key},
|
||||
};
|
||||
|
||||
@@ -151,6 +152,10 @@ impl ServerHandler for Runner {
|
||||
|
||||
check_scopes(authed)?;
|
||||
|
||||
// Parse MCP scopes for authorization
|
||||
let scopes = authed.scopes.as_ref().map(|s| s.as_slice()).unwrap_or(&[]);
|
||||
let scope_config = parse_mcp_scopes(scopes)?;
|
||||
|
||||
if request.name.ends_with("_TRUNC") {
|
||||
return Ok(CallToolResult::error(
|
||||
vec![
|
||||
@@ -197,6 +202,19 @@ impl ServerHandler for Runner {
|
||||
let endpoint_tools = all_endpoint_tools();
|
||||
for endpoint_tool in endpoint_tools {
|
||||
if endpoint_tool.name.as_ref() == request.name {
|
||||
// Validate endpoint scope
|
||||
if scope_config.granular
|
||||
&& !is_resource_allowed(&endpoint_tool.name, &scope_config.endpoints)
|
||||
{
|
||||
return Err(ErrorData::internal_error(
|
||||
format!(
|
||||
"Access denied: endpoint '{}' not in token scope",
|
||||
endpoint_tool.name
|
||||
),
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
// This is an endpoint tool, forward to the actual HTTP endpoint
|
||||
let result =
|
||||
call_endpoint_tool(&endpoint_tool, args.clone(), &workspace_id, &authed)
|
||||
@@ -212,6 +230,21 @@ impl ServerHandler for Runner {
|
||||
ErrorData::internal_error(format!("Failed to reverse transform path: {}", e), None)
|
||||
})?;
|
||||
|
||||
// Validate script/flow scope
|
||||
if !is_hub && scope_config.granular {
|
||||
if tool_type == "script" && !is_resource_allowed(&path, &scope_config.scripts) {
|
||||
return Err(ErrorData::internal_error(
|
||||
format!("Access denied: script '{}' not in token scope", path),
|
||||
None,
|
||||
));
|
||||
} else if tool_type == "flow" && !is_resource_allowed(&path, &scope_config.flows) {
|
||||
return Err(ErrorData::internal_error(
|
||||
format!("Access denied: flow '{}' not in token scope", path),
|
||||
None,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let item_schema = if is_hub {
|
||||
get_hub_script_schema(&format!("hub/{}", path), db).await?
|
||||
} else {
|
||||
@@ -337,53 +370,23 @@ impl ServerHandler for Runner {
|
||||
})
|
||||
.map(|w_id| w_id.0.clone())?;
|
||||
|
||||
let scopes = authed.scopes.as_ref();
|
||||
let owned_scope = scopes.and_then(|scopes| {
|
||||
scopes
|
||||
.iter()
|
||||
.find(|scope| scope.starts_with("mcp:") && !scope.contains("hub"))
|
||||
});
|
||||
let hub_scope =
|
||||
scopes.and_then(|scopes| scopes.iter().find(|scope| scope.starts_with("mcp:hub")));
|
||||
let (scope_type, scope_path) = owned_scope.map_or(("all", None), |scope| {
|
||||
let parts = scope.split(":").collect::<Vec<&str>>();
|
||||
(
|
||||
parts[1],
|
||||
if parts.len() == 3 {
|
||||
Some(parts[2])
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
});
|
||||
let scope_integrations = hub_scope.and_then(|scope| {
|
||||
let parts = scope.split(":").collect::<Vec<&str>>();
|
||||
if parts.len() == 3 {
|
||||
Some(parts[2])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
// Parse MCP scopes to determine what to expose
|
||||
let scopes = authed.scopes.as_ref().map(|s| s.as_slice()).unwrap_or(&[]);
|
||||
let scope_config = parse_mcp_scopes(scopes)?;
|
||||
|
||||
let scripts_fn = get_items::<ScriptInfo>(
|
||||
user_db,
|
||||
authed,
|
||||
&workspace_id,
|
||||
scope_type,
|
||||
"script",
|
||||
scope_path.as_deref(),
|
||||
);
|
||||
let flows_fn = get_items::<FlowInfo>(
|
||||
user_db,
|
||||
authed,
|
||||
&workspace_id,
|
||||
scope_type,
|
||||
"flow",
|
||||
scope_path.as_deref(),
|
||||
);
|
||||
let scope_type = if scope_config.favorites {
|
||||
"favorites"
|
||||
} else {
|
||||
// Fetch all items if either all or granular scope set (we filter later for granular scopes)
|
||||
"all"
|
||||
};
|
||||
|
||||
let scripts_fn =
|
||||
get_items::<ScriptInfo>(user_db, authed, &workspace_id, scope_type, "script");
|
||||
let flows_fn = get_items::<FlowInfo>(user_db, authed, &workspace_id, scope_type, "flow");
|
||||
let resources_types_fn = get_resources_types(user_db, authed, &workspace_id);
|
||||
let hub_scripts_fn = get_scripts_from_hub(db, scope_integrations.as_deref());
|
||||
let (scripts, flows, resources_types, hub_scripts) = if scope_integrations.is_some() {
|
||||
let hub_scripts_fn = get_scripts_from_hub(db, scope_config.hub_apps.as_deref());
|
||||
let (scripts, flows, resources_types, hub_scripts) = if scope_config.hub_apps.is_some() {
|
||||
let (scripts, flows, resources_types, hub_scripts) =
|
||||
try_join!(scripts_fn, flows_fn, resources_types_fn, hub_scripts_fn)?;
|
||||
(scripts, flows, resources_types, hub_scripts)
|
||||
@@ -396,7 +399,13 @@ impl ServerHandler for Runner {
|
||||
let mut resources_cache: HashMap<String, Vec<ResourceInfo>> = HashMap::new();
|
||||
let mut tools: Vec<Tool> = Vec::new();
|
||||
|
||||
// Filter and add scripts based on scope
|
||||
for script in scripts {
|
||||
// For granular scopes, filter by path
|
||||
if scope_config.granular && !is_resource_allowed(&script.path, &scope_config.scripts) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tools.push(
|
||||
Runner::create_tool_from_item(
|
||||
&script,
|
||||
@@ -410,7 +419,13 @@ impl ServerHandler for Runner {
|
||||
);
|
||||
}
|
||||
|
||||
// Filter and add flows based on scope
|
||||
for flow in flows {
|
||||
// For granular scopes, filter by path
|
||||
if scope_config.granular && !is_resource_allowed(&flow.path, &scope_config.flows) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tools.push(
|
||||
Runner::create_tool_from_item(
|
||||
&flow,
|
||||
@@ -438,10 +453,23 @@ impl ServerHandler for Runner {
|
||||
);
|
||||
}
|
||||
|
||||
// Add endpoint tools from the generated MCP tools
|
||||
// Add endpoint tools from the generated MCP tools, filtered by scope
|
||||
let endpoint_tools = all_endpoint_tools();
|
||||
let mcp_tools_converted = endpoint_tools_to_mcp_tools(endpoint_tools);
|
||||
tools.extend(mcp_tools_converted);
|
||||
for endpoint_tool in endpoint_tools {
|
||||
// For granular scopes, filter by endpoint name
|
||||
if scope_config.granular
|
||||
&& !is_resource_allowed(&endpoint_tool.name, &scope_config.endpoints)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
tools.push(
|
||||
endpoint_tools_to_mcp_tools(vec![endpoint_tool])
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(ListToolsResult { tools, next_cursor: None })
|
||||
}
|
||||
|
||||
@@ -18,11 +18,10 @@ use crate::HTTP_CLIENT;
|
||||
pub fn check_scopes(authed: &ApiAuthed) -> Result<(), ErrorData> {
|
||||
let scopes = authed.scopes.as_ref();
|
||||
if scopes.is_none()
|
||||
|| scopes.unwrap().iter().all(|scope| {
|
||||
!scope.starts_with("mcp:all")
|
||||
&& !scope.starts_with("mcp:favorites")
|
||||
&& !scope.starts_with("mcp:hub:")
|
||||
})
|
||||
|| scopes
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|scope| !scope.starts_with("mcp:"))
|
||||
{
|
||||
tracing::error!("Unauthorized: missing mcp scope");
|
||||
return Err(ErrorData::internal_error(
|
||||
@@ -141,7 +140,6 @@ pub async fn get_items<T: for<'a> sqlx::FromRow<'a, sqlx::postgres::PgRow> + Sen
|
||||
workspace_id: &str,
|
||||
scope_type: &str,
|
||||
item_type: &str,
|
||||
scope_path: Option<&str>,
|
||||
) -> Result<Vec<T>, ErrorData> {
|
||||
let mut sqlb = SqlBuilder::select_from(&format!("{} as o", item_type));
|
||||
let fields = vec!["o.path", "o.summary", "o.description", "o.schema"];
|
||||
@@ -159,23 +157,6 @@ pub async fn get_items<T: for<'a> sqlx::FromRow<'a, sqlx::postgres::PgRow> + Sen
|
||||
sqlb.and_where("(o.no_main_func IS NOT TRUE OR o.no_main_func IS NULL)");
|
||||
}
|
||||
|
||||
// scope path is always a folder path, format is f/my_folder/*
|
||||
if let Some(scope_path) = scope_path {
|
||||
if scope_path.split("/").count() != 3
|
||||
|| !scope_path.starts_with("f/")
|
||||
|| !scope_path.ends_with("/*")
|
||||
{
|
||||
return Err(ErrorData::internal_error(
|
||||
format!(
|
||||
"Invalid folder format: {}, expected format is f/my_folder/*",
|
||||
scope_path
|
||||
),
|
||||
None,
|
||||
));
|
||||
}
|
||||
sqlb.and_where_like_left("o.path", &scope_path[..scope_path.len() - 2]);
|
||||
}
|
||||
|
||||
sqlb.order_by(
|
||||
if item_type == "flow" {
|
||||
"o.edited_at"
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
pub mod models;
|
||||
pub mod database;
|
||||
pub mod schema;
|
||||
pub mod transform;
|
||||
pub mod transform;
|
||||
pub mod scope_matcher;
|
||||
@@ -0,0 +1,229 @@
|
||||
//! MCP Scope matching utilities
|
||||
//!
|
||||
//! Contains utilities for parsing and matching MCP token scopes to determine
|
||||
//! which scripts, flows, and endpoints a token has access to.
|
||||
|
||||
use rmcp::ErrorData;
|
||||
|
||||
/// Configuration for MCP scopes parsed from token scopes
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct McpScopeConfig {
|
||||
/// Script paths/patterns allowed by this token
|
||||
pub scripts: Vec<String>,
|
||||
/// Flow paths/patterns allowed by this token
|
||||
pub flows: Vec<String>,
|
||||
/// Endpoint names/patterns allowed by this token
|
||||
pub endpoints: Vec<String>,
|
||||
/// Whether this is a legacy "all" scope
|
||||
pub all: bool,
|
||||
/// Whether this is a "favorites" scope
|
||||
pub favorites: bool,
|
||||
/// Whether a granular scope is detected
|
||||
pub granular: bool,
|
||||
/// Hub app filter (if any)
|
||||
pub hub_apps: Option<String>,
|
||||
}
|
||||
|
||||
/// Parse MCP scopes from token scope strings
|
||||
pub fn parse_mcp_scopes(scopes: &[String]) -> Result<McpScopeConfig, ErrorData> {
|
||||
let mut config = McpScopeConfig::default();
|
||||
|
||||
for scope in scopes {
|
||||
if !scope.starts_with("mcp:") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if scope == "mcp:all" {
|
||||
// Legacy scope: grant access to everything
|
||||
config.all = true;
|
||||
config.scripts.push("*".to_string());
|
||||
config.flows.push("*".to_string());
|
||||
config.endpoints.push("*".to_string());
|
||||
continue;
|
||||
}
|
||||
|
||||
if scope == "mcp:favorites" {
|
||||
// Legacy favorites scope
|
||||
config.favorites = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Legacy folder scope: mcp:all:f/folder/*
|
||||
if scope.starts_with("mcp:all:") {
|
||||
if let Some(folder_pattern) = scope.strip_prefix("mcp:all:") {
|
||||
// Parse as folder pattern - add to both scripts and flows. Also add all endpoints.
|
||||
config.scripts.push(folder_pattern.to_string());
|
||||
config.flows.push(folder_pattern.to_string());
|
||||
config.endpoints.push("*".to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if scope.starts_with("mcp:hub:") {
|
||||
// Legacy hub scope
|
||||
if let Some(apps) = scope.strip_prefix("mcp:hub:") {
|
||||
config.hub_apps = Some(apps.to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(resources) = scope.strip_prefix("mcp:scripts:") {
|
||||
// New granular script scope: mcp:scripts:path1,path2,f/folder/*
|
||||
config.scripts.extend(parse_resource_list(resources)?);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(resources) = scope.strip_prefix("mcp:flows:") {
|
||||
// New granular flow scope: mcp:flows:path1,path2,f/folder/*
|
||||
config.flows.extend(parse_resource_list(resources)?);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(resources) = scope.strip_prefix("mcp:endpoints:") {
|
||||
// New granular endpoint scope: mcp:endpoints:name1,name2
|
||||
config.endpoints.extend(parse_resource_list(resources)?);
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::warn!("Unrecognized MCP scope format: {}", scope);
|
||||
}
|
||||
|
||||
config.granular = !config.all && !config.favorites;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Parse comma-separated resource list
|
||||
fn parse_resource_list(resources: &str) -> Result<Vec<String>, ErrorData> {
|
||||
if resources.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
Ok(resources
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Check if a resource path matches any pattern in the allowed list
|
||||
pub fn is_resource_allowed(resource_path: &str, allowed_patterns: &[String]) -> bool {
|
||||
if allowed_patterns.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wildcard grants all access
|
||||
if allowed_patterns.contains(&"*".to_string()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check against each pattern
|
||||
for pattern in allowed_patterns {
|
||||
if resource_matches_pattern(resource_path, pattern) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if a resource path matches a pattern (supports wildcards like f/folder/*)
|
||||
fn resource_matches_pattern(resource_path: &str, pattern: &str) -> bool {
|
||||
// Exact match
|
||||
if pattern == resource_path {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wildcard pattern matching
|
||||
if pattern.ends_with("/*") {
|
||||
let prefix = &pattern[..pattern.len() - 2];
|
||||
|
||||
if !resource_path.starts_with(prefix) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the resource is exactly the prefix, it matches
|
||||
if resource_path.len() == prefix.len() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the resource is longer, the next character must be '/' for a valid match
|
||||
// This prevents "u/user" from matching "u/use/*"
|
||||
return resource_path.chars().nth(prefix.len()) == Some('/');
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_legacy_scopes() {
|
||||
let scopes = vec!["mcp:all".to_string()];
|
||||
let config = parse_mcp_scopes(&scopes).unwrap();
|
||||
assert!(config.all);
|
||||
assert_eq!(config.scripts, vec!["*"]);
|
||||
assert_eq!(config.flows, vec!["*"]);
|
||||
assert_eq!(config.endpoints, vec!["*"]);
|
||||
|
||||
let scopes = vec!["mcp:favorites".to_string()];
|
||||
let config = parse_mcp_scopes(&scopes).unwrap();
|
||||
assert!(config.favorites);
|
||||
|
||||
let scopes = vec!["mcp:hub:slack".to_string()];
|
||||
let config = parse_mcp_scopes(&scopes).unwrap();
|
||||
assert_eq!(config.hub_apps, Some("slack".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_granular_scopes() {
|
||||
let scopes = vec![
|
||||
"mcp:scripts:u/admin/script1,u/admin/script2".to_string(),
|
||||
"mcp:flows:f/automation/*".to_string(),
|
||||
"mcp:endpoints:list_jobs,get_job".to_string(),
|
||||
];
|
||||
let config = parse_mcp_scopes(&scopes).unwrap();
|
||||
|
||||
assert_eq!(config.scripts, vec!["u/admin/script1", "u/admin/script2"]);
|
||||
assert_eq!(config.flows, vec!["f/automation/*"]);
|
||||
assert_eq!(config.endpoints, vec!["list_jobs", "get_job"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_matching() {
|
||||
// Exact match
|
||||
assert!(resource_matches_pattern("u/admin/script", "u/admin/script"));
|
||||
|
||||
// Wildcard folder match
|
||||
assert!(resource_matches_pattern("f/folder/script", "f/folder/*"));
|
||||
assert!(resource_matches_pattern(
|
||||
"f/folder/sub/script",
|
||||
"f/folder/*"
|
||||
));
|
||||
|
||||
// Should NOT match - prefix is not complete
|
||||
assert!(!resource_matches_pattern("u/username", "u/user/*"));
|
||||
|
||||
// Should match - exact prefix
|
||||
assert!(resource_matches_pattern("u/user/script", "u/user/*"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_resource_allowed() {
|
||||
let patterns = vec!["u/admin/script1".to_string(), "f/folder/*".to_string()];
|
||||
|
||||
assert!(is_resource_allowed("u/admin/script1", &patterns));
|
||||
assert!(is_resource_allowed("f/folder/anything", &patterns));
|
||||
assert!(!is_resource_allowed("u/other/script", &patterns));
|
||||
|
||||
// Test wildcard
|
||||
let wildcard = vec!["*".to_string()];
|
||||
assert!(is_resource_allowed("any/path", &wildcard));
|
||||
|
||||
// Test empty patterns
|
||||
let empty: Vec<String> = vec![];
|
||||
assert!(!is_resource_allowed("any/path", &empty));
|
||||
}
|
||||
}
|
||||
@@ -472,7 +472,7 @@ pub async fn get_resource_value_interpolated_internal(
|
||||
// This is a special syntax to help debugging ducklake catalogs stored in the instance
|
||||
if let Some(dbname) = path.strip_prefix("INSTANCE_DUCKLAKE_CATALOG/") {
|
||||
require_super_admin(db, &authed.email).await?;
|
||||
let pg_creds = parse_postgres_url(&get_database_url().await?)?;
|
||||
let pg_creds = parse_postgres_url(&get_database_url().await?.as_str().await)?;
|
||||
return Ok(Some(serde_json::json!({
|
||||
"dbname": dbname,
|
||||
"host": pg_creds.host,
|
||||
@@ -738,6 +738,31 @@ async fn create_resource(
|
||||
let res_value = resource.value.unwrap_or_default();
|
||||
let raw_json = sqlx::types::Json(res_value.as_ref());
|
||||
|
||||
if resource.path.starts_with("f/app_themes/") {
|
||||
sqlx::query!(
|
||||
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_themes', 'App Themes', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING",
|
||||
w_id,
|
||||
authed.username,
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
} else if resource.path.starts_with("f/app_custom/") {
|
||||
sqlx::query!(
|
||||
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_custom', 'App Custom Components', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING",
|
||||
w_id,
|
||||
authed.username,
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
} else if resource.path.starts_with("f/app_groups/") {
|
||||
sqlx::query!(
|
||||
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_groups', 'App Groups', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING",
|
||||
w_id,
|
||||
authed.username,
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
}
|
||||
sqlx::query!(
|
||||
"INSERT INTO resource
|
||||
(workspace_id, path, value, description, resource_type, created_by, edited_at)
|
||||
|
||||
@@ -647,8 +647,7 @@ async fn setup_ducklake_catalog_db_inner(
|
||||
) -> Result<()> {
|
||||
require_super_admin(db, &authed.email).await?;
|
||||
logs.super_admin = "OK".to_string();
|
||||
let pg_creds = &get_database_url().await?;
|
||||
let pg_creds = parse_postgres_url(pg_creds)?;
|
||||
let pg_creds = parse_postgres_url(&get_database_url().await?.as_str().await)?;
|
||||
logs.database_credentials = "OK".to_string();
|
||||
|
||||
// Validate name to ensure it only contains alphanumeric characters
|
||||
@@ -695,7 +694,7 @@ async fn setup_ducklake_catalog_db_inner(
|
||||
"postgres://{user}:{password}@{host}:{port}/{dbname}?sslmode={sslmode}",
|
||||
user = urlencoding::encode(&pg_creds.username.unwrap_or_else(|| "postgres".to_string())),
|
||||
password = urlencoding::encode(&pg_creds.password.as_deref().unwrap_or("")),
|
||||
host = urlencoding::encode(&pg_creds.host),
|
||||
host = &pg_creds.host,
|
||||
port = pg_creds.port.unwrap_or(5432),
|
||||
dbname = dbname,
|
||||
sslmode = ssl_mode
|
||||
|
||||
@@ -28,7 +28,6 @@ impl TriggerCrud for EmailTrigger {
|
||||
|
||||
const TABLE_NAME: &'static str = "";
|
||||
const TRIGGER_TYPE: &'static str = "";
|
||||
const SUPPORTS_ENABLED: bool = false;
|
||||
const SUPPORTS_SERVER_STATE: bool = false;
|
||||
const SUPPORTS_TEST_CONNECTION: bool = false;
|
||||
const ROUTE_PREFIX: &'static str = "/email_triggers";
|
||||
|
||||
@@ -25,7 +25,6 @@ impl TriggerCrud for GcpTrigger {
|
||||
|
||||
const TABLE_NAME: &'static str = "";
|
||||
const TRIGGER_TYPE: &'static str = "";
|
||||
const SUPPORTS_ENABLED: bool = false;
|
||||
const SUPPORTS_SERVER_STATE: bool = false;
|
||||
const SUPPORTS_TEST_CONNECTION: bool = false;
|
||||
const ROUTE_PREFIX: &'static str = "/gcp_triggers";
|
||||
|
||||
@@ -50,7 +50,6 @@ pub trait TriggerCrud: Send + Sync + 'static {
|
||||
|
||||
const TABLE_NAME: &'static str;
|
||||
const TRIGGER_TYPE: &'static str;
|
||||
const SUPPORTS_ENABLED: bool;
|
||||
const SUPPORTS_SERVER_STATE: bool;
|
||||
const SUPPORTS_TEST_CONNECTION: bool;
|
||||
const ROUTE_PREFIX: &'static str;
|
||||
@@ -144,10 +143,11 @@ pub trait TriggerCrud: Send + Sync + 'static {
|
||||
"email",
|
||||
"edited_at",
|
||||
"extra_perms",
|
||||
"enabled",
|
||||
];
|
||||
|
||||
if Self::SUPPORTS_SERVER_STATE {
|
||||
fields.extend_from_slice(&["enabled", "server_id", "last_server_ping", "error"]);
|
||||
fields.extend_from_slice(&["server_id", "last_server_ping", "error"]);
|
||||
}
|
||||
|
||||
fields.extend_from_slice(&["error_handler_path", "error_handler_args", "retry"]);
|
||||
@@ -206,6 +206,10 @@ pub trait TriggerCrud: Send + Sync + 'static {
|
||||
Ok(deleted > 0)
|
||||
}
|
||||
|
||||
async fn set_enabled_extra_action(&self, _: &mut PgConnection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_enabled(
|
||||
&self,
|
||||
authed: &ApiAuthed,
|
||||
@@ -214,38 +218,59 @@ pub trait TriggerCrud: Send + Sync + 'static {
|
||||
path: &str,
|
||||
enabled: bool,
|
||||
) -> Result<bool> {
|
||||
if !Self::SUPPORTS_SERVER_STATE {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Enable/disable not supported for this trigger type".to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let updated = if Self::SUPPORTS_SERVER_STATE {
|
||||
sqlx::query(&format!(
|
||||
r#"
|
||||
UPDATE
|
||||
{}
|
||||
SET
|
||||
enabled = $1,
|
||||
email = $2,
|
||||
edited_by = $3,
|
||||
edited_at = now(),
|
||||
server_id = NULL,
|
||||
error = NULL
|
||||
WHERE
|
||||
workspace_id = $4 AND
|
||||
path = $5
|
||||
"#,
|
||||
Self::TABLE_NAME
|
||||
))
|
||||
.bind(enabled)
|
||||
.bind(&authed.email)
|
||||
.bind(&authed.username)
|
||||
.bind(workspace_id)
|
||||
.bind(path)
|
||||
.execute(&mut *tx)
|
||||
.await?
|
||||
.rows_affected()
|
||||
} else {
|
||||
sqlx::query(&format!(
|
||||
r#"
|
||||
UPDATE
|
||||
{}
|
||||
SET
|
||||
enabled = $1,
|
||||
email = $2,
|
||||
edited_by = $3,
|
||||
edited_at = now()
|
||||
WHERE
|
||||
workspace_id = $4 AND
|
||||
path = $5
|
||||
"#,
|
||||
Self::TABLE_NAME
|
||||
))
|
||||
.bind(enabled)
|
||||
.bind(&authed.email)
|
||||
.bind(&authed.username)
|
||||
.bind(workspace_id)
|
||||
.bind(path)
|
||||
.execute(&mut *tx)
|
||||
.await?
|
||||
.rows_affected()
|
||||
};
|
||||
|
||||
let updated = sqlx::query(&format!(
|
||||
r#"
|
||||
UPDATE
|
||||
{}
|
||||
SET
|
||||
enabled = $1,
|
||||
email = $2,
|
||||
edited_by = $3,
|
||||
edited_at = now(),
|
||||
server_id = NULL,
|
||||
error = NULL
|
||||
WHERE
|
||||
workspace_id = $4 AND
|
||||
path = $5
|
||||
"#,
|
||||
Self::TABLE_NAME
|
||||
))
|
||||
.bind(enabled)
|
||||
.bind(&authed.email)
|
||||
.bind(&authed.username)
|
||||
.bind(workspace_id)
|
||||
.bind(path)
|
||||
.execute(&mut *tx)
|
||||
.await?
|
||||
.rows_affected();
|
||||
self.set_enabled_extra_action(&mut *tx).await?;
|
||||
|
||||
Ok(updated > 0)
|
||||
}
|
||||
@@ -296,10 +321,11 @@ pub trait TriggerCrud: Send + Sync + 'static {
|
||||
"email",
|
||||
"edited_at",
|
||||
"extra_perms",
|
||||
"enabled",
|
||||
];
|
||||
|
||||
if Self::SUPPORTS_SERVER_STATE {
|
||||
fields.extend_from_slice(&["enabled", "server_id", "last_server_ping", "error"]);
|
||||
fields.extend_from_slice(&["server_id", "last_server_ping", "error"]);
|
||||
}
|
||||
|
||||
fields.extend_from_slice(&["error_handler_path", "error_handler_args", "retry"]);
|
||||
@@ -333,6 +359,7 @@ pub trait TriggerCrud: Send + Sync + 'static {
|
||||
.sql()
|
||||
.map_err(|e| Error::InternalErr(format!("SQL error: {}", e)))?;
|
||||
|
||||
tracing::info!("SQL: {}", sql);
|
||||
let triggers = sqlx::query_as(&sql).fetch_all(&mut *tx).await?;
|
||||
|
||||
Ok(triggers)
|
||||
@@ -346,11 +373,8 @@ pub fn trigger_routes<T: TriggerCrud + 'static>() -> Router {
|
||||
.route("/get/*path", get(get_trigger::<T>))
|
||||
.route("/update/*path", post(update_trigger::<T>))
|
||||
.route("/delete/*path", delete(delete_trigger::<T>))
|
||||
.route("/exists/*path", get(exists_trigger::<T>));
|
||||
|
||||
if T::SUPPORTS_ENABLED {
|
||||
router = router.route("/setenabled/*path", post(set_enabled_trigger::<T>));
|
||||
}
|
||||
.route("/exists/*path", get(exists_trigger::<T>))
|
||||
.route("/setenabled/*path", post(set_enabled_trigger::<T>));
|
||||
|
||||
if T::SUPPORTS_TEST_CONNECTION {
|
||||
router = router.route("/test", post(test_connection::<T>));
|
||||
|
||||
@@ -196,6 +196,7 @@ pub async fn insert_new_trigger_into_db(
|
||||
summary,
|
||||
description,
|
||||
is_flow,
|
||||
enabled,
|
||||
request_type,
|
||||
authentication_method,
|
||||
http_method,
|
||||
@@ -209,7 +210,7 @@ pub async fn insert_new_trigger_into_db(
|
||||
retry
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), $19, $20, $21, $22
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, now(), $20, $21, $22, $23
|
||||
)
|
||||
"#,
|
||||
w_id,
|
||||
@@ -224,6 +225,7 @@ pub async fn insert_new_trigger_into_db(
|
||||
trigger.config.summary,
|
||||
trigger.config.description,
|
||||
trigger.base.is_flow,
|
||||
trigger.base.enabled.unwrap_or(true),
|
||||
request_type as _,
|
||||
trigger.config.authentication_method as _,
|
||||
trigger.config.http_method as _,
|
||||
@@ -356,7 +358,6 @@ impl TriggerCrud for HttpTrigger {
|
||||
|
||||
const TABLE_NAME: &'static str = "http_trigger";
|
||||
const TRIGGER_TYPE: &'static str = "http";
|
||||
const SUPPORTS_ENABLED: bool = false;
|
||||
const SUPPORTS_SERVER_STATE: bool = false;
|
||||
const SUPPORTS_TEST_CONNECTION: bool = false;
|
||||
const ROUTE_PREFIX: &'static str = "/http_triggers";
|
||||
@@ -482,22 +483,23 @@ impl TriggerCrud for HttpTrigger {
|
||||
script_path = $7,
|
||||
path = $8,
|
||||
is_flow = $9,
|
||||
http_method = $10,
|
||||
static_asset_config = $11,
|
||||
edited_by = $12,
|
||||
email = $13,
|
||||
request_type = $14,
|
||||
authentication_method = $15,
|
||||
summary = $16,
|
||||
description = $17,
|
||||
enabled = $10,
|
||||
http_method = $11,
|
||||
static_asset_config = $12,
|
||||
edited_by = $13,
|
||||
email = $14,
|
||||
request_type = $15,
|
||||
authentication_method = $16,
|
||||
summary = $17,
|
||||
description = $18,
|
||||
edited_at = now(),
|
||||
is_static_website = $18,
|
||||
error_handler_path = $19,
|
||||
error_handler_args = $20,
|
||||
retry = $21
|
||||
is_static_website = $19,
|
||||
error_handler_path = $20,
|
||||
error_handler_args = $21,
|
||||
retry = $22
|
||||
WHERE
|
||||
workspace_id = $22 AND
|
||||
path = $23
|
||||
workspace_id = $23 AND
|
||||
path = $24
|
||||
"#,
|
||||
route_path,
|
||||
&route_path_key,
|
||||
@@ -508,6 +510,7 @@ impl TriggerCrud for HttpTrigger {
|
||||
trigger.base.script_path,
|
||||
trigger.base.path,
|
||||
trigger.base.is_flow,
|
||||
trigger.base.enabled.unwrap_or(true),
|
||||
trigger.config.http_method as _,
|
||||
trigger.config.static_asset_config as _,
|
||||
&authed.username,
|
||||
@@ -539,22 +542,23 @@ impl TriggerCrud for HttpTrigger {
|
||||
script_path = $4,
|
||||
path = $5,
|
||||
is_flow = $6,
|
||||
http_method = $7,
|
||||
static_asset_config = $8,
|
||||
edited_by = $9,
|
||||
email = $10,
|
||||
request_type = $11,
|
||||
authentication_method = $12,
|
||||
summary = $13,
|
||||
description = $14,
|
||||
enabled = $7,
|
||||
http_method = $8,
|
||||
static_asset_config = $9,
|
||||
edited_by = $10,
|
||||
email = $11,
|
||||
request_type = $12,
|
||||
authentication_method = $13,
|
||||
summary = $14,
|
||||
description = $15,
|
||||
edited_at = now(),
|
||||
is_static_website = $15,
|
||||
error_handler_path = $16,
|
||||
error_handler_args = $17,
|
||||
retry = $18
|
||||
is_static_website = $16,
|
||||
error_handler_path = $17,
|
||||
error_handler_args = $18,
|
||||
retry = $19
|
||||
WHERE
|
||||
workspace_id = $19 AND
|
||||
path = $20
|
||||
workspace_id = $20 AND
|
||||
path = $21
|
||||
"#,
|
||||
trigger.config.wrap_body,
|
||||
trigger.config.raw_string,
|
||||
@@ -562,6 +566,7 @@ impl TriggerCrud for HttpTrigger {
|
||||
trigger.base.script_path,
|
||||
trigger.base.path,
|
||||
trigger.base.is_flow,
|
||||
trigger.base.enabled.unwrap_or(true),
|
||||
trigger.config.http_method as _,
|
||||
trigger.config.static_asset_config as _,
|
||||
&authed.username,
|
||||
@@ -586,6 +591,10 @@ impl TriggerCrud for HttpTrigger {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_enabled_extra_action(&self, tx: &mut PgConnection) -> Result<()> {
|
||||
increase_trigger_version(tx).await
|
||||
}
|
||||
|
||||
async fn delete_by_path(
|
||||
&self,
|
||||
tx: &mut PgConnection,
|
||||
|
||||
@@ -261,7 +261,8 @@ pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, Route
|
||||
FROM
|
||||
http_trigger
|
||||
WHERE
|
||||
http_method = $1
|
||||
http_method = $1 AND
|
||||
enabled is TRUE
|
||||
"#,
|
||||
&http_method as &HttpMethod
|
||||
)
|
||||
|
||||
@@ -28,7 +28,6 @@ impl TriggerCrud for KafkaTrigger {
|
||||
|
||||
const TABLE_NAME: &'static str = "";
|
||||
const TRIGGER_TYPE: &'static str = "";
|
||||
const SUPPORTS_ENABLED: bool = false;
|
||||
const SUPPORTS_SERVER_STATE: bool = false;
|
||||
const SUPPORTS_TEST_CONNECTION: bool = false;
|
||||
const ROUTE_PREFIX: &'static str = "/kafka_triggers";
|
||||
|
||||
@@ -69,12 +69,12 @@ pub trait Listener: TriggerCrud + TriggerJobArgs {
|
||||
"email",
|
||||
"edited_at",
|
||||
"extra_perms",
|
||||
"enabled",
|
||||
"error_handler_path",
|
||||
"error_handler_args",
|
||||
"retry",
|
||||
];
|
||||
|
||||
if Self::SUPPORTS_SERVER_STATE {
|
||||
fields.extend_from_slice(&["enabled", "server_id", "last_server_ping", "error"]);
|
||||
}
|
||||
fields.extend_from_slice(&["error_handler_path", "error_handler_args", "retry"]);
|
||||
fields.extend_from_slice(Self::ADDITIONAL_SELECT_FIELDS);
|
||||
|
||||
let mut sqlb = SqlBuilder::select_from(Self::TABLE_NAME);
|
||||
|
||||
@@ -47,6 +47,7 @@ pub struct BaseTrigger {
|
||||
pub workspace_id: String,
|
||||
pub path: String,
|
||||
pub script_path: String,
|
||||
pub enabled: Option<bool>,
|
||||
pub is_flow: bool,
|
||||
pub edited_by: String,
|
||||
pub email: String,
|
||||
@@ -56,7 +57,6 @@ pub struct BaseTrigger {
|
||||
|
||||
#[derive(Debug, FromRow, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerState {
|
||||
pub enabled: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub server_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -26,7 +26,6 @@ impl TriggerCrud for MqttTrigger {
|
||||
|
||||
const TABLE_NAME: &'static str = "mqtt_trigger";
|
||||
const TRIGGER_TYPE: &'static str = "mqtt";
|
||||
const SUPPORTS_ENABLED: bool = true;
|
||||
const SUPPORTS_SERVER_STATE: bool = true;
|
||||
const SUPPORTS_TEST_CONNECTION: bool = true;
|
||||
const ROUTE_PREFIX: &'static str = "/mqtt_triggers";
|
||||
|
||||
@@ -25,7 +25,6 @@ impl TriggerCrud for NatsTrigger {
|
||||
|
||||
const TABLE_NAME: &'static str = "";
|
||||
const TRIGGER_TYPE: &'static str = "";
|
||||
const SUPPORTS_ENABLED: bool = false;
|
||||
const SUPPORTS_SERVER_STATE: bool = false;
|
||||
const SUPPORTS_TEST_CONNECTION: bool = false;
|
||||
const ROUTE_PREFIX: &'static str = "/nats_triggers";
|
||||
|
||||
@@ -47,7 +47,6 @@ impl TriggerCrud for PostgresTrigger {
|
||||
|
||||
const TABLE_NAME: &'static str = "postgres_trigger";
|
||||
const TRIGGER_TYPE: &'static str = "postgres";
|
||||
const SUPPORTS_ENABLED: bool = true;
|
||||
const SUPPORTS_SERVER_STATE: bool = true;
|
||||
const SUPPORTS_TEST_CONNECTION: bool = true;
|
||||
const ROUTE_PREFIX: &'static str = "/postgres_triggers";
|
||||
@@ -64,7 +63,6 @@ impl TriggerCrud for PostgresTrigger {
|
||||
DeployedObject::PostgresTrigger { path }
|
||||
}
|
||||
|
||||
|
||||
async fn create_trigger(
|
||||
&self,
|
||||
db: &DB,
|
||||
|
||||
@@ -350,14 +350,14 @@ pub async fn get_raw_postgres_connection(
|
||||
}
|
||||
|
||||
let connector = build_tls_connector(ssl_mode, database.root_certificate_pem.as_ref())?;
|
||||
|
||||
let client = if let Some(connector) = connector {
|
||||
let (client, connection) = config.connect(connector).await.map_err(to_anyhow)?;
|
||||
tokio::spawn(async move {
|
||||
tracing::info!("Successfully connected to PostgreSQL database for trigger execution");
|
||||
if let Err(e) = connection.await {
|
||||
tracing::debug!("{:#?}", e);
|
||||
tracing::debug!("Error during PostgreSQL trigger connection: {:#?}", e);
|
||||
};
|
||||
tracing::info!("Successfully Connected into database");
|
||||
tracing::info!("PostgreSQL trigger connection closed");
|
||||
});
|
||||
client
|
||||
} else {
|
||||
|
||||
@@ -26,7 +26,6 @@ impl TriggerCrud for SqsTrigger {
|
||||
|
||||
const TABLE_NAME: &'static str = "";
|
||||
const TRIGGER_TYPE: &'static str = "";
|
||||
const SUPPORTS_ENABLED: bool = false;
|
||||
const SUPPORTS_SERVER_STATE: bool = false;
|
||||
const SUPPORTS_TEST_CONNECTION: bool = false;
|
||||
const ROUTE_PREFIX: &'static str = "/sqs_triggers";
|
||||
|
||||
@@ -30,7 +30,6 @@ impl TriggerCrud for WebsocketTrigger {
|
||||
|
||||
const TABLE_NAME: &'static str = "websocket_trigger";
|
||||
const TRIGGER_TYPE: &'static str = "websocket";
|
||||
const SUPPORTS_ENABLED: bool = true;
|
||||
const SUPPORTS_SERVER_STATE: bool = true;
|
||||
const SUPPORTS_TEST_CONNECTION: bool = true;
|
||||
const ROUTE_PREFIX: &'static str = "/websocket_triggers";
|
||||
|
||||
@@ -69,6 +69,8 @@ struct WorkerPing {
|
||||
memory_usage: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
wm_memory_usage: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
job_isolation: Option<String>,
|
||||
}
|
||||
|
||||
// #[derive(Serialize, Deserialize)]
|
||||
@@ -97,8 +99,8 @@ async fn list_worker_pings(
|
||||
let rows = sqlx::query_as!(
|
||||
WorkerPing,
|
||||
"SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed,
|
||||
CASE WHEN $4 IS TRUE THEN current_job_id ELSE NULL END as last_job_id, CASE WHEN $4 IS TRUE THEN current_job_workspace_id ELSE NULL END as last_job_workspace_id,
|
||||
custom_tags, worker_group, wm_version, occupancy_rate, occupancy_rate_15s, occupancy_rate_5m, occupancy_rate_30m, memory, vcpus, memory_usage, wm_memory_usage
|
||||
CASE WHEN $4 IS TRUE THEN current_job_id ELSE NULL END as last_job_id, CASE WHEN $4 IS TRUE THEN current_job_workspace_id ELSE NULL END as last_job_workspace_id,
|
||||
custom_tags, worker_group, wm_version, occupancy_rate, occupancy_rate_15s, occupancy_rate_5m, occupancy_rate_30m, memory, vcpus, memory_usage, wm_memory_usage, job_isolation
|
||||
FROM worker_ping
|
||||
WHERE ($1::integer IS NULL AND ping_at > now() - interval '5 minute') OR (ping_at > now() - ($1 || ' seconds')::interval)
|
||||
ORDER BY ping_at desc LIMIT $2 OFFSET $3",
|
||||
|
||||
@@ -2455,38 +2455,6 @@ async fn create_workspace(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_themes', 'App Themes', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING",
|
||||
nw.id,
|
||||
username,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_custom', 'App Custom Components', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING",
|
||||
nw.id,
|
||||
username,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_groups', 'App Groups', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING",
|
||||
nw.id,
|
||||
username,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO resource (workspace_id, path, value, description, resource_type, created_by, edited_at) VALUES ($1, 'f/app_themes/theme_0', '{\"name\": \"Default Theme\", \"value\": \"\"}', 'The default app theme', 'app_theme', $2, now()) ON CONFLICT DO NOTHING",
|
||||
nw.id,
|
||||
username,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
|
||||
@@ -448,6 +448,17 @@ pub(crate) async fn delete_workspace(
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
}
|
||||
|
||||
sqlx::query!("DELETE FROM ai_agent_memory WHERE workspace_id = $1", &w_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM flow_conversation WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!("DELETE FROM workspace_env WHERE workspace_id = $1", &w_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
@@ -7,14 +7,14 @@ edition.workspace = true
|
||||
[features]
|
||||
default = []
|
||||
enterprise = []
|
||||
private = []
|
||||
private = ["dep:aws-sdk-rds"]
|
||||
jemalloc = ["dep:tikv-jemalloc-ctl"]
|
||||
tantivy = []
|
||||
prometheus = ["dep:prometheus"]
|
||||
loki = ["dep:tracing-loki"]
|
||||
benchmark = []
|
||||
parquet = ["dep:object_store", "dep:aws-config", "dep:aws-sdk-sts", "dep:aws-smithy-types-convert", "dep:datafusion"]
|
||||
aws_auth = ["dep:aws-sdk-sts", "dep:aws-config"]
|
||||
parquet = ["dep:object_store", "dep:aws-sdk-sts", "dep:aws-smithy-types-convert", "dep:datafusion"]
|
||||
aws_auth = ["dep:aws-sdk-sts"]
|
||||
otel = ["dep:opentelemetry-semantic-conventions", "dep:opentelemetry-otlp", "dep:opentelemetry_sdk",
|
||||
"dep:opentelemetry", "dep:tracing-opentelemetry", "dep:opentelemetry-appender-tracing", "dep:tonic"]
|
||||
smtp = ["dep:mail-send"]
|
||||
@@ -62,12 +62,15 @@ tracing-loki = { version = "^0", optional = true }
|
||||
magic-crypt.workspace = true
|
||||
object_store = { workspace = true, optional = true }
|
||||
prometheus = { workspace = true, optional = true }
|
||||
aws-config = { workspace = true, optional = true }
|
||||
aws-config.workspace = true
|
||||
aws-sdk-sts = { workspace = true, optional = true }
|
||||
aws-credential-types.workspace = true
|
||||
aws-smithy-types.workspace = true
|
||||
base64.workspace = true
|
||||
bitflags.workspace = true
|
||||
|
||||
aws-smithy-types-convert = { workspace = true, optional = true }
|
||||
aws-sdk-rds = { workspace = true, optional = true }
|
||||
indexmap.workspace = true
|
||||
bytes.workspace = true
|
||||
mail-send = { workspace = true, optional = true }
|
||||
@@ -86,6 +89,7 @@ openidconnect = { workspace = true, optional = true }
|
||||
strum.workspace = true
|
||||
strum_macros.workspace = true
|
||||
url.workspace = true
|
||||
urlencoding.workspace = true
|
||||
async-recursion.workspace = true
|
||||
|
||||
semver.workspace = true
|
||||
|
||||
@@ -10,7 +10,6 @@ lazy_static::lazy_static! {
|
||||
static ref OPENAI_AZURE_BASE_PATH: Option<String> = std::env::var("OPENAI_AZURE_BASE_PATH").ok();
|
||||
}
|
||||
|
||||
pub const AZURE_API_VERSION: &str = "2025-04-01-preview";
|
||||
pub const OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Clone)]
|
||||
@@ -27,11 +26,18 @@ pub enum AIProvider {
|
||||
OpenRouter,
|
||||
TogetherAI,
|
||||
CustomAI,
|
||||
#[serde(rename = "aws_bedrock")]
|
||||
AWSBedrock,
|
||||
}
|
||||
|
||||
impl AIProvider {
|
||||
/// Get the base URL for the AI provider
|
||||
pub async fn get_base_url(&self, resource_base_url: Option<String>, db: &DB) -> Result<String> {
|
||||
pub async fn get_base_url(
|
||||
&self,
|
||||
resource_base_url: Option<String>,
|
||||
region: Option<String>,
|
||||
db: &DB,
|
||||
) -> Result<String> {
|
||||
match self {
|
||||
AIProvider::OpenAI => {
|
||||
// Check for Azure base path override
|
||||
@@ -74,6 +80,10 @@ impl AIProvider {
|
||||
)))
|
||||
}
|
||||
}
|
||||
AIProvider::AWSBedrock => Ok(format!(
|
||||
"https://bedrock-runtime.{}.amazonaws.com",
|
||||
region.unwrap_or_else(|| "us-east-1".to_string())
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,13 +99,12 @@ impl AIProvider {
|
||||
}
|
||||
|
||||
/// Build Azure OpenAI URL with deployment model path
|
||||
pub fn build_azure_openai_url(base_url: &str, model: &str, path: &str) -> String {
|
||||
pub fn build_azure_openai_url(base_url: &str, path: &str) -> String {
|
||||
let base_url = base_url.trim_end_matches('/');
|
||||
|
||||
if base_url.ends_with("/deployments") {
|
||||
format!("{}/{}/{}", base_url, model, path)
|
||||
} else if base_url.ends_with("/openai") {
|
||||
format!("{}/deployments/{}/{}", base_url, model, path)
|
||||
if base_url.ends_with("/openai") {
|
||||
format!("{}/v1/{}", base_url, path)
|
||||
} else if base_url.ends_with("/deployments") {
|
||||
format!("{}/v1/{}", base_url.trim_end_matches("/deployments"), path)
|
||||
} else {
|
||||
format!("{}/{}", base_url, path)
|
||||
}
|
||||
|
||||
@@ -158,6 +158,21 @@ impl JWTAuthClaims {
|
||||
.as_ref()
|
||||
.is_some_and(|token_w_ids| token_w_ids.iter().any(|token_w_id| w_id == token_w_id))
|
||||
}
|
||||
|
||||
pub fn compute_ext_jwt_hash(&self) -> i64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
self.email.hash(&mut hasher);
|
||||
self.username.hash(&mut hasher);
|
||||
self.is_admin.hash(&mut hasher);
|
||||
self.is_operator.hash(&mut hasher);
|
||||
self.groups.hash(&mut hasher);
|
||||
self.folders.hash(&mut hasher);
|
||||
self.workspace_id.hash(&mut hasher);
|
||||
self.workspace_ids.hash(&mut hasher);
|
||||
self.label.hash(&mut hasher);
|
||||
self.scopes.hash(&mut hasher);
|
||||
hasher.finish() as i64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
|
||||
@@ -285,6 +285,21 @@ pub struct FlowData {
|
||||
pub flow: FlowValue,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct FlowNotes {
|
||||
pub notes: Option<Box<RawValue>>,
|
||||
}
|
||||
|
||||
impl FlowData {
|
||||
pub fn notes(&self) -> Option<FlowNotes> {
|
||||
serde_json::from_str::<FlowNotes>(self.raw_flow.get())
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to parse notes into FlowNotes: {}", e);
|
||||
error::Error::internal_err(format!("Failed to parse notes into FlowNotes: {}", e))
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
/// !!!Shouldn't be used. Reverted optimization for ai agent steps.!!!
|
||||
#[derive(Deserialize)]
|
||||
struct RevertedFlowNodeFlow {
|
||||
|
||||
@@ -194,7 +194,7 @@ pub struct FlowValue {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub chat_input_enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub flow_env: Option<HashMap<String, Box<RawValue>>>
|
||||
pub flow_env: Option<HashMap<String, Box<RawValue>>>,
|
||||
}
|
||||
|
||||
impl FlowValue {
|
||||
|
||||
@@ -35,6 +35,8 @@ pub mod bench;
|
||||
pub mod cache;
|
||||
pub mod client;
|
||||
pub mod db;
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
mod db_iam_ee;
|
||||
#[cfg(feature = "private")]
|
||||
pub mod ee;
|
||||
pub mod ee_oss;
|
||||
@@ -341,7 +343,12 @@ pub fn parse_postgres_url(url: &str) -> Result<PostgresUrlComponents, Error> {
|
||||
|
||||
let scheme = parsed_url.scheme().to_string();
|
||||
let username = parsed_url.username().to_string();
|
||||
let username = urlencoding::decode(&username).map_err(to_anyhow)?.to_string();
|
||||
let password = parsed_url.password().map(|p| p.to_string());
|
||||
let password = match password {
|
||||
Some(p) => Some(urlencoding::decode(&p).map_err(to_anyhow)?.to_string()),
|
||||
None => None,
|
||||
};
|
||||
let host = parsed_url
|
||||
.host_str()
|
||||
.ok_or_else(|| Error::BadConfig("Missing host in PostgreSQL URL".to_string()))?
|
||||
@@ -370,27 +377,120 @@ pub fn parse_postgres_url(url: &str) -> Result<PostgresUrlComponents, Error> {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_database_url() -> Result<String, Error> {
|
||||
use std::env::var;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncReadExt;
|
||||
match var("DATABASE_URL_FILE") {
|
||||
Ok(file_path) => {
|
||||
let mut file = File::open(file_path).await?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents).await?;
|
||||
Ok(contents.trim().to_string())
|
||||
#[derive(Clone)]
|
||||
pub enum DatabaseUrl {
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
IamRds(std::sync::Arc<tokio::sync::RwLock<db_iam_ee::IamRdsUrl>>),
|
||||
Static(String),
|
||||
}
|
||||
|
||||
impl DatabaseUrl {
|
||||
pub async fn as_str(&self) -> String {
|
||||
match self {
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
DatabaseUrl::IamRds(rds_url) => {
|
||||
let guard = rds_url.read().await;
|
||||
guard.as_str().to_string()
|
||||
}
|
||||
DatabaseUrl::Static(url) => url.clone(),
|
||||
}
|
||||
Err(_) => var("DATABASE_URL").map_err(|_| {
|
||||
Error::BadConfig(
|
||||
"Either DATABASE_URL_FILE or DATABASE_URL env var is missing".to_string(),
|
||||
)
|
||||
}),
|
||||
}
|
||||
|
||||
pub async fn refresh(&self) -> anyhow::Result<()> {
|
||||
match self {
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
DatabaseUrl::IamRds(rds_url) => rds_url.write().await.refresh().await,
|
||||
DatabaseUrl::Static(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
static DATABASE_URL_CACHE: tokio::sync::OnceCell<DatabaseUrl> =
|
||||
tokio::sync::OnceCell::const_new();
|
||||
|
||||
pub async fn get_database_url() -> Result<DatabaseUrl, Error> {
|
||||
let database_url = DATABASE_URL_CACHE
|
||||
.get_or_try_init(|| async {
|
||||
use std::env::var;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
let url = match var("DATABASE_URL_FILE") {
|
||||
Ok(file_path) => {
|
||||
let mut file = File::open(file_path).await?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents).await?;
|
||||
Ok(contents.trim().to_string())
|
||||
}
|
||||
Err(_) => var("DATABASE_URL").map_err(|_| {
|
||||
Error::BadConfig(
|
||||
"Either DATABASE_URL_FILE or DATABASE_URL env var is missing".to_string(),
|
||||
)
|
||||
}),
|
||||
}?;
|
||||
|
||||
let parsed_url = url::Url::parse(&url)?;
|
||||
|
||||
if parsed_url.password().is_some_and(|x| x == "iamrds") {
|
||||
let region = var("AWS_REGION").map_err(|_| {
|
||||
Error::BadConfig(
|
||||
"AWS_REGION env var is required for IAM RDS authentication".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
tracing::info!("iamrds mode detected, generating IAM RDS URL for region: {region}");
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
{
|
||||
let rds_url = db_iam_ee::generate_database_url(&url, ®ion)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!("Failed to generate IAM database URL: {}", e))
|
||||
})?;
|
||||
tracing::info!("IAM RDS URL generated successfully");
|
||||
Ok::<DatabaseUrl, Error>(DatabaseUrl::IamRds(
|
||||
std::sync::Arc::new(tokio::sync::RwLock::new(rds_url))
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "enterprise", feature = "private")))]
|
||||
{
|
||||
return Err(Error::BadConfig("IAM RDS authentication is not enabled in OSS mode".to_string()));
|
||||
}
|
||||
} else {
|
||||
Ok::<DatabaseUrl, Error>(DatabaseUrl::Static(url.to_string()))
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Check if we need to refresh and do so if necessary
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
if let DatabaseUrl::IamRds(ref rds_url_lock) = database_url {
|
||||
// Check if refresh is needed
|
||||
let needs_refresh = {
|
||||
let read_guard = rds_url_lock.read().await;
|
||||
read_guard.needs_refresh()
|
||||
};
|
||||
|
||||
// If refresh is needed, acquire write lock and refresh
|
||||
if needs_refresh {
|
||||
let mut write_guard = rds_url_lock.write().await;
|
||||
// Double-check after acquiring write lock (another task might have refreshed)
|
||||
if write_guard.needs_refresh() {
|
||||
write_guard.refresh().await.map_err(|e| {
|
||||
Error::InternalErr(format!("Failed to refresh IAM token: {}", e))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the URL string
|
||||
Ok(database_url.clone())
|
||||
}
|
||||
|
||||
pub async fn initial_connection() -> Result<sqlx::Pool<sqlx::Postgres>, error::Error> {
|
||||
let database_url = get_database_url().await?;
|
||||
let database_url = get_database_url().await?.as_str().await;
|
||||
sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(2)
|
||||
.connect_with(sqlx::postgres::PgConnectOptions::from_str(&database_url)?)
|
||||
@@ -402,6 +502,8 @@ pub async fn connect_db(
|
||||
server_mode: bool,
|
||||
indexer_mode: bool,
|
||||
worker_mode: bool,
|
||||
#[cfg(feature = "private")]
|
||||
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) -> anyhow::Result<sqlx::Pool<sqlx::Postgres>> {
|
||||
use anyhow::Context;
|
||||
|
||||
@@ -426,11 +528,58 @@ pub async fn connect_db(
|
||||
}
|
||||
};
|
||||
|
||||
Ok(connect(&database_url, max_connections, worker_mode).await?)
|
||||
|
||||
let pool = connect(database_url.clone(), max_connections, worker_mode).await?;
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
let pool2 = pool.clone();
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
if let DatabaseUrl::IamRds(database_url) = database_url {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = killpill_rx.recv() => {
|
||||
break;
|
||||
}
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {
|
||||
let needs_refresh = {
|
||||
let read_guard = database_url.read().await;
|
||||
read_guard.needs_refresh()
|
||||
};
|
||||
if needs_refresh {
|
||||
let new_url = tokio::time::timeout(std::time::Duration::from_secs(10), get_database_url()).await;
|
||||
match new_url {
|
||||
Ok(Ok(new_url)) => {
|
||||
let new_url = new_url.as_str().await;
|
||||
let connect_options = sqlx::postgres::PgConnectOptions::from_str(&new_url);
|
||||
if let Err(e) = connect_options {
|
||||
tracing::error!("Error parsing IAM RDS URL as connect options, retrying in 10s: {}", e);
|
||||
continue;
|
||||
}
|
||||
pool2.set_connect_options(connect_options.unwrap());
|
||||
tracing::info!("Refreshed IAM RDS URL successfully");
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
tracing::error!("Error refreshing IAM RDS URL, trying again in 10s: {}", e);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Timeout after 10s refreshing IAM RDS URL, trying again in 10 seconds: {}", e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
pub async fn connect(
|
||||
database_url: &str,
|
||||
database_url: DatabaseUrl,
|
||||
max_connections: u32,
|
||||
worker_mode: bool,
|
||||
) -> Result<sqlx::Pool<sqlx::Postgres>, error::Error> {
|
||||
@@ -479,7 +628,7 @@ pub async fn connect(
|
||||
}
|
||||
})
|
||||
.connect_with(
|
||||
sqlx::postgres::PgConnectOptions::from_str(database_url)?.statement_cache_capacity(400),
|
||||
sqlx::postgres::PgConnectOptions::from_str(&database_url.as_str().await)?.statement_cache_capacity(400),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| Error::ConnectingToDatabase(err.to_string()))
|
||||
@@ -490,9 +639,7 @@ type Tag = String;
|
||||
pub use db::DB;
|
||||
|
||||
use crate::{
|
||||
auth::{PermsCache, FLOW_PERMS_CACHE, HASH_PERMS_CACHE},
|
||||
db::{AuthedRef, UserDbWithAuthed},
|
||||
scripts::ScriptHash,
|
||||
auth::{FLOW_PERMS_CACHE, HASH_PERMS_CACHE, PermsCache}, db::{AuthedRef, UserDbWithAuthed}, error::to_anyhow, scripts::ScriptHash
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -396,6 +396,21 @@ pub struct S3Resource {
|
||||
pub port: Option<u16>,
|
||||
}
|
||||
|
||||
impl S3Resource {
|
||||
pub fn endpoint_with_region_fallback(&self, region_fallback: Option<String>) -> String {
|
||||
if self.endpoint.is_empty() {
|
||||
let final_region = if self.region.is_empty() {
|
||||
region_fallback.unwrap_or_else(|| "us-east-1".to_string())
|
||||
} else {
|
||||
self.region.clone()
|
||||
};
|
||||
format!("s3.{}.amazonaws.com", final_region)
|
||||
} else {
|
||||
self.endpoint.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct AzureBlobResource {
|
||||
pub endpoint: Option<String>,
|
||||
@@ -642,7 +657,7 @@ pub async fn build_s3_client(s3_resource_ref: &S3Resource) -> error::Result<Arc<
|
||||
|
||||
let s3_resource = s3_resource_ref.clone();
|
||||
let endpoint = render_endpoint(
|
||||
s3_resource.endpoint,
|
||||
s3_resource.endpoint_with_region_fallback(None),
|
||||
s3_resource.use_ssl,
|
||||
s3_resource.port,
|
||||
s3_resource.path_style,
|
||||
@@ -1216,7 +1231,13 @@ pub fn duckdb_connection_settings_internal(
|
||||
duckdb_settings.push_str("SET s3_url_style='path';\n");
|
||||
}
|
||||
duckdb_settings.push_str(format!("SET s3_region='{}';\n", s3_resource.region).as_str());
|
||||
duckdb_settings.push_str(format!("SET s3_endpoint='{}';\n", s3_resource.endpoint).as_str());
|
||||
duckdb_settings.push_str(
|
||||
format!(
|
||||
"SET s3_endpoint='{}';\n",
|
||||
s3_resource.endpoint_with_region_fallback(None)
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
if !s3_resource.use_ssl {
|
||||
duckdb_settings.push_str("SET s3_use_ssl=0;\n"); // default is true for DuckDB
|
||||
}
|
||||
|
||||
@@ -947,3 +947,9 @@ pub struct ExpiringCacheEntry<T> {
|
||||
pub value: T,
|
||||
pub expiry: std::time::Instant,
|
||||
}
|
||||
|
||||
impl<T> ExpiringCacheEntry<T> {
|
||||
pub fn is_expired(&self) -> bool {
|
||||
self.expiry < std::time::Instant::now()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1301,6 +1301,7 @@ pub struct Ping {
|
||||
pub occupancy_rate_15s: Option<f32>,
|
||||
pub occupancy_rate_5m: Option<f32>,
|
||||
pub occupancy_rate_30m: Option<f32>,
|
||||
pub job_isolation: Option<String>,
|
||||
pub ping_type: PingType,
|
||||
}
|
||||
pub async fn update_ping_http(
|
||||
@@ -1348,6 +1349,7 @@ pub async fn update_ping_http(
|
||||
&insert_ping.version.unwrap(),
|
||||
insert_ping.vcpus,
|
||||
insert_ping.memory,
|
||||
insert_ping.job_isolation,
|
||||
db,
|
||||
)
|
||||
.await?;
|
||||
@@ -1363,6 +1365,7 @@ pub async fn update_ping_http(
|
||||
insert_ping.occupancy_rate_15s,
|
||||
insert_ping.occupancy_rate_5m,
|
||||
insert_ping.occupancy_rate_30m,
|
||||
insert_ping.job_isolation,
|
||||
db,
|
||||
)
|
||||
.await?;
|
||||
@@ -1476,10 +1479,11 @@ pub async fn insert_ping_query(
|
||||
version: &str,
|
||||
vcpus: Option<i64>,
|
||||
memory: Option<i64>,
|
||||
job_isolation: Option<String>,
|
||||
db: &DB,
|
||||
) -> anyhow::Result<()> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO worker_ping (worker_instance, worker, ip, custom_tags, worker_group, dedicated_worker, wm_version, vcpus, memory) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (worker)
|
||||
"INSERT INTO worker_ping (worker_instance, worker, ip, custom_tags, worker_group, dedicated_worker, wm_version, vcpus, memory, job_isolation) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (worker)
|
||||
DO UPDATE set ip = EXCLUDED.ip, custom_tags = EXCLUDED.custom_tags, worker_group = EXCLUDED.worker_group",
|
||||
worker_instance,
|
||||
worker_name,
|
||||
@@ -1489,7 +1493,8 @@ pub async fn insert_ping_query(
|
||||
dw,
|
||||
version,
|
||||
vcpus,
|
||||
memory
|
||||
memory,
|
||||
job_isolation.as_deref()
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
@@ -1506,11 +1511,12 @@ pub async fn update_worker_ping_from_job_query(
|
||||
occupancy_rate_15s: Option<f32>,
|
||||
occupancy_rate_5m: Option<f32>,
|
||||
occupancy_rate_30m: Option<f32>,
|
||||
job_isolation: Option<String>,
|
||||
db: &DB,
|
||||
) -> anyhow::Result<()> {
|
||||
sqlx::query!(
|
||||
"UPDATE worker_ping SET ping_at = now(), current_job_id = $1, current_job_workspace_id = $2, memory_usage = $3, wm_memory_usage = $4,
|
||||
occupancy_rate = $6, occupancy_rate_15s = $7, occupancy_rate_5m = $8, occupancy_rate_30m = $9 WHERE worker = $5",
|
||||
occupancy_rate = $6, occupancy_rate_15s = $7, occupancy_rate_5m = $8, occupancy_rate_30m = $9, job_isolation = $10 WHERE worker = $5",
|
||||
job_id,
|
||||
w_id,
|
||||
memory_usage,
|
||||
@@ -1520,6 +1526,7 @@ pub async fn update_worker_ping_from_job_query(
|
||||
occupancy_rate_15s,
|
||||
occupancy_rate_5m,
|
||||
occupancy_rate_30m,
|
||||
job_isolation,
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
@@ -208,7 +208,7 @@ pub async fn get_ducklake_from_db_unchecked(
|
||||
|
||||
let catalog_resource =
|
||||
if ducklake.catalog.resource_type == DucklakeCatalogResourceType::Instance {
|
||||
let pg_creds = parse_postgres_url(&get_database_url().await?)?;
|
||||
let pg_creds = parse_postgres_url(&get_database_url().await?.as_str().await)?;
|
||||
json!({
|
||||
"dbname": ducklake.catalog.resource_path,
|
||||
"host": pg_creds.host,
|
||||
|
||||
+4
-4
@@ -415,8 +415,8 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "duckdb"
|
||||
version = "1.4.1"
|
||||
source = "git+https://github.com/diegoimbert/duckdb-rs?branch=main#0df52a6941c9996d7ec60d585eaf6430db8c48cf"
|
||||
version = "1.4.2"
|
||||
source = "git+https://github.com/windmill-labs/duckdb-rs.git?rev=fe0702529de6ec5a568337726bba9355503157d2#fe0702529de6ec5a568337726bba9355503157d2"
|
||||
dependencies = [
|
||||
"arrow",
|
||||
"cast",
|
||||
@@ -702,8 +702,8 @@ checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "libduckdb-sys"
|
||||
version = "1.4.1"
|
||||
source = "git+https://github.com/diegoimbert/duckdb-rs?branch=main#0df52a6941c9996d7ec60d585eaf6430db8c48cf"
|
||||
version = "1.4.2"
|
||||
source = "git+https://github.com/windmill-labs/duckdb-rs.git?rev=fe0702529de6ec5a568337726bba9355503157d2#fe0702529de6ec5a568337726bba9355503157d2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"flate2",
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.41"
|
||||
duckdb = { git = "https://github.com/diegoimbert/duckdb-rs", branch = "main", features = ["bundled"] }
|
||||
duckdb = { rev = "fe0702529de6ec5a568337726bba9355503157d2", git = "https://github.com/windmill-labs/duckdb-rs.git", features = ["bundled"] }
|
||||
rust_decimal = "1.37.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
cargo build --release -p windmill_duckdb_ffi_internal
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release -p windmill_duckdb_ffi_internal
|
||||
cp target/release/libwindmill_duckdb_ffi_internal.* ../target/debug/
|
||||
@@ -265,13 +265,22 @@ fn do_duckdb_inner(
|
||||
None => {
|
||||
type_aliases = Some(
|
||||
(0..stmt.column_count())
|
||||
.map(|i| stmt.column_logical_type(i).get_alias())
|
||||
.map(|i| {
|
||||
let logical_type = stmt.column_logical_type(i);
|
||||
if logical_type.is_invalid() {
|
||||
None
|
||||
} else {
|
||||
logical_type.get_alias()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
type_aliases.as_ref().unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
// let type_aliases = (0..stmt.column_count()).map(|_| None).collect::<Vec<_>>();
|
||||
|
||||
let row = row_to_value(row, &column_names.as_slice(), &type_aliases.as_slice())
|
||||
.map_err(|e| e.to_string())?;
|
||||
rows_vec.push(row);
|
||||
|
||||
@@ -47,3 +47,5 @@ regex.workspace = true
|
||||
backon.workspace = true
|
||||
quick_cache.workspace = true
|
||||
thiserror.workspace = true
|
||||
dashmap.workspace = true
|
||||
once_cell.workspace = true
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Author: Ruben Fiszel
|
||||
* Copyright: Windmill Labs, Inc 2022
|
||||
* This file and its contents are licensed under the AGPLv3 License.
|
||||
* Please see the included NOTICE for copyright information and
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use chrono::Datelike;
|
||||
use dashmap::DashMap;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Notify;
|
||||
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
||||
struct UsageKey {
|
||||
id: String,
|
||||
is_workspace: bool,
|
||||
month: i32,
|
||||
}
|
||||
|
||||
pub struct UsageBuffer {
|
||||
buffer: Arc<DashMap<UsageKey, i32>>,
|
||||
db: Pool<Postgres>,
|
||||
shutdown_notify: Arc<Notify>,
|
||||
}
|
||||
|
||||
impl UsageBuffer {
|
||||
pub fn new(db: Pool<Postgres>) -> Arc<Self> {
|
||||
let buffer = Arc::new(Self {
|
||||
buffer: Arc::new(DashMap::new()),
|
||||
db,
|
||||
shutdown_notify: Arc::new(Notify::new()),
|
||||
});
|
||||
|
||||
// Spawn the periodic flush task
|
||||
let buffer_clone = buffer.clone();
|
||||
tokio::spawn(async move {
|
||||
buffer_clone.flush_loop().await;
|
||||
});
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
pub fn increment(&self, workspace_id: String, email: Option<String>) {
|
||||
let month = Self::current_month();
|
||||
|
||||
// Increment workspace usage
|
||||
self.buffer
|
||||
.entry(UsageKey { id: workspace_id, is_workspace: true, month })
|
||||
.and_modify(|counter| *counter += 1)
|
||||
.or_insert(1);
|
||||
|
||||
// Increment user usage if email is provided
|
||||
if let Some(email) = email {
|
||||
self.buffer
|
||||
.entry(UsageKey { id: email, is_workspace: false, month })
|
||||
.and_modify(|counter| *counter += 1)
|
||||
.or_insert(1);
|
||||
}
|
||||
}
|
||||
|
||||
async fn flush_loop(&self) {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(60));
|
||||
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => {
|
||||
self.flush().await;
|
||||
}
|
||||
_ = self.shutdown_notify.notified() => {
|
||||
// Final flush on shutdown
|
||||
self.flush().await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn flush(&self) {
|
||||
if self.buffer.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Drain all buffered usage counts
|
||||
let mut to_flush = Vec::new();
|
||||
self.buffer.retain(|key, value| {
|
||||
to_flush.push((key.clone(), *value));
|
||||
false
|
||||
});
|
||||
|
||||
if to_flush.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
"Flushing {} buffered usage entries to database",
|
||||
to_flush.len()
|
||||
);
|
||||
|
||||
// Batch update to database
|
||||
for (key, count) in to_flush {
|
||||
let result = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
sqlx::query!(
|
||||
"INSERT INTO usage (id, is_workspace, month_, usage)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $4",
|
||||
&key.id,
|
||||
key.is_workspace,
|
||||
key.month,
|
||||
count
|
||||
)
|
||||
.execute(&self.db),
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(_)) => {}
|
||||
Ok(Err(e)) => {
|
||||
tracing::error!(
|
||||
"Failed to flush usage for {} (is_workspace: {}): {:#}",
|
||||
key.id,
|
||||
key.is_workspace,
|
||||
e
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!(
|
||||
"Usage flush timed out for {} (is_workspace: {})",
|
||||
key.id,
|
||||
key.is_workspace
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn current_month() -> i32 {
|
||||
let now = chrono::Utc::now();
|
||||
(now.year() * 12 + now.month() as i32) as i32
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref USAGE_BUFFER: once_cell::sync::OnceCell<Arc<UsageBuffer>> = once_cell::sync::OnceCell::new();
|
||||
}
|
||||
|
||||
pub fn init_usage_buffer(db: Pool<Postgres>) {
|
||||
USAGE_BUFFER.get_or_init(|| UsageBuffer::new(db));
|
||||
}
|
||||
|
||||
pub fn increment_usage_async(db: Pool<Postgres>, workspace_id: String, email: Option<String>) {
|
||||
if let Some(buffer) = USAGE_BUFFER.get() {
|
||||
buffer.increment(workspace_id, email);
|
||||
} else {
|
||||
tracing::warn!("Usage buffer not initialized, falling back to direct database update");
|
||||
// Fallback to old implementation if buffer not initialized
|
||||
tokio::task::spawn(async move {
|
||||
let result = tokio::time::timeout(std::time::Duration::from_secs(10), async {
|
||||
// Update workspace usage
|
||||
let workspace_result = sqlx::query!(
|
||||
"INSERT INTO usage (id, is_workspace, month_, usage)
|
||||
VALUES ($1, TRUE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)
|
||||
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1",
|
||||
&workspace_id
|
||||
)
|
||||
.execute(&db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = workspace_result {
|
||||
tracing::error!("Failed to update workspace usage for {}: {:#}", workspace_id, e);
|
||||
}
|
||||
|
||||
// Update user usage if email is provided (non-premium workspaces only)
|
||||
if let Some(ref email) = email {
|
||||
let user_result = sqlx::query!(
|
||||
"INSERT INTO usage (id, is_workspace, month_, usage)
|
||||
VALUES ($1, FALSE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)
|
||||
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1",
|
||||
email
|
||||
)
|
||||
.execute(&db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = user_result {
|
||||
tracing::error!("Failed to update user usage for {}: {:#}", email, e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Err(_) = result {
|
||||
tracing::error!(
|
||||
"Usage update timed out after 10s for workspace {} and email {:?}",
|
||||
workspace_id,
|
||||
email
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3809,50 +3809,7 @@ async fn check_usage_limits(
|
||||
}
|
||||
|
||||
#[cfg(feature = "cloud")]
|
||||
fn increment_usage_async(db: Pool<Postgres>, workspace_id: String, email: Option<String>) {
|
||||
tokio::task::spawn(async move {
|
||||
let result = tokio::time::timeout(std::time::Duration::from_secs(10), async {
|
||||
// Update workspace usage
|
||||
let workspace_result = sqlx::query!(
|
||||
"INSERT INTO usage (id, is_workspace, month_, usage)
|
||||
VALUES ($1, TRUE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)
|
||||
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1",
|
||||
&workspace_id
|
||||
)
|
||||
.execute(&db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = workspace_result {
|
||||
tracing::error!("Failed to update workspace usage for {}: {:#}", workspace_id, e);
|
||||
}
|
||||
|
||||
// Update user usage if email is provided (non-premium workspaces only)
|
||||
if let Some(ref email) = email {
|
||||
let user_result = sqlx::query!(
|
||||
"INSERT INTO usage (id, is_workspace, month_, usage)
|
||||
VALUES ($1, FALSE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)
|
||||
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1",
|
||||
email
|
||||
)
|
||||
.execute(&db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = user_result {
|
||||
tracing::error!("Failed to update user usage for {}: {:#}", email, e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Err(_) = result {
|
||||
tracing::error!(
|
||||
"Usage update timed out after 10s for workspace {} and email {:?}",
|
||||
workspace_id,
|
||||
email
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
use crate::cloud_usage::increment_usage_async;
|
||||
|
||||
// #[instrument(level = "trace", skip_all)]
|
||||
pub async fn push<'c, 'd>(
|
||||
|
||||
@@ -14,3 +14,8 @@ pub mod schedule;
|
||||
pub use jobs::*;
|
||||
pub mod flow_status;
|
||||
pub mod tags;
|
||||
|
||||
#[cfg(feature = "cloud")]
|
||||
pub mod cloud_usage;
|
||||
#[cfg(feature = "cloud")]
|
||||
pub use cloud_usage::init_usage_buffer;
|
||||
|
||||
@@ -58,6 +58,10 @@ windmill-parser-graphql.workspace = true
|
||||
windmill-parser-php = { workspace = true, optional = true }
|
||||
windmill-git-sync.workspace = true
|
||||
rmcp = { version = "0.8.1", features = ["client", "transport-streamable-http-client", "transport-streamable-http-client-reqwest"] }
|
||||
aws-sdk-bedrockruntime.workspace = true
|
||||
aws-config.workspace = true
|
||||
aws-credential-types.workspace = true
|
||||
aws-smithy-types.workspace = true
|
||||
flume.workspace = true
|
||||
sqlx.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
@@ -4,6 +4,8 @@ use ulid;
|
||||
use windmill_common::{client::AuthedClient, error::Error, s3_helpers::S3Object};
|
||||
use windmill_queue::MiniPulledJob;
|
||||
|
||||
use crate::ai::types::*;
|
||||
|
||||
/// Upload image to S3 and return S3Object
|
||||
pub async fn upload_image_to_s3(
|
||||
base64_image: &str,
|
||||
@@ -66,3 +68,53 @@ pub async fn download_and_encode_s3_image(
|
||||
|
||||
Ok((mime_type.to_string(), base64_data))
|
||||
}
|
||||
|
||||
/// Prepare messages for API by converting S3Objects to base64 ImageUrls
|
||||
pub async fn prepare_messages_for_api(
|
||||
messages: &[OpenAIMessage],
|
||||
client: &AuthedClient,
|
||||
workspace_id: &str,
|
||||
) -> Result<Vec<OpenAIMessage>, Error> {
|
||||
let mut prepared_messages = Vec::new();
|
||||
|
||||
for message in messages {
|
||||
let mut prepared_message = message.clone();
|
||||
|
||||
if let Some(content) = &message.content {
|
||||
match content {
|
||||
OpenAIContent::Text(text) => {
|
||||
prepared_message.content = Some(OpenAIContent::Text(text.clone()));
|
||||
}
|
||||
OpenAIContent::Parts(parts) => {
|
||||
let mut prepared_content = Vec::new();
|
||||
|
||||
for part in parts {
|
||||
match part {
|
||||
ContentPart::S3Object { s3_object } => {
|
||||
// Convert S3Object to base64 image URL
|
||||
let (mime_type, image_bytes) =
|
||||
download_and_encode_s3_image(s3_object, client, workspace_id)
|
||||
.await?;
|
||||
prepared_content.push(ContentPart::ImageUrl {
|
||||
image_url: ImageUrlData {
|
||||
url: format!("data:{};base64,{}", mime_type, image_bytes),
|
||||
},
|
||||
});
|
||||
}
|
||||
other => {
|
||||
// Keep Text and ImageUrl as-is
|
||||
prepared_content.push(other.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prepared_message.content = Some(OpenAIContent::Parts(prepared_content));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prepared_messages.push(prepared_message);
|
||||
}
|
||||
|
||||
Ok(prepared_messages)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,804 @@
|
||||
use crate::ai::{
|
||||
image_handler::prepare_messages_for_api,
|
||||
providers::openai::{OpenAIFunction, OpenAIToolCall},
|
||||
query_builder::{ParsedResponse, StreamEventProcessor},
|
||||
types::StreamingEvent,
|
||||
types::{ContentPart, OpenAIContent, OpenAIMessage, ToolDef},
|
||||
};
|
||||
use aws_config::BehaviorVersion;
|
||||
use aws_credential_types::provider::token::ProvideToken;
|
||||
use aws_sdk_bedrockruntime::types::{
|
||||
ContentBlock, ConversationRole, ConverseStreamOutput, ImageBlock, ImageFormat, ImageSource,
|
||||
InferenceConfiguration, Message, SystemContentBlock, Tool, ToolInputSchema, ToolSpecification,
|
||||
};
|
||||
use aws_sdk_bedrockruntime::Client as BedrockRuntimeClient;
|
||||
use std::collections::HashMap;
|
||||
use windmill_common::{client::AuthedClient, error::Error};
|
||||
|
||||
/// Constants for commonly used strings to avoid allocations
|
||||
const FUNCTION_TYPE: &str = "function";
|
||||
const EMPTY_JSON: &str = "{}";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BearerTokenProvider {
|
||||
token: String,
|
||||
}
|
||||
|
||||
impl BearerTokenProvider {
|
||||
pub fn new(token: String) -> Self {
|
||||
Self { token }
|
||||
}
|
||||
}
|
||||
|
||||
impl ProvideToken for BearerTokenProvider {
|
||||
fn provide_token<'a>(&'a self) -> aws_credential_types::provider::future::ProvideToken<'a>
|
||||
where
|
||||
Self: 'a,
|
||||
{
|
||||
aws_credential_types::provider::future::ProvideToken::ready(Ok(
|
||||
aws_credential_types::Token::new(self.token.clone(), None),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BedrockClient {
|
||||
client: BedrockRuntimeClient,
|
||||
}
|
||||
|
||||
impl BedrockClient {
|
||||
pub async fn from_bearer_token(bearer_token: String, region: &str) -> Result<Self, Error> {
|
||||
let config = aws_sdk_bedrockruntime::config::Builder::new()
|
||||
.region(aws_config::Region::new(region.to_string()))
|
||||
.behavior_version(BehaviorVersion::latest())
|
||||
.token_provider(BearerTokenProvider::new(bearer_token))
|
||||
.build();
|
||||
|
||||
Ok(Self { client: BedrockRuntimeClient::from_conf(config) })
|
||||
}
|
||||
|
||||
pub fn client(&self) -> &BedrockRuntimeClient {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
|
||||
/// Format AWS SDK errors with detailed information
|
||||
fn format_bedrock_error<E, R>(error: &aws_sdk_bedrockruntime::error::SdkError<E, R>) -> String
|
||||
where
|
||||
E: std::fmt::Debug + std::fmt::Display,
|
||||
R: std::fmt::Debug,
|
||||
{
|
||||
use aws_sdk_bedrockruntime::error::SdkError;
|
||||
|
||||
match error {
|
||||
SdkError::ServiceError(err) => {
|
||||
// Include both the display and debug representations for maximum detail
|
||||
format!("Service error: {} (details: {:?})", err.err(), err)
|
||||
}
|
||||
SdkError::ConstructionFailure(err) => {
|
||||
format!("Request construction failed: {:?}", err)
|
||||
}
|
||||
SdkError::DispatchFailure(err) => {
|
||||
format!("Request dispatch failed: {:?}", err)
|
||||
}
|
||||
SdkError::ResponseError(err) => {
|
||||
format!("Response error: {:?}", err)
|
||||
}
|
||||
SdkError::TimeoutError(err) => {
|
||||
format!("Request timeout: {:?}", err)
|
||||
}
|
||||
_ => format!("{:?}", error),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert AWS Smithy Document to serde_json::Value
|
||||
fn document_to_json(doc: &aws_smithy_types::Document) -> serde_json::Value {
|
||||
use aws_smithy_types::Document;
|
||||
|
||||
match doc {
|
||||
Document::Object(map) => {
|
||||
let mut obj = serde_json::Map::new();
|
||||
for (k, v) in map {
|
||||
obj.insert(k.clone(), document_to_json(v));
|
||||
}
|
||||
serde_json::Value::Object(obj)
|
||||
}
|
||||
Document::Array(arr) => {
|
||||
serde_json::Value::Array(arr.iter().map(document_to_json).collect())
|
||||
}
|
||||
Document::Number(num) => {
|
||||
// Try to parse as different number types
|
||||
serde_json::Value::Number(
|
||||
serde_json::Number::from_f64(num.to_f64_lossy())
|
||||
.unwrap_or(serde_json::Number::from(0)),
|
||||
)
|
||||
}
|
||||
Document::String(s) => serde_json::Value::String(s.clone()),
|
||||
Document::Bool(b) => serde_json::Value::Bool(*b),
|
||||
Document::Null => serde_json::Value::Null,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert serde_json::Value to AWS Smithy Document
|
||||
fn json_to_document(value: serde_json::Value) -> aws_smithy_types::Document {
|
||||
use aws_smithy_types::Document;
|
||||
use serde_json::Value;
|
||||
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
let mut doc_map = std::collections::HashMap::new();
|
||||
for (k, v) in map {
|
||||
doc_map.insert(k, json_to_document(v));
|
||||
}
|
||||
Document::Object(doc_map)
|
||||
}
|
||||
Value::Array(arr) => Document::Array(arr.into_iter().map(json_to_document).collect()),
|
||||
Value::Number(num) => {
|
||||
if let Some(i) = num.as_i64() {
|
||||
Document::Number(aws_smithy_types::Number::PosInt(i as u64))
|
||||
} else if let Some(f) = num.as_f64() {
|
||||
Document::Number(aws_smithy_types::Number::Float(f))
|
||||
} else {
|
||||
Document::Number(aws_smithy_types::Number::PosInt(0))
|
||||
}
|
||||
}
|
||||
Value::String(s) => Document::String(s),
|
||||
Value::Bool(b) => Document::Bool(b),
|
||||
Value::Null => Document::Null,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert OpenAI-style messages to Bedrock format
|
||||
///
|
||||
/// Separates system messages from conversation messages as required by Bedrock API.
|
||||
///
|
||||
/// # Returns
|
||||
/// Tuple of (conversation_messages, system_prompts)
|
||||
pub fn openai_messages_to_bedrock(
|
||||
messages: &[OpenAIMessage],
|
||||
) -> Result<(Vec<Message>, Vec<SystemContentBlock>), Error> {
|
||||
let mut bedrock_messages = Vec::new();
|
||||
let mut system_prompts = Vec::new();
|
||||
|
||||
for msg in messages {
|
||||
match msg.role.as_str() {
|
||||
"system" => {
|
||||
// Extract system messages separately
|
||||
if let Some(ref content) = msg.content {
|
||||
let text = content_to_text(content);
|
||||
if !text.is_empty() {
|
||||
system_prompts.push(SystemContentBlock::Text(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
"user" | "assistant" => {
|
||||
bedrock_messages.push(convert_message(msg)?);
|
||||
}
|
||||
"tool" => {
|
||||
// Tool results are handled as user messages with ToolResult content
|
||||
bedrock_messages.push(convert_tool_message(msg)?);
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::BadRequest(format!("Unsupported role: {}", msg.role)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((bedrock_messages, system_prompts))
|
||||
}
|
||||
|
||||
/// Helper to extract text from OpenAIContent (ignoring images)
|
||||
fn content_to_text(content: &OpenAIContent) -> String {
|
||||
match content {
|
||||
OpenAIContent::Text(text) => text.to_string(),
|
||||
OpenAIContent::Parts(parts) => {
|
||||
// Extract only text parts and join them
|
||||
let text_parts: Vec<&str> = parts
|
||||
.iter()
|
||||
.filter_map(|part| match part {
|
||||
ContentPart::Text { text } => Some(text.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
text_parts.join(" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse image data URL and extract format and base64 data
|
||||
fn parse_image_data_url(url: &str) -> Result<(ImageFormat, Vec<u8>), Error> {
|
||||
if !url.starts_with("data:") {
|
||||
return Err(Error::internal_err("Image URL must be a data URL"));
|
||||
}
|
||||
|
||||
// Parse data:image/png;base64,<data>
|
||||
let base64_start = url
|
||||
.find("base64,")
|
||||
.ok_or_else(|| Error::internal_err("Invalid data URL format"))?;
|
||||
|
||||
let base64_data = &url[base64_start + 7..];
|
||||
let mime_type = url
|
||||
.split(';')
|
||||
.next()
|
||||
.and_then(|s| s.strip_prefix("data:"))
|
||||
.unwrap_or("image/png");
|
||||
|
||||
// Extract format from MIME type (e.g., "image/png" -> "png")
|
||||
let format_str = mime_type
|
||||
.rsplit_once('/')
|
||||
.map(|(_, format)| format)
|
||||
.unwrap_or("png");
|
||||
|
||||
// Map to ImageFormat enum
|
||||
let format = match format_str {
|
||||
"png" => ImageFormat::Png,
|
||||
"jpeg" | "jpg" => ImageFormat::Jpeg,
|
||||
"gif" => ImageFormat::Gif,
|
||||
"webp" => ImageFormat::Webp,
|
||||
_ => ImageFormat::Png, // Default to PNG
|
||||
};
|
||||
|
||||
// Decode base64
|
||||
let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, base64_data)
|
||||
.map_err(|e| Error::internal_err(format!("Failed to decode base64 image: {}", e)))?;
|
||||
|
||||
Ok((format, bytes))
|
||||
}
|
||||
|
||||
/// Convert a ContentPart to Bedrock ContentBlock
|
||||
fn content_part_to_block(part: &ContentPart) -> Result<Option<ContentBlock>, Error> {
|
||||
match part {
|
||||
ContentPart::Text { text } => {
|
||||
if text.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(ContentBlock::Text(text.clone())))
|
||||
}
|
||||
}
|
||||
ContentPart::ImageUrl { image_url } => {
|
||||
let (format, bytes) = parse_image_data_url(&image_url.url)?;
|
||||
|
||||
let image_source = ImageSource::Bytes(bytes.into());
|
||||
let image_block = ImageBlock::builder()
|
||||
.format(format)
|
||||
.source(image_source)
|
||||
.build()
|
||||
.map_err(|e| Error::internal_err(format!("Failed to build image block: {}", e)))?;
|
||||
|
||||
Ok(Some(ContentBlock::Image(image_block)))
|
||||
}
|
||||
ContentPart::S3Object { .. } => {
|
||||
// S3Objects are already converted to ImageUrl by prepare_messages_for_api
|
||||
// If we somehow get here, skip it
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a single OpenAI message to Bedrock Message
|
||||
fn convert_message(msg: &OpenAIMessage) -> Result<Message, Error> {
|
||||
let role = match msg.role.as_str() {
|
||||
"user" => ConversationRole::User,
|
||||
"assistant" => ConversationRole::Assistant,
|
||||
_ => {
|
||||
return Err(Error::internal_err(format!(
|
||||
"Unsupported role: {}",
|
||||
msg.role
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let mut content_blocks = Vec::new();
|
||||
|
||||
// Handle content (text and/or images)
|
||||
if let Some(ref content) = msg.content {
|
||||
match content {
|
||||
OpenAIContent::Text(text) => {
|
||||
if !text.is_empty() {
|
||||
content_blocks.push(ContentBlock::Text(text.clone()));
|
||||
}
|
||||
}
|
||||
OpenAIContent::Parts(parts) => {
|
||||
for part in parts {
|
||||
if let Some(block) = content_part_to_block(part)? {
|
||||
content_blocks.push(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tool calls (for assistant messages)
|
||||
if let Some(ref tool_calls) = msg.tool_calls {
|
||||
for tc in tool_calls {
|
||||
content_blocks.push(convert_tool_call_to_content(tc)?);
|
||||
}
|
||||
}
|
||||
|
||||
// Bedrock requires at least one content block
|
||||
if content_blocks.is_empty() {
|
||||
content_blocks.push(ContentBlock::Text(String::new()));
|
||||
}
|
||||
|
||||
Message::builder()
|
||||
.role(role)
|
||||
.set_content(Some(content_blocks))
|
||||
.build()
|
||||
.map_err(|e| Error::internal_err(format!("Failed to build message: {}", e)))
|
||||
}
|
||||
|
||||
/// Convert OpenAI tool call to Bedrock ToolUse content block
|
||||
fn convert_tool_call_to_content(tool_call: &OpenAIToolCall) -> Result<ContentBlock, Error> {
|
||||
let input = json_to_document(
|
||||
serde_json::from_str(&tool_call.function.arguments)
|
||||
.unwrap_or_else(|_| serde_json::json!({})),
|
||||
);
|
||||
Ok(ContentBlock::ToolUse(
|
||||
aws_sdk_bedrockruntime::types::ToolUseBlock::builder()
|
||||
.tool_use_id(&tool_call.id)
|
||||
.name(&tool_call.function.name)
|
||||
.input(input)
|
||||
.build()
|
||||
.map_err(|e| Error::internal_err(format!("Failed to build tool use: {}", e)))?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Convert tool result message to Bedrock format
|
||||
fn convert_tool_message(msg: &OpenAIMessage) -> Result<Message, Error> {
|
||||
let tool_call_id = msg
|
||||
.tool_call_id
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::internal_err("Tool message missing tool_call_id"))?;
|
||||
|
||||
let content_str = msg
|
||||
.content
|
||||
.as_ref()
|
||||
.map(|c| content_to_text(c))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Try to parse as JSON, otherwise use text
|
||||
let tool_result_content =
|
||||
if let Ok(json_val) = serde_json::from_str::<serde_json::Value>(&content_str) {
|
||||
if json_val.is_object() {
|
||||
vec![aws_sdk_bedrockruntime::types::ToolResultContentBlock::Json(
|
||||
json_to_document(json_val),
|
||||
)]
|
||||
} else {
|
||||
// Wrap primitives and arrays in an object
|
||||
vec![aws_sdk_bedrockruntime::types::ToolResultContentBlock::Json(
|
||||
json_to_document(serde_json::json!({"result": json_val})),
|
||||
)]
|
||||
}
|
||||
} else {
|
||||
vec![aws_sdk_bedrockruntime::types::ToolResultContentBlock::Text(
|
||||
content_str.to_string(),
|
||||
)]
|
||||
};
|
||||
|
||||
let tool_result = ContentBlock::ToolResult(
|
||||
aws_sdk_bedrockruntime::types::ToolResultBlock::builder()
|
||||
.tool_use_id(tool_call_id)
|
||||
.set_content(Some(tool_result_content))
|
||||
.build()
|
||||
.map_err(|e| Error::internal_err(format!("Failed to build tool result: {}", e)))?,
|
||||
);
|
||||
|
||||
Message::builder()
|
||||
.role(ConversationRole::User)
|
||||
.content(tool_result)
|
||||
.build()
|
||||
.map_err(|e| Error::internal_err(format!("Failed to build tool result message: {}", e)))
|
||||
}
|
||||
|
||||
/// Convert OpenAI tool definitions to Bedrock format
|
||||
pub fn openai_tools_to_bedrock(tools: &[ToolDef]) -> Result<Vec<Tool>, Error> {
|
||||
tools
|
||||
.iter()
|
||||
.map(|tool_def| {
|
||||
let spec = &tool_def.function;
|
||||
|
||||
// Convert parameters (RawValue) to Document via serde_json::Value
|
||||
let param_value: serde_json::Value = serde_json::from_str(spec.parameters.get())
|
||||
.map_err(|e| Error::internal_err(format!("Invalid tool schema: {}", e)))?;
|
||||
let input_schema = ToolInputSchema::Json(json_to_document(param_value));
|
||||
|
||||
let tool_spec = ToolSpecification::builder()
|
||||
.name(&spec.name)
|
||||
.set_description(spec.description.clone())
|
||||
.input_schema(input_schema)
|
||||
.build()
|
||||
.map_err(|e| Error::internal_err(format!("Failed to build tool spec: {}", e)))?;
|
||||
|
||||
Ok(Tool::ToolSpec(tool_spec))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Create inference configuration from parameters
|
||||
pub fn create_inference_config(
|
||||
temperature: Option<f32>,
|
||||
max_tokens: Option<i32>,
|
||||
) -> Option<InferenceConfiguration> {
|
||||
if temperature.is_none() && max_tokens.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut builder = InferenceConfiguration::builder();
|
||||
|
||||
if let Some(temp) = temperature {
|
||||
builder = builder.temperature(temp);
|
||||
}
|
||||
|
||||
if let Some(max_tok) = max_tokens {
|
||||
builder = builder.max_tokens(max_tok);
|
||||
}
|
||||
|
||||
Some(builder.build())
|
||||
}
|
||||
|
||||
/// Extract text content and tool calls from Bedrock Converse response
|
||||
pub fn bedrock_response_to_openai(
|
||||
output: &aws_sdk_bedrockruntime::operation::converse::ConverseOutput,
|
||||
) -> Result<(Option<String>, Vec<OpenAIToolCall>), Error> {
|
||||
let mut text_content = String::new();
|
||||
let mut tool_calls = Vec::new();
|
||||
|
||||
if let Some(message) = output.output().and_then(|o| o.as_message().ok()) {
|
||||
let content_blocks = message.content();
|
||||
if !content_blocks.is_empty() {
|
||||
for block in content_blocks {
|
||||
match block {
|
||||
ContentBlock::Text(text) => {
|
||||
text_content.push_str(&text);
|
||||
}
|
||||
ContentBlock::ToolUse(tool_use) => {
|
||||
// Convert to OpenAI tool call format
|
||||
// Convert aws_smithy_types::Document to serde_json::Value
|
||||
let input_value = document_to_json(tool_use.input());
|
||||
let arguments = serde_json::to_string(&input_value)
|
||||
.unwrap_or_else(|_| EMPTY_JSON.to_string());
|
||||
|
||||
tool_calls.push(OpenAIToolCall {
|
||||
id: tool_use.tool_use_id().to_string(),
|
||||
r#type: FUNCTION_TYPE.to_string(),
|
||||
function: OpenAIFunction {
|
||||
name: tool_use.name().to_string(),
|
||||
arguments,
|
||||
},
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let content = if text_content.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(text_content)
|
||||
};
|
||||
|
||||
Ok((content, tool_calls))
|
||||
}
|
||||
|
||||
/// Extract text delta from Bedrock stream event
|
||||
pub fn bedrock_stream_event_to_text(event: &ConverseStreamOutput) -> Option<String> {
|
||||
match event {
|
||||
ConverseStreamOutput::ContentBlockDelta(delta) => delta
|
||||
.delta()
|
||||
.and_then(|d| d.as_text().ok())
|
||||
.map(|s| s.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a streaming tool call being accumulated
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StreamingToolCall {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub arguments: String,
|
||||
}
|
||||
|
||||
/// Extract tool use start event from stream
|
||||
pub fn bedrock_stream_event_to_tool_start(
|
||||
event: &ConverseStreamOutput,
|
||||
) -> Option<StreamingToolCall> {
|
||||
match event {
|
||||
ConverseStreamOutput::ContentBlockStart(start) => {
|
||||
if let Some(tool_use) = start.start().and_then(|s| s.as_tool_use().ok()) {
|
||||
Some(StreamingToolCall {
|
||||
id: tool_use.tool_use_id().to_string(),
|
||||
name: tool_use.name().to_string(),
|
||||
arguments: String::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract tool use input delta from stream
|
||||
pub fn bedrock_stream_event_to_tool_delta(event: &ConverseStreamOutput) -> Option<String> {
|
||||
match event {
|
||||
ConverseStreamOutput::ContentBlockDelta(delta) => delta
|
||||
.delta()
|
||||
.and_then(|d| d.as_tool_use().ok())
|
||||
.map(|tool_use| tool_use.input().to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if stream event indicates content block stop
|
||||
pub fn bedrock_stream_event_is_block_stop(event: &ConverseStreamOutput) -> bool {
|
||||
matches!(event, ConverseStreamOutput::ContentBlockStop(_))
|
||||
}
|
||||
|
||||
/// Convert accumulated streaming tool calls to OpenAI format
|
||||
pub fn streaming_tool_calls_to_openai(tool_calls: Vec<StreamingToolCall>) -> Vec<OpenAIToolCall> {
|
||||
tool_calls
|
||||
.into_iter()
|
||||
.map(|tc| OpenAIToolCall {
|
||||
id: tc.id,
|
||||
function: OpenAIFunction { name: tc.name, arguments: tc.arguments },
|
||||
r#type: FUNCTION_TYPE.to_string(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BedrockQueryBuilder;
|
||||
|
||||
impl BedrockQueryBuilder {
|
||||
/// Execute Bedrock request (streaming or non-streaming)
|
||||
pub async fn execute_request(
|
||||
&self,
|
||||
messages: &[OpenAIMessage],
|
||||
tools: Option<&[ToolDef]>,
|
||||
model: &str,
|
||||
temperature: Option<f32>,
|
||||
max_tokens: Option<u32>,
|
||||
api_key: &str,
|
||||
region: &str,
|
||||
should_stream: bool,
|
||||
stream_event_processor: Option<StreamEventProcessor>,
|
||||
client: &AuthedClient,
|
||||
workspace_id: &str,
|
||||
structured_output_tool_name: Option<&str>,
|
||||
) -> Result<ParsedResponse, Error> {
|
||||
// Create Bedrock client with bearer token authentication
|
||||
let bedrock_client = BedrockClient::from_bearer_token(api_key.to_string(), region).await?;
|
||||
|
||||
// Prepare messages: convert S3Objects to ImageUrls by downloading from S3
|
||||
let prepared_messages = prepare_messages_for_api(messages, client, workspace_id).await?;
|
||||
|
||||
// Convert messages to Bedrock format (separates system prompts)
|
||||
let (bedrock_messages, system_prompts) = openai_messages_to_bedrock(&prepared_messages)?;
|
||||
|
||||
// Build inference configuration
|
||||
let inference_config = create_inference_config(temperature, max_tokens.map(|t| t as i32));
|
||||
|
||||
// Build tool configuration with optional ToolChoice
|
||||
let tool_config = self.build_tool_config(tools, structured_output_tool_name.is_some())?;
|
||||
|
||||
if should_stream {
|
||||
self.execute_converse_stream(
|
||||
&bedrock_client,
|
||||
model,
|
||||
bedrock_messages,
|
||||
system_prompts,
|
||||
inference_config,
|
||||
tool_config,
|
||||
stream_event_processor,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
self.execute_converse(
|
||||
&bedrock_client,
|
||||
model,
|
||||
bedrock_messages,
|
||||
system_prompts,
|
||||
inference_config,
|
||||
tool_config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Build tool configuration with optional ToolChoice for structured output
|
||||
fn build_tool_config(
|
||||
&self,
|
||||
tools: Option<&[ToolDef]>,
|
||||
force_tool_use: bool,
|
||||
) -> Result<Option<aws_sdk_bedrockruntime::types::ToolConfiguration>, Error> {
|
||||
if let Some(tools) = tools {
|
||||
let bedrock_tools = openai_tools_to_bedrock(tools)?;
|
||||
let mut tool_config_builder =
|
||||
aws_sdk_bedrockruntime::types::ToolConfiguration::builder()
|
||||
.set_tools(Some(bedrock_tools));
|
||||
|
||||
// For structured output, force the model to use the tool
|
||||
if force_tool_use {
|
||||
tool_config_builder = tool_config_builder.tool_choice(
|
||||
aws_sdk_bedrockruntime::types::ToolChoice::Any(
|
||||
aws_sdk_bedrockruntime::types::AnyToolChoice::builder().build(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Some(tool_config_builder.build().map_err(|e| {
|
||||
Error::internal_err(format!("Failed to build tool configuration: {}", e))
|
||||
})?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute non-streaming Bedrock request
|
||||
async fn execute_converse(
|
||||
&self,
|
||||
bedrock_client: &BedrockClient,
|
||||
model: &str,
|
||||
bedrock_messages: Vec<aws_sdk_bedrockruntime::types::Message>,
|
||||
system_prompts: Vec<aws_sdk_bedrockruntime::types::SystemContentBlock>,
|
||||
inference_config: Option<aws_sdk_bedrockruntime::types::InferenceConfiguration>,
|
||||
tool_config: Option<aws_sdk_bedrockruntime::types::ToolConfiguration>,
|
||||
) -> Result<ParsedResponse, Error> {
|
||||
let mut request_builder = bedrock_client
|
||||
.client()
|
||||
.converse()
|
||||
.model_id(model)
|
||||
.set_messages(Some(bedrock_messages));
|
||||
|
||||
if !system_prompts.is_empty() {
|
||||
request_builder = request_builder.set_system(Some(system_prompts));
|
||||
}
|
||||
|
||||
if let Some(config) = inference_config {
|
||||
request_builder = request_builder.inference_config(config);
|
||||
}
|
||||
|
||||
if let Some(config) = tool_config {
|
||||
request_builder = request_builder.set_tool_config(Some(config));
|
||||
}
|
||||
|
||||
// Execute the request
|
||||
let response = request_builder.send().await.map_err(|e| {
|
||||
let error_msg = format!("Bedrock API error: {}", format_bedrock_error(&e));
|
||||
Error::internal_err(error_msg)
|
||||
})?;
|
||||
|
||||
// Convert response back to OpenAI format
|
||||
let (content, tool_calls) = bedrock_response_to_openai(&response)?;
|
||||
|
||||
Ok(ParsedResponse::Text { content, tool_calls, events_str: None })
|
||||
}
|
||||
|
||||
/// Execute streaming Bedrock request
|
||||
async fn execute_converse_stream(
|
||||
&self,
|
||||
bedrock_client: &BedrockClient,
|
||||
model: &str,
|
||||
bedrock_messages: Vec<aws_sdk_bedrockruntime::types::Message>,
|
||||
system_prompts: Vec<aws_sdk_bedrockruntime::types::SystemContentBlock>,
|
||||
inference_config: Option<aws_sdk_bedrockruntime::types::InferenceConfiguration>,
|
||||
tool_config: Option<aws_sdk_bedrockruntime::types::ToolConfiguration>,
|
||||
stream_event_processor: Option<StreamEventProcessor>,
|
||||
) -> Result<ParsedResponse, Error> {
|
||||
// Build streaming request
|
||||
let mut request_builder = bedrock_client
|
||||
.client()
|
||||
.converse_stream()
|
||||
.model_id(model)
|
||||
.set_messages(Some(bedrock_messages));
|
||||
|
||||
if !system_prompts.is_empty() {
|
||||
request_builder = request_builder.set_system(Some(system_prompts));
|
||||
}
|
||||
|
||||
if let Some(config) = inference_config {
|
||||
request_builder = request_builder.inference_config(config);
|
||||
}
|
||||
|
||||
if let Some(config) = tool_config {
|
||||
request_builder = request_builder.set_tool_config(Some(config));
|
||||
}
|
||||
|
||||
// Execute streaming request
|
||||
let mut stream = request_builder
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let error_msg =
|
||||
format!("Bedrock streaming API error: {}", format_bedrock_error(&e));
|
||||
Error::internal_err(error_msg)
|
||||
})?
|
||||
.stream;
|
||||
|
||||
let mut accumulated_text = String::new();
|
||||
let mut events_str = String::new();
|
||||
let mut accumulated_tool_calls: HashMap<String, StreamingToolCall> = HashMap::new();
|
||||
let mut current_tool_use_id: Option<String> = None;
|
||||
|
||||
// Process stream events
|
||||
loop {
|
||||
match stream.recv().await {
|
||||
Ok(Some(event)) => {
|
||||
// Handle tool use start
|
||||
if let Some(tool_call) = bedrock_stream_event_to_tool_start(&event) {
|
||||
current_tool_use_id = Some(tool_call.id.clone());
|
||||
accumulated_tool_calls.insert(tool_call.id.clone(), tool_call);
|
||||
}
|
||||
|
||||
// Handle text delta
|
||||
if let Some(text_delta) = bedrock_stream_event_to_text(&event) {
|
||||
accumulated_text.push_str(&text_delta);
|
||||
if let Some(processor) = stream_event_processor.as_ref() {
|
||||
processor
|
||||
.send(
|
||||
StreamingEvent::TokenDelta { content: text_delta },
|
||||
&mut events_str,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tool use input delta
|
||||
if let Some(input_delta) = bedrock_stream_event_to_tool_delta(&event) {
|
||||
if let Some(tool_id) = ¤t_tool_use_id {
|
||||
if let Some(tool_call) = accumulated_tool_calls.get_mut(tool_id) {
|
||||
tool_call.arguments.push_str(&input_delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle content block stop
|
||||
if bedrock_stream_event_is_block_stop(&event) {
|
||||
current_tool_use_id = None;
|
||||
}
|
||||
}
|
||||
Ok(None) => break, // Stream ended
|
||||
Err(e) => {
|
||||
return Err(Error::internal_err(format!("Bedrock stream error: {}", e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send tool call events to stream processor
|
||||
if let Some(processor) = stream_event_processor.as_ref() {
|
||||
for tool_call in accumulated_tool_calls.values() {
|
||||
processor
|
||||
.send(
|
||||
StreamingEvent::ToolCallArguments {
|
||||
call_id: tool_call.id.clone(),
|
||||
function_name: tool_call.name.clone(),
|
||||
arguments: tool_call.arguments.clone(),
|
||||
},
|
||||
&mut events_str,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
let content = if accumulated_text.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(accumulated_text)
|
||||
};
|
||||
|
||||
let tool_calls =
|
||||
streaming_tool_calls_to_openai(accumulated_tool_calls.into_values().collect());
|
||||
|
||||
Ok(ParsedResponse::Text {
|
||||
content,
|
||||
tool_calls,
|
||||
events_str: if events_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(events_str)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user