Compare commits

..

11 Commits

Author SHA1 Message Date
tristantr
bd1bedf69e nit 2025-12-09 12:38:00 +01:00
tristantr
048368b1f8 Change banner wordings if new tutorial available 2025-12-09 12:37:38 +01:00
tristantr
c92ab37930 Put back banner when new tutorial is available for a user who completed all tutotorials, and never skipped all 2025-12-09 12:10:10 +01:00
tristantr
0bcacc1f10 Create shared Constants for TUTORIAL_DISMISSED_KEY 2025-12-09 11:34:36 +01:00
tristantr
635bb207af Add comments 2025-12-09 11:29:55 +01:00
tristantr
b5029f9a61 Add error handling 2025-12-09 11:27:12 +01:00
tristantr
2033ded4b4 Use derived instead of function for Svelte 5 good practices 2025-12-09 11:24:44 +01:00
tristantr
05099b891a Remove banner if all tutorials marked as completed, by role 2025-12-09 11:09:29 +01:00
tristantr
8ab0ffe2d1 Remove tutorials banner if all tutorials marked as completed 2025-12-09 11:03:31 +01:00
tristantr
3383eaf060 Remove banner if user has skipped all tutorials 2025-12-09 10:50:53 +01:00
tristantr
4be122c6fc Allow new operator so see the tutorial banner 2025-12-09 10:42:34 +01:00
393 changed files with 9050 additions and 33513 deletions

View File

@@ -1,62 +0,0 @@
{
"permissions": {
"allow": [
"Read(**/*.rs)",
"Bash(ls:*)",
"Bash(grep:*)",
"Bash(cat:*)",
"Bash(head:*)",
"Bash(tail:*)",
"Bash(less:*)",
"Bash(more:*)",
"Bash(find:*)",
"Bash(wc:*)",
"Bash(diff:*)",
"Bash(file:*)",
"Bash(stat:*)",
"Bash(tree:*)",
"Bash(pwd)",
"Bash(which:*)",
"Bash(whereis:*)",
"Bash(echo:*)",
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git branch:*)",
"Bash(git show:*)",
"Bash(git blame:*)"
],
"deny": [
"Read(.env)",
"Read(.env.*)",
"Read(**/.env)",
"Read(**/.env.*)",
"Read(**/secrets/**)",
"Read(**/*.pem)",
"Read(**/*.key)",
"Read(**/credentials.json)",
"Read(**/*secret*)",
"Edit(.env)",
"Edit(.env.*)",
"Edit(**/.env)",
"Edit(**/.env.*)"
],
"ask": [
"Bash(rm:*)",
"Bash(rmdir:*)",
"Bash(mv:*)",
"Bash(chmod:*)",
"Bash(chown:*)",
"Bash(truncate:*)",
"Bash(shred:*)",
"Bash(unlink:*)",
"Bash(git push:*)",
"Bash(git reset:*)",
"Bash(git revert:*)",
"Bash(git checkout:*)",
"Bash(git merge:*)",
"Bash(git rebase:*)"
]
},
"enableAllProjectMcpServers": true
}

View File

@@ -84,7 +84,6 @@ jobs:
RUST_LOG_STYLE: never
CARGO_NET_GIT_FETCH_WITH_CLI: true
WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES: 1
WMDEBUG_FORCE_RUNNABLE_SETTINGS_V0: 1
run: |
deno --version && bun -v && go version && python3 --version
cd windmill-duckdb-ffi-internal && ./build_dev.sh && cd ..

View File

@@ -29,11 +29,6 @@ on:
required: false
default: false
type: boolean
slim:
description: "Build slim image (true, false)"
required: false
default: false
type: boolean
concurrency:
group: ${{ github.ref }}
cancel-in-progress: false
@@ -593,7 +588,7 @@ jobs:
${{ steps.meta-ee-public.outputs.labels }}
build_ee_slim:
if: ${{ startsWith(github.ref, 'refs/tags/v') }} || ((github.event_name != 'workflow_dispatch') || (github.event.inputs.slim))
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
needs: [build_ee]
runs-on: ubicloud
steps:
@@ -613,7 +608,6 @@ jobs:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee-slim
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
@@ -628,7 +622,7 @@ jobs:
uses: depot/build-push-action@v1
with:
context: .
platforms: linux/amd64,linux/arm64
platforms: linux/amd64
push: true
file: "./docker/DockerfileSlimEe"
tags: |

View File

@@ -1,98 +1,5 @@
# Changelog
## [1.593.0](https://github.com/windmill-labs/windmill/compare/v1.592.1...v1.593.0) (2025-12-17)
### Features
* **ai:** support IAM auth for bedrock provider ([#7379](https://github.com/windmill-labs/windmill/issues/7379)) ([8c55f61](https://github.com/windmill-labs/windmill/commit/8c55f61bbad81bc81509660b5d54d3289c1edfca))
* **backend:** stop schedules and cancel jobs when archiving a workspace ([#7377](https://github.com/windmill-labs/windmill/issues/7377)) ([ebc82db](https://github.com/windmill-labs/windmill/commit/ebc82dbe58eef19ca1e049f0b2099b702fe3725e))
* data table schemas ([#7353](https://github.com/windmill-labs/windmill/issues/7353)) ([75fdc2c](https://github.com/windmill-labs/windmill/commit/75fdc2cdc96ae06ee8a7891fe670acec8a58afe3))
* http triggers scopes ([#7385](https://github.com/windmill-labs/windmill/issues/7385)) ([b4eb7c6](https://github.com/windmill-labs/windmill/commit/b4eb7c6ac076261aed2d9c97f3b09ac52f7fe0da))
* **internal:** runnable settings ([#7298](https://github.com/windmill-labs/windmill/issues/7298)) ([fe56191](https://github.com/windmill-labs/windmill/commit/fe5619142228ea5370b64112e3a2e38aed507b66))
* workspace forks merge UI ([#7333](https://github.com/windmill-labs/windmill/issues/7333)) ([9d06c15](https://github.com/windmill-labs/windmill/commit/9d06c152ee5c2ab1f76a631411f3603bb0575f5e))
### Bug Fixes
* add history directly viewable in folder/group viewer ([#7365](https://github.com/windmill-labs/windmill/issues/7365)) ([b3603d8](https://github.com/windmill-labs/windmill/commit/b3603d872090c354a9ee82714a6a0e4e79019428))
* add history to raw app builder ([#7362](https://github.com/windmill-labs/windmill/issues/7362)) ([431074d](https://github.com/windmill-labs/windmill/commit/431074d2493d6e87148806a09f60a7eacef552ff))
* **aiagent:** fix gemini-3.0 usage ([#7382](https://github.com/windmill-labs/windmill/issues/7382)) ([f64d918](https://github.com/windmill-labs/windmill/commit/f64d918af6e1d9c0e5b1c0abfee081625f3410cb))
* **aichat:** fix for azure responses api not available in some region ([#7387](https://github.com/windmill-labs/windmill/issues/7387)) ([e7719d2](https://github.com/windmill-labs/windmill/commit/e7719d2cda1c636f0f0acd7cb9bd52c6b3712ebe))
* **backend:** better trigger listening logs ([#7392](https://github.com/windmill-labs/windmill/issues/7392)) ([3ba361a](https://github.com/windmill-labs/windmill/commit/3ba361ad1ae19130b8bd72a3d940ddc529f0471b))
* **frontend:** http/email triggers UI nits ([#7378](https://github.com/windmill-labs/windmill/issues/7378)) ([75e1e90](https://github.com/windmill-labs/windmill/commit/75e1e902734e755f2979f882dd4b2889ce13dfef))
* **mcp:** fix unresovled schema ([#7383](https://github.com/windmill-labs/windmill/issues/7383)) ([1b86a39](https://github.com/windmill-labs/windmill/commit/1b86a39051df1344718ed868a15714f4cee90680))
* propagate canceled_by in flows ([#7396](https://github.com/windmill-labs/windmill/issues/7396)) ([0454f39](https://github.com/windmill-labs/windmill/commit/0454f392e7d9c77f47252b18c1d7ec2ba2cc8cca))
* **rawapp:** make popup work with runnables ([2f5fdd6](https://github.com/windmill-labs/windmill/commit/2f5fdd6b3f742a614cfba590408b88a64d0c86a3))
* **rawapp:** schema for openai ([#7364](https://github.com/windmill-labs/windmill/issues/7364)) ([37394d6](https://github.com/windmill-labs/windmill/commit/37394d6d532923aa273b50c94799ed7a0161e2af))
* SCIM 2.0 RFC compliance + displayName support ([#7380](https://github.com/windmill-labs/windmill/issues/7380)) ([6ffb80d](https://github.com/windmill-labs/windmill/commit/6ffb80d1e1631385ea1bc2b5ad447431f52d892f))
## [1.592.1](https://github.com/windmill-labs/windmill/compare/v1.592.0...v1.592.1) (2025-12-12)
### Bug Fixes
* **ai:** improve share system prompts ([490114d](https://github.com/windmill-labs/windmill/commit/490114d133a08ef7f61ed216796b01fbec32a677))
## [1.592.0](https://github.com/windmill-labs/windmill/compare/v1.591.4...v1.592.0) (2025-12-12)
### Features
* **ai:** standardize and improve system prompts ([#7346](https://github.com/windmill-labs/windmill/issues/7346)) ([31e002a](https://github.com/windmill-labs/windmill/commit/31e002ad411bfbf08c933700bf5ae12b253ac0b8))
### Bug Fixes
* **bun:** deployment error on workspace dependencies ([#7355](https://github.com/windmill-labs/windmill/issues/7355)) ([6859670](https://github.com/windmill-labs/windmill/commit/68596701f1e9da460d7bd3246dc797fca1e66a62))
* clear datetime input in schedule sets input to null ([#7358](https://github.com/windmill-labs/windmill/issues/7358)) ([8f1343e](https://github.com/windmill-labs/windmill/commit/8f1343e155620b047e746da40653ec627f97a1a3))
## [1.591.4](https://github.com/windmill-labs/windmill/compare/v1.591.3...v1.591.4) (2025-12-12)
### Bug Fixes
* **app:** fix raw scripts forbidden by policy if no args ([857adf5](https://github.com/windmill-labs/windmill/commit/857adf5b63fe243736366c38a7e573678552a99b))
## [1.591.3](https://github.com/windmill-labs/windmill/compare/v1.591.2...v1.591.3) (2025-12-11)
### Bug Fixes
* fix test up to step ([#7348](https://github.com/windmill-labs/windmill/issues/7348)) ([4337a88](https://github.com/windmill-labs/windmill/commit/4337a8810e7db874cd75006aacf4e4a85bc33836))
* S3 SDK nits + Presigned S3 Public URL function ([#7342](https://github.com/windmill-labs/windmill/issues/7342)) ([2ee00b3](https://github.com/windmill-labs/windmill/commit/2ee00b3c7b0f7ba9a5997ec4474949500ff2c67b))
## [1.591.2](https://github.com/windmill-labs/windmill/compare/v1.591.1...v1.591.2) (2025-12-11)
### Bug Fixes
* **backend:** pin reqwest to 0.12.24 for better handling of redirects ([#7336](https://github.com/windmill-labs/windmill/issues/7336)) ([99bf866](https://github.com/windmill-labs/windmill/commit/99bf866f90d96f706a517b7626df999bce9e36ac))
## [1.591.1](https://github.com/windmill-labs/windmill/compare/v1.591.0...v1.591.1) (2025-12-10)
### Bug Fixes
* update git sync to latest cli ([da65ddd](https://github.com/windmill-labs/windmill/commit/da65ddd8f82ae283557526d0e97b1057976ec2ff))
## [1.591.0](https://github.com/windmill-labs/windmill/compare/v1.590.0...v1.591.0) (2025-12-10)
### Features
* **aichat:** add test pipeline ([#7321](https://github.com/windmill-labs/windmill/issues/7321)) ([36cb5bd](https://github.com/windmill-labs/windmill/commit/36cb5bde1a887effee2e2ba83a6ddd85f3e79775))
* **aichat:** use single tool for flow chat ([#7326](https://github.com/windmill-labs/windmill/issues/7326)) ([888a6a4](https://github.com/windmill-labs/windmill/commit/888a6a4e607f814cd92f3f489b40ac886dbafc0d))
* **mcp:** add documentation endpoint ([#7331](https://github.com/windmill-labs/windmill/issues/7331)) ([bd3271f](https://github.com/windmill-labs/windmill/commit/bd3271f967ed1996060c590bac0587a387b7869c))
### Bug Fixes
* alining group name normalization between scim and igroup api ([#7325](https://github.com/windmill-labs/windmill/issues/7325)) ([8159b8e](https://github.com/windmill-labs/windmill/commit/8159b8e017660f3b3aeed6beb97a42e3ee658824))
* **backend:** add presigned url support for object storage ([#7328](https://github.com/windmill-labs/windmill/issues/7328)) ([651681b](https://github.com/windmill-labs/windmill/commit/651681b7efb6793d2939217aafcfb3e44f5b0e84))
* **cli:** do not remove schema for normal apps ([3470c92](https://github.com/windmill-labs/windmill/commit/3470c927aeae1627984fff535ec36ece4349f329))
* **frontend:** do not consider advanced module settings when testing ai agent step ([72a38f3](https://github.com/windmill-labs/windmill/commit/72a38f36fbaf3ceaec03612b9932187fa134755c))
## [1.590.0](https://github.com/windmill-labs/windmill/compare/v1.589.3...v1.590.0) (2025-12-08)

View File

@@ -1,6 +1,16 @@
ARG DEBIAN_IMAGE=debian:bookworm-slim
ARG RUST_IMAGE=rust:1.90-slim-bookworm
# Build libwindmill_duckdb_ffi_internal.so separately
FROM ${RUST_IMAGE} AS windmill_duckdb_ffi_internal_builder
WORKDIR /windmill-duckdb-ffi-internal
RUN apt-get update && apt-get install -y pkg-config clang=1:14.0-55.* libclang-dev=1:14.0-55.* cmake=3.25.* && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
COPY ./backend/windmill-duckdb-ffi-internal .
RUN cargo build --release -p windmill_duckdb_ffi_internal
FROM ${RUST_IMAGE} AS rust_base
RUN apt-get update && apt-get install -y git libssl-dev pkg-config npm
@@ -20,20 +30,6 @@ WORKDIR /windmill
ENV SQLX_OFFLINE=true
# ENV CARGO_INCREMENTAL=1
FROM rust_base AS windmill_duckdb_ffi_internal_builder
WORKDIR /windmill-duckdb-ffi-internal
RUN apt-get update && apt-get install -y clang=1:14.0-55.* libclang-dev=1:14.0-55.* cmake=3.25.* && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
COPY ./backend/windmill-duckdb-ffi-internal .
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=$SCCACHE_DIR,sharing=locked \
cargo build --release -p windmill_duckdb_ffi_internal
FROM node:24-alpine as frontend
# install dependencies
@@ -48,7 +44,6 @@ RUN mkdir /backend
COPY /backend/windmill-api/openapi.yaml /backend/windmill-api/openapi.yaml
COPY /openflow.openapi.yaml /openflow.openapi.yaml
COPY /backend/windmill-api/build_openapi.sh /backend/windmill-api/build_openapi.sh
COPY /system_prompts/auto-generated /system_prompts/auto-generated
RUN cd /backend/windmill-api && . ./build_openapi.sh
COPY /backend/parsers/windmill-parser-wasm/pkg/ /backend/parsers/windmill-parser-wasm/pkg/
@@ -234,9 +229,7 @@ COPY ./frontend/src/lib/hubPaths.json ${APP}/hubPaths.json
RUN windmill cache ${APP}/hubPaths.json && rm ${APP}/hubPaths.json && chmod -R 777 /tmp/windmill
# Cr,.eate a non-root user 'windmill' with UID and GID 1000
# Create a non-root user 'windmill' with UID and GID 1000
RUN addgroup --gid 1000 windmill && \
adduser --disabled-password --gecos "" --uid 1000 --gid 1000 windmill

View File

@@ -1,19 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_diff SET has_changes = true, exists_in_source = $5, exists_in_fork = $6\n WHERE path = $3 AND kind = $4 AND (\n (source_workspace_id = $1 AND fork_workspace_id = $2)\n OR (source_workspace_id = $2 AND fork_workspace_id =$1)\n )",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Text",
"Bool",
"Bool"
]
},
"nullable": []
},
"hash": "034a8519198daf30e0eb8a74ed92f896c83bb39e1cb52fe3c29c1a224c3859c2"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE schedule SET enabled = false WHERE workspace_id = $1 AND enabled = true RETURNING path",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "03669873e4e3b22c737d5170821f677925474aad885bf1c0780bdb978225517e"
}

View File

@@ -0,0 +1,156 @@
{
"db_name": "PostgreSQL",
"query": "select hash, tag, concurrency_key, concurrent_limit, concurrency_time_window_s, debounce_key, debounce_delay_s, cache_ttl, cache_ignore_s3_path, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use, timeout, has_preprocessor, on_behalf_of_email, created_by, path from script where hash = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "hash",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "concurrency_key",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "concurrent_limit",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "concurrency_time_window_s",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "debounce_key",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "debounce_delay_s",
"type_info": "Int4"
},
{
"ordinal": 7,
"name": "cache_ttl",
"type_info": "Int4"
},
{
"ordinal": 8,
"name": "cache_ignore_s3_path",
"type_info": "Bool"
},
{
"ordinal": 9,
"name": "language: ScriptLang",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
},
{
"ordinal": 10,
"name": "dedicated_worker",
"type_info": "Bool"
},
{
"ordinal": 11,
"name": "priority",
"type_info": "Int2"
},
{
"ordinal": 12,
"name": "delete_after_use",
"type_info": "Bool"
},
{
"ordinal": 13,
"name": "timeout",
"type_info": "Int4"
},
{
"ordinal": 14,
"name": "has_preprocessor",
"type_info": "Bool"
},
{
"ordinal": 15,
"name": "on_behalf_of_email",
"type_info": "Text"
},
{
"ordinal": 16,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 17,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
false,
true,
true,
true,
true,
true,
true,
true,
true,
false,
true,
true,
true,
true,
true,
true,
false,
false
]
},
"hash": "05b69dcef0f4f649513e186e73089979c49b4b8113ee832ea7539b56a0415f32"
}

View File

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

View File

@@ -29,7 +29,8 @@
"postgres",
"sqs",
"gcp",
"mqtt"
"mqtt",
"nextcloud"
]
}
}

View File

@@ -1,59 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path, kind, ahead, behind, has_changes, exists_in_source, exists_in_fork FROM workspace_diff\n WHERE source_workspace_id = $1 AND fork_workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "kind",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "ahead",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "behind",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "has_changes",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "exists_in_source",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "exists_in_fork",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
true,
true,
true
]
},
"hash": "0b8e5fe95f4a2855678ca041b50405b698a368626da42dd9f4ce9d0681d016a1"
}

View File

@@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM workspace_diff WHERE path = $3 AND kind = $4 AND (\n (source_workspace_id = $1 AND fork_workspace_id = $2)\n OR (source_workspace_id = $2 AND fork_workspace_id =$1)\n )",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "0f689b9bd1c9a24f3c6cdafef0215f102122665bc3cc15718831b991052b4caf"
}

View File

@@ -39,7 +39,8 @@
"postgres",
"sqs",
"gcp",
"mqtt"
"mqtt",
"nextcloud"
]
}
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE instance_group SET scim_display_name = $1 where id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "2241ed0c5a47ac715de3ef13a850e514e0fb7b062f4147bffb0e9badfea478d0"
}

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script\n (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s)\n\n SELECT workspace_id, $1, path, array_prepend($2::bigint, COALESCE(parent_hashes, '{}'::bigint[])), summary, description, content, created_by, schema, is_template, extra_perms, NULL, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s\n\n FROM script WHERE hash = $2 AND workspace_id = $3;\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Text"
]
},
"nullable": []
},
"hash": "23759cb515e926e272bbc8e5d8a0a9d039b99bc2026e381e99ef41cdaf6ea19f"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path FROM script\n WHERE workspace_id = $1 AND path = ANY($2) AND archived = false",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"TextArray"
]
},
"nullable": [
false
]
},
"hash": "2d1ba3c92c0385c530934082284cf548a50d533dc1bef58dfd0ecc163c9920f3"
}

View File

@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value, is_secret, description\n FROM variable\n WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "is_secret",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "description",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "2d5ff8fc102ae0c452c1f9cd5cd30fd0a3b4e6e746c659da1767edafd139d45e"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, cache_ignore_s3_path, runnable_settings_handle) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38)",
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, cache_ignore_s3_path) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37)",
"describe": {
"columns": [],
"parameters": {
@@ -86,11 +86,10 @@
"Jsonb",
"Varchar",
"Int4",
"Bool",
"Int8"
"Bool"
]
},
"nullable": []
},
"hash": "b4eb72b0274cbdce7490f63c36d0d16ee847294fadc138593a1baa417cbb3652"
"hash": "3d05d9d7e087eb6e1c14c2b8a20598581e6c7493ed99cb9ad1c2ee5d0b212d38"
}

View File

@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO runnable_settings (hash, debouncing_settings, concurrency_settings)\n VALUES ($1, $2, $3)\n ON CONFLICT (hash)\n DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Int8"
]
},
"nullable": []
},
"hash": "451d9cde90d14071e21ffb5f615052b7ba7fc315fc301ed5c0ff50d9a3ab0d4a"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id FROM workspace WHERE parent_workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "4d04e436d46530f2f4c9dbc5c6c472581b9d90d330957539c0019ec2e7d9a68a"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path FROM app\n WHERE workspace_id = $1 AND path = ANY($2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"TextArray"
]
},
"nullable": [
false
]
},
"hash": "50ef2eac143273c7b7b7414b9be3c89130c709a57aca712e2af996bd4a4fa101"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path FROM resource\n WHERE workspace_id = $1 AND path = ANY($2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"TextArray"
]
},
"nullable": [
false
]
},
"hash": "5169d793f3820095839bb69d6c7c8cbea692ef6c1a838b970f382ee72db492d3"
}

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE instance_group SET scim_display_name = $1, name = $2 where id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "574d9a2bb6eceb62f3d2c2be3f18b29ef8bba3d6da1b1e21f2ca307ccbebee89"
}

View File

@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script\n (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle)\n\n SELECT workspace_id, $1, path, array_prepend($2::bigint, COALESCE(parent_hashes, '{}'::bigint[])), summary, description, content, created_by, schema, is_template, extra_perms, NULL, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle\n\n FROM script WHERE hash = $2 AND workspace_id = $3;\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Text"
]
},
"nullable": []
},
"hash": "5c056ad6cc8967393729288437205c605a24118021fdb2b21b6b61695dc4ff28"
}

View File

@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT app.summary, app.policy, app_version.value\n FROM app\n JOIN app_version\n ON app_version.id = app.versions[array_upper(app.versions, 1)]\n WHERE app.workspace_id = $1 AND app.path = $2 AND COALESCE(app.draft_only, false) = false",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "policy",
"type_info": "Jsonb"
},
{
"ordinal": 2,
"name": "value",
"type_info": "Json"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "66a9cb11fcc4757a3b35154840b703b87daa538cf3a72007649bbb074f56131e"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COALESCE((SELECT MIN(started_at) as min_started_at\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN runnable_settings rs ON rs.hash = v2_job_queue.runnable_settings_handle LEFT JOIN concurrency_settings cs ON cs.hash = rs.concurrency_settings\n WHERE v2_job.runnable_path = $1 AND v2_job.kind != 'dependencies' AND v2_job_queue.running = true AND v2_job_queue.workspace_id = $2 AND v2_job_queue.canceled_by IS NULL AND COALESCE(cs.concurrent_limit, v2_job.concurrent_limit) > 0), $3) as min_started_at, now() AS now",
"query": "SELECT COALESCE((SELECT MIN(started_at) as min_started_at\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id\n WHERE v2_job.runnable_path = $1 AND v2_job.kind != 'dependencies' AND v2_job_queue.running = true AND v2_job_queue.workspace_id = $2 AND v2_job_queue.canceled_by IS NULL AND v2_job.concurrent_limit > 0), $3) as min_started_at, now() AS now",
"describe": {
"columns": [
{
@@ -26,5 +26,5 @@
null
]
},
"hash": "a2e52f033120a3f0b64e0a5ba125df7ce0d25096a23f0b655846a1c07b41f620"
"hash": "6b6f8f7b4a6b6e7e41a9da8b6dfdbcae842ff252cc355bd91aeeb5e26dcc74f3"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n v2_job_queue.runnable_settings_handle,\n scheduled_for,\n runnable_path,\n kind as \"kind: JobKind\",\n runnable_id as \"runnable_id: ScriptHash\",\n canceled_reason,\n canceled_by,\n permissioned_as,\n permissioned_as_email,\n flow_status as \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n v2_job.tag,\n script_lang as \"script_lang: ScriptLang\",\n same_worker,\n pre_run_error,\n concurrent_limit,\n concurrency_time_window_s,\n flow_innermost_root_job,\n root_job,\n timeout,\n flow_step_id,\n cache_ttl,\n cache_ignore_s3_path,\n v2_job_queue.priority,\n preprocessed,\n script_entrypoint_override,\n trigger,\n trigger_kind as \"trigger_kind: JobTriggerKind\",\n visible_to_owner,\n NULL as permissioned_as_end_user_email\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1",
"query": "SELECT \n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n scheduled_for,\n runnable_path,\n kind as \"kind: JobKind\",\n runnable_id as \"runnable_id: ScriptHash\",\n canceled_reason,\n canceled_by,\n permissioned_as,\n permissioned_as_email,\n flow_status as \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n v2_job.tag,\n script_lang as \"script_lang: ScriptLang\",\n same_worker,\n pre_run_error,\n concurrent_limit,\n concurrency_time_window_s,\n flow_innermost_root_job,\n root_job,\n timeout,\n flow_step_id,\n cache_ttl,\n cache_ignore_s3_path,\n v2_job_queue.priority,\n preprocessed,\n script_entrypoint_override,\n trigger,\n trigger_kind as \"trigger_kind: JobTriggerKind\",\n visible_to_owner,\n NULL as permissioned_as_end_user_email\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1",
"describe": {
"columns": [
{
@@ -35,21 +35,16 @@
},
{
"ordinal": 6,
"name": "runnable_settings_handle",
"type_info": "Int8"
},
{
"ordinal": 7,
"name": "scheduled_for",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"ordinal": 7,
"name": "runnable_path",
"type_info": "Varchar"
},
{
"ordinal": 9,
"ordinal": 8,
"name": "kind: JobKind",
"type_info": {
"Custom": {
@@ -84,42 +79,42 @@
}
},
{
"ordinal": 10,
"ordinal": 9,
"name": "runnable_id: ScriptHash",
"type_info": "Int8"
},
{
"ordinal": 11,
"ordinal": 10,
"name": "canceled_reason",
"type_info": "Text"
},
{
"ordinal": 12,
"ordinal": 11,
"name": "canceled_by",
"type_info": "Varchar"
},
{
"ordinal": 13,
"ordinal": 12,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 14,
"ordinal": 13,
"name": "permissioned_as_email",
"type_info": "Varchar"
},
{
"ordinal": 15,
"ordinal": 14,
"name": "flow_status: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 16,
"ordinal": 15,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 17,
"ordinal": 16,
"name": "script_lang: ScriptLang",
"type_info": {
"Custom": {
@@ -155,77 +150,77 @@
}
},
{
"ordinal": 18,
"ordinal": 17,
"name": "same_worker",
"type_info": "Bool"
},
{
"ordinal": 19,
"ordinal": 18,
"name": "pre_run_error",
"type_info": "Text"
},
{
"ordinal": 20,
"ordinal": 19,
"name": "concurrent_limit",
"type_info": "Int4"
},
{
"ordinal": 21,
"ordinal": 20,
"name": "concurrency_time_window_s",
"type_info": "Int4"
},
{
"ordinal": 22,
"ordinal": 21,
"name": "flow_innermost_root_job",
"type_info": "Uuid"
},
{
"ordinal": 23,
"ordinal": 22,
"name": "root_job",
"type_info": "Uuid"
},
{
"ordinal": 24,
"ordinal": 23,
"name": "timeout",
"type_info": "Int4"
},
{
"ordinal": 25,
"ordinal": 24,
"name": "flow_step_id",
"type_info": "Varchar"
},
{
"ordinal": 26,
"ordinal": 25,
"name": "cache_ttl",
"type_info": "Int4"
},
{
"ordinal": 27,
"ordinal": 26,
"name": "cache_ignore_s3_path",
"type_info": "Bool"
},
{
"ordinal": 28,
"ordinal": 27,
"name": "priority",
"type_info": "Int2"
},
{
"ordinal": 29,
"ordinal": 28,
"name": "preprocessed",
"type_info": "Bool"
},
{
"ordinal": 30,
"ordinal": 29,
"name": "script_entrypoint_override",
"type_info": "Varchar"
},
{
"ordinal": 31,
"ordinal": 30,
"name": "trigger",
"type_info": "Varchar"
},
{
"ordinal": 32,
"ordinal": 31,
"name": "trigger_kind: JobTriggerKind",
"type_info": {
"Custom": {
@@ -244,19 +239,20 @@
"postgres",
"sqs",
"gcp",
"mqtt"
"mqtt",
"nextcloud"
]
}
}
}
},
{
"ordinal": 33,
"ordinal": 32,
"name": "visible_to_owner",
"type_info": "Bool"
},
{
"ordinal": 34,
"ordinal": 33,
"name": "permissioned_as_end_user_email",
"type_info": "Text"
}
@@ -273,7 +269,6 @@
true,
false,
true,
true,
false,
true,
false,
@@ -304,5 +299,5 @@
null
]
},
"hash": "5bf200f2c8db25ddf231b564503c6c70f7f3958564a79bb0c6b3863b1ebb0cbf"
"hash": "6c97ab28ab47b75fb3ff39ea70fa3627f08b61bbd33aecb9ea816f8f78a04ec5"
}

View File

@@ -39,7 +39,8 @@
"postgres",
"sqs",
"gcp",
"mqtt"
"mqtt",
"nextcloud"
]
}
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n j.id, j.workspace_id, j.runnable_id AS \"runnable_id: ScriptHash\", q.scheduled_for, q.started_at, j.parent_job, j.flow_innermost_root_job, j.runnable_path, j.kind as \"kind!: JobKind\", j.permissioned_as, \n j.created_by, j.script_lang AS \"script_lang: ScriptLang\", j.permissioned_as_email, j.flow_step_id, j.trigger_kind AS \"trigger_kind: JobTriggerKind\", j.trigger, j.priority, j.concurrent_limit, j.tag, j.cache_ttl, q.cache_ignore_s3_path, q.runnable_settings_handle\n FROM v2_job j LEFT JOIN v2_job_queue q ON j.id = q.id\n WHERE j.id = $1 AND j.workspace_id = $2",
"query": "SELECT \n j.id, j.workspace_id, j.runnable_id AS \"runnable_id: ScriptHash\", q.scheduled_for, q.started_at, j.parent_job, j.flow_innermost_root_job, j.runnable_path, j.kind as \"kind!: JobKind\", j.permissioned_as, \n j.created_by, j.script_lang AS \"script_lang: ScriptLang\", j.permissioned_as_email, j.flow_step_id, j.trigger_kind AS \"trigger_kind: JobTriggerKind\", j.trigger, j.priority, j.concurrent_limit, j.tag, j.cache_ttl, q.cache_ignore_s3_path\n FROM v2_job j LEFT JOIN v2_job_queue q ON j.id = q.id\n WHERE j.id = $1 AND j.workspace_id = $2",
"describe": {
"columns": [
{
@@ -154,7 +154,8 @@
"postgres",
"sqs",
"gcp",
"mqtt"
"mqtt",
"nextcloud"
]
}
}
@@ -189,11 +190,6 @@
"ordinal": 20,
"name": "cache_ignore_s3_path",
"type_info": "Bool"
},
{
"ordinal": 21,
"name": "runnable_settings_handle",
"type_info": "Int8"
}
],
"parameters": {
@@ -223,9 +219,8 @@
true,
false,
true,
true,
true
]
},
"hash": "a1745a4f525b251d2f5a602ab2b2ede46b4471e21b11f607573a844013911abe"
"hash": "7b5ad10af2a9b34fa86429499ea24c0c09c6e7e9ebfa3af90035570133f7c579"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "select hash, tag, concurrency_key, concurrent_limit, concurrency_time_window_s, debounce_key, debounce_delay_s, cache_ttl, cache_ignore_s3_path, runnable_settings_handle, language as \"language: ScriptLang\", dedicated_worker, priority, timeout, on_behalf_of_email, created_by FROM script\n WHERE path = $1 AND workspace_id = $2 AND archived = false AND (lock IS NOT NULL OR $3 = false)\n ORDER BY created_at DESC LIMIT 1",
"query": "select hash, tag, concurrency_key, concurrent_limit, concurrency_time_window_s, debounce_key, debounce_delay_s, cache_ttl, cache_ignore_s3_path, language as \"language: ScriptLang\", dedicated_worker, priority, timeout, on_behalf_of_email, created_by FROM script\n WHERE path = $1 AND workspace_id = $2 AND archived = false AND (lock IS NOT NULL OR $3 = false)\n ORDER BY created_at DESC LIMIT 1",
"describe": {
"columns": [
{
@@ -50,11 +50,6 @@
},
{
"ordinal": 9,
"name": "runnable_settings_handle",
"type_info": "Int8"
},
{
"ordinal": 10,
"name": "language: ScriptLang",
"type_info": {
"Custom": {
@@ -90,27 +85,27 @@
}
},
{
"ordinal": 11,
"ordinal": 10,
"name": "dedicated_worker",
"type_info": "Bool"
},
{
"ordinal": 12,
"ordinal": 11,
"name": "priority",
"type_info": "Int2"
},
{
"ordinal": 13,
"ordinal": 12,
"name": "timeout",
"type_info": "Int4"
},
{
"ordinal": 14,
"ordinal": 13,
"name": "on_behalf_of_email",
"type_info": "Text"
},
{
"ordinal": 15,
"ordinal": 14,
"name": "created_by",
"type_info": "Varchar"
}
@@ -132,7 +127,6 @@
true,
true,
true,
true,
false,
true,
true,
@@ -141,5 +135,5 @@
false
]
},
"hash": "a33673ebc4d1eb4c3513987dbc43e2c80974598e1d9fe7203145bfc29928ba65"
"hash": "7f9b7ab9bec6a0f745273d0cd5602ceab46a7ec9fd225f7b9d16a2ddb9bad7b3"
}

View File

@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value, description, resource_type\n FROM resource\n WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "resource_type",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true,
true,
false
]
},
"hash": "819c233915383e89af1bcf1a56c5f67c4e1fc217f216f609e36a9944a7807b33"
}

View File

@@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_diff (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n SELECT $1, unnest($2::varchar[]), $3, $4, 0, 1, NULL\n ON CONFLICT (source_workspace_id, fork_workspace_id, path, kind)\n DO UPDATE SET\n behind = workspace_diff.behind + 1,\n has_changes = NULL",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"VarcharArray",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "90a1797f8d5ef7f9b67557c1fb919d1165860c5daf0de76fb2ac2201de11553c"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n jsonb_strip_nulls(jsonb_build_object(\n 'path', asset.path,\n 'kind', asset.kind,\n 'usages', ARRAY_AGG(jsonb_build_object(\n 'path', asset.usage_path,\n 'kind', asset.usage_kind,\n 'access_type', asset.usage_access_type\n )),\n 'metadata', (CASE\n WHEN asset.kind = 'resource' THEN\n jsonb_build_object('resource_type', resource.resource_type)\n ELSE\n NULL\n END\n )\n )) as \"list!: _\"\n FROM asset\n LEFT JOIN resource ON asset.kind = 'resource'\n AND array_to_string((string_to_array(asset.path, '/'))[1:3], '/') = resource.path -- With specific table, asset path can be e.g u/diego/pg_db/table_name\n AND resource.workspace_id = $1\n WHERE asset.workspace_id = $1\n AND (asset.kind <> 'resource' OR resource.path IS NOT NULL)\n AND (asset.usage_kind <> 'flow' OR asset.usage_path = ANY(SELECT path FROM flow WHERE workspace_id = $1))\n AND (asset.usage_kind <> 'script' OR asset.usage_path = ANY(SELECT path FROM script WHERE workspace_id = $1))\n GROUP BY asset.path, asset.kind, resource.resource_type\n ORDER BY asset.path, asset.kind",
"query": "SELECT\n jsonb_strip_nulls(jsonb_build_object(\n 'path', asset.path,\n 'kind', asset.kind,\n 'usages', ARRAY_AGG(jsonb_build_object(\n 'path', asset.usage_path,\n 'kind', asset.usage_kind,\n 'access_type', asset.usage_access_type\n )),\n 'metadata', (CASE\n WHEN asset.kind = 'resource' THEN\n jsonb_build_object('resource_type', resource.resource_type)\n ELSE\n NULL\n END\n )\n )) as \"list!: _\"\n FROM asset\n LEFT JOIN resource ON asset.kind = 'resource' AND asset.path = resource.path AND resource.workspace_id = $1\n WHERE asset.workspace_id = $1\n AND (asset.kind <> 'resource' OR resource.path IS NOT NULL)\n AND (asset.usage_kind <> 'flow' OR asset.usage_path = ANY(SELECT path FROM flow WHERE workspace_id = $1))\n AND (asset.usage_kind <> 'script' OR asset.usage_path = ANY(SELECT path FROM script WHERE workspace_id = $1))\n GROUP BY asset.path, asset.kind, resource.resource_type\n ORDER BY asset.path, asset.kind",
"describe": {
"columns": [
{
@@ -18,5 +18,5 @@
null
]
},
"hash": "c94cd50ff1233025b170efee6489e6637676a8b0435a4612a7efbfff6ea2543d"
"hash": "92f03f4df5e86eb40b255ad0f2cc85e0302c37b0f312366098104cd280a91ef6"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO tutorial_progress VALUES ($2, $1::bigint::bit(64)) ON CONFLICT (email) DO UPDATE SET progress = EXCLUDED.progress",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Varchar"
]
},
"nullable": []
},
"hash": "99e6bffe177e69448b09e82b30d24af00edc86a9bc498f319c1e5bee55d77a8a"
}

View File

@@ -29,7 +29,8 @@
"postgres",
"sqs",
"gcp",
"mqtt"
"mqtt",
"nextcloud"
]
}
}

View File

@@ -184,7 +184,8 @@
"postgres",
"sqs",
"gcp",
"mqtt"
"mqtt",
"nextcloud"
]
}
}

View File

@@ -1,16 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO password (email, login_type, verified, username, name) VALUES ($1, 'saml', true, $2, $3) ON CONFLICT DO NOTHING",
"query": "INSERT INTO password (email, login_type, verified, username) VALUES ($1, 'saml', true, $2) ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "638d3c2ba1198dce5b5b0e47df59a92ff8011e19fbefcc3960d6f0fe167e55b6"
"hash": "a59aac0bc593d99aedd14fd9606f11191590a12ea98a671e172b867306959884"
}

View File

@@ -1,59 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT hash, created_at, content, summary, description, lock, schema\n FROM script\n WHERE workspace_id = $1 AND path = $2 AND archived = false\n ORDER BY created_at DESC\n LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "hash",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "content",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "summary",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "lock",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "schema",
"type_info": "Json"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
true,
true
]
},
"hash": "a6fe41e36e06c88e1387abaea17107ceab2c5b17258958bde03b3e492bb2790e"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT progress::bigint FROM tutorial_progress WHERE email = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "progress",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "a82eec879838b02e3e0722352fba9f537374dc0658733f7e15bfe42f646d22e2"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n id,\n q.runnable_settings_handle,\n q.workspace_id,\n j.runnable_id as \"runnable_id: ScriptHash\",\n scheduled_for,\n parent_job,\n flow_innermost_root_job,\n runnable_path,\n kind as \"kind: JobKind\",\n started_at,\n permissioned_as,\n created_by,\n script_lang as \"script_lang: ScriptLang\",\n permissioned_as_email,\n flow_step_id,\n trigger_kind as \"trigger_kind: JobTriggerKind\",\n trigger,\n q.priority,\n concurrent_limit,\n q.tag,\n cache_ttl,\n cache_ignore_s3_path,\n r.ping as last_ping,\n worker,\n memory_peak,\n running\n FROM v2_job_queue q\n JOIN v2_job j USING (id)\n LEFT JOIN v2_job_runtime r USING (id)\n LEFT JOIN v2_job_status s USING (id)\n WHERE j.id = $1",
"query": "SELECT id, q.workspace_id, j.runnable_id as \"runnable_id: ScriptHash\", scheduled_for, parent_job, flow_innermost_root_job, runnable_path, kind as \"kind: JobKind\", started_at, permissioned_as, created_by, script_lang as \"script_lang: ScriptLang\", \n permissioned_as_email, flow_step_id, trigger_kind as \"trigger_kind: JobTriggerKind\", trigger, q.priority, concurrent_limit, q.tag, cache_ttl, cache_ignore_s3_path, r.ping as last_ping, worker, memory_peak, running\n FROM v2_job_queue q JOIN v2_job j USING (id) LEFT JOIN v2_job_runtime r USING (id) LEFT JOIN v2_job_status s USING (id)\n WHERE j.id = $1",
"describe": {
"columns": [
{
@@ -10,41 +10,36 @@
},
{
"ordinal": 1,
"name": "runnable_settings_handle",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 3,
"ordinal": 2,
"name": "runnable_id: ScriptHash",
"type_info": "Int8"
},
{
"ordinal": 4,
"ordinal": 3,
"name": "scheduled_for",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"ordinal": 4,
"name": "parent_job",
"type_info": "Uuid"
},
{
"ordinal": 6,
"ordinal": 5,
"name": "flow_innermost_root_job",
"type_info": "Uuid"
},
{
"ordinal": 7,
"ordinal": 6,
"name": "runnable_path",
"type_info": "Varchar"
},
{
"ordinal": 8,
"ordinal": 7,
"name": "kind: JobKind",
"type_info": {
"Custom": {
@@ -79,22 +74,22 @@
}
},
{
"ordinal": 9,
"ordinal": 8,
"name": "started_at",
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"ordinal": 9,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 11,
"ordinal": 10,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 12,
"ordinal": 11,
"name": "script_lang: ScriptLang",
"type_info": {
"Custom": {
@@ -130,17 +125,17 @@
}
},
{
"ordinal": 13,
"ordinal": 12,
"name": "permissioned_as_email",
"type_info": "Varchar"
},
{
"ordinal": 14,
"ordinal": 13,
"name": "flow_step_id",
"type_info": "Varchar"
},
{
"ordinal": 15,
"ordinal": 14,
"name": "trigger_kind: JobTriggerKind",
"type_info": {
"Custom": {
@@ -159,59 +154,60 @@
"postgres",
"sqs",
"gcp",
"mqtt"
"mqtt",
"nextcloud"
]
}
}
}
},
{
"ordinal": 16,
"ordinal": 15,
"name": "trigger",
"type_info": "Varchar"
},
{
"ordinal": 17,
"ordinal": 16,
"name": "priority",
"type_info": "Int2"
},
{
"ordinal": 18,
"ordinal": 17,
"name": "concurrent_limit",
"type_info": "Int4"
},
{
"ordinal": 19,
"ordinal": 18,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 20,
"ordinal": 19,
"name": "cache_ttl",
"type_info": "Int4"
},
{
"ordinal": 21,
"ordinal": 20,
"name": "cache_ignore_s3_path",
"type_info": "Bool"
},
{
"ordinal": 22,
"ordinal": 21,
"name": "last_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 23,
"ordinal": 22,
"name": "worker",
"type_info": "Varchar"
},
{
"ordinal": 24,
"ordinal": 23,
"name": "memory_peak",
"type_info": "Int4"
},
{
"ordinal": 25,
"ordinal": 24,
"name": "running",
"type_info": "Bool"
}
@@ -223,7 +219,6 @@
},
"nullable": [
false,
true,
false,
true,
false,
@@ -250,5 +245,5 @@
false
]
},
"hash": "b3771b690c5966272b1f42c9965bb6a8f961c119516e4c33dc928cd3b4f4edbc"
"hash": "a84e67035584bbdb02482026b9cc0808086c50f78947d43bb88628a481f41a1d"
}

View File

@@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_diff (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n VALUES ($1, $2, $3, $4, 1, 0, NULL)\n ON CONFLICT (source_workspace_id, fork_workspace_id, path, kind)\n DO UPDATE SET\n ahead = workspace_diff.ahead + 1,\n has_changes = NULL",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "ac3001bd72248efe36ae3c96af5450f0721f888d18475fae862d675d40779a30"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET git_app_installations = (\n SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)\n FROM jsonb_array_elements(git_app_installations) AS elem\n WHERE (elem->>'installation_id')::bigint != $1\n )\n WHERE workspace_id = $2\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": []
},
"hash": "ad288f9f242f930fc571d04d5ba217f653437f60c890b6b73059fdaa7eb45302"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM workspace_diff WHERE has_changes = false AND (\n (source_workspace_id = $1 AND fork_workspace_id = $2)\n OR (source_workspace_id = $2 AND fork_workspace_id =$1)\n )",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "af6aea925527c486e56375b6cd215b0e36938a5853285be485647bc8cc846973"
}

View File

@@ -1,49 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, changed_by, changed_at, change_type, member_affected\n FROM group_permission_history\n WHERE workspace_id = $1 AND group_name = $2\n ORDER BY id DESC\n LIMIT $3 OFFSET $4",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "changed_by",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "changed_at",
"type_info": "Timestamptz"
},
{
"ordinal": 3,
"name": "change_type",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "member_affected",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
true
]
},
"hash": "b11034489786626184e2f6b7f99469a945a4bbfe0e6a33c5a3e009dadd56d52c"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (\n id, -- 1\n workspace_id, -- 2\n raw_code, -- 3\n raw_lock, -- 4\n raw_flow, -- 5\n tag, -- 6\n parent_job, -- 7\n created_by, -- 8\n permissioned_as, -- 9\n runnable_id, -- 10\n runnable_path, -- 11\n args, -- 12\n kind, -- 13\n trigger, -- 14\n script_lang, -- 15\n same_worker, -- 16\n pre_run_error, -- 17 \n permissioned_as_email, -- 18\n visible_to_owner, -- 19\n flow_innermost_root_job, -- 20\n root_job, -- 38\n concurrent_limit, -- 21\n concurrency_time_window_s, -- 22\n timeout, -- 23\n flow_step_id, -- 24\n cache_ttl, -- 25\n priority, -- 26\n trigger_kind, -- 39\n script_entrypoint_override, -- 12\n preprocessed -- 27,\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email) \n values ($1, $32, $33, $34, $35, $36, $37, $2, $41) \n ON CONFLICT (job_id) DO UPDATE SET email = EXCLUDED.email, username = EXCLUDED.username, is_admin = EXCLUDED.is_admin, is_operator = EXCLUDED.is_operator, folders = EXCLUDED.folders, groups = EXCLUDED.groups, workspace_id = EXCLUDED.workspace_id, end_user_email = EXCLUDED.end_user_email\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority, cache_ignore_s3_path, runnable_settings_handle)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31, $42, $43)",
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email) \n values ($1, $32, $33, $34, $35, $36, $37, $2, $41) \n ON CONFLICT (job_id) DO UPDATE SET email = EXCLUDED.email, username = EXCLUDED.username, is_admin = EXCLUDED.is_admin, is_operator = EXCLUDED.is_operator, folders = EXCLUDED.folders, groups = EXCLUDED.groups, workspace_id = EXCLUDED.workspace_id, end_user_email = EXCLUDED.end_user_email\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority, cache_ignore_s3_path)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31, $42)",
"describe": {
"columns": [],
"parameters": {
@@ -121,18 +121,18 @@
"postgres",
"sqs",
"gcp",
"mqtt"
"mqtt",
"nextcloud"
]
}
}
},
"Bool",
"Varchar",
"Bool",
"Int8"
"Bool"
]
},
"nullable": []
},
"hash": "14276a040cb4db88d71fccdc3579e8c0bb132b70668301b535872d1632753e30"
"hash": "b179a3f876ca659bed892d464bf51a733cc86a3204fcd9edccda63fddc97dced"
}

View File

@@ -104,7 +104,8 @@
"postgres",
"sqs",
"gcp",
"mqtt"
"mqtt",
"nextcloud"
]
}
}

View File

@@ -1,49 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, changed_by, changed_at, change_type, affected\n FROM folder_permission_history\n WHERE workspace_id = $1 AND folder_name = $2\n ORDER BY id DESC\n LIMIT $3 OFFSET $4",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "changed_by",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "changed_at",
"type_info": "Timestamptz"
},
{
"ordinal": 3,
"name": "change_type",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "affected",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
true
]
},
"hash": "b24eeafc2fc26664ef38d15d3cb47ca19549bcd9372d7a61ec2e28d82897bacf"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id FROM v2_job_queue WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "b9c520fdc29b17977bf1c0b30311c536ebea8a76b75cda188ec91fcc0fc274a7"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO instance_group (name, scim_display_name, id, external_id) VALUES ($1, $2, $3, $4)",
"query": "INSERT INTO instance_group (name, scim_display_name, id, external_id) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
@@ -13,5 +13,5 @@
},
"nullable": []
},
"hash": "7cb7dcf8b20deb59fb1c3d4ad0ca4f9a209ce0d80682182e56946392f800c24c"
"hash": "bd829646d08f68106211f97c75dce13b6fc7d35bbaf7f503dcc73ae48fd07489"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path FROM flow\n WHERE workspace_id = $1 AND path = ANY($2) AND archived = false",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"TextArray"
]
},
"nullable": [
false
]
},
"hash": "c5897802334f09596a0297f9b9a1e24f3c6f8b1628965438a9360c073d70924e"
}

View File

@@ -1,41 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value, summary, description, schema\n FROM flow\n WHERE workspace_id = $1 AND path = $2 AND archived = false",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "summary",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "schema",
"type_info": "Json"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
true
]
},
"hash": "dc6ffc9946325e794f3a9d1cf621a9c07d211351195c399f6ff25c1dc7adb01a"
}

View File

@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM instance_group",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "e1c1e25053ae4b1635780c7e472c9d86806fa8cc762786b7a29bd121837c8ebc"
}

View File

@@ -184,7 +184,8 @@
"postgres",
"sqs",
"gcp",
"mqtt"
"mqtt",
"nextcloud"
]
}
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE folder SET owners = array_remove(owners, $1::varchar) WHERE name = $2 AND workspace_id = $3 AND $1 = ANY(owners) RETURNING name",
"query": "UPDATE folder SET owners = array_remove(owners, $1::varchar) WHERE name = $2 AND workspace_id = $3 RETURNING name",
"describe": {
"columns": [
{
@@ -11,7 +11,7 @@
],
"parameters": {
"Left": [
"Text",
"Varchar",
"Text",
"Text"
]
@@ -20,5 +20,5 @@
false
]
},
"hash": "a3ca0af5d84acba93776828c090a79bf6554180b9db4951b69f7070f769b2962"
"hash": "e500a422fa986faf5c612c08f4a139e10b25a3106551e7c92f43dcf9758a5ec7"
}

View File

@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT concurrency_settings, debouncing_settings FROM runnable_settings WHERE hash = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "concurrency_settings",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "debouncing_settings",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true,
true
]
},
"hash": "ebbbd069e0f33be9609604025d159fe1ecbefc2e9c11f7c4900b7121d4367e01"
}

View File

@@ -1,18 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO folder_permission_history\n (workspace_id, folder_name, changed_by, change_type, affected)\n VALUES ($1, $2, $3, $4, $5)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "f1206b00c4b81e59943385646d653330efba9cd1e731621f7efc91f04567841f"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id FROM instance_group WHERE name = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "f2eb05fe3581772d985e2ace82706d824b208ee9a1cb65a85be389e14672620c"
}

View File

@@ -1,18 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO group_permission_history\n (workspace_id, group_name, changed_by, change_type, member_affected)\n VALUES ($1, $2, $3, $4, $5)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "f95358255e55d68dd453d173e23ab3cb1f1c2ba5b1cfc78f706b0b014f045477"
}

View File

@@ -104,7 +104,8 @@
"postgres",
"sqs",
"gcp",
"mqtt"
"mqtt",
"nextcloud"
]
}
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path FROM variable\n WHERE workspace_id = $1 AND path = ANY($2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"TextArray"
]
},
"nullable": [
false
]
},
"hash": "ff24cfb1f603b4ea612e48cee5880fc516207c8a474446a1ee09e8324f85ba09"
}

225
backend/Cargo.lock generated
View File

@@ -788,9 +788,9 @@ dependencies = [
[[package]]
name = "aws-credential-types"
version = "1.2.11"
version = "1.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cd362783681b15d136480ad555a099e82ecd8e2d10a841e14dfd0078d67fee3"
checksum = "b01c9521fa01558f750d183c8c68c81b0155b9d193a4ba7f84c36bd1b6d04a06"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
@@ -800,9 +800,9 @@ dependencies = [
[[package]]
name = "aws-lc-rs"
version = "1.15.2"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288"
checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f"
dependencies = [
"aws-lc-sys",
"zeroize",
@@ -810,9 +810,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
version = "0.35.0"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1"
checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6"
dependencies = [
"cc",
"cmake",
@@ -822,9 +822,9 @@ dependencies = [
[[package]]
name = "aws-runtime"
version = "1.5.17"
version = "1.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d81b5b2898f6798ad58f484856768bca817e3cd9de0974c24ae0f1113fe88f1b"
checksum = "7ce527fb7e53ba9626fc47824f25e256250556c40d8f81d27dd92aa38239d632"
dependencies = [
"aws-credential-types",
"aws-sigv4",
@@ -895,9 +895,9 @@ dependencies = [
[[package]]
name = "aws-sdk-rds"
version = "1.121.0"
version = "1.119.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15640ecd44797fa9396c66793bd06e57f63f66419117f83518f4d51057b01945"
checksum = "70fbf24cf9f84d18f6296857982dd970dd1c23a54a3bac208c7e9abc13a35c1b"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1009,9 +1009,9 @@ dependencies = [
[[package]]
name = "aws-sigv4"
version = "1.3.7"
version = "1.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e523e1c4e8e7e8ff219d732988e22bfeae8a1cafdbe6d9eca1546fa080be7c"
checksum = "c35452ec3f001e1f2f6db107b6373f1f48f05ec63ba2c5c9fa91f07dad32af11"
dependencies = [
"aws-credential-types",
"aws-smithy-eventstream",
@@ -1032,9 +1032,9 @@ dependencies = [
[[package]]
name = "aws-smithy-async"
version = "1.2.7"
version = "1.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ee19095c7c4dda59f1697d028ce704c24b2d33c6718790c7f1d5a3015b4107c"
checksum = "127fcfad33b7dfc531141fda7e1c402ac65f88aca5511a4d31e2e3d2cd01ce9c"
dependencies = [
"futures-util",
"pin-project-lite",
@@ -1043,9 +1043,9 @@ dependencies = [
[[package]]
name = "aws-smithy-eventstream"
version = "0.60.14"
version = "0.60.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc12f8b310e38cad85cf3bef45ad236f470717393c613266ce0a89512286b650"
checksum = "e29a304f8319781a39808847efb39561351b1bb76e933da7aa90232673638658"
dependencies = [
"aws-smithy-types",
"bytes",
@@ -1054,9 +1054,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http"
version = "0.62.6"
version = "0.62.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b"
checksum = "445d5d720c99eed0b4aa674ed00d835d9b1427dd73e04adaf2f94c6b2d6f9fca"
dependencies = [
"aws-smithy-eventstream",
"aws-smithy-runtime-api",
@@ -1076,9 +1076,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http-client"
version = "1.1.5"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59e62db736db19c488966c8d787f52e6270be565727236fd5579eaa301e7bc4a"
checksum = "623254723e8dfd535f566ee7b2381645f8981da086b5c4aa26c0c41582bb1d2c"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
@@ -1106,27 +1106,27 @@ dependencies = [
[[package]]
name = "aws-smithy-json"
version = "0.61.9"
version = "0.61.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551"
checksum = "2db31f727935fc63c6eeae8b37b438847639ec330a9161ece694efba257e0c54"
dependencies = [
"aws-smithy-types",
]
[[package]]
name = "aws-smithy-observability"
version = "0.1.5"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17f616c3f2260612fe44cede278bafa18e73e6479c4e393e2c4518cf2a9a228a"
checksum = "2d1881b1ea6d313f9890710d65c158bdab6fb08c91ea825f74c1c8c357baf4cc"
dependencies = [
"aws-smithy-runtime-api",
]
[[package]]
name = "aws-smithy-query"
version = "0.60.9"
version = "0.60.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae5d689cf437eae90460e944a58b5668530d433b4ff85789e69d2f2a556e057d"
checksum = "d28a63441360c477465f80c7abac3b9c4d075ca638f982e605b7dc2a2c7156c9"
dependencies = [
"aws-smithy-types",
"urlencoding",
@@ -1134,9 +1134,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime"
version = "1.9.6"
version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65fda37911905ea4d3141a01364bc5509a0f32ae3f3b22d6e330c0abfb62d247"
checksum = "0bbe9d018d646b96c7be063dd07987849862b0e6d07c778aad7d93d1be6c1ef0"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
@@ -1158,9 +1158,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime-api"
version = "1.9.3"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab0d43d899f9e508300e587bf582ba54c27a452dd0a9ea294690669138ae14a2"
checksum = "ec7204f9fd94749a7c53b26da1b961b4ac36bf070ef1e0b94bb09f79d4f6c193"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
@@ -1175,9 +1175,9 @@ dependencies = [
[[package]]
name = "aws-smithy-types"
version = "1.3.5"
version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "905cb13a9895626d49cf2ced759b062d913834c7482c38e49557eac4e6193f01"
checksum = "25f535879a207fce0db74b679cfc3e91a3159c8144d717d55f5832aea9eef46e"
dependencies = [
"base64-simd 0.8.0",
"bytes",
@@ -1201,9 +1201,9 @@ dependencies = [
[[package]]
name = "aws-smithy-types-convert"
version = "0.60.11"
version = "0.60.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b70bc27e41d5ed80b376602ff4becdab6ea8489403fad3abbfea2c9c825c1e1e"
checksum = "c99945c7033e37bfe8dc76c19862ebd7544b14b4f34a8551ccf2690b433eba0e"
dependencies = [
"aws-smithy-types",
"chrono",
@@ -1211,18 +1211,18 @@ dependencies = [
[[package]]
name = "aws-smithy-xml"
version = "0.60.13"
version = "0.60.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11b2f670422ff42bf7065031e72b45bc52a3508bd089f743ea90731ca2b6ea57"
checksum = "eab77cdd036b11056d2a30a7af7b775789fb024bf216acc13884c6c97752ae56"
dependencies = [
"xmlparser",
]
[[package]]
name = "aws-types"
version = "1.3.11"
version = "1.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d980627d2dd7bfc32a3c025685a033eeab8d365cc840c631ef59d1b8f428164"
checksum = "d79fb68e3d7fe5d4833ea34dc87d2e97d26d3086cb3da660bb6b1f76d98680b6"
dependencies = [
"aws-credential-types",
"aws-smithy-async",
@@ -1793,9 +1793,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.19.1"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
dependencies = [
"allocator-api2",
]
@@ -2166,9 +2166,9 @@ dependencies = [
[[package]]
name = "cmake"
version = "0.1.56"
version = "0.1.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b042e5d8a74ae91bb0961acd039822472ec99f8ab0948cbf6d1369588f8be586"
checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
dependencies = [
"cc",
]
@@ -2822,7 +2822,7 @@ dependencies = [
"parquet",
"rand 0.8.5",
"regex",
"sqlparser 0.55.0",
"sqlparser",
"tempfile",
"tokio",
"url",
@@ -2899,7 +2899,7 @@ dependencies = [
"parquet",
"paste",
"recursive",
"sqlparser 0.55.0",
"sqlparser",
"tokio",
"web-time",
]
@@ -3075,7 +3075,7 @@ dependencies = [
"paste",
"recursive",
"serde_json",
"sqlparser 0.55.0",
"sqlparser",
]
[[package]]
@@ -3371,7 +3371,7 @@ dependencies = [
"log",
"recursive",
"regex",
"sqlparser 0.55.0",
"sqlparser",
]
[[package]]
@@ -3404,7 +3404,7 @@ dependencies = [
"once_cell",
"percent-encoding",
"serde",
"sourcemap 9.3.1",
"sourcemap 9.3.0",
"swc_atoms",
"swc_common",
"swc_config",
@@ -6922,9 +6922,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
[[package]]
name = "icu_properties"
version = "2.1.2"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
dependencies = [
"icu_collections",
"icu_locale_core",
@@ -6936,9 +6936,9 @@ dependencies = [
[[package]]
name = "icu_properties_data"
version = "2.1.2"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
[[package]]
name = "icu_provider"
@@ -7723,13 +7723,13 @@ dependencies = [
[[package]]
name = "libredox"
version = "0.1.11"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags 2.9.4",
"libc",
"redox_syscall 0.6.0",
"redox_syscall 0.5.18",
]
[[package]]
@@ -7766,9 +7766,9 @@ dependencies = [
[[package]]
name = "libz-rs-sys"
version = "0.5.4"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15413ef615ad868d4d65dce091cb233b229419c7c0c4bcaa746c0901c49ff39c"
checksum = "8b484ba8d4f775eeca644c452a56650e544bf7e617f1d170fe7298122ead5222"
dependencies = [
"zlib-rs",
]
@@ -8602,6 +8602,15 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]]
name = "notify"
version = "6.1.1"
@@ -10570,15 +10579,6 @@ dependencies = [
"bitflags 2.9.4",
]
[[package]]
name = "redox_syscall"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5"
dependencies = [
"bitflags 2.9.4",
]
[[package]]
name = "redox_users"
version = "0.4.6"
@@ -12175,9 +12175,9 @@ dependencies = [
[[package]]
name = "sourcemap"
version = "9.3.1"
version = "9.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ccaaa78a0ca68b20f8f711eaa2522a00131c48a3de5b892ca5c36cec1ce9bb"
checksum = "c8131753a3c444a6177b92fc9e1bd0d2ecaf1c8953e6a41cc599e8e7ca317ef2"
dependencies = [
"base64-simd 0.8.0",
"bitvec",
@@ -12264,17 +12264,6 @@ dependencies = [
"sqlparser_derive",
]
[[package]]
name = "sqlparser"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4591acadbcf52f0af60eafbb2c003232b2b4cd8de5f0e9437cb8b1b59046cc0f"
dependencies = [
"log",
"recursive",
"sqlparser_derive",
]
[[package]]
name = "sqlparser_derive"
version = "0.3.0"
@@ -12636,9 +12625,9 @@ dependencies = [
[[package]]
name = "supports-hyperlinks"
version = "3.2.0"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91"
checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b"
[[package]]
name = "supports-unicode"
@@ -12702,7 +12691,7 @@ dependencies = [
"rustc-hash 1.1.0",
"serde",
"siphasher 0.3.11",
"sourcemap 9.3.1",
"sourcemap 9.3.0",
"swc_allocator",
"swc_atoms",
"swc_eq_ignore_macros",
@@ -12766,7 +12755,7 @@ dependencies = [
"num-bigint",
"once_cell",
"serde",
"sourcemap 9.3.1",
"sourcemap 9.3.0",
"swc_allocator",
"swc_atoms",
"swc_common",
@@ -14253,9 +14242,9 @@ dependencies = [
[[package]]
name = "tree-sitter-language"
version = "0.1.6"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae62f7eae5eb549c71b76658648b72cc6111f2d87d24a1e31fa907f4943e3ce"
checksum = "c4013970217383f67b18aef68f6fb2e8d409bc5755227092d32efb0422ba24b8"
[[package]]
name = "tree-sitter-ruby"
@@ -15174,7 +15163,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"aws-sdk-config",
@@ -15195,6 +15184,7 @@ dependencies = [
"lazy_static",
"libloading 0.8.9",
"memchr",
"nom 8.0.0",
"object_store",
"once_cell",
"pep440_rs",
@@ -15210,7 +15200,6 @@ dependencies = [
"sha1",
"sha2 0.10.9",
"size",
"sql-builder",
"sqlx",
"strum 0.27.2",
"systemstat",
@@ -15237,7 +15226,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"argon2",
@@ -15248,7 +15237,6 @@ dependencies = [
"async-trait",
"async_zip",
"aws-config",
"aws-credential-types",
"aws-sdk-config",
"aws-sdk-sqs",
"aws-sdk-sso",
@@ -15359,7 +15347,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -15374,7 +15362,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"chrono",
"lazy_static",
@@ -15388,7 +15376,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"axum",
@@ -15407,7 +15395,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -15445,7 +15433,6 @@ dependencies = [
"lazy_static",
"magic-crypt",
"mail-send",
"native-tls",
"object_store",
"once_cell",
"openidconnect",
@@ -15457,7 +15444,6 @@ dependencies = [
"pep440_rs",
"phf 0.11.3",
"pin-project-lite",
"postgres-native-tls 0.5.1",
"prometheus",
"quick_cache",
"rand 0.9.0",
@@ -15482,7 +15468,6 @@ dependencies = [
"thiserror 2.0.17",
"tikv-jemalloc-ctl",
"tokio",
"tokio-postgres 0.7.13",
"tokio-stream",
"tokio-util",
"tonic",
@@ -15503,7 +15488,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"regex",
"serde",
@@ -15518,7 +15503,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"bytes",
@@ -15542,7 +15527,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -15558,7 +15543,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -15567,7 +15552,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15579,7 +15564,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"serde_json",
@@ -15591,7 +15576,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"gosyn",
@@ -15603,7 +15588,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15615,7 +15600,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"serde_json",
@@ -15627,7 +15612,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -15638,7 +15623,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15649,7 +15634,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15657,12 +15642,11 @@ dependencies = [
"rustpython-parser",
"serde_json",
"windmill-parser",
"windmill-parser-sql",
]
[[package]]
name = "windmill-parser-py-imports"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -15686,7 +15670,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15700,7 +15684,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -15717,21 +15701,21 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"lazy_static",
"nom 8.0.0",
"regex",
"regex-lite",
"serde",
"serde_json",
"sqlparser 0.59.0",
"windmill-parser",
]
[[package]]
name = "windmill-parser-ts"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15745,12 +15729,11 @@ dependencies = [
"triomphe",
"wasm-bindgen",
"windmill-parser",
"windmill-parser-sql",
]
[[package]]
name = "windmill-parser-yaml"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"serde",
@@ -15761,7 +15744,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -15798,7 +15781,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -15808,7 +15791,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.593.0"
version = "1.590.0"
dependencies = [
"anyhow",
"async-once-cell",
@@ -16714,9 +16697,9 @@ dependencies = [
[[package]]
name = "zlib-rs"
version = "0.5.4"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51f936044d677be1a1168fae1d03b583a285a5dd9d8cbf7b24c23aa1fc775235"
checksum = "36134c44663532e6519d7a6dfdbbe06f6f8192bde8ae9ed076e9b213f0e31df7"
[[package]]
name = "zstd"

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.593.0"
version = "1.590.0"
authors.workspace = true
edition.workspace = true
@@ -33,7 +33,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.593.0"
version = "1.590.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -125,7 +125,6 @@ windmill-autoscaling = { workspace = true, optional = true }
futures.workspace = true
tracing.workspace = true
sqlx.workspace = true
sql-builder.workspace = true
rand.workspace = true
chrono.workspace = true
git-version.workspace = true
@@ -159,6 +158,7 @@ kube.workspace = true
k8s-openapi.workspace = true
libloading.workspace = true
bitflags.workspace = true
nom.workspace = true
globset.workspace = true
@@ -256,7 +256,7 @@ mail-send = { version = "0.4.0", features = ["builder"], default-features=false
urlencoding = "^2"
url = { version = "^2" , features = ["serde"]}
async-oauth2 = "0.5.1"
reqwest = { version = "=0.12.24", features = ["json", "stream", "gzip", "multipart"] }
reqwest = { version = "^0.12", features = ["json", "stream", "gzip", "multipart"] }
eventsource-stream = "0.2.3"
time = "^0"
serde_urlencoded = "^0"
@@ -378,6 +378,7 @@ pg_escape = "0.1.1"
async-nats = "0.38.0"
nkeys = "0.4.4"
nu-parser = { version = "0.101.0", default-features = false }
nom = "8.0.0"
globset = "0.4.16"
process-wrap = { version = "8.2.1", features = ["tokio1"] }

View File

@@ -1 +1 @@
505eadbff32d102ea5245a2bef88ce6f1bb95395
0d72102809e766d5285a162dcc30055847700ee2

View File

@@ -43,7 +43,7 @@ def load_openapi_spec(file_path: str) -> Dict[str, Any]:
print(f"Error loading OpenAPI spec: {e}", file=sys.stderr)
sys.exit(1)
def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]], spec: Dict[str, Any], required_fields: Optional[List[str]] = None, base_path: str = "") -> tuple:
def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]], spec: Dict[str, Any], required_fields: Optional[List[str]] = None) -> tuple:
"""Extract separate schemas for path parameters, query parameters, and request body."""
path_params_schema = {
"type": "object",
@@ -63,16 +63,16 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt
for param in parameters:
# Resolve $ref if present
if '$ref' in param:
param = resolve_schema_refs(param, spec, base_path)
param = resolve_schema_refs(param, spec)
param_name = param.get('name', '')
param_schema = param.get('schema', {'type': 'string'})
param_required = param.get('required', False)
param_description = param.get('description', '')
param_in = param.get('in', 'query')
# Resolve any refs in the parameter schema
param_schema = resolve_schema_refs(param_schema, spec, base_path)
param_schema = resolve_schema_refs(param_schema, spec)
# Add description if available
if param_description:
@@ -93,7 +93,7 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt
# Process request body if present
if request_body:
body_schema = extract_request_body_schema(request_body, spec, base_path)
body_schema = extract_request_body_schema(request_body, spec)
# If we have required fields specified and a body schema, update the required array
if body_schema and required_fields:
@@ -115,109 +115,68 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt
return (path_params_schema, query_params_schema, body_schema)
# Cache for loaded external files
_external_file_cache: Dict[str, Dict[str, Any]] = {}
def load_external_file(file_path: str, base_path: str) -> Optional[Dict[str, Any]]:
"""Load an external YAML file relative to the base path."""
if file_path in _external_file_cache:
return _external_file_cache[file_path]
try:
import yaml
from pathlib import Path
# Resolve the path relative to the base file
base_dir = Path(base_path).parent
full_path = (base_dir / file_path).resolve()
with open(full_path, 'r', encoding='utf-8') as f:
content = yaml.safe_load(f)
_external_file_cache[file_path] = content
return content
except Exception as e:
print(f"Warning: Could not load external file {file_path}: {e}", file=sys.stderr)
return None
def resolve_ref(ref_path: str, spec: Dict[str, Any], base_path: str = "") -> tuple:
"""Resolve a $ref path to the actual schema definition.
Handles both internal refs (#/...) and external file refs (file.yaml#/...).
Returns a tuple of (resolved_schema, resolved_spec) where resolved_spec is the spec
that should be used for resolving any nested refs within the resolved schema.
"""
# Check if this is an external file reference
if '#' in ref_path and not ref_path.startswith('#'):
# External file reference: "../../openflow.openapi.yaml#/components/schemas/Retry"
file_part, fragment = ref_path.split('#', 1)
external_spec = load_external_file(file_part, base_path)
if external_spec is None:
return None, spec
# Resolve the fragment within the external file, and return external_spec for nested refs
resolved, _ = resolve_ref('#' + fragment, external_spec, base_path)
return resolved, external_spec
def resolve_ref(ref_path: str, spec: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Resolve a $ref path to the actual schema definition."""
if not ref_path.startswith('#/'):
return None, spec
return None
# Remove the '#/' prefix and split by '/'
path_parts = ref_path[2:].split('/')
# Navigate through the spec following the path
current = spec
for part in path_parts:
if isinstance(current, dict) and part in current:
current = current[part]
else:
return None, spec
return None
return current if isinstance(current, dict) else None
return (current if isinstance(current, dict) else None), spec
def resolve_schema_refs(schema: Dict[str, Any], spec: Dict[str, Any], base_path: str = "") -> Dict[str, Any]:
def resolve_schema_refs(schema: Dict[str, Any], spec: Dict[str, Any]) -> Dict[str, Any]:
"""Recursively resolve all $ref references in a schema."""
if not isinstance(schema, dict):
return schema
# If this is a $ref, resolve it
if '$ref' in schema:
ref_path = schema['$ref']
resolved, resolved_spec = resolve_ref(ref_path, spec, base_path)
resolved = resolve_ref(ref_path, spec)
if resolved:
# Recursively resolve any refs in the resolved schema using the appropriate spec
return resolve_schema_refs(resolved, resolved_spec, base_path)
# Recursively resolve any refs in the resolved schema
return resolve_schema_refs(resolved, spec)
else:
print(f"Warning: Could not resolve $ref: {ref_path}")
return schema
# Recursively process all values in the schema
resolved_schema = {}
for key, value in schema.items():
if isinstance(value, dict):
resolved_schema[key] = resolve_schema_refs(value, spec, base_path)
resolved_schema[key] = resolve_schema_refs(value, spec)
elif isinstance(value, list):
resolved_schema[key] = [
resolve_schema_refs(item, spec, base_path) if isinstance(item, dict) else item
resolve_schema_refs(item, spec) if isinstance(item, dict) else item
for item in value
]
else:
resolved_schema[key] = value
return resolved_schema
def extract_request_body_schema(request_body: Dict[str, Any], spec: Dict[str, Any], base_path: str = "") -> Optional[Dict[str, Any]]:
def extract_request_body_schema(request_body: Dict[str, Any], spec: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Extract request body schema from OpenAPI requestBody definition and resolve refs."""
if not request_body:
return None
content = request_body.get('content', {})
json_content = content.get('application/json', {})
schema = json_content.get('schema', {})
if schema:
# Resolve any $ref references in the schema
return resolve_schema_refs(schema, spec, base_path)
return resolve_schema_refs(schema, spec)
return None
def http_method_to_rust(method: str) -> str:
@@ -262,7 +221,7 @@ def find_mcp_tools(spec: Dict[str, Any]) -> List[Dict[str, Any]]:
return tools
def generate_typescript_code(tools: List[Dict[str, Any]], spec: Dict[str, Any], base_path: str = "") -> str:
def generate_typescript_code(tools: List[Dict[str, Any]], spec: Dict[str, Any]) -> str:
"""Generate TypeScript code with MCP endpoint tools."""
if not tools:
return """// Auto-generated MCP tools from OpenAPI specification
@@ -293,7 +252,7 @@ export const mcpEndpointTools: EndpointTool[] = [];
# Generate separate schemas
path_params_schema, query_params_schema, body_schema = extract_separate_schemas(
tool['parameters'], tool['requestBody'], spec, tool['required_fields'], base_path
tool['parameters'], tool['requestBody'], spec, tool['required_fields']
)
# Convert schemas to TypeScript - use 'as const' for better type inference
@@ -338,7 +297,7 @@ export const mcpEndpointTools: EndpointTool[] = [
return typescript_code
def generate_rust_code(tools: List[Dict[str, Any]], spec: Dict[str, Any], base_path: str = "") -> str:
def generate_rust_code(tools: List[Dict[str, Any]], spec: Dict[str, Any]) -> str:
"""Generate the complete Rust code with MCP tools."""
if not tools:
return """// No MCP tools found in the OpenAPI specification
@@ -361,7 +320,7 @@ pub fn all_tools() -> Vec<EndpointTool> {
# Generate separate schemas
path_params_schema, query_params_schema, body_schema = extract_separate_schemas(
tool['parameters'], tool['requestBody'], spec, tool['required_fields'], base_path
tool['parameters'], tool['requestBody'], spec, tool['required_fields']
)
path_params_rust = schema_to_rust_value(path_params_schema)
@@ -427,7 +386,7 @@ def main():
# Generate and write Rust code
print(f"Generating Rust code...")
rust_code = generate_rust_code(tools, spec, str(openapi_file))
rust_code = generate_rust_code(tools, spec)
print(f"Writing Rust code to: {rust_output_file}")
rust_output_file.parent.mkdir(parents=True, exist_ok=True)
@@ -436,7 +395,7 @@ def main():
# Generate and write TypeScript code
print(f"Generating TypeScript code...")
typescript_code = generate_typescript_code(tools, spec, str(openapi_file))
typescript_code = generate_typescript_code(tools, spec)
print(f"Writing TypeScript code to: {ts_output_file}")
ts_output_file.parent.mkdir(parents=True, exist_ok=True)

View File

@@ -1,6 +0,0 @@
-- Add down migration script here
DROP INDEX IF EXISTS idx_group_perm_history_workspace_group;
DROP TABLE IF EXISTS group_permission_history;
DROP INDEX IF EXISTS idx_folder_perm_history_workspace_folder;
DROP TABLE IF EXISTS folder_permission_history;

View File

@@ -1,90 +0,0 @@
-- Add up migration script here
-- Folder permission changes history
CREATE TABLE IF NOT EXISTS folder_permission_history (
id BIGSERIAL PRIMARY KEY,
workspace_id VARCHAR(50) NOT NULL,
folder_name VARCHAR(255) NOT NULL,
changed_by VARCHAR(50) NOT NULL,
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
change_type VARCHAR(50) NOT NULL,
affected VARCHAR(100),
FOREIGN KEY (workspace_id, folder_name) REFERENCES folder(workspace_id, name) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_folder_perm_history_workspace_folder
ON folder_permission_history(workspace_id, folder_name, id DESC);
-- Group permission changes history
CREATE TABLE IF NOT EXISTS group_permission_history (
id BIGSERIAL PRIMARY KEY,
workspace_id VARCHAR(50) NOT NULL,
group_name VARCHAR(255) NOT NULL,
changed_by VARCHAR(50) NOT NULL,
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
change_type VARCHAR(50) NOT NULL,
member_affected VARCHAR(100),
FOREIGN KEY (workspace_id, group_name) REFERENCES group_(workspace_id, name) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_group_perm_history_workspace_group
ON group_permission_history(workspace_id, group_name, id DESC);
GRANT ALL ON TABLE folder_permission_history TO windmill_user;
GRANT ALL ON TABLE group_permission_history TO windmill_user;
GRANT ALL ON TABLE folder_permission_history TO windmill_admin;
GRANT ALL ON TABLE group_permission_history TO windmill_admin;
-- Enable RLS on folder_permission_history
ALTER TABLE folder_permission_history ENABLE ROW LEVEL SECURITY;
-- Admin policies (windmill_admin can always do everything)
CREATE POLICY admin_all ON folder_permission_history FOR ALL TO windmill_admin USING (true) WITH CHECK (true);
CREATE POLICY admin_all ON group_permission_history FOR ALL TO windmill_admin USING (true) WITH CHECK (true);
-- Enable RLS on group_permission_history
ALTER TABLE group_permission_history ENABLE ROW LEVEL SECURITY;
-- RLS policies for folder_permission_history
-- Anyone can insert
CREATE POLICY allow_insert ON folder_permission_history FOR INSERT TO windmill_user WITH CHECK (true);
-- Select requires being in extra_perms (as user or via group)
CREATE POLICY see_extra_perms_user ON folder_permission_history FOR SELECT TO windmill_user
USING (EXISTS (
SELECT 1 FROM folder f
WHERE f.workspace_id = folder_permission_history.workspace_id
AND f.name = folder_permission_history.folder_name
AND f.extra_perms ? CONCAT('u/', current_setting('session.user'))
));
CREATE POLICY see_extra_perms_groups ON folder_permission_history FOR SELECT TO windmill_user
USING (EXISTS (
SELECT 1 FROM folder f
WHERE f.workspace_id = folder_permission_history.workspace_id
AND f.name = folder_permission_history.folder_name
AND f.extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]
));
-- RLS policies for group_permission_history
-- Anyone can insert
CREATE POLICY allow_insert ON group_permission_history FOR INSERT TO windmill_user WITH CHECK (true);
-- Select requires being in extra_perms (as user or via group)
CREATE POLICY see_extra_perms_user ON group_permission_history FOR SELECT TO windmill_user
USING (EXISTS (
SELECT 1 FROM group_ g
WHERE g.workspace_id = group_permission_history.workspace_id
AND g.name = group_permission_history.group_name
AND (g.extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean
));
CREATE POLICY see_extra_perms_groups ON group_permission_history FOR SELECT TO windmill_user
USING (EXISTS (
SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f
WHERE g.workspace_id = group_permission_history.workspace_id
AND g.name = group_permission_history.group_name
AND SPLIT_PART(f.key, '/', 1) = 'g'
AND f.key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
AND f.value::boolean
));

View File

@@ -1,3 +0,0 @@
-- Add down migration script here
DROP TABLE IF EXISTS skip_workspace_diff_tally;
DROP TABLE IF EXISTS workspace_diff;

View File

@@ -1,24 +0,0 @@
-- Add up migration script here
CREATE TABLE workspace_diff (
source_workspace_id VARCHAR(50) NOT NULL,
fork_workspace_id VARCHAR(50) NOT NULL,
path VARCHAR(255) NOT NULL,
kind VARCHAR(50) NOT NULL,
ahead INTEGER NOT NULL DEFAULT 0,
behind INTEGER NOT NULL DEFAULT 0,
has_changes BOOLEAN DEFAULT NULL,
exists_in_source BOOLEAN DEFAULT NULL,
exists_in_fork BOOLEAN DEFAULT NULL,
PRIMARY KEY (source_workspace_id, fork_workspace_id, path, kind)
);
-- Create table to track workspaces that should be excluded from diff tallying
-- Old workspaces that are linked but have already diverged need to be skipped
CREATE TABLE skip_workspace_diff_tally (
workspace_id VARCHAR(50) PRIMARY KEY,
added_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Populate with all existing workspaces to exclude them from new tallying logic
INSERT INTO skip_workspace_diff_tally (workspace_id)
SELECT id FROM workspace;

View File

@@ -1,2 +1 @@
-- Add down migration script here
ALTER TABLE email_trigger DROP COLUMN enabled;

View File

@@ -1,11 +0,0 @@
ALTER TABLE v2_job_queue
DROP COLUMN runnable_settings_handle;
ALTER TABLE script
DROP COLUMN runnable_settings_handle;
DROP TABLE IF EXISTS job_settings;
DROP TABLE IF EXISTS runnable_settings;
DROP TABLE IF EXISTS concurrency_settings;
DROP TABLE IF EXISTS debouncing_settings;

View File

@@ -1,42 +0,0 @@
CREATE TABLE IF NOT EXISTS concurrency_settings(
hash BIGINT PRIMARY KEY,
concurrency_key VARCHAR(255),
concurrent_limit INTEGER,
concurrency_time_window_s INTEGER
);
CREATE TABLE IF NOT EXISTS debouncing_settings(
hash BIGINT PRIMARY KEY,
debounce_key VARCHAR(255),
debounce_delay_s INTEGER,
max_total_debouncing_time INTEGER,
max_total_debounces_amount INTEGER,
debounce_args_to_accumulate TEXT[]
);
CREATE TABLE IF NOT EXISTS runnable_settings(
hash BIGINT PRIMARY KEY,
debouncing_settings BIGINT DEFAULT NULL,
concurrency_settings BIGINT DEFAULT NULL
);
CREATE TABLE IF NOT EXISTS job_settings(
job_id UUID PRIMARY KEY,
runnable_settings BIGINT DEFAULT NULL
);
ALTER TABLE script
ADD COLUMN runnable_settings_handle BIGINT DEFAULT NULL;
ALTER TABLE v2_job_queue
ADD COLUMN runnable_settings_handle BIGINT DEFAULT NULL;
GRANT ALL ON concurrency_settings TO windmill_admin;
GRANT ALL ON concurrency_settings TO windmill_user;
GRANT ALL ON debouncing_settings TO windmill_admin;
GRANT ALL ON debouncing_settings TO windmill_user;
GRANT ALL ON runnable_settings TO windmill_admin;
GRANT ALL ON runnable_settings TO windmill_user;
GRANT ALL ON job_settings TO windmill_admin;
GRANT ALL ON job_settings TO windmill_user;

View File

@@ -1,38 +0,0 @@
-- Grant CREATE privilege on all databases where custom_instance_user has CONNECT
-- This allows custom_instance_user to create schemas in databases it can already access
DO $$
DECLARE
db_record RECORD;
grant_command TEXT;
BEGIN
-- Find all databases where custom_instance_user has CONNECT privilege
-- We check if the datacl array contains an entry for custom_instance_user with 'c' (CONNECT) privilege
FOR db_record IN
SELECT d.datname
FROM pg_database d
WHERE d.datname NOT IN ('template0', 'template1') -- Skip template databases
AND d.datallowconn = true -- Only consider databases that allow connections
AND d.datacl IS NOT NULL -- Has ACL entries
AND EXISTS (
SELECT 1
FROM unnest(d.datacl) AS acl_entry
WHERE acl_entry::text LIKE 'custom_instance_user=c/%' -- 'c' is the privilege code for CONNECT
)
LOOP
BEGIN
-- Grant CREATE privilege on the database
EXECUTE format('GRANT CREATE ON DATABASE %I TO custom_instance_user', db_record.datname);
RAISE NOTICE 'Granted CREATE on database % to custom_instance_user', db_record.datname;
EXCEPTION
WHEN others THEN
RAISE NOTICE 'Failed to grant CREATE on database %: %', db_record.datname, SQLERRM;
END;
END LOOP;
RAISE NOTICE 'Completed granting CREATE privileges to custom_instance_user on all accessible databases';
EXCEPTION
WHEN others THEN
RAISE NOTICE 'Error in custom_instance_user CREATE privilege migration: %', SQLERRM;
-- Continue without failing the migration
END
$$;

View File

@@ -10,7 +10,6 @@ path = "./src/lib.rs"
[dependencies]
windmill-parser.workspace = true
windmill-parser-sql.workspace = true
rustpython-parser.workspace = true
itertools.workspace = true
serde_json.workspace = true

View File

@@ -2,12 +2,12 @@ use rustpython_ast::{Constant, Expr, ExprConstant, Visitor};
use rustpython_parser::{ast::Suite, Parse};
use std::collections::HashMap;
use windmill_parser::asset_parser::{
asset_was_used, merge_assets, parse_asset_syntax, AssetKind, AssetUsageAccessType,
detect_sql_access_type, merge_assets, parse_asset_syntax, AssetKind, AssetUsageAccessType,
ParseAssetsResult,
};
use AssetUsageAccessType::*;
pub fn parse_assets(input: &str) -> anyhow::Result<Vec<ParseAssetsResult>> {
pub fn parse_assets(input: &str) -> anyhow::Result<Vec<ParseAssetsResult<String>>> {
let ast = Suite::parse(input, "main.py")
.map_err(|e| anyhow::anyhow!("Error parsing code: {}", e.to_string()))?;
@@ -15,51 +15,67 @@ pub fn parse_assets(input: &str) -> anyhow::Result<Vec<ParseAssetsResult>> {
ast.into_iter()
.for_each(|stmt| assets_finder.visit_stmt(stmt));
for (kind, path, _) in assets_finder.var_identifiers.into_values() {
for (kind, name) in assets_finder.var_identifiers.into_values() {
// if a db = wmill.datatable() was never used (e.g db.query(...)),
// we still want to register the asset as unknown access type
if asset_was_used(&assets_finder.assets, (kind, &path)) == false {
if assets_finder
.assets
.iter()
.all(|a| !(a.kind == kind && a.path == name))
{
assets_finder
.assets
.push(ParseAssetsResult { kind, access_type: None, path });
.push(ParseAssetsResult { kind, access_type: None, path: name });
}
}
Ok(merge_assets(assets_finder.assets))
}
type VarAssetName = String;
type VarAssetSchema = Option<String>;
struct AssetsFinder {
assets: Vec<ParseAssetsResult>,
var_identifiers: HashMap<String, (AssetKind, VarAssetName, VarAssetSchema)>,
assets: Vec<ParseAssetsResult<String>>,
var_identifiers: HashMap<String, (AssetKind, String)>,
}
impl Visitor for AssetsFinder {
// Handle assignment statements like: x = wmill.datatable('name')
fn visit_stmt_assign(&mut self, node: rustpython_ast::StmtAssign) {
// Check if the value is a call to a tracked function
if let Some(Expr::Name(expr_name)) = node.targets.first() {
// Remove any tracked variables with that name in case of reassignment
let Ok(var_name) = expr_name.id.parse::<String>();
let removed = self.var_identifiers.remove(&var_name);
// if a db = wmill.datatable() or similar was removed, but never used (e.g db.query(...)),
// we still want to register the asset as unknown access type
match removed {
Some((kind, path, _)) => {
if !asset_was_used(&self.assets, (kind, &path)) {
self.assets
.push(ParseAssetsResult { kind, access_type: None, path });
if let Some((kind, name)) = self.extract_asset_from_call(&node.value) {
// Track all target variables
for target in &node.targets {
if let Expr::Name(name_expr) = target {
let Ok(var_name) = name_expr.id.parse::<String>();
self.var_identifiers
.insert(var_name, (kind.clone(), name.clone()));
}
}
} else {
// If not wmill.datatable or similar, remove any tracked variables
// It means the identifier is no longer refering to an asset
for target in &node.targets {
if let Expr::Name(name_expr) = target {
let Ok(var_name) = name_expr.id.parse::<String>();
let removed = self.var_identifiers.remove(&var_name);
// if a db = wmill.datatable() or similar was removed, but never used (e.g db.query(...)),
// we still want to register the asset as unknown access type
match removed {
Some((kind, name)) => {
if self
.assets
.iter()
.all(|a| !(a.kind == kind && a.path == name))
{
self.assets.push(ParseAssetsResult {
kind,
access_type: None,
path: name,
});
}
}
None => {}
}
}
None => {}
}
if let Some((kind, name, schema)) = self.extract_asset_from_call(&node.value) {
// Track target variable
let Ok(var_name) = expr_name.id.parse::<String>();
self.var_identifiers
.insert(var_name, (kind.clone(), name.clone(), schema.clone()));
}
}
// Continue with generic visit to catch any other assets in the expression
@@ -71,7 +87,7 @@ impl Visitor for AssetsFinder {
fn visit_expr_constant(&mut self, node: ExprConstant) {
match node.value {
Constant::Str(s) => {
if let Some((kind, path)) = parse_asset_syntax(&s, false) {
if let Some((kind, path)) = parse_asset_syntax(&s) {
self.assets.push(ParseAssetsResult {
kind,
path: path.to_string(),
@@ -92,7 +108,7 @@ impl Visitor for AssetsFinder {
if let Expr::Constant(ExprConstant { value: Constant::Str(s), .. }) =
&keyword.value
{
if let Some((kind, path)) = parse_asset_syntax(s, false) {
if let Some((kind, path)) = parse_asset_syntax(s) {
self.assets.push(ParseAssetsResult {
kind,
path: path.to_string(),
@@ -109,10 +125,7 @@ impl Visitor for AssetsFinder {
impl AssetsFinder {
/// Extract asset info from calls like wmill.datatable('name'), wmill.ducklake('name'), etc.
fn extract_asset_from_call(
&self,
expr: &Expr,
) -> Option<(AssetKind, VarAssetName, VarAssetSchema)> {
fn extract_asset_from_call(&self, expr: &Expr) -> Option<(AssetKind, String)> {
let call = expr.as_call_expr()?;
// Check for wmill.datatable, wmill.ducklake pattern
@@ -148,21 +161,10 @@ impl AssetsFinder {
} else {
None
}
});
let (name, schema) = match name {
None => ("main".to_string(), None),
Some(name) => {
if let Some((name, s)) = name.split_once(':') {
let schema = Some(s.to_string());
let name = if name.is_empty() { "main" } else { name };
(name.to_string(), schema)
} else {
(name, None)
}
}
};
})
.unwrap_or_else(|| "main".to_string());
Some((kind, name, schema))
Some((kind, name))
}
fn visit_expr_call_inner(&mut self, node: &rustpython_ast::ExprCall) -> Result<(), ()> {
@@ -182,8 +184,8 @@ impl AssetsFinder {
value, ..
}) = node.func.as_ref()
{
if let Expr::Name(expr_name) = value.as_ref() {
expr_name.id.parse().map_err(|_| ())?
if let Expr::Name(name_expr) = value.as_ref() {
name_expr.id.parse().map_err(|_| ())?
} else {
return Err(());
}
@@ -193,45 +195,28 @@ impl AssetsFinder {
if obj_name == "wmill" {
// Continue
} else if let Some((kind, ref path, ref schema)) = self.var_identifiers.get(&obj_name) {
} else if let Some((kind, ref path)) = self.var_identifiers.get(&obj_name) {
if ident == "query" {
let expr_name = node.args.get(0).or_else(|| {
let name_expr = node.args.get(0).or_else(|| {
node.keywords
.iter()
.find(|kw| kw.arg.as_deref() == Some("name"))
.map(|kw| &kw.value)
});
let sql = match expr_name {
Some(Expr::Constant(ExprConstant { value: Constant::Str(sql), .. })) => sql,
match name_expr {
Some(Expr::Constant(ExprConstant {
value: Constant::Str(sql_query), ..
})) => {
let access_type = detect_sql_access_type(&sql_query);
self.assets.push(ParseAssetsResult {
kind: *kind,
path: path.to_string(),
access_type,
});
return Ok(());
}
_ => return Err(()),
};
let duckdb_conn_prefix = match kind {
AssetKind::DataTable => "datatable",
AssetKind::Ducklake => "ducklake",
_ => return Ok(()),
};
let sql = format!("ATTACH '{duckdb_conn_prefix}://{path}' AS dt; USE dt; {sql}");
// We use the SQL parser to detect if it's a read or write query
match windmill_parser_sql::parse_assets(&sql) {
Ok(mut sql_assets) => {
if let Some(schema_name) = schema {
for asset in &mut sql_assets {
if asset.kind == *kind && asset.path.starts_with(path.as_str()) {
asset.path = format!(
"{}/{}.{}",
path,
schema_name,
&asset.path[path.len() + 1..]
);
}
}
}
self.assets.extend(sql_assets);
}
_ => {}
}
return Ok(());
} else {
return Err(());
}
@@ -264,9 +249,7 @@ impl AssetsFinder {
match arg_val {
Some(Expr::Constant(ExprConstant { value: Constant::Str(value), .. })) => {
let path = parse_asset_syntax(&value, false)
.map(|(_, p)| p)
.unwrap_or(&value);
let path = parse_asset_syntax(&value).map(|(_, p)| p).unwrap_or(&value);
self.assets
.push(ParseAssetsResult { kind, path: path.to_string(), access_type });
}
@@ -276,233 +259,5 @@ impl AssetsFinder {
}
}
// Positional arguments in python can also be used by their name
struct Arg(usize, &'static str);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_py_asset_parser_load_s3() {
let input = r#"
import wmill
def main():
wmill.load_s3_file('s3:///test.csv')
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::S3Object,
path: "/test.csv".to_string(),
access_type: Some(R)
},])
);
}
#[test]
fn test_py_asset_parser_unused_sql() {
let input = r#"
import wmill
def main():
db = wmill.datatable()
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::DataTable,
path: "main".to_string(),
access_type: None
},])
);
}
#[test]
fn test_py_asset_parser_sql_read() {
let input = r#"
import wmill
def main(x: int):
db = wmill.datatable('dt')
return db.query('SELECT * FROM friends WHERE age = $1', x).fetch()
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::DataTable,
path: "dt/friends".to_string(),
access_type: Some(R)
},])
);
}
#[test]
fn test_py_asset_parser_sql_read_write() {
let input = r#"
import wmill
def main(x: int):
db = wmill.datatable('dt')
db.query('UPDATE friends SET x = $1', x).fetch()
db.query('SELECT * FROM friends WHERE age = $1', x).fetch_one()
db.query('SELECT * FROM analytics').fetch()
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![
ParseAssetsResult {
kind: AssetKind::DataTable,
path: "dt/analytics".to_string(),
access_type: Some(R)
},
ParseAssetsResult {
kind: AssetKind::DataTable,
path: "dt/friends".to_string(),
access_type: Some(RW)
},
])
);
}
#[test]
fn test_py_asset_parser_multiple_sql_scopes() {
let input = r#"
import wmill
def main():
def f(x: int):
db = wmill.datatable()
return db.query('SELECT * FROM friends WHERE age = $1', x)
db = wmill.datatable('another1')
return db.query('INSERT INTO customers VALUES ($1)', 0)
def g():
db = wmill.ducklake('another2')
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![
ParseAssetsResult {
kind: AssetKind::DataTable,
path: "another1/customers".to_string(),
access_type: Some(W)
},
ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "another2".to_string(),
access_type: None
},
ParseAssetsResult {
kind: AssetKind::DataTable,
path: "main/friends".to_string(),
access_type: Some(R)
},
])
);
}
#[test]
fn test_py_asset_parser_overriden_var_identifier() {
let input = r#"
import wmill
def main():
db = wmill.datatable('another1')
def g():
db = wmill.ducklake()
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![
ParseAssetsResult {
kind: AssetKind::DataTable,
path: "another1".to_string(),
access_type: None
},
ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "main".to_string(),
access_type: None
},
])
);
}
#[test]
fn test_py_asset_parser_datatable_with_schema() {
let input = r#"
import wmill
def main(x: int):
db = wmill.datatable('dt:public')
return db.query('SELECT * FROM friends WHERE age = $1', x).fetch()
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::DataTable,
path: "dt/public.friends".to_string(),
access_type: Some(R)
},])
);
}
#[test]
fn test_py_asset_parser_ducklake_with_schema() {
let input = r#"
import wmill
def main():
db = wmill.ducklake('lake1:analytics')
return db.query('SELECT * FROM metrics').fetch()
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "lake1/analytics.metrics".to_string(),
access_type: Some(R)
},])
);
}
#[test]
fn test_py_asset_parser_schema_with_write() {
let input = r#"
import wmill
def main(x: int):
db = wmill.datatable('dt:public')
db.query('INSERT INTO users VALUES ($1)', x).fetch()
return db.query('SELECT * FROM users').fetch()
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::DataTable,
path: "dt/public.users".to_string(),
access_type: Some(RW)
},])
);
}
#[test]
fn test_py_asset_parser_unused_datatable_with_schema() {
let input = r#"
import wmill
def main():
db = wmill.datatable('dt:public')
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::DataTable,
path: "dt".to_string(),
access_type: None
},])
);
}
}
// Positional arguments in python can also be used by their name

View File

@@ -20,4 +20,4 @@ anyhow.workspace = true
lazy_static.workspace = true
serde_json.workspace = true
serde.workspace = true
sqlparser = { version = "0.59.0", features = ["visitor"] }
nom.workspace = true

View File

@@ -1,676 +1,151 @@
use std::collections::HashMap;
use sqlparser::{
ast::{
CopyTarget, Expr, ObjectName, TableFactor, TableObject, Value, ValueWithSpan, Visit,
Visitor,
},
dialect::DuckDbDialect,
parser::Parser,
};
use windmill_parser::asset_parser::{
asset_was_used, merge_assets, parse_asset_syntax, AssetKind, AssetUsageAccessType,
ParseAssetsResult,
merge_assets, AssetKind, AssetUsageAccessType, ParseAssetsResult,
};
use AssetUsageAccessType::*;
pub fn parse_assets(input: &str) -> anyhow::Result<Vec<ParseAssetsResult>> {
let statements = Parser::parse_sql(&DuckDbDialect, input)?;
use nom::{
branch::alt,
bytes::complete::{tag, tag_no_case, take_while},
character::complete::{char, multispace0},
combinator::opt,
sequence::preceded,
IResult, Parser,
};
let mut collector = AssetCollector::new();
for statement in statements {
let _ = statement.visit(&mut collector);
}
pub fn parse_assets<'a>(input: &str) -> anyhow::Result<Vec<ParseAssetsResult<&str>>> {
let mut assets = Vec::new();
let mut remaining = input;
for (_, (kind, path)) in collector.var_identifiers {
if !asset_was_used(&collector.assets, (kind, &path)) {
collector
.assets
.push(ParseAssetsResult { kind, access_type: None, path: path });
while !remaining.trim().is_empty() {
if let Ok((rest, _)) = parse_comment(remaining) {
remaining = rest; // skip comment
}
}
Ok(merge_assets(collector.assets))
}
/// Visitor that collects S3 asset literals from SQL statements
struct AssetCollector {
assets: Vec<ParseAssetsResult>,
// e.g set to Read when we are inside a SELECT ... FROM ... statement
current_access_type_stack: Vec<AssetUsageAccessType>,
// e.g ATTACH 'ducklake://a' AS dl; => { "dl": (Ducklake, "a") }
var_identifiers: HashMap<String, (AssetKind, String)>,
// e.g USE dl;
currently_used_asset: Option<(AssetKind, String)>,
}
impl AssetCollector {
fn new() -> Self {
Self {
assets: Vec::new(),
current_access_type_stack: Vec::with_capacity(8),
var_identifiers: HashMap::new(),
currently_used_asset: None,
}
}
// Detect when we do 'a.b' and 'a' is associated with an asset in var_identifiers
// Or when we access 'b' and we did USE a;
fn get_associated_asset_from_obj_name(&self, name: &ObjectName) -> Option<ParseAssetsResult> {
let access_type = self.current_access_type_stack.last().copied();
if let Some((kind, path)) = &self.currently_used_asset {
// We don't want to infer that any simple identifier refers to an asset if
// we are not in a known R/W context
if access_type.is_none() {
return None;
}
if name.0.len() == 1 || name.0.len() == 2 {
if name
.0
.iter()
.any(|id| id.as_ident().and_then(|id| id.quote_style).is_some())
{
return None;
}
let specific_table = &name
.0
.iter()
.map(|id| id.as_ident().map(|id| id.value.clone()))
.collect::<Option<Vec<String>>>()?
.join(".");
let path = format!("{}/{}", path, specific_table);
return Some(ParseAssetsResult { kind: *kind, access_type, path });
}
}
// Check if the first part of the name (the a in a.b) is associated with an asset
if name.0.len() < 2 {
return None;
}
let ident = name.0.first()?.as_ident()?;
let (kind, path) = self.var_identifiers.get(&ident.value)?;
let path = if name.0.len() == 2 || name.0.len() == 3 {
let specific_table = &name.0[1..]
.iter()
.map(|id| id.as_ident().map(|id| id.value.clone()))
.collect::<Option<Vec<String>>>()?
.join(".");
format!("{}/{}", path, specific_table)
if let Ok((rest, res)) = parse_asset(remaining) {
assets.push(res);
remaining = rest;
} else {
path.clone()
};
Some(ParseAssetsResult { kind: *kind, access_type, path })
}
fn handle_string_literal(&mut self, s: &str) {
// Check if the string matches our asset syntax patterns
if let Some((kind, path)) = parse_asset_syntax(s, false) {
if kind == AssetKind::S3Object {
self.assets.push(ParseAssetsResult {
kind,
path: path.to_string(),
access_type: self.current_access_type_stack.last().copied(),
});
}
remaining = &remaining[1..]; // skip 1 char and continue
}
}
fn handle_obj_name_pre(&mut self, name: &ObjectName) {
if let Some(fname) = get_trivial_obj_name(name) {
if is_read_fn(fname) {
self.current_access_type_stack.push(R);
}
}
if let Some(str_lit) = get_str_lit_from_obj_name(name) {
self.handle_string_literal(str_lit);
}
// Writes to tables should be handled directly when visiting the statement
if self.current_access_type_stack.last() == Some(&R) {
if let Some(asset) = self.get_associated_asset_from_obj_name(name) {
self.assets.push(asset);
}
}
}
fn handle_obj_name_post(&mut self, name: &ObjectName) {
if self.current_access_type_stack.is_empty() {
return;
}
if let Some(fname) = get_trivial_obj_name(name) {
if is_read_fn(fname) {
self.current_access_type_stack.pop();
}
}
}
fn handle_table_with_joins(&mut self, table_with_joins: &sqlparser::ast::TableWithJoins) {
if let TableFactor::Table { name, .. } = &table_with_joins.relation {
if let Some(asset) = self.get_associated_asset_from_obj_name(name) {
self.assets.push(asset);
}
}
for join in &table_with_joins.joins {
if let TableFactor::Table { name, .. } = &join.relation {
if let Some(asset) = self.get_associated_asset_from_obj_name(name) {
self.assets.push(asset);
}
}
}
}
Ok(merge_assets(assets))
}
impl Visitor for AssetCollector {
type Break = ();
fn pre_visit_table_factor(
&mut self,
table_factor: &TableFactor,
) -> std::ops::ControlFlow<Self::Break> {
match table_factor {
TableFactor::Table { name, args, .. } => {
if args.is_none() {
// Avoid Table Functions
self.handle_obj_name_pre(name);
}
}
_ => {}
}
std::ops::ControlFlow::Continue(())
}
fn post_visit_table_factor(
&mut self,
table_factor: &TableFactor,
) -> std::ops::ControlFlow<Self::Break> {
match table_factor {
TableFactor::Table { name, .. } => self.handle_obj_name_post(name),
_ => {}
}
std::ops::ControlFlow::Continue(())
}
fn pre_visit_expr(&mut self, expr: &Expr) -> std::ops::ControlFlow<Self::Break> {
match expr {
Expr::Value(ValueWithSpan { value: Value::SingleQuotedString(s), .. }) => {
self.handle_string_literal(s)
}
Expr::Value(ValueWithSpan { value: Value::DoubleQuotedString(s), .. }) => {
self.handle_string_literal(s);
}
_ => {}
}
std::ops::ControlFlow::Continue(())
}
fn post_visit_expr(&mut self, expr: &Expr) -> std::ops::ControlFlow<Self::Break> {
match expr {
Expr::Function(func) => self.handle_obj_name_post(&func.name),
_ => {}
}
std::ops::ControlFlow::Continue(())
}
fn pre_visit_statement(
&mut self,
statement: &sqlparser::ast::Statement,
) -> std::ops::ControlFlow<Self::Break> {
match statement {
sqlparser::ast::Statement::Query(_) => {
// don't forget pop() in post_visit_statement
self.current_access_type_stack.push(R);
}
sqlparser::ast::Statement::Insert(insert) => {
let access_type = if insert.returning.is_some() { RW } else { W };
self.current_access_type_stack.push(access_type);
match insert.table {
TableObject::TableName(ref name) => {
if let Some(asset) = self.get_associated_asset_from_obj_name(name) {
self.assets.push(asset);
}
}
_ => {}
}
self.current_access_type_stack.pop();
}
sqlparser::ast::Statement::Update { returning, table, from, .. } => {
if let Some(from_tables) = from {
let from_tables = match from_tables {
sqlparser::ast::UpdateTableFromKind::AfterSet(tables) => tables,
sqlparser::ast::UpdateTableFromKind::BeforeSet(tables) => tables,
};
self.current_access_type_stack.push(R);
for table_with_joins in from_tables {
self.handle_table_with_joins(table_with_joins);
}
self.current_access_type_stack.pop();
}
let access_type = if returning.is_some() { RW } else { W };
self.current_access_type_stack.push(access_type);
self.handle_table_with_joins(table);
self.current_access_type_stack.pop();
}
sqlparser::ast::Statement::Delete(delete) => {
let access_type = if delete.returning.is_some() { RW } else { W };
self.current_access_type_stack.push(access_type);
for name in &delete.tables {
if let Some(asset) = self.get_associated_asset_from_obj_name(name) {
self.assets.push(asset);
}
}
let tables = match &delete.from {
sqlparser::ast::FromTable::WithFromKeyword(tables) => tables,
sqlparser::ast::FromTable::WithoutKeyword(tables) => tables,
};
for table_with_joins in tables {
self.handle_table_with_joins(table_with_joins);
}
self.current_access_type_stack.pop();
}
sqlparser::ast::Statement::CreateTable(create_table) => {
self.current_access_type_stack.push(W);
if let Some(asset) = self.get_associated_asset_from_obj_name(&create_table.name) {
self.assets.push(asset);
}
self.current_access_type_stack.pop();
}
sqlparser::ast::Statement::CreateView { name, .. } => {
self.current_access_type_stack.push(W);
if let Some(asset) = self.get_associated_asset_from_obj_name(name) {
self.assets.push(asset);
}
self.current_access_type_stack.pop();
}
sqlparser::ast::Statement::Copy { target: CopyTarget::File { filename }, .. } => {
self.current_access_type_stack.push(W);
self.handle_string_literal(filename);
self.current_access_type_stack.pop();
}
sqlparser::ast::Statement::AttachDuckDBDatabase {
database_path,
database_alias,
..
} => {
if let Some((kind, path)) = parse_asset_syntax(&database_path.value, true) {
if kind == AssetKind::Ducklake
|| kind == AssetKind::DataTable
|| kind == AssetKind::Resource
{
if let Some(database_alias) = database_alias {
self.var_identifiers
.insert(database_alias.value.clone(), (kind, path.to_string()));
}
}
}
}
sqlparser::ast::Statement::DetachDuckDBDatabase { database_alias, .. } => {
let asset = self.var_identifiers.remove(&database_alias.value);
if self.currently_used_asset == asset {
self.currently_used_asset = None;
}
}
sqlparser::ast::Statement::Use(sqlparser::ast::Use::Object(obj_name)) => {
if let Some((kind, path)) = self.var_identifiers.get(&obj_name.to_string()) {
self.currently_used_asset = Some((*kind, path.clone()));
} else {
self.currently_used_asset = None;
}
}
_ => {}
}
std::ops::ControlFlow::Continue(())
}
fn post_visit_statement(
&mut self,
statement: &sqlparser::ast::Statement,
) -> std::ops::ControlFlow<Self::Break> {
match statement {
sqlparser::ast::Statement::Query(_) => {
self.current_access_type_stack.pop();
}
_ => {}
}
std::ops::ControlFlow::Continue(())
}
fn pre_visit_query(
&mut self,
_query: &sqlparser::ast::Query,
) -> std::ops::ControlFlow<Self::Break> {
self.current_access_type_stack.push(R);
std::ops::ControlFlow::Continue(())
}
fn post_visit_query(
&mut self,
_query: &sqlparser::ast::Query,
) -> std::ops::ControlFlow<Self::Break> {
self.current_access_type_stack.pop();
std::ops::ControlFlow::Continue(())
}
// We do not use pre_visit_relation because we cannot know if an ObjectName is a table or a function
fn parse_asset(input: &str) -> IResult<&str, ParseAssetsResult<&str>> {
alt((
parse_s3_object_read.map(|path| ParseAssetsResult {
path,
kind: AssetKind::S3Object,
access_type: Some(R),
}),
parse_s3_object_write.map(|path| ParseAssetsResult {
path,
kind: AssetKind::S3Object,
access_type: Some(W),
}),
// Parse ambiguous access_types at the end if we could not find precisely read or copy
parse_s3_object_lit.map(|path| ParseAssetsResult {
path,
kind: AssetKind::S3Object,
access_type: None,
}),
parse_resource_lit.map(|path| ParseAssetsResult {
path,
kind: AssetKind::Resource,
access_type: None,
}),
parse_ducklake_lit.map(|path| ParseAssetsResult {
path,
kind: AssetKind::Ducklake,
access_type: None,
}),
parse_datatable_lit.map(|path| ParseAssetsResult {
path,
kind: AssetKind::DataTable,
access_type: None,
}),
))
.parse(input)
}
fn is_read_fn(fname: &str) -> bool {
fname.eq_ignore_ascii_case("read_parquet")
|| fname.eq_ignore_ascii_case("read_csv")
|| fname.eq_ignore_ascii_case("read_json")
/// Any expression that reads an s3 asset
fn parse_s3_object_read(input: &str) -> IResult<&str, &str> {
alt((parse_s3_object_read_fn, parse_s3_object_select_from)).parse(input)
}
fn get_trivial_obj_name(name: &sqlparser::ast::ObjectName) -> Option<&str> {
if name.0.len() != 1 {
return None;
}
Some(name.0.first()?.as_ident()?.value.as_str())
/// Any expression that writes to an s3 asset
fn parse_s3_object_write(input: &str) -> IResult<&str, &str> {
// COPY (...) TO 's3://...'
let (input, _) = (tag_no_case("TO"), multispace0).parse(input)?;
let (input, path) = parse_s3_object_lit(input)?;
Ok((input, path))
}
fn get_str_lit_from_obj_name(name: &ObjectName) -> Option<&str> {
if name.0.len() != 1 {
return None;
}
let ident = name.0.first()?.as_ident()?;
if ident.quote_style != Some('\'') {
return None;
}
Some(ident.value.as_str())
/// read_parquet('s3://...')
fn parse_s3_object_read_fn(input: &str) -> IResult<&str, &str> {
let (input, _) = alt((
tag_no_case("read_parquet"),
tag_no_case("read_csv"),
tag_no_case("read_json"),
))
.parse(input)?;
let (input, _) = multispace0(input)?;
let (input, _) = char('(')(input)?;
let (input, _) = multispace0(input)?;
let (input, path) = parse_s3_object_lit(input)?;
let (input, _) = multispace0(input)?;
let (input, _) = char(')')(input)?;
Ok((input, path))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sql_asset_parser_s3_literals() {
let input = r#"
SELECT * FROM read_parquet('s3:///a.parquet');
COPY (SELECT * FROM 's3://snd/b.parquet') TO 's3:///c.parquet';
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![
ParseAssetsResult {
kind: AssetKind::S3Object,
path: "/a.parquet".to_string(),
access_type: Some(R)
},
ParseAssetsResult {
kind: AssetKind::S3Object,
path: "/c.parquet".to_string(),
access_type: Some(W)
},
ParseAssetsResult {
kind: AssetKind::S3Object,
path: "snd/b.parquet".to_string(),
access_type: Some(R)
},
])
);
}
#[test]
fn test_sql_asset_parser_attach_no_usage_is_registered_as_unknown() {
let input = r#"
ATTACH 'ducklake://my_dl' AS dl;
SELECT 2;
USE dl;
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl".to_string(),
access_type: None
},])
);
}
#[test]
fn test_sql_asset_parser_attach_dot_notation_read() {
let input = r#"
ATTACH 'ducklake://my_dl' AS dl;
SELECT * FROM dl.table1;
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl/table1".to_string(),
access_type: Some(R)
},])
);
}
#[test]
fn test_sql_asset_parser_attach_dot_notation_write() {
let input = r#"
ATTACH 'datatable://my_dt' AS dt;
SELECT dt.read_bait FROM unrelated_table; -- dt. doesn't access the asset
INSERT INTO dt.table1 VALUES ('test');
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::DataTable,
path: "my_dt/table1".to_string(),
access_type: Some(W)
},])
);
}
#[test]
fn test_sql_asset_parser_detach() {
let input = r#"
ATTACH 'ducklake://my_dl' AS dl;
DETACH dl;
SELECT * FROM dl.table1;
"#;
let s = parse_assets(input);
assert_eq!(s.map_err(|e| e.to_string()), Ok(vec![]));
}
#[test]
fn test_sql_asset_parser_implicit_use_asset() {
let input = r#"
ATTACH 'ducklake://my_dl' AS dl;
USE dl;
INSERT INTO table1 VALUES ('test');
USE memory;
SELECT * FROM table1;
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl/table1".to_string(),
access_type: Some(W)
},])
);
}
#[test]
fn test_sql_asset_parser_default_main() {
let input = r#"
ATTACH 'datatable' AS dl;
INSERT INTO dl.table1 VALUES ('test');
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::DataTable,
path: "main/table1".to_string(),
access_type: Some(W)
},])
);
}
#[test]
fn test_sql_asset_parser_create_table() {
let input = r#"
ATTACH 'ducklake' AS dl; USE dl;
CREATE TABLE friends (
name text,
age int
);
INSERT INTO friends VALUES ($name, $age);
SELECT * FROM friends;
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "main/friends".to_string(),
access_type: Some(RW)
},])
);
}
// Make sure a_function is not detected as main/a_function
#[test]
fn test_sql_asset_parser_function_table() {
let input = r#"
ATTACH 'ducklake' AS dl; USE dl;
SELECT * FROM a_function('');
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "main".to_string(),
access_type: None
},])
);
}
#[test]
fn test_sql_asset_parser_delete() {
let input = r#"
ATTACH 'ducklake' AS dl;
USE dl;
DELETE FROM table1;
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "main/table1".to_string(),
access_type: Some(W)
},])
);
}
#[test]
fn test_sql_asset_parser_update() {
let input = r#"
ATTACH 'ducklake' AS dl;
USE dl;
UPDATE table1 SET id = NULL;
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "main/table1".to_string(),
access_type: Some(W)
},])
);
}
#[test]
fn test_sql_asset_parser_resource() {
let input = r#"
ATTACH 'res://u/user/pg_resource' AS db (TYPE postgres);
USE db;
SELECT * FROM table1;
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Resource,
path: "u/user/pg_resource/table1".to_string(),
access_type: Some(R)
},])
);
}
#[test]
fn test_sql_asset_parser_update_with_dot_notation() {
let input = r#"
ATTACH 'ducklake' AS dl;
UPDATE dl.table1 SET id = NULL;
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "main/table1".to_string(),
access_type: Some(W)
},])
);
}
#[test]
fn test_sql_asset_parser_table_with_schema() {
let input = r#"
ATTACH 'ducklake' AS dl;
UPDATE dl.sch.table1 SET id = NULL;
SELECT * FROM dl.sch.table1;
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "main/sch.table1".to_string(),
access_type: Some(RW)
},])
);
}
#[test]
fn test_sql_asset_parser_table_with_schema_implicit() {
let input = r#"
ATTACH 'ducklake' AS dl;
USE dl;
UPDATE sch.table1 SET id = NULL;
SELECT * FROM sch.table1;
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "main/sch.table1".to_string(),
access_type: Some(RW)
},])
);
}
/// SELECT ... FROM 's3://...'
fn parse_s3_object_select_from(input: &str) -> IResult<&str, &str> {
let (input, _) = tag_no_case("FROM").parse(input)?;
let (input, _) = multispace0(input)?;
let (input, path) = parse_s3_object_lit(input)?;
Ok((input, path))
}
/// 's3://...'
fn parse_s3_object_lit(input: &str) -> IResult<&str, &str> {
let (input, _) = quote(input)?;
let (input, _) = tag("s3://").parse(input)?;
let (input, path) = take_while(|c| c != '\'' && c != '"')(input)?;
let (input, _) = quote(input)?;
Ok((input, path))
}
fn quote(input: &str) -> IResult<&str, char> {
alt((char('\''), char('\"'))).parse(input)
}
fn parse_resource_lit(input: &str) -> IResult<&str, &str> {
let (input, _) = quote(input)?;
let (input, _) = alt((tag("$res:"), tag("res://"))).parse(input)?;
let (input, path) = take_while(|c| c != '\'' && c != '"')(input)?;
let (input, _) = quote(input)?;
Ok((input, path))
}
fn parse_ducklake_lit(input: &str) -> IResult<&str, &str> {
let (input, _) = quote(input)?;
let (input, _) = tag("ducklake").parse(input)?;
let (input, path) =
opt(preceded(tag("://"), take_while(|c| c != '\'' && c != '"'))).parse(input)?;
let (input, _) = quote(input)?;
Ok((input, path.unwrap_or("main")))
}
fn parse_datatable_lit(input: &str) -> IResult<&str, &str> {
let (input, _) = quote(input)?;
let (input, _) = tag("datatable").parse(input)?;
let (input, path) =
opt(preceded(tag("://"), take_while(|c| c != '\'' && c != '"'))).parse(input)?;
let (input, _) = quote(input)?;
Ok((input, path.unwrap_or("main")))
}
fn parse_comment(input: &str) -> IResult<&str, &str> {
let (input, _) = tag("--").parse(input)?;
let (input, comment) = take_while(|c| c != '\n')(input)?;
Ok((input, comment))
}

View File

@@ -15,7 +15,6 @@ serde-wasm-bindgen.workspace = true
[dependencies]
windmill-parser.workspace = true
windmill-parser-sql.workspace = true
swc_common.workspace = true
triomphe.workspace = true
swc_ecma_parser.workspace = true

View File

@@ -5,12 +5,12 @@ use swc_ecma_ast::{CallExpr, Expr, Lit, MemberExpr, MemberProp, Str};
use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsSyntax};
use swc_ecma_visit::{Visit, VisitWith};
use windmill_parser::asset_parser::{
asset_was_used, merge_assets, parse_asset_syntax, AssetKind, AssetUsageAccessType,
detect_sql_access_type, merge_assets, parse_asset_syntax, AssetKind, AssetUsageAccessType,
ParseAssetsResult,
};
use AssetUsageAccessType::*;
pub fn parse_assets(code: &str) -> anyhow::Result<Vec<ParseAssetsResult>> {
pub fn parse_assets(code: &str) -> anyhow::Result<Vec<ParseAssetsResult<String>>> {
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(FileName::Custom("main.ts".into()).into(), code.into());
let lexer = Lexer::new(
@@ -40,10 +40,8 @@ pub fn parse_assets(code: &str) -> anyhow::Result<Vec<ParseAssetsResult>> {
Ok(merge_assets(assets_finder.assets))
}
type VarAssetName = String;
type VarAssetSchema = Option<String>;
struct AssetsFinder {
assets: Vec<ParseAssetsResult>,
assets: Vec<ParseAssetsResult<String>>,
// The user will write code like:
// let sql = wmill.datatable('main')
@@ -51,62 +49,16 @@ struct AssetsFinder {
// The goal is to remember that the identifier "sql" corresponds to the datatable "main"
// so that when we see a tagged template expression with tag "sql" we know which datatable it
// corresponds to. This allows us to infer if a datatable is Read or Write based on the SQL query.
var_identifiers: HashMap<String, (AssetKind, VarAssetName, VarAssetSchema)>,
var_identifiers: HashMap<String, (AssetKind, String)>,
}
/// Helper function to extract wmill.datatable() or wmill.ducklake() calls,
/// Returns (AssetKind, asset_name, optional_schema_name)
fn extract_wmill_datatable_call(expr: &Expr) -> Option<(AssetKind, String, Option<String>)> {
if let Expr::Call(call_expr) = expr {
if let Some(Expr::Member(member)) = call_expr.callee.as_expr().map(AsRef::as_ref) {
// Check if object is "wmill"
let is_wmill = matches!(
member.obj.as_ref(),
Expr::Ident(ident) if ident.sym.as_str() == "wmill"
);
if is_wmill {
if let MemberProp::Ident(prop) = &member.prop {
// Get the asset name from first arg, default to "main"
let asset_name = call_expr
.args
.first()
.and_then(|arg| match arg.expr.as_ref() {
Expr::Lit(Lit::Str(s)) => Some(s.value.to_string()),
_ => None,
})
.unwrap_or_else(|| "main".to_string());
let (asset_name, schema_name) = asset_name.split_once(':').map_or_else(
|| (asset_name.clone(), None),
|(name, schema)| {
(
(if name.is_empty() { "main" } else { name }).to_string(),
Some(schema.to_string()),
)
},
);
let kind = match prop.sym.as_str() {
"datatable" => Some(AssetKind::DataTable),
"ducklake" => Some(AssetKind::Ducklake),
_ => None,
};
return kind.map(|k| (k, asset_name, schema_name));
}
}
}
}
None
}
impl Visit for AssetsFinder {
// visit_call_expr will not recurse if it detects an asset,
// so this will only be called when no further context was found
fn visit_lit(&mut self, node: &swc_ecma_ast::Lit) {
match node {
swc_ecma_ast::Lit::Str(str) => {
if let Some((kind, path)) = parse_asset_syntax(str.value.as_str(), false) {
if let Some((kind, path)) = parse_asset_syntax(str.value.as_str()) {
self.assets.push(ParseAssetsResult {
kind,
path: path.to_string(),
@@ -125,37 +77,6 @@ impl Visit for AssetsFinder {
}
}
fn visit_assign_expr(&mut self, node: &swc_ecma_ast::AssignExpr) {
// Handle reassignments like: sql = wmill.datatable('main')
// Extract the variable name from the left side
let var_name = match &node.left {
swc_ecma_ast::AssignTarget::Simple(simple_target) => match simple_target {
swc_ecma_ast::SimpleAssignTarget::Ident(ident_binding) => {
ident_binding.id.sym.as_str().to_string()
}
_ => {
node.visit_children_with(self);
return;
}
},
_ => {
node.visit_children_with(self);
return;
}
};
// Check if right side is a wmill.datatable() or wmill.ducklake() call
if let Some((kind, asset_name, schema)) = extract_wmill_datatable_call(node.right.as_ref())
{
self.var_identifiers
.insert(var_name, (kind, asset_name, schema));
return;
}
// Default: visit children
node.visit_children_with(self);
}
fn visit_block_stmt(&mut self, node: &swc_ecma_ast::BlockStmt) {
// Save current state before entering the block
let saved_var_identifiers = self.var_identifiers.clone();
@@ -163,12 +84,19 @@ impl Visit for AssetsFinder {
// Visit children (this may add new identifiers)
node.visit_children_with(self);
// If we find 'let sql = wmill.datatable(...)',
// but no sql`` tagged templates were used, we add
// the asset with unknown access type
for var in self.var_identifiers.keys() {
if saved_var_identifiers.contains_key(var) {
continue;
}
let (kind, ref path, _) = self.var_identifiers[var];
if asset_was_used(&self.assets, (kind, path)) {
let (kind, ref path) = self.var_identifiers[var];
if self
.assets
.iter()
.any(|a| a.kind == kind && &a.path == path)
{
continue;
}
self.assets
@@ -190,12 +118,43 @@ impl Visit for AssetsFinder {
};
// Check if init is a call to wmill.datatable(...) or wmill.ducklake(...)
// optionally with .schema() chained
if let Some(init) = &node.init {
if let Some((kind, asset_name, schema)) = extract_wmill_datatable_call(init.as_ref()) {
self.var_identifiers
.insert(var_name, (kind, asset_name, schema));
return;
if let Expr::Call(call_expr) = init.as_ref() {
if let Some(Expr::Member(member)) = call_expr.callee.as_expr().map(AsRef::as_ref) {
// Check if object is "wmill"
let is_wmill = matches!(
member.obj.as_ref(),
Expr::Ident(ident) if ident.sym.as_str() == "wmill"
);
if is_wmill {
if let MemberProp::Ident(prop) = &member.prop {
// Get the asset name from first arg, default to "main"
let asset_name = call_expr
.args
.first()
.and_then(|arg| match arg.expr.as_ref() {
Expr::Lit(Lit::Str(s)) => Some(s.value.to_string()),
_ => None,
})
.unwrap_or_else(|| "main".to_string());
match prop.sym.as_str() {
"datatable" => {
self.var_identifiers
.insert(var_name, (AssetKind::DataTable, asset_name));
return;
}
"ducklake" => {
self.var_identifiers
.insert(var_name, (AssetKind::Ducklake, asset_name));
return;
}
_ => {}
}
}
}
}
}
}
@@ -214,7 +173,9 @@ impl Visit for AssetsFinder {
};
// Check if it's a known identifier
let Some((kind, asset_name, schema)) = self.var_identifiers.get(tag_name) else {
let (kind, asset_name) = if let Some((kind, name)) = self.var_identifiers.get(tag_name) {
(*kind, name.clone())
} else {
node.visit_children_with(self);
return;
};
@@ -226,34 +187,13 @@ impl Visit for AssetsFinder {
.iter()
.map(|quasi| quasi.raw.as_str())
.collect::<Vec<_>>()
.join("$1"); // placeholder for expressions
.join(" ");
let duckdb_conn_prefix = match kind {
AssetKind::DataTable => "datatable",
AssetKind::Ducklake => "ducklake",
_ => return,
};
let sql = format!("ATTACH '{duckdb_conn_prefix}://{asset_name}' AS dt; USE dt; {sql}");
// Determine access type based on SQL keywords
let access_type = detect_sql_access_type(&sql);
// We use the SQL parser to detect if it's a read or write query
match windmill_parser_sql::parse_assets(&sql) {
Ok(mut sql_assets) => {
if let Some(schema) = schema {
for asset in &mut sql_assets {
if asset.kind == *kind && asset.path.starts_with(asset_name) {
asset.path = format!(
"{}/{}.{}",
asset_name,
schema,
&asset.path[asset_name.len() + 1..]
);
}
}
}
self.assets.extend(sql_assets);
}
_ => {}
}
self.assets
.push(ParseAssetsResult { kind, path: asset_name, access_type });
}
}
@@ -281,9 +221,7 @@ impl AssetsFinder {
match arg_value.map(|e| e.expr.as_ref()) {
Some(Expr::Lit(Lit::Str(Str { value, .. }))) => {
let path = parse_asset_syntax(&value, false)
.map(|(_, p)| p)
.unwrap_or(&value);
let path = parse_asset_syntax(&value).map(|(_, p)| p).unwrap_or(&value);
self.assets
.push(ParseAssetsResult { kind, path: path.to_string(), access_type });
}
@@ -292,274 +230,3 @@ impl AssetsFinder {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ts_asset_parser_load_s3() {
let input = r#"
import * as wmill from "windmill-client"
export async function main() {
wmill.loadS3File('s3:///test.csv')
}
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::S3Object,
path: "/test.csv".to_string(),
access_type: Some(R)
},])
);
}
#[test]
fn test_ts_asset_parser_unused_sql() {
let input = r#"
import * as wmill from "windmill-client"
export async function main() {
let sql = wmill.datatable('dt')
}
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::DataTable,
path: "dt".to_string(),
access_type: None
},])
);
}
#[test]
fn test_ts_asset_parser_sql_read() {
let input = r#"
import * as wmill from "windmill-client"
export async function main(x: number) {
let sql = wmill.datatable('dt')
return await sql`SELECT * FROM friends WHERE age = ${x}`.fetch()
}
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::DataTable,
path: "dt/friends".to_string(),
access_type: Some(R)
},])
);
}
#[test]
fn test_ts_asset_parser_sql_read_write() {
let input = r#"
import * as wmill from "windmill-client"
export async function main(x: number) {
let sql = wmill.datatable('dt')
await sql`UPDATE friends SET name = 'Pierre' WHERE age = ${x}`.fetch()
let pierre = await sql`SELECT * FROM friends WHERE age = ${x}`.fetchOne()
return await sql`SELECT * FROM analytics`.fetch()
}
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![
ParseAssetsResult {
kind: AssetKind::DataTable,
path: "dt/analytics".to_string(),
access_type: Some(R)
},
ParseAssetsResult {
kind: AssetKind::DataTable,
path: "dt/friends".to_string(),
access_type: Some(RW)
},
])
);
}
#[test]
fn test_ts_asset_parser_multiple_sql_scopes() {
let input = r#"
import * as wmill from "windmill-client"
export async function main() {
async function f(x: number) {
let sql = wmill.datatable()
return await sql`SELECT * FROM friends WHERE age = ${x}`.fetch()
}
let sql = wmill.datatable('another1')
return await sql`INSERT INTO customers VALUES (${0})`.fetch()
}
function unused() {
let sql = wmill.ducklake('another2')
}
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![
ParseAssetsResult {
kind: AssetKind::DataTable,
path: "another1/customers".to_string(),
access_type: Some(W)
},
ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "another2".to_string(),
access_type: None
},
ParseAssetsResult {
kind: AssetKind::DataTable,
path: "main/friends".to_string(),
access_type: Some(R)
},
])
);
}
#[test]
fn test_ts_asset_parser_overriden_var_identifier() {
let input = r#"
import * as wmill from "windmill-client"
export async function main() {
let sql = wmill.datatable('another1')
}
function g() {
let sql = wmill.ducklake()
}
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![
ParseAssetsResult {
kind: AssetKind::DataTable,
path: "another1".to_string(),
access_type: None
},
ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "main".to_string(),
access_type: None
},
])
);
}
#[test]
fn test_ts_asset_parser_datatable_with_schema() {
let input = r#"
import * as wmill from "windmill-client"
export async function main(x: number) {
let sql = wmill.datatable(':myschema')
return await sql`SELECT * FROM friends WHERE age = ${x}`.fetch()
}
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::DataTable,
path: "main/myschema.friends".to_string(),
access_type: Some(R)
},])
);
}
#[test]
fn test_ts_asset_parser_schema_with_write() {
let input = r#"
import * as wmill from "windmill-client"
export async function main(x: number) {
let sql = wmill.datatable('dt:public')
await sql`INSERT INTO users VALUES (${x})`.fetch()
return await sql`SELECT * FROM users`.fetch()
}
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::DataTable,
path: "dt/public.users".to_string(),
access_type: Some(RW)
},])
);
}
#[test]
fn test_ts_asset_parser_unused_datatable_with_schema() {
let input = r#"
import * as wmill from "windmill-client"
export async function main() {
let sql = wmill.datatable('dt:myschema')
}
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::DataTable,
path: "dt".to_string(),
access_type: None
},])
);
}
#[test]
fn test_ts_asset_parser_reassignment() {
let input = r#"
import * as wmill from "windmill-client"
export async function main(x: number) {
let sql;
sql = wmill.datatable('dt')
return await sql`SELECT * FROM users WHERE id = ${x}`.fetch()
}
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::DataTable,
path: "dt/users".to_string(),
access_type: Some(R)
},])
);
}
#[test]
fn test_ts_asset_parser_reassignment_with_schema() {
let input = r#"
import * as wmill from "windmill-client"
export async function main(x: number) {
let sql = wmill.datatable('dt')
await sql`INSERT INTO test VALUES ('')`.fetch()
sql = wmill.datatable('dt:private')
return await sql`SELECT * FROM users WHERE id = ${x}`.fetch()
}
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![
ParseAssetsResult {
kind: AssetKind::DataTable,
path: "dt/private.users".to_string(),
access_type: Some(R)
},
ParseAssetsResult {
kind: AssetKind::DataTable,
path: "dt/test".to_string(),
access_type: Some(W)
},
])
);
}
}

View File

@@ -190,36 +190,41 @@ pub fn parse_ruby(code: &str) -> String {
#[cfg(feature = "sql-parser")]
#[wasm_bindgen]
pub fn parse_assets_sql(code: &str) -> String {
match windmill_parser_sql::parse_assets(code) {
Ok(r) => serde_json::to_string(&r).unwrap(),
Err(err) => format!("err: {:?}", err),
if let Ok(r) = windmill_parser_sql::parse_assets(code) {
return serde_json::to_string(&r).unwrap();
} else {
return "Invalid".to_string();
}
}
#[cfg(feature = "ts-parser")]
#[wasm_bindgen]
pub fn parse_assets_ts(code: &str) -> String {
match windmill_parser_ts::parse_assets(code) {
Ok(r) => serde_json::to_string(&r).unwrap(),
Err(err) => format!("err: {:?}", err),
if let Ok(r) = windmill_parser_ts::parse_assets(code) {
return serde_json::to_string(&r).unwrap();
} else {
return "Invalid".to_string();
}
}
#[cfg(feature = "py-parser")]
#[wasm_bindgen]
pub fn parse_assets_py(code: &str) -> String {
match windmill_parser_py::parse_assets(code) {
Ok(r) => serde_json::to_string(&r).unwrap(),
Err(err) => format!("err: {:?}", err),
if let Ok(r) = windmill_parser_py::parse_assets(code) {
return serde_json::to_string(&r).unwrap();
} else {
return "Invalid".to_string();
}
}
#[cfg(feature = "ansible-parser")]
#[wasm_bindgen]
pub fn parse_assets_ansible(code: &str) -> String {
match windmill_parser_yaml::parse_assets(code) {
Ok(r) => serde_json::to_string(&r).unwrap(),
Err(err) => format!("err: {:?}", err),
let o = windmill_parser_yaml::parse_assets(code);
if let Ok(r) = o {
return serde_json::to_string(&r).unwrap();
} else {
return format!("err: {:?}", o.err().unwrap());
}
}

View File

@@ -1,10 +1,10 @@
use windmill_parser::asset_parser::{
merge_assets, AssetKind, AssetUsageAccessType, ParseAssetsResult,
};
merge_assets, AssetKind, AssetUsageAccessType, ParseAssetsResult,
};
use crate::{parse_ansible_reqs, ResourceOrVariablePath};
pub fn parse_assets(input: &str) -> anyhow::Result<Vec<ParseAssetsResult>> {
pub fn parse_assets(input: &str) -> anyhow::Result<Vec<ParseAssetsResult<String>>> {
let mut assets = vec![];
if let (_, Some(ansible_reqs), _) = parse_ansible_reqs(input)? {
if let Some(delegate_to_git_repo_details) = ansible_reqs.delegate_to_git_repo {
@@ -38,3 +38,4 @@ pub fn parse_assets(input: &str) -> anyhow::Result<Vec<ParseAssetsResult>> {
Ok(merge_assets(assets))
}

View File

@@ -1,6 +1,6 @@
use serde::Serialize;
#[derive(Serialize, PartialEq, Clone, Copy, Debug)]
#[derive(Serialize, PartialEq, Clone, Copy)]
#[serde(rename_all(serialize = "lowercase"))]
pub enum AssetUsageAccessType {
R,
@@ -10,7 +10,7 @@ pub enum AssetUsageAccessType {
use AssetUsageAccessType::*;
#[derive(Serialize, PartialEq, Clone, Copy, Debug)]
#[derive(Serialize, PartialEq, Clone, Copy)]
#[serde(rename_all(serialize = "lowercase"))]
pub enum AssetKind {
S3Object,
@@ -19,10 +19,10 @@ pub enum AssetKind {
DataTable,
}
#[derive(Serialize, Debug, PartialEq)]
pub struct ParseAssetsResult {
#[derive(Serialize)]
pub struct ParseAssetsResult<S: AsRef<str>> {
pub kind: AssetKind,
pub path: String,
pub path: S,
#[serde(skip_serializing_if = "Option::is_none")]
pub access_type: Option<AssetUsageAccessType>, // None in case of ambiguity
}
@@ -34,13 +34,13 @@ pub struct DelegateToGitRepoDetails {
pub commit: Option<String>,
}
pub fn merge_assets(assets: Vec<ParseAssetsResult>) -> Vec<ParseAssetsResult> {
let mut arr: Vec<ParseAssetsResult> = vec![];
pub fn merge_assets<S: AsRef<str>>(assets: Vec<ParseAssetsResult<S>>) -> Vec<ParseAssetsResult<S>> {
let mut arr: Vec<ParseAssetsResult<S>> = vec![];
for asset in assets {
// Remove duplicates
if let Some(existing) = arr
.iter_mut()
.find(|x| x.path == asset.path && x.kind == asset.kind)
.find(|x| x.path.as_ref() == asset.path.as_ref() && x.kind == asset.kind)
{
// merge access types
existing.access_type = match (asset.access_type, existing.access_type) {
@@ -54,33 +54,12 @@ pub fn merge_assets(assets: Vec<ParseAssetsResult>) -> Vec<ParseAssetsResult> {
arr.push(asset);
}
}
arr.sort_by(|a, b| a.path.cmp(&b.path));
arr.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
arr
}
// Will return false if the user assigned an asset to a variable like:
// let sql = wmill.datatable('main')
// But never used it. In that case we don't know which table is being used,
// but we still want to add the main datatable as an asset with unknown access type.
//
// This function takes care of the fact that assets can be suffixed (e.g. "main/users")
pub fn asset_was_used(assets: &Vec<ParseAssetsResult>, (kind, path): (AssetKind, &String)) -> bool {
assets.iter().any(|a| {
let a_path = a.path.as_str();
let has_same_path_base = a_path
.strip_prefix(path)
.map(|p| p.starts_with('/'))
.unwrap_or(false);
(has_same_path_base || a_path == path) && a.kind == kind
})
}
pub fn parse_asset_syntax(s: &str, enable_default_syntax: bool) -> Option<(AssetKind, &str)> {
if enable_default_syntax && s == "datatable" {
Some((AssetKind::DataTable, "main"))
} else if enable_default_syntax && s == "ducklake" {
Some((AssetKind::Ducklake, "main"))
} else if s.starts_with("s3://") {
pub fn parse_asset_syntax(s: &str) -> Option<(AssetKind, &str)> {
if s.starts_with("s3://") {
Some((AssetKind::S3Object, &s[5..]))
} else if s.starts_with("res://") {
Some((AssetKind::Resource, &s[6..]))
@@ -89,8 +68,41 @@ pub fn parse_asset_syntax(s: &str, enable_default_syntax: bool) -> Option<(Asset
} else if s.starts_with("ducklake://") {
Some((AssetKind::Ducklake, &s[11..]))
} else if s.starts_with("datatable://") {
Some((AssetKind::DataTable, &s[12..]))
Some((AssetKind::DataTable, &s[11..]))
} else {
None
}
}
pub fn detect_sql_access_type(sql: &str) -> Option<AssetUsageAccessType> {
let first_kw = sql
.trim()
.split_whitespace()
.next()
.unwrap_or("")
.to_lowercase();
// Check for write operations
let has_write = first_kw.starts_with("insert")
|| first_kw.starts_with("update")
|| first_kw.starts_with("delete")
|| first_kw.starts_with("drop")
|| first_kw.starts_with("create")
|| first_kw.starts_with("alter")
|| first_kw.starts_with("truncate")
|| first_kw.starts_with("merge");
// Check for read operations
let has_read = first_kw.starts_with("select")
|| first_kw.starts_with("with") // CTEs, usually for reads
|| first_kw.starts_with("show")
|| first_kw.starts_with("describe")
|| first_kw.starts_with("explain");
match (has_read, has_write) {
(true, true) => Some(RW),
(true, false) => Some(R),
(false, true) => Some(W),
(false, false) => None, // Unknown - couldn't determine
}
}

View File

@@ -691,8 +691,8 @@ pub async fn run_deployed_relative_imports(
language,
priority: None,
apply_preprocessor: false,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
})
.push(&db2)
.await;
@@ -741,8 +741,8 @@ pub async fn run_preview_relative_imports(
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
}))
.push(&db2)
.await;

View File

@@ -52,8 +52,8 @@ mod job_payload {
let result = RunJob::from(JobPayload::ScriptHash {
hash: ScriptHash(123412),
path: "f/system/hello".to_string(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
@@ -90,8 +90,8 @@ mod job_payload {
language: ScriptLang::Deno,
priority: None,
apply_preprocessor: true,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
})
.run_until_complete_with(db, false, port, |id| async move {
let job = sqlx::query!("SELECT preprocessed FROM v2_job WHERE id = $1", id)
@@ -163,7 +163,7 @@ mod job_payload {
let result = RunJob::from(JobPayload::FlowScript {
id: flow_scripts[0],
language: ScriptLang::Deno,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
@@ -182,7 +182,7 @@ mod job_payload {
let result = RunJob::from(JobPayload::FlowScript {
id: flow_scripts[1],
language: ScriptLang::Deno,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,

View File

@@ -189,8 +189,8 @@ def main():
path: None,
language: ScriptLang::Python3,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
@@ -238,8 +238,8 @@ def main():
path: None,
language: ScriptLang::Python3,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
@@ -272,8 +272,8 @@ def main():
path: None,
language: ScriptLang::Python3,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
@@ -311,8 +311,8 @@ def main():
path: None,
language: ScriptLang::Python3,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
@@ -348,8 +348,8 @@ def main():
path: None,
language: ScriptLang::Python3,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,

View File

@@ -188,7 +188,7 @@ async fn test_deno_flow(db: Pool<Postgres>) -> anyhow::Result<()> {
path: None,
lock: None,
tag: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default()
.into(),
is_trigger: None,
assets: None,
@@ -235,7 +235,7 @@ async fn test_deno_flow(db: Pool<Postgres>) -> anyhow::Result<()> {
lock: None,
tag: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
windmill_common::jobs::ConcurrencySettings::default().into(),
is_trigger: None,
assets: None,
}
@@ -369,7 +369,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) -> anyhow::Result<()> {
path: None,
lock: None,
tag: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
is_trigger: None,
assets: None,
@@ -425,7 +425,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) -> anyhow::Result<()> {
path: None,
lock: None,
tag: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
is_trigger: None,
assets: None,
}.into(),
@@ -465,7 +465,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) -> anyhow::Result<()> {
path: None,
lock: None,
tag: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
is_trigger: None,
assets: None,
@@ -533,7 +533,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) -> anyhow::Result<()> {
path: None,
lock: None,
tag: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
is_trigger: None,
assets: None,
}.into(),
@@ -865,8 +865,8 @@ func main(derp string) (string, error) {
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
}))
.arg("derp", json!("world"))
.run_until_complete(&db, false, port)
@@ -900,8 +900,8 @@ fn main(world: String) -> Result<String, String> {
lock: None,
language: ScriptLang::Rust,
cache_ignore_s3_path: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
cache_ttl: None,
dedicated_worker: None,
}))
@@ -978,8 +978,8 @@ echo "hello $msg"
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
}))
.arg("msg", json!("world"))
.run_until_complete(&db, false, port)
@@ -1011,8 +1011,8 @@ def main [ msg: string ] {
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
}))
.arg("msg", json!("world"))
.run_until_complete(&db, false, port)
@@ -1064,8 +1064,8 @@ def main [
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
}))
.arg("a", json!("3"))
.arg("b", json!("null"))
@@ -1126,8 +1126,8 @@ public class Main {
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
}))
.arg("a", json!(3))
.arg("b", json!(3.0))
@@ -1161,8 +1161,8 @@ export async function main(a: Date) {
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
}))
.arg("a", json!("2024-09-24T10:00:00.000Z"))
.run_until_complete(&db, false, port)
@@ -1196,8 +1196,8 @@ export async function main(a: Date) {
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
}))
.arg("a", json!("2024-09-24T10:00:00.000Z"))
.run_until_complete(&db, false, port)
@@ -1232,8 +1232,8 @@ def main(a: datetime, b: bytes):
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
concurrency_settings: windmill_common::jobs::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::jobs::DebouncingSettings::default(),
}))
.arg("a", json!("2024-09-24T10:00:00.000Z"))
.arg("b", json!("dGVzdA=="))

View File

@@ -149,7 +149,6 @@ rustls = { workspace = true }
aws-sigv4.workspace = true
aws-sdk-config.workspace = true
aws-config = { workspace = true, optional = true }
aws-credential-types.workspace = true
async-trait.workspace = true
google-cloud-pubsub = { workspace = true, optional = true }
google-cloud-googleapis = { workspace = true , optional = true }

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.593.0
version: 1.590.0
title: Windmill API
contact:
@@ -69,41 +69,6 @@ paths:
schema:
type: string
/inkeep:
post:
summary: query Windmill AI documentation assistant (EE only)
operationId: queryDocumentation
x-mcp-tool: true
tags:
- documentation
requestBody:
description: query to send to the AI documentation assistant
required: true
content:
application/json:
schema:
type: object
properties:
query:
type: string
description: The documentation query to send to the AI assistant
required:
- query
responses:
"200":
description: AI documentation assistant response
content:
application/json:
schema:
type: object
description: Response from Inkeep service
"403":
description: Enterprise Edition required
content:
text/plain:
schema:
type: string
/openapi.yaml:
get:
summary: get openapi yaml spec
@@ -2024,52 +1989,6 @@ paths:
schema:
type: string
/w/{workspace}/workspaces/compare/{target_workspace_id}:
get:
operationId: compareWorkspaces
summary: Compare two workspaces
description: Compares the current workspace with a target workspace to find differences in scripts, flows, apps, resources, and variables. Returns information about items that are ahead, behind, or in conflict.
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: target_workspace_id
in: path
required: true
schema:
type: string
description: The ID of the workspace to compare with
responses:
"200":
description: Workspace comparison results
content:
application/json:
schema:
$ref: "#/components/schemas/WorkspaceComparison"
/w/{workspace}/workspaces/reset_diff_tally/{fork_workspace_id}:
post:
operationId: resetDiffTally
summary: Resets the ahead and behind deployement counter after a deployement
description: This endpoint should be called after a fork deployement
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: fork_workspace_id
in: path
required: true
schema:
type: string
description: The ID of the workspace to compare with
responses:
"200":
description: status
content:
application/json:
schema: {}
/users/exists/{email}:
get:
summary: exists email
@@ -9011,9 +8930,9 @@ paths:
"200":
description: confirmation message
content:
application/json:
schema:
type: string
application/json:
schema:
type: string
/w/{workspace}/trigger/{trigger_kind}/cancel_suspended_trigger_jobs/{trigger_path}:
post:
@@ -13021,40 +12940,6 @@ paths:
schema:
type: string
/w/{workspace}/groups_history/get/{name}:
get:
summary: get group permission history
operationId: getGroupPermissionHistory
tags:
- group
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Name"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: group permission history
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: integer
changed_by:
type: string
changed_at:
type: string
format: date-time
change_type:
type: string
member_affected:
type: string
nullable: true
/w/{workspace}/folders/list:
get:
summary: list folders
@@ -13318,40 +13203,6 @@ paths:
schema:
type: string
/w/{workspace}/folders_history/get/{name}:
get:
summary: get folder permission history
operationId: getFolderPermissionHistory
tags:
- folder
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Name"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: folder permission history
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: integer
changed_by:
type: string
changed_at:
type: string
format: date-time
change_type:
type: string
affected:
type: string
nullable: true
/workers/list:
get:
summary: list workers
@@ -16792,7 +16643,7 @@ components:
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow",
"unassigned_singlestepflow"
]
schedule_path:
type: string
@@ -16908,7 +16759,7 @@ components:
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow",
"unassigned_singlestepflow"
]
schedule_path:
type: string
@@ -17032,7 +16883,7 @@ components:
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow",
"unassigned_singlestepflow"
]
trigger:
type: string
@@ -17180,7 +17031,7 @@ components:
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow",
"unassigned_singlestepflow"
]
trigger:
type: string
@@ -21041,108 +20892,6 @@ components:
type: boolean
description: Whether operators can view workers page
WorkspaceComparison:
type: object
required:
- all_ahead_items_visible
- all_behind_items_visible
- skipped_comparison
- diffs
- summary
properties:
all_ahead_items_visible:
type: boolean
description: All items with changes ahead are visible by the user of the request.
all_behind_items_visible:
type: boolean
description: All items with changes behind are visible by the user of the request.
skipped_comparison:
type: boolean
description: Whether the comparison was skipped. This happens with old forks that where not being kept track of
diffs:
type: array
description: List of differences found between workspaces
items:
$ref: "#/components/schemas/WorkspaceItemDiff"
summary:
$ref: "#/components/schemas/CompareSummary"
description: Summary statistics of the comparison
WorkspaceItemDiff:
type: object
required:
- kind
- path
- ahead
- behind
- has_changes
- exists_in_source
- exists_in_fork
properties:
kind:
type: string
enum: ["script", "flow", "app", "resource", "variable"]
description: Type of the item
path:
type: string
description: Path of the item in the workspace
ahead:
type: integer
description: Number of versions source is ahead of target
behind:
type: integer
description: Number of versions source is behind target
has_changes:
type: boolean
description: Whether the item has any differences
exists_in_source:
type: boolean
description: If the item exists in the source workspace
exists_in_fork:
type: boolean
description: If the item exists in the fork workspace
CompareSummary:
type: object
required:
- total_diffs
- total_ahead
- total_behind
- scripts_changed
- flows_changed
- apps_changed
- resources_changed
- variables_changed
- conflicts
properties:
total_diffs:
type: integer
description: Total number of items with differences
total_ahead:
type: integer
description: Total number of ahead changes
total_behind:
type: integer
description: Total number of behind changes
scripts_changed:
type: integer
description: Number of scripts with differences
flows_changed:
type: integer
description: Number of flows with differences
apps_changed:
type: integer
description: Number of apps with differences
resources_changed:
type: integer
description: Number of resources with differences
variables_changed:
type: integer
description: Number of variables with differences
conflicts:
type: integer
description: Number of items that are both ahead and behind (conflicts)
TeamInfo:
type: object
required:
@@ -21218,9 +20967,6 @@ components:
per_page:
type: number
description: Number of repositories loaded per page
error:
type: string
description: Error message if token retrieval failed
required:
- installation_id
- account_id

View File

@@ -133,10 +133,6 @@ struct AIStandardResource {
api_key: Option<String>,
organization_id: Option<String>,
region: Option<String>,
#[serde(alias = "awsAccessKeyId")]
aws_access_key_id: Option<String>,
#[serde(alias = "awsSecretAccessKey")]
aws_secret_access_key: Option<String>,
}
#[derive(Deserialize, Debug)]
@@ -158,9 +154,6 @@ struct AIRequestConfig {
pub access_token: Option<String>,
pub organization_id: Option<String>,
pub user: Option<String>,
pub region: Option<String>,
pub aws_access_key_id: Option<String>,
pub aws_secret_access_key: Option<String>,
}
impl AIRequestConfig {
@@ -170,18 +163,8 @@ impl AIRequestConfig {
w_id: &str,
resource: AIResource,
) -> Result<Self> {
let (
api_key,
access_token,
organization_id,
base_url,
user,
region,
aws_access_key_id,
aws_secret_access_key,
) = match resource {
let (api_key, access_token, organization_id, base_url, user) = match resource {
AIResource::Standard(resource) => {
let region = resource.region.clone();
let base_url = provider
.get_base_url(resource.base_url, resource.region, db)
.await?;
@@ -195,28 +178,8 @@ impl AIRequestConfig {
} else {
None
};
let aws_access_key_id = if let Some(access_key_id) = resource.aws_access_key_id {
Some(get_variable_or_self(access_key_id, db, w_id).await?)
} else {
None
};
let aws_secret_access_key =
if let Some(secret_access_key) = resource.aws_secret_access_key {
Some(get_variable_or_self(secret_access_key, db, w_id).await?)
} else {
None
};
(
api_key,
None,
organization_id,
base_url,
None,
region,
aws_access_key_id,
aws_secret_access_key,
)
(api_key, None, organization_id, base_url, None)
}
AIResource::OAuth(resource) => {
let user = if let Some(user) = resource.user.clone() {
@@ -227,20 +190,11 @@ impl AIRequestConfig {
let token = Self::get_token_using_oauth(resource, db, w_id).await?;
let base_url = provider.get_base_url(None, None, db).await?;
(None, Some(token), None, base_url, user, None, None, None)
(None, Some(token), None, base_url, user)
}
};
Ok(Self {
base_url,
organization_id,
api_key,
access_token,
user,
region,
aws_access_key_id,
aws_secret_access_key,
})
Ok(Self { base_url, organization_id, api_key, access_token, user })
}
async fn get_token_using_oauth(
@@ -297,10 +251,6 @@ impl AIRequestConfig {
let is_anthropic_sdk = headers.get("X-Anthropic-SDK").is_some();
let is_bedrock = matches!(provider, AIProvider::AWSBedrock);
// Check if using IAM credentials for Bedrock (instead of bearer token)
let use_iam_auth =
is_bedrock && self.aws_access_key_id.is_some() && self.aws_secret_access_key.is_some();
// Handle AWS Bedrock transformation
let (url, body) = if is_bedrock && method != Method::GET {
let (model, transformed_body, is_streaming) =
@@ -332,7 +282,7 @@ impl AIRequestConfig {
tracing::debug!("AI request URL: {}", url);
let mut request = HTTP_CLIENT
.request(method.clone(), &url)
.request(method, url)
.header("content-type", "application/json");
for (header_name, header_value) in headers.iter() {
@@ -341,42 +291,22 @@ impl AIRequestConfig {
}
}
// For Bedrock with IAM credentials, sign the request using SigV4
if use_iam_auth {
let region = self.region.as_deref().ok_or_else(|| {
Error::internal_err("AWS region must be set for IAM authentication with Bedrock")
})?;
let signed_headers = bedrock::sign_bedrock_request(
method.as_str(),
&url,
&body,
self.aws_access_key_id.as_ref().unwrap(),
self.aws_secret_access_key.as_ref().unwrap(),
region,
)?;
request = request.body(body);
for (header_name, header_value) in signed_headers {
request = request.header(header_name, header_value);
if let Some(api_key) = self.api_key {
if is_azure {
request = request.header("api-key", api_key.clone())
} else {
request = request.header("authorization", format!("Bearer {}", api_key.clone()))
}
} else {
// For non-IAM auth, use bearer token or API key
if let Some(api_key) = self.api_key {
if is_azure {
request = request.header("api-key", api_key.clone())
} else {
request = request.header("authorization", format!("Bearer {}", api_key.clone()))
}
if is_anthropic {
request = request.header("X-API-Key", api_key);
}
}
if let Some(access_token) = self.access_token {
request = request.header("authorization", format!("Bearer {}", access_token))
if is_anthropic {
request = request.header("X-API-Key", api_key);
}
}
request = request.body(body);
if let Some(access_token) = self.access_token {
request = request.header("authorization", format!("Bearer {}", access_token))
}
if let Some(org_id) = self.organization_id {
request = request.header("OpenAI-Organization", org_id);

View File

@@ -55,7 +55,7 @@ use windmill_common::{
apps::{AppScriptId, ListAppQuery, APP_WORKSPACED_ROUTE},
auth::TOKEN_PREFIX_LEN,
cache::{self, future::FutureCachedExt},
db::{DbWithOptAuthed, UserDB},
db::UserDB,
error::{to_anyhow, Error, JsonResult, Result},
jobs::{get_payload_tag_from_prefixed_path, JobPayload, RawCode},
users::username_to_permissioned_as,
@@ -1777,11 +1777,7 @@ async fn execute_component(
..
} => (
&Policy { execution_mode: ExecutionMode::Viewer, ..Default::default() },
&PolicyTriggerableInputs {
static_inputs,
one_of_inputs: force_viewer_one_of_fields.unwrap_or_default(),
allow_user_resources: force_viewer_allow_user_resources.unwrap_or_default(),
},
&PolicyTriggerableInputs { static_inputs, one_of_inputs: force_viewer_one_of_fields.unwrap_or_default(), allow_user_resources: force_viewer_allow_user_resources.unwrap_or_default() },
),
// 2. "run" mode.
_ => {
@@ -2063,6 +2059,44 @@ async fn sign_s3_objects(
Ok(Json(signed_s3_objects))
}
#[cfg(feature = "parquet")]
async fn validate_s3_signature(file_query: &AppS3FileQuery, w_id: &str, db: &DB) -> Result<()> {
let workspace_key = get_workspace_key(w_id, &db).await?;
let Some(exp) = file_query
.exp
.as_ref()
.map(|e| e.parse::<i64>().unwrap_or_default())
else {
return Err(Error::BadRequest("Missing exp".to_string()));
};
let Some(ref sig) = file_query.sig else {
return Err(Error::BadRequest("Missing signature".to_string()));
};
let mut message = format!("file_key={}&exp={}", file_query.s3, exp);
if let Some(ref storage) = file_query.storage {
message = format!("{}&storage={}", message, storage);
}
let mut mac = HmacSha256::new_from_slice(workspace_key.as_bytes())
.map_err(|err| Error::internal_err(format!("Failed to create hmac: {}", err)))?;
mac.update(message.as_bytes());
let sig_bytes = hex::decode(sig)?;
mac.verify_slice(&sig_bytes)
.map_err(|err| Error::BadRequest(format!("Invalid signature: {}", err)))?;
if exp < chrono::Utc::now().timestamp() {
return Err(Error::BadRequest("Signature expired".to_string()));
}
Ok(())
}
#[cfg(not(feature = "parquet"))]
async fn sign_s3_objects() -> Result<()> {
return Err(Error::BadRequest(
@@ -2121,66 +2155,89 @@ async fn upload_s3_file_from_app(
let user_db = UserDB::new(db.clone());
let (s3_resource_opt, file_key, on_behalf_of_email, permissioned_as, username) = if policy
.as_ref()
.is_some_and(|p| p.s3_inputs.is_some())
{
let policy = policy.unwrap();
let s3_inputs = policy.s3_inputs.as_ref().unwrap();
let (s3_resource_opt, file_key, on_behalf_of_email, permissioned_as, username) =
if policy.as_ref().is_some_and(|p| p.s3_inputs.is_some()) {
let policy = policy.unwrap();
let s3_inputs = policy.s3_inputs.as_ref().unwrap();
let (username, permissioned_as, email) =
get_on_behalf_details_from_policy_and_authed(&policy, &opt_authed).await?;
let (username, permissioned_as, email) =
get_on_behalf_details_from_policy_and_authed(&policy, &opt_authed).await?;
let on_behalf_authed = fetch_api_authed_from_permissioned_as(
permissioned_as.clone(),
email.clone(),
&w_id,
&db,
Some(username.clone()),
)
.await?;
let on_behalf_authed = fetch_api_authed_from_permissioned_as(
permissioned_as.clone(),
email.clone(),
&w_id,
&db,
Some(username.clone()),
)
.await?;
if let Some(file_key) = query.file_key {
// file key is provided => requires workspace, user or list policy and must match the regex
let matching_s3_inputs = if let Some(ref s3_resource_path) = query.s3_resource_path {
s3_inputs
.iter()
.filter(|s3_input| {
s3_input.allowed_resources.contains(s3_resource_path)
|| s3_input.allow_user_resources
})
.sorted_by_key(|i| i.allow_user_resources) // consider user resources last
.collect::<Vec<_>>()
} else {
s3_inputs
.iter()
.filter(|s3_input| s3_input.allow_workspace_resource)
.collect::<Vec<_>>()
};
if let Some(file_key) = query.file_key {
// file key is provided => requires workspace, user or list policy and must match the regex
let matching_s3_inputs = if let Some(ref s3_resource_path) = query.s3_resource_path
{
s3_inputs
.iter()
.filter(|s3_input| {
s3_input.allowed_resources.contains(s3_resource_path)
|| s3_input.allow_user_resources
})
.sorted_by_key(|i| i.allow_user_resources) // consider user resources last
.collect::<Vec<_>>()
} else {
s3_inputs
.iter()
.filter(|s3_input| s3_input.allow_workspace_resource)
.collect::<Vec<_>>()
};
let matched_input = matching_s3_inputs.iter().find(|s3_input| {
match Regex::new(&s3_input.file_key_regex) {
Ok(re) => re.is_match(&file_key),
Err(e) => {
tracing::error!("Error compiling regex: {}", e);
false
let matched_input = matching_s3_inputs.iter().find(|s3_input| {
match Regex::new(&s3_input.file_key_regex) {
Ok(re) => re.is_match(&file_key),
Err(e) => {
tracing::error!("Error compiling regex: {}", e);
false
}
}
}
});
});
if let Some(matched_input) = matched_input {
if let Some(ref s3_resource_path) = query.s3_resource_path {
if matched_input.allow_user_resources {
if let Some(authed) = opt_authed {
let db_with_opt_authed = DbWithOptAuthed::from_authed(
&authed,
db.clone(),
Some(user_db.clone()),
);
if let Some(matched_input) = matched_input {
if let Some(ref s3_resource_path) = query.s3_resource_path {
if matched_input.allow_user_resources {
if let Some(authed) = opt_authed {
(
Some(
get_s3_resource(
&authed,
&db,
Some(user_db),
"",
&w_id,
s3_resource_path,
None,
None,
)
.await?,
),
file_key,
email,
permissioned_as,
username,
)
} else {
return Err(Error::BadRequest(
"User resources are not allowed without being logged in"
.to_string(),
));
}
} else {
(
Some(
get_s3_resource(
&db_with_opt_authed,
&on_behalf_authed,
&db,
Some(user_db),
"",
&w_id,
s3_resource_path,
None,
@@ -2193,131 +2250,112 @@ async fn upload_s3_file_from_app(
permissioned_as,
username,
)
} else {
return Err(Error::BadRequest(
"User resources are not allowed without being logged in"
.to_string(),
));
}
} else {
let db_with_opt_authed = DbWithOptAuthed::from_authed(
let (_, s3_resource_opt) = get_workspace_s3_resource_and_check_paths(
&on_behalf_authed,
db.clone(),
Some(user_db.clone()),
);
(
Some(
get_s3_resource(
&db_with_opt_authed,
&w_id,
s3_resource_path,
None,
None,
)
.await?,
),
file_key,
email,
permissioned_as,
username,
&db,
None,
"",
&w_id,
None,
&[(&file_key, S3Permission::WRITE)],
)
.await?;
(s3_resource_opt, file_key, email, permissioned_as, username)
}
} else {
let db_with_opt_authed = DbWithOptAuthed::from_authed(
&on_behalf_authed,
db.clone(),
Some(user_db.clone()),
);
let (_, s3_resource_opt) = get_workspace_s3_resource_and_check_paths(
&db_with_opt_authed,
&w_id,
None,
&[(&file_key, S3Permission::WRITE)],
)
.await?;
(s3_resource_opt, file_key, email, permissioned_as, username)
return Err(Error::BadRequest(
"No matching s3 resource found for the given file key".to_string(),
));
}
} else {
return Err(Error::BadRequest(
"No matching s3 resource found for the given file key".to_string(),
));
}
} else {
// no file key => requires unnamed upload policy => allow workspace resource and file_key_regex is empty
let has_unnamed_policy = s3_inputs.iter().any(|s3_input| {
s3_input.allow_workspace_resource && s3_input.file_key_regex.is_empty()
});
// no file key => requires unnamed upload policy => allow workspace resource and file_key_regex is empty
let has_unnamed_policy = s3_inputs.iter().any(|s3_input| {
s3_input.allow_workspace_resource && s3_input.file_key_regex.is_empty()
});
if !has_unnamed_policy {
return Err(Error::BadRequest(
"no policy found for unnamed s3 file upload".to_string(),
));
}
if !has_unnamed_policy {
return Err(Error::BadRequest(
"no policy found for unnamed s3 file upload".to_string(),
));
}
// for now, we place all files into `windmill_uploads` folder with a random name
// TODO: make the folder configurable via the workspace settings
let file_key = get_random_file_name(query.file_extension);
let db_with_opt_authed =
DbWithOptAuthed::from_authed(&on_behalf_authed, db.clone(), None);
let (_, s3_resource_opt) = get_workspace_s3_resource_and_check_paths(
&db_with_opt_authed,
&w_id,
None,
&[(&file_key, S3Permission::WRITE)],
)
.await?;
// for now, we place all files into `windmill_uploads` folder with a random name
// TODO: make the folder configurable via the workspace settings
let file_key = get_random_file_name(query.file_extension);
(s3_resource_opt, file_key, email, permissioned_as, username)
}
} else {
// backward compatibility (no policy)
// if no policy but logged in, use the user's auth to get the s3 resource
if let Some(authed) = opt_authed {
let file_key = query
.file_key
.unwrap_or_else(|| get_random_file_name(query.file_extension));
let (on_behalf_of_email, permissioned_as, username) = (
authed.email.clone(),
username_to_permissioned_as(&authed.username),
authed.display_username().to_string(),
);
if let Some(ref s3_resource_path) = query.s3_resource_path {
let db_with_opt_authed =
DbWithOptAuthed::from_authed(&authed, db.clone(), Some(user_db.clone()));
(
Some(
get_s3_resource(&db_with_opt_authed, &w_id, s3_resource_path, None, None)
.await?,
),
file_key,
on_behalf_of_email,
permissioned_as,
username,
)
} else {
let db_with_opt_authed = DbWithOptAuthed::from_authed(&authed, db.clone(), None);
let (_, s3_resource) = get_workspace_s3_resource_and_check_paths(
&db_with_opt_authed,
let (_, s3_resource_opt) = get_workspace_s3_resource_and_check_paths(
&on_behalf_authed,
&db,
None,
"",
&w_id,
None,
&[(&file_key, S3Permission::WRITE)],
)
.await?;
(
s3_resource,
file_key,
on_behalf_of_email,
permissioned_as,
username,
)
(s3_resource_opt, file_key, email, permissioned_as, username)
}
} else {
return Err(Error::BadRequest("Missing s3 policy".to_string()));
}
};
// backward compatibility (no policy)
// if no policy but logged in, use the user's auth to get the s3 resource
if let Some(authed) = opt_authed {
let file_key = query
.file_key
.unwrap_or_else(|| get_random_file_name(query.file_extension));
let (on_behalf_of_email, permissioned_as, username) = (
authed.email.clone(),
username_to_permissioned_as(&authed.username),
authed.display_username().to_string(),
);
if let Some(ref s3_resource_path) = query.s3_resource_path {
(
Some(
get_s3_resource(
&authed,
&db,
Some(user_db),
"",
&w_id,
s3_resource_path,
None,
None,
)
.await?,
),
file_key,
on_behalf_of_email,
permissioned_as,
username,
)
} else {
let (_, s3_resource) = get_workspace_s3_resource_and_check_paths(
&authed,
&db,
None,
"",
&w_id,
None,
&[(&file_key, S3Permission::WRITE)],
)
.await?;
(
s3_resource,
file_key,
on_behalf_of_email,
permissioned_as,
username,
)
}
} else {
return Err(Error::BadRequest("Missing s3 policy".to_string()));
}
};
let s3_resource = s3_resource_opt.ok_or(Error::internal_err(
"No files storage resource defined at the workspace level".to_string(),
@@ -2396,10 +2434,11 @@ async fn delete_s3_file_from_app(
.await?;
let s3_resource = if let Some(s3_resource_path) = s3_resource_path {
let db_with_opt_authed =
DbWithOptAuthed::from_authed(&on_behalf_authed, db.clone(), Some(user_db.clone()));
get_s3_resource(
&db_with_opt_authed,
&on_behalf_authed,
&db,
Some(user_db),
"",
&w_id,
s3_resource_path.as_str(),
None,
@@ -2407,9 +2446,11 @@ async fn delete_s3_file_from_app(
)
.await?
} else {
let db_with_opt_authed = DbWithOptAuthed::from_authed(&on_behalf_authed, db.clone(), None);
let (_, s3_resource) = get_workspace_s3_resource_and_check_paths(
&db_with_opt_authed,
&on_behalf_authed,
&db,
None,
"",
&w_id,
None,
&[(&path.to_string(), S3Permission::DELETE)],
@@ -2503,23 +2544,7 @@ async fn check_if_allowed_to_access_s3_file_from_app(
// otherwise, if logged in, allow any file (TODO: change that when we implement better s3 policy)
if file_query.sig.is_some() {
#[cfg(feature = "private")]
{
crate::s3_proxy_ee::validate_s3_signature(
&file_query.s3,
&file_query.sig,
&file_query.exp,
&file_query.storage,
w_id,
&db,
)
.await?;
Ok(())
}
#[cfg(not(feature = "private"))]
return Err(Error::InternalErr(
"Internal error: signature validation is not supported in open source mode".to_string(),
));
validate_s3_signature(file_query, w_id, &db).await
} else if opt_authed.is_some() {
Ok(())
} else {
@@ -2563,7 +2588,6 @@ struct AppS3FileQuery {
s3: String,
storage: Option<String>,
sig: Option<String>,
#[cfg(feature = "private")]
exp: Option<String>,
}
@@ -2610,6 +2634,7 @@ async fn download_s3_file_from_app(
on_behalf_authed,
&db,
None,
"",
&w_id,
DownloadFileQuery {
file_key: query.file_query.s3,
@@ -2696,14 +2721,14 @@ async fn build_args(
key.and_then(|x| x.clone().strip_prefix("$res:").map(|x| x.to_string()))
{
if let Some(authed) = authed {
let db_with_opt_authed =
DbWithOptAuthed::from_authed(authed, db.clone(), Some(user_db.clone()));
let res = get_resource_value_interpolated_internal(
&db_with_opt_authed,
authed,
Some(user_db.clone()),
db,
w_id,
&path,
None,
None,
"",
false,
)
.await?;

View File

@@ -94,7 +94,7 @@ impl RawWebhookArgs {
use object_store::{Attribute, Attributes};
use windmill_common::s3_helpers::build_object_store_client;
let (_, s3_resource) = get_workspace_s3_resource(authed, db, None, w_id, None).await?;
let (_, s3_resource) = get_workspace_s3_resource(authed, db, None, "", w_id, None).await?;
if let Some(s3_resource) = s3_resource {
let s3_client = build_object_store_client(&s3_resource).await?;

View File

@@ -39,9 +39,7 @@ async fn list_assets(
)
)) as "list!: _"
FROM asset
LEFT JOIN resource ON asset.kind = 'resource'
AND array_to_string((string_to_array(asset.path, '/'))[1:3], '/') = resource.path -- With specific table, asset path can be e.g u/diego/pg_db/table_name
AND resource.workspace_id = $1
LEFT JOIN resource ON asset.kind = 'resource' AND asset.path = resource.path AND resource.workspace_id = $1
WHERE asset.workspace_id = $1
AND (asset.kind <> 'resource' OR resource.path IS NOT NULL)
AND (asset.usage_kind <> 'flow' OR asset.usage_path = ANY(SELECT path FROM flow WHERE workspace_id = $1))

View File

@@ -1,73 +1,9 @@
use axum::body::Bytes;
use aws_sigv4::http_request::{sign, SignableBody, SignableRequest, SigningSettings};
use aws_sigv4::sign::v4;
use bytes;
use futures;
use std::time::SystemTime;
use uuid;
use windmill_common::error::{Error, Result};
/// Sign a request for AWS Bedrock using SigV4
///
/// Returns a vector of (header_name, header_value) tuples to add to the request
pub fn sign_bedrock_request(
method: &str,
uri: &str,
body: &[u8],
access_key_id: &str,
secret_access_key: &str,
region: &str,
) -> Result<Vec<(String, String)>> {
let identity = aws_credential_types::Credentials::new(
access_key_id,
secret_access_key,
None, // session token
None, // expiration
"windmill",
)
.into();
let signing_settings = SigningSettings::default();
let signing_params = v4::SigningParams::builder()
.identity(&identity)
.region(region)
.name("bedrock")
.time(SystemTime::now())
.settings(signing_settings)
.build()
.map_err(|e| Error::internal_err(format!("Failed to build signing params: {}", e)))?;
// Parse the URI to extract path and query
let parsed_uri: http::Uri = uri
.parse()
.map_err(|e| Error::internal_err(format!("Failed to parse URI: {}", e)))?;
let path_and_query = parsed_uri
.path_and_query()
.map(|pq| pq.as_str())
.unwrap_or("/");
let signable_request = SignableRequest::new(
method,
path_and_query,
std::iter::once(("host", parsed_uri.host().unwrap_or(""))),
SignableBody::Bytes(body),
)
.map_err(|e| Error::internal_err(format!("Failed to create signable request: {}", e)))?;
let (signing_instructions, _signature) = sign(signable_request, &signing_params.into())
.map_err(|e| Error::internal_err(format!("Failed to sign request: {}", e)))?
.into_parts();
// Collect the headers to add
let mut headers = Vec::new();
for (name, value) in signing_instructions.headers() {
headers.push((name.to_string(), value.to_string()));
}
Ok(headers)
}
/// 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)> {

View File

@@ -14,7 +14,7 @@ use sqlx::{
};
use tokio::task::JoinHandle;
use windmill_audit::audit_oss::AuditAuthorable;
use windmill_audit::audit_oss::{AuditAuthor, AuditAuthorable};
pub use windmill_common::db::DB;
use windmill_common::{
db::{Authable, Authed, AuthedRef},
@@ -309,6 +309,17 @@ impl From<Authed> for ApiAuthed {
}
}
impl From<&ApiAuthed> for AuditAuthor {
fn from(value: &ApiAuthed) -> Self {
Self {
email: value.email.clone(),
username: value.username.clone(),
username_override: value.username_override.clone(),
token_prefix: value.token_prefix.clone(),
}
}
}
impl ApiAuthed {
pub fn display_username(&self) -> &str {
self.username_override.as_ref().unwrap_or(&self.username)

View File

@@ -32,7 +32,6 @@ use sql_builder::prelude::*;
use sqlx::{FromRow, Postgres, Transaction};
use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_common::runnable_settings::RunnableSettingsTrait;
use windmill_common::utils::{query_elems_from_hub, WarnAfterExt};
use windmill_common::worker::{to_raw_value, CLOUD_HOSTED, MIN_VERSION_SUPPORTS_DEBOUNCING};
use windmill_common::HUB_BASE_URL;
@@ -1602,9 +1601,7 @@ mod tests {
ConstantDelay, ExponentialDelay, FlowModule, FlowModuleValue, FlowValue,
InputTransform, Retry, StopAfterIf,
},
runnable_settings::{
ConcurrencySettings, ConcurrencySettingsWithCustom, DebouncingSettings,
},
jobs::{ConcurrencySettings, ConcurrencySettingsWithCustom, DebouncingSettings},
scripts,
};

View File

@@ -1,68 +0,0 @@
/*
* 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 crate::db::ApiAuthed;
use axum::{
extract::{Extension, Path, Query},
routing::get,
Router,
};
use windmill_common::{
db::UserDB,
error::JsonResult,
utils::{paginate, Pagination},
};
use serde::Serialize;
use sqlx::FromRow;
pub fn workspaced_service() -> Router {
Router::new().route("/get/:name", get(get_folder_permission_history))
}
#[derive(Serialize, FromRow)]
pub struct FolderPermissionChange {
pub id: i64,
pub changed_by: String,
pub changed_at: chrono::DateTime<chrono::Utc>,
pub change_type: String,
pub affected: Option<String>,
}
async fn get_folder_permission_history(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, name)): Path<(String, String)>,
Query(pagination): Query<Pagination>,
) -> JsonResult<Vec<FolderPermissionChange>> {
// Check if user is owner of the folder (before starting transaction for performance)
crate::folders::require_is_owner(&authed, &name)?;
let mut tx = user_db.begin(&authed).await?;
let (per_page, offset) = paginate(pagination);
let history = sqlx::query_as!(
FolderPermissionChange,
"SELECT id, changed_by, changed_at, change_type, affected
FROM folder_permission_history
WHERE workspace_id = $1 AND folder_name = $2
ORDER BY id DESC
LIMIT $3 OFFSET $4",
w_id,
name,
per_page as i64,
offset as i64
)
.fetch_all(&mut *tx)
.await?;
tx.commit().await?;
Ok(axum::Json(history))
}

View File

@@ -261,17 +261,6 @@ async fn create_folder(
None,
)
.await?;
log_folder_permission_change(
&mut *tx,
&w_id,
&ng.name,
&authed.username,
"create",
None,
)
.await?;
tx.commit().await?;
handle_deployment_metadata(
@@ -343,10 +332,6 @@ async fn update_folder(
sqlb.set("edited_at", "now()");
// Track whether permission-related fields are being updated
let owners_changed = ng.owners.is_some();
let extra_perms_changed = ng.extra_perms.is_some();
if !authed.is_admin {
let prefixed_username = format!("u/{}", authed.username);
if ng.owners.as_ref().is_some_and(|x| {
@@ -433,31 +418,6 @@ async fn update_folder(
None,
)
.await?;
// Log permission changes if owners or extra_perms were updated
if owners_changed {
log_folder_permission_change(
&mut *tx,
&w_id,
&name,
&authed.username,
"update_owners",
None,
)
.await?;
}
if extra_perms_changed {
log_folder_permission_change(
&mut *tx,
&w_id,
&name,
&authed.username,
"update_acl",
None,
)
.await?;
}
tx.commit().await?;
handle_deployment_metadata(
@@ -715,17 +675,6 @@ async fn add_owner(
Some([("owner", owner.as_str())].into()),
)
.await?;
log_folder_permission_change(
&mut *tx,
&w_id,
&name,
&authed.username,
"grant_admin",
Some(&owner),
)
.await?;
tx.commit().await?;
webhook.send_message(
@@ -748,8 +697,8 @@ async fn remove_owner(
not_found_if_none(get_folderopt(&mut tx, &w_id, &name).await?, "Folder", &name)?;
require_is_owner(&authed, &name)?;
let folder = sqlx::query!(
"UPDATE folder SET owners = array_remove(owners, $1::varchar) WHERE name = $2 AND workspace_id = $3 AND $1 = ANY(owners) RETURNING name",
sqlx::query!(
"UPDATE folder SET owners = array_remove(owners, $1::varchar) WHERE name = $2 AND workspace_id = $3 RETURNING name",
owner,
&name,
&w_id,
@@ -757,29 +706,18 @@ async fn remove_owner(
.fetch_optional(&mut *tx)
.await?;
if folder.is_none() && write.is_none() {
return Ok(format!("Owner {} is already not a member of folder {}", owner, name));
}
if let Some(write) = write {
let old_write = sqlx::query_scalar::<_, Option<bool>>(&format!(
"UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \
true) FROM (SELECT (extra_perms->>'{owner}')::boolean as old_val FROM folder WHERE name = $2 AND workspace_id = $3) old \
WHERE name = $2 AND workspace_id = $3 RETURNING old.old_val"
))
sqlx::query(&format!(
"UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \
true) WHERE name = $2 AND workspace_id = $3 RETURNING extra_perms"
))
.bind(write)
.bind(&name)
.bind(&w_id)
.fetch_optional(&mut *tx)
.await?
.flatten();
if folder.is_none() && old_write.is_none_or(|ow| ow == write) {
return Ok(format!("Owner {} is already not a member of folder {} and write permission was already {}", owner, name, write));
}
.await?;
}
audit_log(
&mut *tx,
&authed,
@@ -790,14 +728,6 @@ async fn remove_owner(
Some([("owner", owner.as_str())].into()),
)
.await?;
let change_type = match write {
Some(true) => "grant_writer_only",
Some(false) => "grant_viewer_only",
None => "revoke_all",
};
log_folder_permission_change(&mut *tx, &w_id, &name, &authed.username, change_type, Some(&owner)).await?;
tx.commit().await?;
webhook.send_message(
@@ -807,26 +737,3 @@ async fn remove_owner(
Ok(format!("Removed {} to folder {}", owner, name))
}
pub async fn log_folder_permission_change<'c, E: sqlx::Executor<'c, Database = Postgres>>(
db: E,
workspace_id: &str,
folder_name: &str,
changed_by: &str,
change_type: &str,
affected: Option<&str>,
) -> Result<()> {
sqlx::query!(
"INSERT INTO folder_permission_history
(workspace_id, folder_name, changed_by, change_type, affected)
VALUES ($1, $2, $3, $4, $5)",
workspace_id,
folder_name,
changed_by,
change_type,
affected
)
.execute(db)
.await?;
Ok(())
}

View File

@@ -116,7 +116,7 @@ async fn add_granular_acl(
"UPDATE {kind} SET extra_perms = jsonb_set(extra_perms, $1, to_jsonb($2), \
true) WHERE {identifier} = $3 AND workspace_id = $4 RETURNING extra_perms"
))
.bind(vec![owner.clone()])
.bind(vec![owner])
.bind(write.unwrap_or(false))
.bind(path)
.bind(&w_id)
@@ -124,39 +124,6 @@ async fn add_granular_acl(
.await?;
let _ = not_found_if_none(obj_o, &kind, &path)?;
if kind == "folder" {
let change_type = if write.unwrap_or(false) {
"grant_read"
} else {
"grant_write"
};
crate::folders::log_folder_permission_change(
&mut *tx,
&w_id,
path,
&authed.username,
change_type,
Some(&owner),
)
.await?;
} else if kind == "group_" {
let change_type = if write.unwrap_or(false) {
"grant_admin"
} else {
"grant_member_only"
};
crate::groups::log_group_permission_change(
&mut *tx,
&w_id,
path,
&authed.username,
change_type,
Some(&owner),
)
.await?;
}
tx.commit().await?;
match kind {
@@ -223,7 +190,7 @@ async fn remove_granular_acl(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(GranularAcl { owner, .. }): Json<GranularAcl>,
Json(GranularAcl { owner, write: _ }): Json<GranularAcl>,
) -> Result<String> {
let path = path.to_path();
@@ -258,104 +225,73 @@ async fn remove_granular_acl(
require_owner_of_path(&authed, path)?;
}
let obj_o = sqlx::query_scalar::<_, bool>(&format!(
"WITH old AS (
SELECT extra_perms->$1 as old_write FROM {kind}
WHERE {identifier} = $2 AND workspace_id = $3 AND extra_perms ? $1
)
UPDATE {kind} SET extra_perms = extra_perms - $1
WHERE {identifier} = $2 AND workspace_id = $3 AND extra_perms ? $1
RETURNING (SELECT old_write FROM old)::bool"
let obj_o = sqlx::query_scalar::<_, serde_json::Value>(&format!(
"UPDATE {kind} SET extra_perms = extra_perms - $1 WHERE {identifier} = $2 AND \
workspace_id = $3 RETURNING extra_perms"
))
.bind(&owner)
.bind(owner)
.bind(path)
.bind(&w_id)
.fetch_optional(&mut *tx)
.await?;
// Only log if something was actually removed (obj_o is Some)
if let Some(write) = obj_o {
// Log permission changes for folders and groups
if kind == "folder" {
let change_type = if write { "revoke_write" } else { "revoke_read" };
crate::folders::log_folder_permission_change(
&mut *tx,
&w_id,
path,
&authed.username,
change_type,
Some(&owner),
)
.await?;
} else if kind == "group_" {
crate::groups::log_group_permission_change(
&mut *tx,
&w_id,
path,
&authed.username,
"revoke_admin",
Some(&owner),
)
.await?;
}
let _ = not_found_if_none(obj_o, &kind, &path)?;
tx.commit().await?;
tx.commit().await?;
match kind {
"folder" => {
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::Folder { path: format!("f/{}", path) },
Some(format!("Folder '{}' changed permissions", path)),
true,
)
.await?
}
// "app" => {
// handle_deployment_metadata(
// &authed.email,
// &authed.username,
// &db,
// &w_id,
// DeployedObject::App { path: path.to_string(), parent_path: None, version: 0 },
// Some(format!("App '{}' changed permissions", path)),
// // true,
// )
// .await?
// }
// "script" => {
// handle_deployment_metadata(
// &authed.email,
// &authed.username,
// &db,
// &w_id,
// DeployedObject::Script {
// path: path.to_string(),
// parent_path: None,
// hash: ScriptHash(0),
// },
// Some(format!("Script '{}' changed permissions", path)),
// // true,
// )
// .await?
// }
// "flow" => {
// handle_deployment_metadata(
// &authed.email,
// &authed.username,
// &db,
// &w_id,
// DeployedObject::Flow { path: path.to_string(), parent_path: None },
// Some(format!("Flow '{}' changed permissions", path)),
// // true,
// )
// .await?
// }
_ => (),
match kind {
"folder" => {
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::Folder { path: format!("f/{}", path) },
Some(format!("Folder '{}' changed permissions", path)),
true,
)
.await?
}
// "app" => {
// handle_deployment_metadata(
// &authed.email,
// &authed.username,
// &db,
// &w_id,
// DeployedObject::App { path: path.to_string(), parent_path: None, version: 0 },
// Some(format!("App '{}' changed permissions", path)),
// // true,
// )
// .await?
// }
// "script" => {
// handle_deployment_metadata(
// &authed.email,
// &authed.username,
// &db,
// &w_id,
// DeployedObject::Script {
// path: path.to_string(),
// parent_path: None,
// hash: ScriptHash(0),
// },
// Some(format!("Script '{}' changed permissions", path)),
// // true,
// )
// .await?
// }
// "flow" => {
// handle_deployment_metadata(
// &authed.email,
// &authed.username,
// &db,
// &w_id,
// DeployedObject::Flow { path: path.to_string(), parent_path: None },
// Some(format!("Flow '{}' changed permissions", path)),
// // true,
// )
// .await?
// }
_ => (),
}
Ok("Successfully removed granular acl".to_string())

View File

@@ -1,65 +0,0 @@
/*
* 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 crate::db::ApiAuthed;
use axum::{
extract::{Extension, Path, Query},
routing::get,
Router,
};
use windmill_common::{
db::UserDB,
error::JsonResult,
utils::{paginate, Pagination},
};
use serde::Serialize;
use sqlx::FromRow;
pub fn workspaced_service() -> Router {
Router::new().route("/get/:name", get(get_group_permission_history))
}
#[derive(Serialize, FromRow)]
pub struct GroupPermissionChange {
pub id: i64,
pub changed_by: String,
pub changed_at: chrono::DateTime<chrono::Utc>,
pub change_type: String,
pub member_affected: Option<String>,
}
async fn get_group_permission_history(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, name)): Path<(String, String)>,
Query(pagination): Query<Pagination>,
) -> JsonResult<Vec<GroupPermissionChange>> {
let mut tx = user_db.begin(&authed).await?;
let (per_page, offset) = paginate(pagination);
let history = sqlx::query_as!(
GroupPermissionChange,
"SELECT id, changed_by, changed_at, change_type, member_affected
FROM group_permission_history
WHERE workspace_id = $1 AND group_name = $2
ORDER BY id DESC
LIMIT $3 OFFSET $4",
w_id,
name,
per_page as i64,
offset as i64
)
.fetch_all(&mut *tx)
.await?;
tx.commit().await?;
Ok(axum::Json(history))
}

View File

@@ -55,12 +55,6 @@ pub fn global_service() -> Router {
.route("/overwrite", post(overwrite_igroups))
}
/// Normalize group names: replace spaces with underscores and convert to lowercase
/// Used when manually creating groups and SCIM-managed groups
pub fn convert_name(name: &str) -> String {
name.replace(" ", "_").to_lowercase()
}
#[derive(FromRow, Serialize, Deserialize)]
pub struct Group {
pub workspace_id: String,
@@ -271,16 +265,6 @@ async fn create_group(
)
.await?;
log_group_permission_change(
&mut *tx,
&w_id,
&ng.name,
&authed.username,
"create",
None,
)
.await?;
tx.commit().await?;
handle_deployment_metadata(
@@ -307,12 +291,10 @@ async fn create_igroup(
require_super_admin(&db, &authed.email).await?;
let mut tx = db.begin().await?;
let normalized_name = convert_name(&ng.name);
let id = Uuid::new_v4().to_string();
sqlx::query!(
"INSERT INTO instance_group (name, summary, id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
normalized_name,
ng.name,
ng.summary,
id,
)
@@ -325,13 +307,13 @@ async fn create_igroup(
"igroup.create",
ActionKind::Create,
"global",
Some(&normalized_name),
Some(&ng.name.to_string()),
None,
)
.await?;
tx.commit().await?;
Ok(format!("Created group {}", normalized_name))
Ok(format!("Created group {}", ng.name))
}
#[derive(Deserialize)]
@@ -552,17 +534,6 @@ async fn update_group(
None,
)
.await?;
log_group_permission_change(
&mut *tx,
&w_id,
&name,
&authed.username,
"update_summary",
None,
)
.await?;
tx.commit().await?;
handle_deployment_metadata(
@@ -593,7 +564,7 @@ async fn add_user(
not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?;
let result = sqlx::query!(
sqlx::query!(
"INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
&w_id,
user_username,
@@ -602,10 +573,6 @@ async fn add_user(
.execute(&mut *tx)
.await?;
if result.rows_affected() == 0 {
return Ok(format!("{} is already a member of group {}", user_username, name));
}
audit_log(
&mut *tx,
&authed,
@@ -616,17 +583,6 @@ async fn add_user(
Some([("user", user_username.as_str())].into()),
)
.await?;
log_group_permission_change(
&mut *tx,
&w_id,
&name,
&authed.username,
"add_member",
Some(&user_username),
)
.await?;
tx.commit().await?;
handle_deployment_metadata(
@@ -887,16 +843,6 @@ async fn remove_user(
)
.await?;
log_group_permission_change(
&mut *tx,
&w_id,
&name,
&authed.username,
"remove_member",
Some(&user_username),
)
.await?;
tx.commit().await?;
handle_deployment_metadata(
@@ -1027,26 +973,3 @@ async fn overwrite_igroups() -> JsonResult<String> {
"This feature is only available in the enterprise version".to_string(),
))
}
pub async fn log_group_permission_change<'c, E: sqlx::Executor<'c, Database = Postgres>>(
db: E,
workspace_id: &str,
group_name: &str,
changed_by: &str,
change_type: &str,
member_affected: Option<&str>,
) -> Result<()> {
sqlx::query!(
"INSERT INTO group_permission_history
(workspace_id, group_name, changed_by, change_type, member_affected)
VALUES ($1, $2, $3, $4, $5)",
workspace_id,
group_name,
changed_by,
change_type,
member_affected
)
.execute(db)
.await?;
Ok(())
}

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