Merge remote-tracking branch 'origin/main' into win-1084-more-ansible-improvements

This commit is contained in:
wendrul
2025-04-23 21:32:29 +02:00
251 changed files with 8954 additions and 47152 deletions
+1 -1
View File
@@ -10,4 +10,4 @@ WM_IMAGE=ghcr.io/windmill-labs/windmill:main
# To rotate logs, set the following variables:
#LOG_MAX_SIZE=10m
#LOG_MAX_FILE=3
#LOG_MAX_FILE=3
+1 -1
View File
@@ -24,4 +24,4 @@ sed -i -e "/^wmill_pg =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
sed -i -zE "s/name = \"windmill\"\nversion = \"[^\"]*\"\\n(.*)/name = \"windmill\"\nversion = \"$VERSION\"\\n\\1/" ${root_dirpath}/backend/Cargo.lock
cd ${root_dirpath}/frontend && npm i --package-lock-only
cd ${root_dirpath}/frontend && npm i --package-lock-only --ignore-scripts
+3 -4
View File
@@ -64,7 +64,7 @@ jobs:
platforms: linux/amd64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages,deno_core
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
@@ -81,7 +81,7 @@ jobs:
platforms: linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages,deno_core
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
@@ -111,8 +111,7 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: RHEL9-amd64 build
path:
${{ steps.extract-ee-amd64.outputs.destination
path: ${{ steps.extract-ee-amd64.outputs.destination
}}/windmill-ee-amd64-rhel9
# - uses: actions/upload-artifact@v4
+1 -1
View File
@@ -67,7 +67,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=embedding,parquet,openidconnect,license,http_trigger,zip,oauth2,postgres_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages
features=embedding,parquet,openidconnect,license,http_trigger,zip,oauth2,postgres_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages,deno_core
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
${{ steps.meta-public.outputs.tags }}
+10 -16
View File
@@ -1,10 +1,8 @@
env:
REGISTRY: ghcr.io
IMAGE_NAME:
${{ github.event_name != 'pull_request' && github.event_name !=
IMAGE_NAME: ${{ github.event_name != 'pull_request' && github.event_name !=
'workflow_dispatch' && github.repository || 'windmill-labs/windmill-test' }}
DEV_SHA:
${{ github.event_name != 'pull_request' && github.event_name !=
DEV_SHA: ${{ github.event_name != 'pull_request' && github.event_name !=
'workflow_dispatch' && 'dev' || github.event.inputs.tag || github.sha }}
name: Build windmill:main
on:
@@ -40,8 +38,7 @@ permissions: write-all
jobs:
build:
runs-on: ubicloud
if:
(github.event_name != 'workflow_dispatch') || (github.event.inputs &&
if: (github.event_name != 'workflow_dispatch') || (github.event.inputs &&
!github.event.inputs.ee)
steps:
- uses: actions/checkout@v4
@@ -95,7 +92,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=embedding,parquet,openidconnect,jemalloc,license,http_trigger,zip,oauth2,dind,postgres_trigger,mqtt_trigger,websocket,smtp,static_frontend,agent_worker_server,all_languages
features=embedding,parquet,openidconnect,jemalloc,license,http_trigger,zip,oauth2,dind,postgres_trigger,mqtt_trigger,websocket,smtp,static_frontend,agent_worker_server,all_languages,deno_core
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEV_SHA }}
${{ steps.meta-public.outputs.tags }}
@@ -157,7 +154,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,agent_worker_server,tantivy,license,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,otel,dind,postgres_trigger,mqtt_trigger,gcp_trigger,websocket,smtp,static_frontend,all_languages
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,agent_worker_server,tantivy,license,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,otel,dind,postgres_trigger,mqtt_trigger,gcp_trigger,websocket,smtp,static_frontend,all_languages,deno_core
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:${{ env.DEV_SHA }}
${{ steps.meta-ee-public.outputs.tags }}
@@ -315,7 +312,7 @@ jobs:
needs: [run_integration_test, build]
if:
github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' ||
startsWith(github.ref, 'refs/tags/v')) && (github.event_name != 'workflow_dispatch')
startsWith(github.ref, 'refs/tags/v')) && (github.event_name != 'workflow_dispatch')
steps:
- uses: actions/checkout@v4
with:
@@ -355,7 +352,7 @@ jobs:
verify_ee_image_vulnerabilities:
runs-on: ubicloud
needs: [tag_latest_ee]
if: startsWith(github.ref, 'refs/tags/v') && (github.event_name != 'workflow_dispatch')
if: startsWith(github.ref, 'refs/tags/v') && (github.event_name != 'workflow_dispatch')
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -397,8 +394,7 @@ jobs:
build_ee_nsjail:
needs: [build_ee]
runs-on: ubicloud
if:
(github.event_name != 'pull_request') && ((github.event_name != 'workflow_dispatch') || (github.event.inputs.ee || github.event.inputs.nsjail))
if: (github.event_name != 'pull_request') && ((github.event_name != 'workflow_dispatch') || (github.event.inputs.ee || github.event.inputs.nsjail))
steps:
- uses: actions/checkout@v4
@@ -437,7 +433,7 @@ jobs:
run: |
sed -i 's|FROM ghcr.io/windmill-labs/windmill-ee:dev|FROM ghcr.io/${{ env.IMAGE_NAME }}-ee:${{ env.DEV_SHA }}|' ./docker/DockerfileNsjail
cat ./docker/DockerfileNsjail | grep "FROM"
- name: Build and push publicly ee
uses: depot/build-push-action@v1
with:
@@ -451,12 +447,10 @@ jobs:
${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License
publish_ecr_s3:
needs: [build_ee_nsjail]
runs-on: ubicloud-standard-2-arm
if:
(github.event_name != 'pull_request') && (github.event_name !=
if: (github.event_name != 'pull_request') && (github.event_name !=
'workflow_dispatch')
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
+88
View File
@@ -1,5 +1,93 @@
# Changelog
## [1.483.2](https://github.com/windmill-labs/windmill/compare/v1.483.1...v1.483.2) (2025-04-23)
### Bug Fixes
* batch reruns query missing workspace_id check in subquery ([#5652](https://github.com/windmill-labs/windmill/issues/5652)) ([444a6ab](https://github.com/windmill-labs/windmill/commit/444a6abad670114c52e44f3606bf6fefc5d3fd98))
* **frontend:** fix validity check ([#5654](https://github.com/windmill-labs/windmill/issues/5654)) ([c41c1eb](https://github.com/windmill-labs/windmill/commit/c41c1eb587bf22364f1202310a0c64b6040ab968))
* improve MySQL datetime parser timezone handling (WIN-1155) ([#5645](https://github.com/windmill-labs/windmill/issues/5645)) ([5bca8f6](https://github.com/windmill-labs/windmill/commit/5bca8f60e970cc67839edb5dc491685f36cf0499))
* track relative imports in python and ts even if lockfile is provided ([e316dbd](https://github.com/windmill-labs/windmill/commit/e316dbd9bdd5c59e9aaba6a4472bb7d832834e84))
## [1.483.1](https://github.com/windmill-labs/windmill/compare/v1.483.0...v1.483.1) (2025-04-19)
### Bug Fixes
* pin libxml to 0.3.3 ([e5595e4](https://github.com/windmill-labs/windmill/commit/e5595e41b5c87704d814eff95bc00b82195728ba))
## [1.483.0](https://github.com/windmill-labs/windmill/compare/v1.482.1...v1.483.0) (2025-04-19)
### Features
* handle different aws auth resource type ([#5637](https://github.com/windmill-labs/windmill/issues/5637)) ([5b123b0](https://github.com/windmill-labs/windmill/commit/5b123b01a1318208450789b5bcade447a0b331c7))
* oidc support for sqs trigger ([#5614](https://github.com/windmill-labs/windmill/issues/5614)) ([34b307b](https://github.com/windmill-labs/windmill/commit/34b307b2be1f6cf92a81694325f4c333bdd7b055))
### Bug Fixes
* fix click outside popover fullscreen ([#5631](https://github.com/windmill-labs/windmill/issues/5631)) ([0811457](https://github.com/windmill-labs/windmill/commit/081145726a5d4ab81510a7637126a036823a1565))
* improve flow editor step switch performance ([58fa4c8](https://github.com/windmill-labs/windmill/commit/58fa4c80062a5704bbd13ddda1b2f00c7c9e40dd))
* linter in early stop doesn't include flow_input ([#5638](https://github.com/windmill-labs/windmill/issues/5638)) ([6a9bdfd](https://github.com/windmill-labs/windmill/commit/6a9bdfd3bd52ff802b6a71c3ae9504bfe7d0421f))
* output picker output opening doesn't change id ([#5641](https://github.com/windmill-labs/windmill/issues/5641)) ([64c72b6](https://github.com/windmill-labs/windmill/commit/64c72b6fce669e47f04dc620750840857cbe66cf))
## [1.482.1](https://github.com/windmill-labs/windmill/compare/v1.482.0...v1.482.1) (2025-04-16)
### Bug Fixes
* flow editor workspace script test use actual workspace script hash ([24e893b](https://github.com/windmill-labs/windmill/commit/24e893b8c50fafdb41f4b6e1777cb34aceafc466))
* **frontend:** postgres remove selectedTable ([#5386](https://github.com/windmill-labs/windmill/issues/5386)) ([bd7c6a2](https://github.com/windmill-labs/windmill/commit/bd7c6a2a46047de5fe89753decdfdf1f4851ee3f))
* **openapi:** fix openapi def of batch re-run jobs ([cb8731e](https://github.com/windmill-labs/windmill/commit/cb8731e7e37fb6cd052f5dae6fdce46e6ca2409c))
* show workspace color if superadmin and not in workspace + change workspace name when switching workspace ([#5625](https://github.com/windmill-labs/windmill/issues/5625)) ([cc4384f](https://github.com/windmill-labs/windmill/commit/cc4384f48cc89f883237a2082d854d69a7b5dc56))
## [1.482.0](https://github.com/windmill-labs/windmill/compare/v1.481.0...v1.482.0) (2025-04-15)
### Features
* add diff toggle to flow inline scripts ([#5550](https://github.com/windmill-labs/windmill/issues/5550)) ([b3ecde3](https://github.com/windmill-labs/windmill/commit/b3ecde3316252bcd7323de98149786349019ba7e))
* add gcp trigger ([#5501](https://github.com/windmill-labs/windmill/issues/5501)) ([6339775](https://github.com/windmill-labs/windmill/commit/63397754046eed41d32e28d4698db37b4c9b9710))
* add wildcards filter for worker/label/tags ([62f14d1](https://github.com/windmill-labs/windmill/commit/62f14d1cb95e3f1c7de85c46e1c6bb092247656c))
* add windmill context to autocomplete ([#5548](https://github.com/windmill-labs/windmill/issues/5548)) ([b47c151](https://github.com/windmill-labs/windmill/commit/b47c15165f93ca68a58f81cf2b86fc9467155482))
* agent workers v2 using http ([#5588](https://github.com/windmill-labs/windmill/issues/5588)) ([63fa499](https://github.com/windmill-labs/windmill/commit/63fa4990153f33434b49269922f7803d04e407cd))
* Batch re-run ([#5553](https://github.com/windmill-labs/windmill/issues/5553)) ([26b5ea5](https://github.com/windmill-labs/windmill/commit/26b5ea5023a100c57d077910c99a5e5703edf1c1))
* **frontend:** app editor code input component (monaco) ([#5566](https://github.com/windmill-labs/windmill/issues/5566)) ([177e16b](https://github.com/windmill-labs/windmill/commit/177e16bb18eed0d1c454b967aaa59547f61e8d26))
* handle sending selected lines to ai context ([#5527](https://github.com/windmill-labs/windmill/issues/5527)) ([5abdc3e](https://github.com/windmill-labs/windmill/commit/5abdc3e4403b5c604309bd99a24d7a2847a17b9b))
* Implement sending diff to ai ([#5510](https://github.com/windmill-labs/windmill/issues/5510)) ([e118d2c](https://github.com/windmill-labs/windmill/commit/e118d2cd5f9c641884a76229802a5228ef41f1a5))
* make azure a standalone AI provider ([#5558](https://github.com/windmill-labs/windmill/issues/5558)) ([2c5e58c](https://github.com/windmill-labs/windmill/commit/2c5e58cf1ab9225d516540b38d9e4dde482a3a7f))
* migrate to svelte5 + vite6 ([#4813](https://github.com/windmill-labs/windmill/issues/4813)) ([3c99b3f](https://github.com/windmill-labs/windmill/commit/3c99b3fdc7b78b1cdc7d8fb21d999296695f7889))
* **postgres-trigger:** postgres trigger fix circular dependencies and add remove associate resource ([#5606](https://github.com/windmill-labs/windmill/issues/5606)) ([1daeb2f](https://github.com/windmill-labs/windmill/commit/1daeb2f48f3026621b3ffc58e10f048d5911906c))
* **python:** per import requirement pin ([#5520](https://github.com/windmill-labs/windmill/issues/5520)) ([0b6d017](https://github.com/windmill-labs/windmill/commit/0b6d017fedc31e790a76cf29a1adaaf2a72acc61))
* signed s3 objects ([#5593](https://github.com/windmill-labs/windmill/issues/5593)) ([b9e8796](https://github.com/windmill-labs/windmill/commit/b9e879618bc223ce17effde8bb4c5d1df2ad6df5))
### Bug Fixes
* add support for ${} syntax without default in bash ([#5594](https://github.com/windmill-labs/windmill/issues/5594)) ([3950cfd](https://github.com/windmill-labs/windmill/commit/3950cfd7e3297d7f8ec56430d6462f6b67ecd3c2))
* app editor svelte 5 fixes ([#5570](https://github.com/windmill-labs/windmill/issues/5570)) ([b926076](https://github.com/windmill-labs/windmill/commit/b9260769883348ecd5aeb5684f527a8bf0073928))
* binding not working in nested array script arg ([#5585](https://github.com/windmill-labs/windmill/issues/5585)) ([f5d46d5](https://github.com/windmill-labs/windmill/commit/f5d46d5751bc875b7f4da1db06be40571ac55ab8))
* **cli:** properly handle enabled/disabled updates of schedules ([2629458](https://github.com/windmill-labs/windmill/commit/26294584d6c2ca02bbc4fc5f28cb8df6a5fb3790))
* **cli:** wmill-locks improvement ([8d062c4](https://github.com/windmill-labs/windmill/commit/8d062c47ecd9e84a81140d5c59814da9217dd434))
* Dynamic select does not work with tag //native ([#5576](https://github.com/windmill-labs/windmill/issues/5576)) ([1f3e7d9](https://github.com/windmill-labs/windmill/commit/1f3e7d9029051832db6ab1755b3cad38176a9e96)), closes [#5490](https://github.com/windmill-labs/windmill/issues/5490)
* fix list jobs by tag ([0c3cb37](https://github.com/windmill-labs/windmill/commit/0c3cb3700a3fb9b69e396487bd7491dbbd8861c0))
* flow editor svelte 5 issues ([#5567](https://github.com/windmill-labs/windmill/issues/5567)) ([4f6be6e](https://github.com/windmill-labs/windmill/commit/4f6be6ed340e26bf1ed95398a9dc9f1eb41b33dd))
* freeze when clicking script history diff button ([#5581](https://github.com/windmill-labs/windmill/issues/5581)) ([07094b6](https://github.com/windmill-labs/windmill/commit/07094b6aa21f10688b138d2a81d4fd5833f003fc))
* **frontend:** app builder - force json configuration in rich result ([#5565](https://github.com/windmill-labs/windmill/issues/5565)) ([6fae3a5](https://github.com/windmill-labs/windmill/commit/6fae3a566be06dae88ece8ec23f5723cd8f3f2b9))
* **frontend:** load all step jobs ([#5617](https://github.com/windmill-labs/windmill/issues/5617)) ([16bed59](https://github.com/windmill-labs/windmill/commit/16bed593dfd0b735a92d0928df5091547b98ae79))
* **frontend:** prevent deploy popover to show if deploy dropdown is open ([#5542](https://github.com/windmill-labs/windmill/issues/5542)) ([c2180c6](https://github.com/windmill-labs/windmill/commit/c2180c6eb34e14fe2292ff40aa6a99c627698d5e))
* **frontend:** proper each block binding + better app settings reactivity ([#5568](https://github.com/windmill-labs/windmill/issues/5568)) ([4c71af8](https://github.com/windmill-labs/windmill/commit/4c71af8a74627d0ba76917e0dac0ac9e5e984cca))
* improve app image picker UX ([#5589](https://github.com/windmill-labs/windmill/issues/5589)) ([f497a4b](https://github.com/windmill-labs/windmill/commit/f497a4bfae8d1bff097e0c2c9df8381a531dfeb9))
* legacy script gen model selection ([#5574](https://github.com/windmill-labs/windmill/issues/5574)) ([3507925](https://github.com/windmill-labs/windmill/commit/3507925624a43804a3be463b6f7913cea5821384))
* mssql ca_cert deserializing ([#5587](https://github.com/windmill-labs/windmill/issues/5587)) ([b4f8c88](https://github.com/windmill-labs/windmill/commit/b4f8c88c19bd4f844c3ecb53ececc340ee326b0e))
* number input in app multiselect yields NOT_NUMBER ([#5616](https://github.com/windmill-labs/windmill/issues/5616)) ([4aae6ab](https://github.com/windmill-labs/windmill/commit/4aae6ab634280adc1de9abd890100b7c12c89158))
* prevent invalid returned ai completion object errors ([#5564](https://github.com/windmill-labs/windmill/issues/5564)) ([9276c71](https://github.com/windmill-labs/windmill/commit/9276c717a21aaee3241845a9cc00d3fb6bce9eb9))
* Remaining svelte 5 bugs ([#5563](https://github.com/windmill-labs/windmill/issues/5563)) ([6e9ec63](https://github.com/windmill-labs/windmill/commit/6e9ec6323c265a747ef8696865297e6d47abb016))
* tenant id to never be undefined on teams ([#5572](https://github.com/windmill-labs/windmill/issues/5572)) ([102b58a](https://github.com/windmill-labs/windmill/commit/102b58a5f40dde22f15700d4b6c11eb7f3fbf4bb))
* validate saved module before passing to flow module editor ([#5580](https://github.com/windmill-labs/windmill/issues/5580)) ([2eb1a16](https://github.com/windmill-labs/windmill/commit/2eb1a161d15627b440195b65eec54998561f4ef6))
## [1.481.0](https://github.com/windmill-labs/windmill/compare/v1.480.1...v1.481.0) (2025-04-02)
+1
View File
@@ -25,6 +25,7 @@ FROM node:20-alpine as frontend
# install dependencies
WORKDIR /frontend
COPY ./frontend/package.json ./frontend/package-lock.json ./
COPY ./frontend/scripts/ ./scripts/
RUN npm ci
# Copy all local files into the image.
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM \n gcp_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM \n gcp_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
"describe": {
"columns": [
{
@@ -45,56 +45,71 @@
},
{
"ordinal": 6,
"name": "subscription_mode: _",
"type_info": {
"Custom": {
"name": "gcp_subscription_mode",
"kind": {
"Enum": [
"create_update",
"existing"
]
}
}
}
},
{
"ordinal": 7,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 10,
"ordinal": 11,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 11,
"ordinal": 12,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"ordinal": 13,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 14,
"ordinal": 15,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 15,
"ordinal": 16,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 16,
"ordinal": 17,
"name": "enabled",
"type_info": "Bool"
}
@@ -118,6 +133,7 @@
false,
false,
false,
false,
true,
true,
false,
@@ -125,5 +141,5 @@
false
]
},
"hash": "f4f6336fc671b00bed7835124892f7a4d3bbe673f7c48153819dab385a5cb357"
"hash": "1312b7fd622cc814a406c85dbbff61f003c29185c267642cfd898075ebda855d"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT jsonb_build_object(\n 'kind', jb.kind,\n 'script_path', jb.runnable_path,\n 'latest_schema', COALESCE(\n (SELECT DISTINCT ON (s.path) s.schema FROM script s WHERE s.workspace_id = $1 AND s.path = jb.runnable_path AND jb.kind = 'script' ORDER BY s.path, s.created_at DESC),\n (SELECT flow_version.schema FROM flow LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)] WHERE flow.workspace_id = $1 AND flow.path = jb.runnable_path AND jb.kind = 'flow')\n ),\n 'schemas', ARRAY(\n SELECT jsonb_build_object(\n 'script_hash', LPAD(TO_HEX(COALESCE(s.hash, f.id)), 16, '0'),\n 'job_ids', ARRAY_AGG(DISTINCT j.id),\n 'schema', (ARRAY_AGG(COALESCE(s.schema, f.schema)))[1]\n ) FROM v2_job j\n LEFT JOIN script s ON s.hash = j.runnable_id AND j.kind = 'script'\n LEFT JOIN flow_version f ON f.id = j.runnable_id AND j.kind = 'flow'\n WHERE j.id = ANY(ARRAY_AGG(jb.id))\n GROUP BY COALESCE(s.hash, f.id)\n )\n ) FROM v2_job jb\n WHERE (jb.kind = 'flow' OR jb.kind = 'script')\n AND jb.workspace_id = $1 AND jb.id = ANY($2)\n GROUP BY jb.kind, jb.runnable_path",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "jsonb_build_object",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
"UuidArray"
]
},
"nullable": [
null
]
},
"hash": "26761fbd7953416eb391de47b1694e0f4ab2bb96a6d838f1b1fdce4b58a8f5d4"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM\n gcp_trigger\n WHERE\n delivery_type != 'push'::DELIVERY_MODE AND\n enabled IS TRUE\n AND (last_server_ping IS NULL OR\n last_server_ping < now() - interval '15 seconds'\n )\n ",
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM\n gcp_trigger\n WHERE\n delivery_type != 'push'::DELIVERY_MODE AND\n enabled IS TRUE\n AND (last_server_ping IS NULL OR\n last_server_ping < now() - interval '15 seconds'\n )\n ",
"describe": {
"columns": [
{
@@ -45,56 +45,71 @@
},
{
"ordinal": 6,
"name": "subscription_mode: _",
"type_info": {
"Custom": {
"name": "gcp_subscription_mode",
"kind": {
"Enum": [
"create_update",
"existing"
]
}
}
}
},
{
"ordinal": 7,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 10,
"ordinal": 11,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 11,
"ordinal": 12,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"ordinal": 13,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 14,
"ordinal": 15,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 15,
"ordinal": 16,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 16,
"ordinal": 17,
"name": "enabled",
"type_info": "Bool"
}
@@ -115,6 +130,7 @@
false,
false,
false,
false,
true,
true,
false,
@@ -122,5 +138,5 @@
false
]
},
"hash": "15fbe481789a7817bf37415fb935f9ed537fd7d3b266d928af4d5d3dd8bb5c18"
"hash": "2dcff8b16da75740c362015b2293f578a21813f038d75184a5034c37d4daf36e"
}
@@ -1,79 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n j.id,\n j.kind AS \"kind: _\",\n COALESCE(s.path, f.path) AS \"script_path!\",\n COALESCE(s.hash, f.id) AS \"script_hash!: _\",\n COALESCE(jc.started_at, jq.scheduled_for, make_date(1970, 1, 1)) AS \"scheduled_for!: _\",\n args AS input\n FROM v2_job j\n LEFT JOIN script s ON j.runnable_id = s.hash AND j.kind = 'script'\n LEFT JOIN flow_version f ON j.runnable_id = f.id AND j.runnable_path = f.path AND j.kind = 'flow'\n LEFT JOIN v2_job_completed jc ON jc.id = j.id\n LEFT JOIN v2_job_queue jq ON jq.id = j.id\n WHERE j.id = ANY($1)\n AND j.workspace_id = $2\n AND COALESCE(s.hash, f.id) IS NOT NULL\n AND COALESCE(s.path, f.path) IS NOT NULL",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "kind: _",
"type_info": {
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
]
}
}
}
},
{
"ordinal": 2,
"name": "script_path!",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "script_hash!: _",
"type_info": "Int8"
},
{
"ordinal": 4,
"name": "scheduled_for!: _",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "input",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"UuidArray",
"Text"
]
},
"nullable": [
false,
false,
null,
null,
null,
true
]
},
"hash": "3376b42d273c2499f3517c7754f4cdb6eae2e71e56bc8e5215559af967c81bd8"
}
@@ -1,80 +1,95 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n aws_resource_path,\n message_attributes,\n queue_url,\n workspace_id,\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM \n sqs_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
"query": "\n SELECT\n aws_auth_resource_type AS \"aws_auth_resource_type: _\",\n aws_resource_path,\n message_attributes,\n queue_url,\n workspace_id,\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM \n sqs_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "aws_auth_resource_type: _",
"type_info": {
"Custom": {
"name": "aws_auth_resource_type",
"kind": {
"Enum": [
"oidc",
"credentials"
]
}
}
}
},
{
"ordinal": 1,
"name": "aws_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"ordinal": 2,
"name": "message_attributes",
"type_info": "TextArray"
},
{
"ordinal": 2,
"ordinal": 3,
"name": "queue_url",
"type_info": "Varchar"
},
{
"ordinal": 3,
"ordinal": 4,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 4,
"ordinal": 5,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 5,
"ordinal": 6,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"ordinal": 11,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 11,
"ordinal": 12,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"ordinal": 13,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 14,
"ordinal": 15,
"name": "enabled",
"type_info": "Bool"
}
@@ -86,6 +101,7 @@
]
},
"nullable": [
false,
false,
true,
false,
@@ -103,5 +119,5 @@
false
]
},
"hash": "2ef82fad8a6ccdc66228cfbce5393de351653ab9ac171fa0eea447c905440867"
"hash": "5237f9d3f57f4e799968a4a9630c984775d23c7a4dc1a7ad37528a46ab89d6bd"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO app_version\n (app_id, value, created_by)\n VALUES ($1, $2::text::json, $3) RETURNING id",
"query": "INSERT INTO app_version\n (app_id, value, created_by, raw_app)\n VALUES ($1, $2::text::json, $3, $4) RETURNING id",
"describe": {
"columns": [
{
@@ -13,12 +13,13 @@
"Left": [
"Int8",
"Text",
"Varchar"
"Varchar",
"Bool"
]
},
"nullable": [
false
]
},
"hash": "2a3992b5e9abcfbb032d10e142d98efa969dae26a7242eb7ac12593ed5421ef3"
"hash": "56264b88a9f428e79c6e531a0011e0dbed634f3b12dedaa5fee0362d670887f6"
}
@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT jsonb_build_object(\n 'kind', jb.kind,\n 'script_path', jb.runnable_path,\n 'latest_schema', COALESCE(\n (SELECT DISTINCT ON (s.path) s.schema FROM script s WHERE s.path = jb.runnable_path AND jb.kind = 'script' ORDER BY s.path, s.created_at DESC),\n (SELECT flow_version.schema FROM flow LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)] WHERE flow.path = jb.runnable_path AND jb.kind = 'flow')\n ),\n 'schemas', ARRAY(\n SELECT jsonb_build_object(\n 'script_hash', LPAD(TO_HEX(COALESCE(s.hash, f.id)), 16, '0'),\n 'job_ids', ARRAY_AGG(DISTINCT j.id),\n 'schema', ANY_VALUE(COALESCE(s.schema, f.schema))\n ) FROM v2_job j\n LEFT JOIN script s ON s.hash = j.runnable_id AND j.kind = 'script'\n LEFT JOIN flow_version f ON f.id = j.runnable_id AND j.kind = 'flow'\n WHERE j.id = ANY(ARRAY_AGG(jb.id))\n GROUP BY COALESCE(s.hash, f.id)\n )\n ) FROM v2_job jb\n WHERE (jb.kind = 'flow' OR jb.kind = 'script')\n AND jb.workspace_id = $1 AND jb.id = ANY($2)\n GROUP BY jb.kind, jb.runnable_path",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "jsonb_build_object",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
"UuidArray"
]
},
"nullable": [
null
]
},
"hash": "5ea1d8c87a17690942ca9e70e0b953e3b32a9a7a73784219d48f8e8932cdb0c8"
}
@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sqs_trigger (\n aws_auth_resource_type,\n aws_resource_path,\n queue_url,\n message_attributes,\n workspace_id, \n path, \n script_path, \n is_flow, \n email, \n enabled, \n edited_by\n ) \n VALUES (\n $1, \n $2, \n $3, \n $4, \n $5, \n $6, \n $7,\n $8,\n $9,\n $10,\n $11\n )",
"describe": {
"columns": [],
"parameters": {
"Left": [
{
"Custom": {
"name": "aws_auth_resource_type",
"kind": {
"Enum": [
"oidc",
"credentials"
]
}
}
},
"Varchar",
"Varchar",
"TextArray",
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Bool",
"Varchar"
]
},
"nullable": []
},
"hash": "776909d3452aaf9e0ed41a5ec314b7bfc5bc8b6a76c98bf4daf7d715d18c52e6"
}
@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n sqs_trigger \n SET \n aws_auth_resource_type = $1,\n aws_resource_path = $2,\n queue_url = $3,\n message_attributes = $4, \n is_flow = $5, \n edited_by = $6, \n email = $7,\n script_path = $8,\n path = $9,\n edited_at = now(), \n error = NULL,\n server_id = NULL\n WHERE \n workspace_id = $10 AND \n path = $11\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
{
"Custom": {
"name": "aws_auth_resource_type",
"kind": {
"Enum": [
"oidc",
"credentials"
]
}
}
},
"Varchar",
"Varchar",
"TextArray",
"Bool",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "8490024b96aa689d3c1bc5cbf94fcd8c5491732818404fee2397f00cccee0ad3"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id, path, url, script_path, is_flow, edited_by, email, edited_at, server_id, last_server_ping, extra_perms, error, enabled, filters as \"filters: _\", initial_messages as \"initial_messages: _\", url_runnable_args as \"url_runnable_args: _\", can_return_message FROM websocket_trigger\n WHERE workspace_id = $1",
"query": "\n SELECT \n workspace_id,\n path,\n url,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n filters AS \"filters: _\",\n initial_messages AS \"initial_messages: _\",\n url_runnable_args AS \"url_runnable_args: _\",\n can_return_message\n FROM \n websocket_trigger\n WHERE \n workspace_id = $1\n ",
"describe": {
"columns": [
{
@@ -114,5 +114,5 @@
false
]
},
"hash": "1aa8ead10f8d994f6685d266fcbd409b0fff43111d9600e64b2348401ed8929d"
"hash": "8eabeee5527da4aad3f46ad185015262b4304667449f3e1a71efd7477a39f7fc"
}
@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sqs_trigger (\n aws_resource_path,\n queue_url,\n message_attributes,\n workspace_id, \n path, \n script_path, \n is_flow, \n email, \n enabled, \n edited_by\n ) \n VALUES (\n $1, \n $2, \n $3, \n $4, \n $5, \n $6, \n $7,\n $8,\n $9,\n $10\n )",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"TextArray",
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Bool",
"Varchar"
]
},
"nullable": []
},
"hash": "9b9bc21023f10a0b4bb45879c7da8e119bffd2982b97fc900358506a0a14bbb8"
}
@@ -1,80 +1,95 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM sqs_trigger\n WHERE workspace_id = $1",
"query": "\n SELECT\n aws_auth_resource_type AS \"aws_auth_resource_type: _\",\n aws_resource_path,\n message_attributes,\n queue_url,\n workspace_id,\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM \n sqs_trigger\n WHERE \n workspace_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
"name": "aws_auth_resource_type: _",
"type_info": {
"Custom": {
"name": "aws_auth_resource_type",
"kind": {
"Enum": [
"oidc",
"credentials"
]
}
}
}
},
{
"ordinal": 1,
"name": "queue_url",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "aws_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"ordinal": 2,
"name": "message_attributes",
"type_info": "TextArray"
},
{
"ordinal": 4,
"name": "script_path",
"ordinal": 3,
"name": "queue_url",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 6,
"ordinal": 4,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 8,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 11,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 13,
"ordinal": 12,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 13,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 14,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 15,
"name": "enabled",
"type_info": "Bool"
}
@@ -85,7 +100,6 @@
]
},
"nullable": [
false,
false,
false,
true,
@@ -95,6 +109,8 @@
false,
false,
false,
false,
false,
true,
true,
true,
@@ -102,5 +118,5 @@
false
]
},
"hash": "2b6c13191484b0c664f35e2c811082b00c44fd4a7e98a11b714133674a7b6da7"
"hash": "a7df493316f632fc636e4c3c90bef4c98035b5dc808bb8379251c0b35d945ba0"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO app_version\n (app_id, value, created_by)\n VALUES ($1, $2::text::json, $3) RETURNING id",
"query": "INSERT INTO app_version\n (app_id, value, created_by, raw_app)\n VALUES ($1, $2::text::json, $3, $4) RETURNING id",
"describe": {
"columns": [
{
@@ -13,12 +13,13 @@
"Left": [
"Int8",
"Text",
"Varchar"
"Varchar",
"Bool"
]
},
"nullable": [
false
]
},
"hash": "83d18f4e1cda2c1867168551d855d5626e766b5469c6976c986bdafc8b9407c5"
"hash": "a9f6e5720f748418ae52cf0e31778b9bbef26008803f2dd2a043bdb091b2ff20"
}
@@ -1,73 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n j.kind AS \"kind!: JobKind\",\n j.runnable_id AS \"script_hash: _\",\n j.runnable_path AS script_path,\n COUNT(*) AS \"count!\",\n ANY_VALUE(COALESCE(f.schema, s.schema)) AS schema\n FROM v2_job j\n LEFT JOIN script s ON s.hash = j.runnable_id AND j.kind = 'script'\n LEFT JOIN flow_version f ON f.id = j.runnable_id AND f.path = j.runnable_path AND j.kind = 'flow'\n WHERE COALESCE(s.hash, f.id) IS NOT NULL\n AND COALESCE(s.path, f.path) IS NOT NULL\n AND j.workspace_id = $1 AND j.id = ANY($2)\n GROUP BY j.runnable_id, j.runnable_path, j.kind",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "kind!: JobKind",
"type_info": {
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
]
}
}
}
},
{
"ordinal": 1,
"name": "script_hash: _",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "count!",
"type_info": "Int8"
},
{
"ordinal": 4,
"name": "schema",
"type_info": "Json"
}
],
"parameters": {
"Left": [
"Text",
"UuidArray"
]
},
"nullable": [
false,
true,
true,
null,
null
]
},
"hash": "b61a00e6a8ca0a1d24e64fdc9223b077c957da8455ea747b0680923d8282425b"
}
@@ -1,80 +1,95 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n queue_url,\n aws_resource_path,\n message_attributes,\n workspace_id,\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM\n sqs_trigger\n WHERE\n enabled IS TRUE\n AND (last_server_ping IS NULL OR\n last_server_ping < now() - interval '15 seconds'\n )\n ",
"query": "\n SELECT\n aws_auth_resource_type AS \"aws_auth_resource_type: _\",\n queue_url,\n aws_resource_path,\n message_attributes,\n workspace_id,\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM\n sqs_trigger\n WHERE\n enabled IS TRUE\n AND (last_server_ping IS NULL OR\n last_server_ping < now() - interval '15 seconds'\n )\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "aws_auth_resource_type: _",
"type_info": {
"Custom": {
"name": "aws_auth_resource_type",
"kind": {
"Enum": [
"oidc",
"credentials"
]
}
}
}
},
{
"ordinal": 1,
"name": "queue_url",
"type_info": "Varchar"
},
{
"ordinal": 1,
"ordinal": 2,
"name": "aws_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"ordinal": 3,
"name": "message_attributes",
"type_info": "TextArray"
},
{
"ordinal": 3,
"ordinal": 4,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 4,
"ordinal": 5,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 5,
"ordinal": 6,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"ordinal": 11,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 11,
"ordinal": 12,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"ordinal": 13,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 14,
"ordinal": 15,
"name": "enabled",
"type_info": "Bool"
}
@@ -83,6 +98,7 @@
"Left": []
},
"nullable": [
false,
false,
false,
true,
@@ -100,5 +116,5 @@
false
]
},
"hash": "e6adaebcade2e25be800e8b888b23c94caae2421f3cae7c06d6346bd6de1d94a"
"hash": "bc0bedddcafad216c30c5061eef3f4ed8573cda89f655d6490d22f182e3f2f36"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM \n gcp_trigger\n WHERE \n workspace_id = $1\n ",
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM \n gcp_trigger\n WHERE \n workspace_id = $1\n ",
"describe": {
"columns": [
{
@@ -45,56 +45,71 @@
},
{
"ordinal": 6,
"name": "subscription_mode: _",
"type_info": {
"Custom": {
"name": "gcp_subscription_mode",
"kind": {
"Enum": [
"create_update",
"existing"
]
}
}
}
},
{
"ordinal": 7,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 10,
"ordinal": 11,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 11,
"ordinal": 12,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"ordinal": 13,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 14,
"ordinal": 15,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 15,
"ordinal": 16,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 16,
"ordinal": 17,
"name": "enabled",
"type_info": "Bool"
}
@@ -117,6 +132,7 @@
false,
false,
false,
false,
true,
true,
false,
@@ -124,5 +140,5 @@
false
]
},
"hash": "0102308ffa1c0dbfba54d29246535bb81146a4cfae0ec408435570e0813a3bef"
"hash": "c2925a28212265bd9ac8e2d498b3588fc040c7a29ae6d9bab1f05af0b535e2ac"
}
@@ -1,73 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT DISTINCT ON (j.runnable_path, j.kind) \n j.kind AS \"kind!: JobKind\",\n j.runnable_path AS script_path,\n NULL as \"script_hash: _\",\n -1::bigint as \"count!: _\",\n COALESCE(f.schema, s.schema) AS schema\n FROM v2_job j\n LEFT JOIN script s ON s.path = j.runnable_path AND j.kind = 'script'\n LEFT JOIN flow_version f ON f.path = j.runnable_path AND j.kind = 'flow'\n WHERE COALESCE(s.hash, f.id) IS NOT NULL\n AND j.workspace_id = $1 AND j.id = ANY($2)\n ORDER BY j.runnable_path, j.kind, COALESCE(f.created_at, s.created_at) DESC",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "kind!: JobKind",
"type_info": {
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
]
}
}
}
},
{
"ordinal": 1,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "script_hash: _",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "count!: _",
"type_info": "Int8"
},
{
"ordinal": 4,
"name": "schema",
"type_info": "Json"
}
],
"parameters": {
"Left": [
"Text",
"UuidArray"
]
},
"nullable": [
false,
true,
null,
null,
null
]
},
"hash": "c4f382045e5c47986e02f1e57667e6b3cb1d60f15fed9a24e3f05b1bd60c0fc8"
}
@@ -1,67 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT j.kind AS \"kind: _\", COALESCE(s.path, f.path) AS \"script_path!\", COALESCE(s.hash, f.id) AS \"script_hash!: _\", args\n FROM v2_job j\n LEFT JOIN script s ON j.runnable_id = s.hash AND j.kind = 'script'\n LEFT JOIN flow_version f ON j.runnable_id = f.id AND j.runnable_path = f.path AND j.kind = 'flow'\n WHERE j.id = ANY($1)\n AND j.workspace_id = $2\n AND COALESCE(s.hash, f.id) IS NOT NULL\n AND COALESCE(s.path, f.path) IS NOT NULL",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "kind: _",
"type_info": {
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
]
}
}
}
},
{
"ordinal": 1,
"name": "script_path!",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "script_hash!: _",
"type_info": "Int8"
},
{
"ordinal": 3,
"name": "args",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"UuidArray",
"Text"
]
},
"nullable": [
false,
null,
null,
true
]
},
"hash": "ccd4c7fe5fbdf1ab4bbbbd7d2a9b1ec3fc844ed286ff563f0c9a5c4778cfb718"
}
@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n sqs_trigger \n SET \n aws_resource_path = $1,\n queue_url = $2,\n message_attributes = $3, \n is_flow = $4, \n edited_by = $5, \n email = $6,\n script_path = $7,\n path = $8,\n edited_at = now(), \n error = NULL,\n server_id = NULL\n WHERE \n workspace_id = $9 AND \n path = $10\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"TextArray",
"Bool",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "f68d23841e0e31cdf8633aaf0f32777e04e4965682e5b37afbe84194b756d5f5"
}
+1 -1
View File
@@ -11,5 +11,5 @@
"remote.autoForwardPorts": true,
"conventionalCommits.scopes": [
"restructring triggers, decoding trigger message on work"
],
]
}
+198 -214
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.481.0"
version = "1.483.2"
authors.workspace = true
edition.workspace = true
@@ -32,7 +32,7 @@ members = [
]
[workspace.package]
version = "1.481.0"
version = "1.483.2"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -73,7 +73,7 @@ websocket = ["windmill-api/websocket"]
http_trigger = ["windmill-api/http_trigger"]
postgres_trigger = ["windmill-api/postgres_trigger"]
mqtt_trigger = ["windmill-api/mqtt_trigger"]
sqs_trigger = ["windmill-api/sqs_trigger"]
sqs_trigger = ["windmill-api/sqs_trigger", "windmill-common/aws_auth", "windmill-api/openidconnect"]
gcp_trigger = ["windmill-api/gcp_trigger"]
smtp = ["windmill-api/smtp", "windmill-common/smtp"]
license = ["windmill-api/license"]
@@ -304,6 +304,7 @@ postgres-native-tls = "^0"
native-tls = "^0"
# samael will break compilation on MacOS. Use this fork instead to make it work
# samael = { git="https://github.com/njaremko/samael", rev="464d015e3ae393e4b5dd00b4d6baa1b617de0dd6", features = ["xmlsec"] }
libxml = { version = "=0.3.3" }
samael = { version="0.0.14", features = ["xmlsec"] }
gcp_auth = "0.9.0"
rust_decimal = { version = "^1", features = ["db-postgres", "serde-float"]}
+1 -1
View File
@@ -1 +1 @@
1475a504133766e005ec551004c2af2283663352
b9ca3ccdd188683c81832fd3ed41c8dd632a0c9c
@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE app_version DROP COLUMN IF EXISTS raw_app;
@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE app_version ADD COLUMN IF NOT EXISTS raw_app BOOLEAN NOT NULL DEFAULT FALSE;
@@ -0,0 +1,3 @@
-- Add down migration script here
ALTER TABLE sqs_trigger DROP COLUMN aws_auth_resource_type;
DROP TYPE IF EXISTS AWS_AUTH_RESOURCE_TYPE;
@@ -0,0 +1,4 @@
-- Add up migration script here
CREATE TYPE AWS_AUTH_RESOURCE_TYPE AS ENUM ('oidc', 'credentials');
ALTER TABLE sqs_trigger
ADD COLUMN aws_auth_resource_type AWS_AUTH_RESOURCE_TYPE DEFAULT 'credentials'::AWS_AUTH_RESOURCE_TYPE NOT NULL;
@@ -0,0 +1,3 @@
-- Add down migration script here
ALTER TABLE gcp_trigger DROP COLUMN subscription_mode;
DROP TYPE GCP_SUBSCRIPTION_MODE;
@@ -0,0 +1,3 @@
-- Add up migration script here
CREATE TYPE GCP_SUBSCRIPTION_MODE AS ENUM ('create_update', 'existing');
ALTER TABLE gcp_trigger ADD COLUMN subscription_mode GCP_SUBSCRIPTION_MODE NOT NULL DEFAULT 'create_update'::GCP_SUBSCRIPTION_MODE;
@@ -0,0 +1,3 @@
-- Add down migration script here
ALTER TABLE websocket_trigger
ALTER COLUMN url TYPE VARCHAR(255);
@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE websocket_trigger
ALTER COLUMN url TYPE VARCHAR(1000);
@@ -49,8 +49,12 @@ impl Visit for ImportsFinder {
pub fn parse_expr_for_imports(code: &str) -> anyhow::Result<Vec<String>> {
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(FileName::Custom("main.d.ts".into()).into(), code.into());
let mut tss = TsSyntax::default();
tss.disallow_ambiguous_jsx_like;
tss.tsx = true;
tss.no_early_errors = true;
let lexer = Lexer::new(
Syntax::Typescript(TsSyntax::default()),
Syntax::Typescript(tss),
// EsVersion defaults to es5
Default::default(),
StringInput::from(&*fm),
+3 -1
View File
@@ -13,7 +13,7 @@ default = []
enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "windmill-worker/enterprise"]
stripe = []
agent_worker_server = []
enterprise_saml = ["dep:samael"]
enterprise_saml = ["dep:samael", "dep:libxml"]
benchmark = []
embedding = ["dep:tinyvector", "dep:hf-hub", "dep:tokenizers", "dep:candle-core", "dep:candle-transformers", "dep:candle-nn", "dep:half"]
parquet = ["dep:datafusion", "dep:object_store", "dep:url", "windmill-common/parquet", "windmill-worker/parquet"]
@@ -100,6 +100,7 @@ async_zip = { workspace = true, optional = true }
regex.workspace = true
bytes.workspace = true
samael = { workspace = true, optional = true }
libxml = { workspace = true, optional = true }
async-recursion.workspace = true
rsa = { workspace = true, optional = true}
uuid.workspace = true
@@ -133,6 +134,7 @@ rust-postgres-native-tls = { workspace = true, optional = true}
rumqttc = { workspace = true, optional = true }
aws-sdk-sqs = { workspace = true, optional = true }
aws-config = { workspace = true, optional = true }
aws-sdk-sts = { workspace = true, optional = true }
google-cloud-pubsub = { workspace = true, optional = true }
google-cloud-googleapis = { workspace = true , optional = true }
tonic = { workspace = true, optional = true }
+130 -74
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.481.0
version: 1.483.2
title: Windmill API
contact:
@@ -5985,6 +5985,55 @@ paths:
schema:
type: string
/w/{workspace}/apps/create_raw:
post:
summary: create app raw
operationId: createAppRaw
tags:
- app
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: new app
required: true
content:
multipart/form-data:
schema:
type: object
properties:
app:
type: object
properties:
path:
type: string
value: {}
summary:
type: string
policy:
$ref: "#/components/schemas/Policy"
draft_only:
type: boolean
deployment_message:
type: string
custom_path:
type: string
required:
- path
- value
- summary
- policy
js:
type: string
css:
type: string
responses:
"201":
description: app created
content:
text/plain:
schema:
type: string
/w/{workspace}/apps/exists/{path}:
get:
summary: does an app exisst at path
@@ -6344,6 +6393,49 @@ paths:
schema:
type: string
/w/{workspace}/apps/update_raw/{path}:
post:
summary: update app
operationId: updateAppRaw
tags:
- app
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
requestBody:
description: update app
required: true
content:
multipart/form-data:
schema:
type: object
properties:
app:
type: object
properties:
path:
type: string
summary:
type: string
value: {}
policy:
$ref: "#/components/schemas/Policy"
deployment_message:
type: string
custom_path:
type: string
js:
type: string
css:
type: string
responses:
"200":
description: app updated
content:
text/plain:
schema:
type: string
/w/{workspace}/apps/custom_path_exists/{custom_path}:
get:
summary: check if custom path exists
@@ -6623,7 +6715,8 @@ paths:
type: object
additionalProperties:
$ref: "#/components/schemas/InputTransform"
use_latest_version: boolean
use_latest_version:
type: boolean
flow_options_by_path:
type: object
additionalProperties:
@@ -6633,7 +6726,8 @@ paths:
type: object
additionalProperties:
$ref: "#/components/schemas/InputTransform"
use_latest_version: boolean
use_latest_version:
type: boolean
responses:
"201":
description: stream of created job uuids separated by \n. Lines may start with 'Error:'
@@ -9445,7 +9539,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/NewGcpTrigger"
$ref: "#/components/schemas/GcpTriggerData"
responses:
"201":
description: gcp trigger created
@@ -9469,7 +9563,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/EditGcpTrigger"
$ref: "#/components/schemas/GcpTriggerData"
responses:
"200":
description: gcp trigger updated
@@ -14048,6 +14142,8 @@ components:
type: string
path:
type: string
script_hash:
type: string
args:
$ref: "#/components/schemas/ScriptArgs"
language:
@@ -14949,8 +15045,6 @@ components:
type: string
authenticate:
type: boolean
base_endpoint:
type: string
required:
- authenticate
- base_endpoint
@@ -14972,6 +15066,8 @@ components:
$ref: "#/components/schemas/DeliveryType"
delivery_config:
$ref: "#/components/schemas/PushConfig"
subscription_mode:
$ref: "#/components/schemas/SubscriptionMode"
last_server_ping:
type: string
format: date-time
@@ -14985,6 +15081,8 @@ components:
- subscription_id
- enabled
- delivery_type
- subscription_mode
SubscriptionMode:
type: string
@@ -14993,55 +15091,24 @@ components:
- create_update
description: "The mode of subscription. 'existing' means using an existing GCP subscription, while 'create_update' involves creating or updating a new subscription."
GcpExistingSubscription:
GcpTriggerData:
type: object
properties:
gcp_resource_path:
type: string
subscription_mode:
$ref: "#/components/schemas/SubscriptionMode"
topic_id:
type: string
subscription_id:
type: string
base_endpoint:
type: string
required:
- subscription_id
- base_endpoint
GcpCreateUpdateSubscription:
type: object
properties:
subscription_id:
type: string
delivery_type:
$ref: "#/components/schemas/DeliveryType"
delivery_config:
$ref: "#/components/schemas/PushConfig"
required:
- delivery_type
GcpSubscriptionModeConfig:
type: object
properties:
subscription_mode:
$ref: "#/components/schemas/SubscriptionMode"
required:
- subscription_mode
allOf:
- oneOf:
- $ref: "#/components/schemas/GcpExistingSubscription"
- $ref: "#/components/schemas/GcpCreateUpdateSubscription"
description: |
"This is a union type representing the subscription mode.
- 'existing': Represents an existing GCP subscription, and should be accompanied by an 'ExistingGcpSubscription' object.
- 'create_update': Represents a new or updated GCP subscription, and should be accompanied by a 'CreateUpdateConfig' object."
NewGcpTrigger:
type: object
properties:
gcp_resource_path:
type: string
topic_id:
type: string
subscription_mode:
$ref: "#/components/schemas/GcpSubscriptionModeConfig"
path:
type: string
script_path:
@@ -15058,34 +15125,6 @@ components:
- topic_id
- subscription_mode
EditGcpTrigger:
type: object
properties:
gcp_resource_path:
type: string
topic_id:
type: string
subscription_mode:
$ref: "#/components/schemas/GcpSubscriptionModeConfig"
path:
type: string
script_path:
type: string
is_flow:
type: boolean
enabled:
type: boolean
required:
- path
- script_path
- is_flow
- enabled
- mqtt_resource_path
- subscription_id
- delivery_type
- topic_id
- subscription_mode
GetAllTopicSubscription:
type: object
properties:
@@ -15102,6 +15141,12 @@ components:
type: string
required:
- subscription_id
AwsAuthResourceType:
type: string
enum:
- oidc
- credentials
SqsTrigger:
allOf:
@@ -15110,6 +15155,8 @@ components:
properties:
queue_url:
type: string
aws_auth_resource_type:
$ref: "#/components/schemas/AwsAuthResourceType"
aws_resource_path:
type: string
message_attributes:
@@ -15130,12 +15177,15 @@ components:
- queue_url
- aws_resource_path
- enabled
- aws_auth_resource_type
NewSqsTrigger:
type: object
properties:
queue_url:
type: string
aws_auth_resource_type:
$ref: "#/components/schemas/AwsAuthResourceType"
aws_resource_path:
type: string
message_attributes:
@@ -15156,12 +15206,15 @@ components:
- path
- script_path
- is_flow
- aws_auth_resource_type
EditSqsTrigger:
type: object
properties:
queue_url:
type: string
aws_auth_resource_type:
$ref: "#/components/schemas/AwsAuthResourceType"
aws_resource_path:
type: string
message_attributes:
@@ -15183,6 +15236,7 @@ components:
- script_path
- is_flow
- enabled
- aws_auth_resource_type
Slot:
type: object
@@ -15902,6 +15956,8 @@ components:
execution_mode:
type: string
enum: [viewer, publisher, anonymous]
raw_app:
type: boolean
required:
- id
- workspace_id
+248 -53
View File
@@ -24,10 +24,10 @@ use crate::{
},
users::fetch_api_authed_from_permissioned_as,
};
#[cfg(feature = "parquet")]
use axum::response::Response;
use axum::{
extract::{Extension, Json, Path, Query},
body::Body,
extract::{Extension, Json, Multipart, Path, Query},
response::IntoResponse,
routing::{delete, get, post},
Router,
@@ -88,10 +88,13 @@ pub fn workspaced_service() -> Router {
.route("/get/draft/*path", get(get_app_w_draft))
.route("/secret_of/*path", get(get_secret_id))
.route("/get/v/*id", get(get_app_by_id))
.route("/get_data/v/*id", get(get_raw_app_data))
.route("/exists/*path", get(exists_app))
.route("/update/*path", post(update_app))
.route("/update_raw/*path", post(update_app_raw))
.route("/delete/*path", delete(delete_app))
.route("/create", post(create_app))
.route("/create_raw", post(create_app_raw))
.route("/history/p/*path", get(get_app_history))
.route("/get_latest_version/*path", get(get_latest_version))
.route("/history_update/a/:id/v/:version", post(update_app_history))
@@ -135,6 +138,12 @@ pub struct ListableApp {
#[sqlx(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub deployment_msg: Option<String>,
#[serde(skip_serializing_if = "is_false")]
pub raw_app: bool,
}
fn is_false(b: &bool) -> bool {
!b
}
#[derive(FromRow, Serialize, Deserialize)]
@@ -328,7 +337,8 @@ async fn list_apps(
"app.extra_perms",
"favorite.path IS NOT NULL as starred",
"draft.path IS NOT NULL as has_draft",
"draft_only"
"draft_only",
"app_version.raw_app",
])
.left()
.join("favorite")
@@ -387,6 +397,44 @@ async fn list_apps(
Ok(Json(rows))
}
async fn get_raw_app_data(Path((w_id, version_id)): Path<(String, String)>) -> Result<Response> {
let file_path = format!("/tmp/wmill/{}/{}", w_id, version_id);
let file = tokio::fs::File::open(file_path).await?;
let stream = tokio_util::io::ReaderStream::new(file);
let res = Response::builder().header(
http::header::CONTENT_TYPE,
if version_id.ends_with(".css") {
"text/css"
} else {
"text/javascript"
},
);
Ok(res.body(Body::from_stream(stream)).unwrap())
}
// async fn get_app_version(
// authed: ApiAuthed,
// Extension(user_db): Extension<UserDB>,
// Path((w_id, path)): Path<(String, StripPath)>,
// ) -> JsonResult<i64> {
// let path = path.to_path();
// let mut tx = user_db.begin(&authed).await?;
// let version_o = sqlx::query_scalar!(
// "SELECT app.versions[array_upper(app.versions, 1)] as version FROM app
// WHERE app.path = $1 AND app.workspace_id = $2",
// path,
// &w_id,
// )
// .fetch_optional(&mut *tx)
// .await?
// .flatten();
// tx.commit().await?;
// let version = not_found_if_none(version_o, "App", path)?;
// Ok(Json(version))
// }
async fn get_app(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
@@ -727,6 +775,97 @@ async fn get_secret_id(
Ok(hx)
}
macro_rules! process_app_multipart {
($authed:expr, $user_db:expr, $db:expr, $w_id:expr, $path:expr, $multipart:expr, $internal_fn:expr) => {
async {
let mut saved_app = None;
let mut uploaded_js = false;
//todo: use s3 instead
let file_path = format!("/tmp/wmill/{}", $w_id);
std::fs::create_dir_all(&file_path).unwrap();
let mut multipart = $multipart;
while let Some(field) = multipart.next_field().await.unwrap() {
let name = field.name().unwrap().to_string();
let data = field.bytes().await.unwrap();
if name == "app" {
let app = serde_json::from_slice(&data).map_err(to_anyhow)?;
let (ntx, npath, nid) = $internal_fn(
$authed.clone(),
$db.clone(),
$user_db.clone(),
$w_id,
$path,
true,
app,
)
.await?;
saved_app = Some((npath, nid, ntx));
} else if name == "js" {
if let Some((_npath, id, _tx)) = saved_app.as_ref() {
let file_path = format!("{}/{}.js", file_path, id);
std::fs::write(file_path, data).unwrap();
uploaded_js = true;
} else {
return Err(Error::BadRequest(
"App payload need to be created first".to_string(),
));
}
} else if name == "css" {
if let Some((_npath, id, _tx)) = saved_app.as_ref() {
let file_path = format!("{}/{}.css", file_path, id);
std::fs::write(file_path, data).unwrap();
} else {
return Err(Error::BadRequest(
"App payload need to be created first".to_string(),
));
}
} else {
return Err(Error::BadRequest(format!("Unsupported field: {}", name)));
}
}
if !uploaded_js {
return Err(Error::BadRequest("js or css file not uploaded".to_string()));
}
if let Some((npath, id, tx)) = saved_app {
tx.commit().await?;
Ok((npath, id))
} else {
Err(Error::BadRequest("App not created".to_string()))
}
}
};
}
async fn create_app_raw<'a>(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
multipart: Multipart,
) -> Result<(StatusCode, String)> {
let (path, _id) = process_app_multipart!(
authed,
user_db,
db,
&w_id,
"",
multipart,
|authed, db, user_db, w_id, _path, raw_app, app| create_app_internal(
authed, db, user_db, w_id, raw_app, app
)
)
.await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateApp { workspace: w_id, path: path.clone() },
);
Ok((StatusCode::CREATED, path))
}
async fn list_paths_from_workspace_runnable(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
@@ -755,17 +894,36 @@ async fn create_app(
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(mut app): Json<CreateApp>,
Json(app): Json<CreateApp>,
) -> Result<(StatusCode, String)> {
let mut tx = user_db.clone().begin(&authed).await?;
let path = app.path.clone();
let (new_tx, _path, _id) = create_app_internal(authed, db, user_db, &w_id, false, app).await?;
new_tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateApp { workspace: w_id, path: path.clone() },
);
Ok((StatusCode::CREATED, path))
}
async fn create_app_internal<'a>(
authed: ApiAuthed,
db: sqlx::Pool<sqlx::Postgres>,
user_db: UserDB,
w_id: &String,
raw_app: bool,
mut app: CreateApp,
) -> Result<(sqlx::Transaction<'a, sqlx::Postgres>, String, i64)> {
let mut tx = user_db.clone().begin(&authed).await?;
app.policy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
app.policy.on_behalf_of_email = Some(authed.email.clone());
let path = app.path.clone();
if &app.path == "" {
return Err(Error::BadRequest("App path cannot be empty".to_string()));
}
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE path = $1 AND workspace_id = $2)",
&app.path,
@@ -774,21 +932,19 @@ async fn create_app(
.fetch_one(&mut *tx)
.await?
.unwrap_or(false);
if exists {
return Err(Error::BadRequest(format!(
"App with path {} already exists",
&app.path
)));
}
if let Some(custom_path) = &app.custom_path {
require_admin(authed.is_admin, &authed.username)?;
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2))",
custom_path,
if *CLOUD_HOSTED { Some(&w_id) } else { None }
if *CLOUD_HOSTED { Some(w_id) } else { None }
)
.fetch_one(&mut *tx)
.await?.unwrap_or(false);
@@ -800,7 +956,6 @@ async fn create_app(
)));
}
}
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app'",
&app.path,
@@ -808,7 +963,6 @@ async fn create_app(
)
.execute(&mut *tx)
.await?;
let id = sqlx::query_scalar!(
"INSERT INTO app
(workspace_id, path, summary, policy, versions, draft_only, custom_path)
@@ -819,24 +973,24 @@ async fn create_app(
json!(app.policy),
app.draft_only,
app.custom_path
.as_ref()
.map(|s| if s.is_empty() { None } else { Some(s) })
.flatten()
)
.fetch_one(&mut *tx)
.await?;
let v_id = sqlx::query_scalar!(
"INSERT INTO app_version
(app_id, value, created_by)
VALUES ($1, $2::text::json, $3) RETURNING id",
(app_id, value, created_by, raw_app)
VALUES ($1, $2::text::json, $3, $4) RETURNING id",
id,
//to preserve key orders
serde_json::to_string(&app.value).unwrap(),
authed.username,
raw_app
)
.fetch_one(&mut *tx)
.await?;
sqlx::query!(
"UPDATE app SET versions = array_append(versions, $1::bigint) WHERE id = $2",
v_id,
@@ -850,22 +1004,20 @@ async fn create_app(
&authed,
"apps.create",
ActionKind::Create,
&w_id,
w_id,
Some(&app.path),
None,
)
.await?;
let mut args: HashMap<String, Box<serde_json::value::RawValue>> = HashMap::new();
if let Some(dm) = app.deployment_message {
if let Some(dm) = &app.deployment_message {
args.insert("deployment_message".to_string(), to_raw_value(&dm));
}
let tx = PushIsolationLevel::Transaction(tx);
let (dependency_job_uuid, new_tx) = push(
&db,
tx,
&w_id,
w_id,
JobPayload::AppDependencies { path: app.path.clone(), version: v_id },
PushArgs { args: &args, extra: None },
&authed.username,
@@ -889,14 +1041,7 @@ async fn create_app(
.await?;
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
new_tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateApp { workspace: w_id, path: app.path.clone() },
);
Ok((StatusCode::CREATED, app.path))
Ok((new_tx, path, v_id))
}
async fn list_hub_apps(Extension(db): Extension<DB>) -> impl IntoResponse {
@@ -1017,12 +1162,76 @@ async fn update_app(
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditApp>,
) -> Result<String> {
use sql_builder::prelude::*;
// create_app_internal(authed, user_db, db, &w_id, &mut app).await?;
let path = path.to_path();
let opath = path.to_string();
let (new_tx, npath, _v_id) =
update_app_internal(authed, db, user_db, &w_id, path, false, ns).await?;
new_tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateApp {
workspace: w_id.clone(),
old_path: opath.clone(),
new_path: npath.clone(),
},
);
Ok(format!("app {} updated (npath: {:?})", opath, npath))
}
async fn update_app_raw<'a>(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
multipart: Multipart,
) -> Result<String> {
let path = path.to_path();
let opath = path.to_string();
let (npath, _id) = process_app_multipart!(
authed,
user_db,
db,
&w_id,
path,
multipart,
update_app_internal
)
.await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateApp {
workspace: w_id.clone(),
old_path: opath.to_owned(),
new_path: npath.clone(),
},
);
Ok(format!("app {} updated (npath: {:?})", opath, npath))
}
// async fn create_app_internal<'a>(
// authed: ApiAuthed,
// db: sqlx::Pool<sqlx::Postgres>,
// user_db: UserDB,
// w_id: &String,
// app: &mut CreateApp,
// )
async fn update_app_internal<'a>(
authed: ApiAuthed,
db: sqlx::Pool<sqlx::Postgres>,
user_db: UserDB,
w_id: &str,
path: &str,
raw_app: bool,
ns: EditApp,
) -> Result<(sqlx::Transaction<'a, sqlx::Postgres>, String, i64)> {
use sql_builder::prelude::*;
let mut tx = user_db.clone().begin(&authed).await?;
let npath = if ns.policy.is_some()
|| ns.path.is_some()
|| ns.summary.is_some()
@@ -1069,7 +1278,7 @@ async fn update_app(
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2) AND NOT (path = $3 AND workspace_id = $4))",
ncustom_path,
if *CLOUD_HOSTED { Some(&w_id) } else { None },
if *CLOUD_HOSTED { Some(w_id) } else { None },
path,
w_id
)
@@ -1116,12 +1325,13 @@ async fn update_app(
let v_id = sqlx::query_scalar!(
"INSERT INTO app_version
(app_id, value, created_by)
VALUES ($1, $2::text::json, $3) RETURNING id",
(app_id, value, created_by, raw_app)
VALUES ($1, $2::text::json, $3, $4) RETURNING id",
app_id,
//to preserve key orders
serde_json::to_string(&nvalue).unwrap(),
authed.username,
raw_app
)
.fetch_one(&mut *tx)
.await?;
@@ -1152,7 +1362,6 @@ async fn update_app(
)));
}
};
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app'",
path,
@@ -1160,29 +1369,26 @@ async fn update_app(
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed,
"apps.update",
ActionKind::Update,
&w_id,
w_id,
Some(&npath),
None,
)
.await?;
let tx = PushIsolationLevel::Transaction(tx);
let mut args: HashMap<String, Box<serde_json::value::RawValue>> = HashMap::new();
if let Some(dm) = ns.deployment_message {
args.insert("deployment_message".to_string(), to_raw_value(&dm));
}
args.insert("parent_path".to_string(), to_raw_value(&path));
let (dependency_job_uuid, new_tx) = push(
&db,
tx,
&w_id,
w_id,
JobPayload::AppDependencies { path: npath.clone(), version: v_id },
PushArgs { args: &args, extra: None },
&authed.username,
@@ -1205,18 +1411,7 @@ async fn update_app(
)
.await?;
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
new_tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateApp {
workspace: w_id,
old_path: path.to_owned(),
new_path: npath.clone(),
},
);
Ok(format!("app {} updated (npath: {:?})", path, npath))
Ok((new_tx, npath, v_id))
}
#[derive(Debug, Deserialize, Clone)]
+23 -7
View File
@@ -18,16 +18,22 @@ use {
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
use crate::gcp_triggers_ee::{
manage_google_subscription, process_google_push_request, validate_jwt_token, SubscriptionMode,
manage_google_subscription, process_google_push_request, validate_jwt_token,
CreateUpdateConfig, SubscriptionMode,
};
#[cfg(all(feature = "enterprise", feature = "sqs_trigger"))]
use windmill_common::auth::aws::AwsAuthResourceType;
#[cfg(any(
feature = "http_trigger",
all(feature = "enterprise", feature = "gcp_trigger")
))]
use {
axum::extract::Request, http::HeaderMap, serde::de::DeserializeOwned,
windmill_common::error::Error,
axum::extract::Request,
http::HeaderMap,
serde::de::DeserializeOwned,
windmill_common::{error::Error, utils::empty_string_as_none},
};
#[cfg(all(feature = "enterprise", feature = "kafka"))]
@@ -150,14 +156,20 @@ pub struct SqsTriggerConfig {
pub queue_url: String,
pub aws_resource_path: String,
pub message_attributes: Option<Vec<String>>,
pub aws_auth_resource_type: AwsAuthResourceType,
}
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
#[derive(Debug, Serialize, Deserialize)]
pub struct GcpTriggerConfig {
pub gcp_resource_path: String,
#[serde(flatten)]
pub subscription_mode: SubscriptionMode,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub subscription_id: Option<String>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub base_endpoint: Option<String>,
#[serde(flatten)]
pub create_update: Option<CreateUpdateConfig>,
pub topic_id: String,
}
@@ -366,11 +378,15 @@ async fn set_gcp_trigger_config(
&gcp_config.gcp_resource_path,
&capture_config.path,
&gcp_config.topic_id,
&mut gcp_config.subscription_id,
&mut gcp_config.base_endpoint,
gcp_config.subscription_mode,
gcp_config.create_update,
false,
)
.await?;
gcp_config.subscription_mode = SubscriptionMode::CreateUpdate(config);
gcp_config.create_update = Some(config);
gcp_config.subscription_mode = SubscriptionMode::CreateUpdate;
capture_config.trigger_config = Some(TriggerConfig::Gcp(gcp_config));
Ok(capture_config)
@@ -887,7 +903,7 @@ async fn gcp_payload(
let authed = fetch_api_authed(owner.clone(), email, &w_id, &db, None).await?;
let SubscriptionMode::CreateUpdate(config) = &gcp_trigger_config.subscription_mode else {
let Some(config) = &gcp_trigger_config.create_update else {
return Err(Error::BadConfig("Bad config".to_string()));
};
+10 -4
View File
@@ -52,11 +52,12 @@ pub struct ExistingGcpSubscription {
pub base_endpoint: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "subscription_mode", rename_all = "snake_case")]
#[derive(Debug, Deserialize, Serialize, sqlx::Type)]
#[serde(rename_all = "snake_case")]
#[sqlx(type_name = "GCP_SUBSCRIPTION_MODE", rename_all = "snake_case")]
pub enum SubscriptionMode {
Existing(ExistingGcpSubscription),
CreateUpdate(CreateUpdateConfig),
Existing,
CreateUpdate,
}
pub fn workspaced_service() -> Router {
@@ -77,7 +78,11 @@ pub async fn manage_google_subscription(
_gcp_resource_path: &str,
_path: &str,
_topic_id: &str,
_subscription_id: &mut Option<String>,
_base_endpoint: &mut Option<String>,
_subscription_mode: SubscriptionMode,
_create_update_config: Option<CreateUpdateConfig>,
_trigger_mode: bool,
) -> WindmillResult<CreateUpdateConfig> {
Ok(CreateUpdateConfig::default())
}
@@ -117,6 +122,7 @@ pub struct GcpTrigger {
pub subscription_id: String,
pub delivery_type: DeliveryType,
pub delivery_config: Option<SqlxJson<PushConfig>>,
pub subscription_mode: SubscriptionMode,
pub topic_id: String,
pub path: String,
pub script_path: String,
@@ -253,8 +253,8 @@ mod zoom {
use super::*;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
struct ZoomPayload {
#[serde(rename = "plainToken")]
plain_token: String,
}
+15 -7
View File
@@ -674,14 +674,14 @@ async fn list_selected_job_groups(
'kind', jb.kind,
'script_path', jb.runnable_path,
'latest_schema', COALESCE(
(SELECT DISTINCT ON (s.path) s.schema FROM script s WHERE s.path = jb.runnable_path AND jb.kind = 'script' ORDER BY s.path, s.created_at DESC),
(SELECT flow_version.schema FROM flow LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)] WHERE flow.path = jb.runnable_path AND jb.kind = 'flow')
(SELECT DISTINCT ON (s.path) s.schema FROM script s WHERE s.workspace_id = $1 AND s.path = jb.runnable_path AND jb.kind = 'script' ORDER BY s.path, s.created_at DESC),
(SELECT flow_version.schema FROM flow LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)] WHERE flow.workspace_id = $1 AND flow.path = jb.runnable_path AND jb.kind = 'flow')
),
'schemas', ARRAY(
SELECT jsonb_build_object(
'script_hash', LPAD(TO_HEX(COALESCE(s.hash, f.id)), 16, '0'),
'job_ids', ARRAY_AGG(DISTINCT j.id),
'schema', ANY_VALUE(COALESCE(s.schema, f.schema))
'schema', (ARRAY_AGG(COALESCE(s.schema, f.schema)))[1]
) FROM v2_job j
LEFT JOIN script s ON s.hash = j.runnable_id AND j.kind = 'script'
LEFT JOIN flow_version f ON f.id = j.runnable_id AND j.kind = 'flow'
@@ -3095,16 +3095,17 @@ struct CancelJob {
enum PreviewKind {
Code,
Identity,
Http,
Noop,
Bundle,
Tarbundle,
ScriptHash,
}
#[derive(Deserialize)]
struct Preview {
content: Option<String>,
kind: Option<PreviewKind>,
script_hash: Option<String>,
path: Option<String>,
args: Option<HashMap<String, Box<JsonRawValue>>>,
language: Option<ScriptLang>,
@@ -3440,7 +3441,11 @@ async fn batch_rerun_handle_job(
}
InputTransform::Javascript { expr } => {
#[cfg(not(feature = "deno_core"))]
tracing::error!("deno_core feature is not activated, cannot evaluate: {expr}");
Err(error::Error::ExecutionErr(
format!("deno_core feature is not activated, cannot evaluate: {expr}")
.to_string(),
))?;
#[cfg(feature = "deno_core")]
args.insert(
property_name.clone(),
@@ -4851,7 +4856,10 @@ async fn run_preview_script(
Some(PreviewKind::Identity) => JobPayload::Identity,
Some(PreviewKind::Noop) => JobPayload::Noop,
_ => JobPayload::Code(RawCode {
hash: None,
hash: preview
.script_hash
.as_ref()
.and_then(|s| windmill_common::scripts::to_i64(s).ok()),
content: preview.content.unwrap_or_default(),
path: preview.path,
language: preview.language.unwrap_or(ScriptLang::Deno),
@@ -5831,7 +5839,7 @@ pub fn filter_list_completed_query(
if let Some(label) = &lq.label {
if lq.allow_wildcards.unwrap_or(false) {
let wh = format!(
"EXISTS (SELECT 1 FROM jsonb_array_elements_text(result->'wm_labels') label WHERE label LIKE '{}')",
"EXISTS (SELECT 1 FROM jsonb_array_elements_text(result->'wm_labels') label WHERE jsonb_typeof(result->'wm_labels') = 'array' AND label LIKE '{}')",
&label.replace("*", "%").replace("'", "''")
);
sqlb.and_where("result ? 'wm_labels'");
@@ -395,7 +395,7 @@ pub async fn create_postgres_trigger(
"Missing replication slot name".to_string(),
));
}
(publication_name.unwrap(), replication_slot_name.unwrap(), )
(publication_name.unwrap(), replication_slot_name.unwrap())
};
let mut tx = user_db.begin(&authed).await?;
@@ -124,11 +124,13 @@ export async function main(
transaction_type: "insert" | "update" | "delete",
schema_name: string,
table_name: string,
row: {}
row: {},
old_row?: {}
) {{
}}
"#,
struct_definition
&struct_definition,
&struct_definition
)
}
}
@@ -47,7 +47,7 @@ impl RelationConverter {
.ok_or(RelationConversionError::FailToFindMatchingTable)
}
pub fn body_to_json(
pub fn row_to_json(
&self,
to_decode: (Oid, Vec<TupleData>),
) -> Result<Map<String, Value>, RelationConversionError> {
@@ -650,37 +650,79 @@ async fn listen_to_transactions(
None
}
Insert(insert) => {
Some((insert.o_id, relations.body_to_json((insert.o_id, insert.tuple)), "insert"))
Some((insert.o_id, Ok(None), relations.row_to_json((insert.o_id, insert.tuple)), "insert"))
}
Update(update) => {
Some((update.o_id, relations.body_to_json((update.o_id, update.new_tuple)), "update"))
let old_row = update.old_tuple.map(|old_tuple| relations.row_to_json((update.o_id, old_tuple))).transpose();
let row = relations.row_to_json((update.o_id, update.new_tuple));
Some((update.o_id, old_row, row, "update"))
}
Delete(delete) => {
let body = delete.old_tuple.unwrap_or_else(|| delete.key_tuple.unwrap());
Some((delete.o_id, relations.body_to_json((delete.o_id, body)), "delete"))
let row = delete.old_tuple.unwrap_or_else(|| delete.key_tuple.unwrap());
Some((delete.o_id, Ok(None), relations.row_to_json((delete.o_id, row)), "delete"))
}
};
if let Some((o_id, Ok(body), transaction_type)) = json {
let relation = match relations.get_relation(o_id) {
Ok(relation) => relation,
Err(err) => {
tracing::error!("Postgres trigger named: {}, error: {}", pg.get_path(), err.to_string());
continue;
match json {
Some((o_id, Ok(old_row), Ok(row), transaction_type)) => {
let relation = match relations.get_relation(o_id) {
Ok(relation) => relation,
Err(err) => {
tracing::error!("Postgres trigger named: {}, error: {}", pg.get_path(), err.to_string());
continue;
}
};
let database_info = HashMap::from([
("schema_name".to_string(), to_raw_value(&relation.namespace)),
("table_name".to_string(), to_raw_value(&relation.name)),
("transaction_type".to_string(), to_raw_value(&transaction_type)),
("old_row".to_string(), to_raw_value(&old_row)),
("row".to_string(), to_raw_value(&row)),
]);
let extra = Some(HashMap::from([(
"wm_trigger".to_string(),
to_raw_value(&serde_json::json!({"kind": "postgres", })),
)]));
let _ = pg.handle(&db, Some(database_info), extra).await;
}
Some((o_id, old_row, row, transaction_type)) => {
let relation = match relations.get_relation(o_id) {
Ok(relation) => relation,
Err(err) => {
tracing::error!("Postgres trigger named: {}, error: {}", pg.get_path(), err.to_string());
continue;
}
};
if let Err(err) = old_row {
tracing::error!(
transaction_type = ?transaction_type,
schema = %relation.namespace,
table = %relation.name,
error = %err,
"Failed to decode OLD row for {} transaction on {}.{}",
transaction_type,
relation.namespace,
relation.name,
);
}
};
let database_info = HashMap::from([
("schema_name".to_string(), to_raw_value(&relation.namespace)),
("table_name".to_string(), to_raw_value(&relation.name)),
("transaction_type".to_string(), to_raw_value(&transaction_type)),
("row".to_string(), to_raw_value(&body)),
]);
let extra = Some(HashMap::from([(
"wm_trigger".to_string(),
to_raw_value(&serde_json::json!({"kind": "postgres", })),
)]));
let _ = pg.handle(&db, Some(database_info), extra).await;
if let Err(err) = row {
tracing::error!(
transaction_type = ?transaction_type,
schema = %relation.namespace,
table = %relation.name,
error = %err,
"Failed to decode NEW row for {} transaction on {}.{}",
transaction_type,
relation.namespace,
relation.name,
);
}
}
_ => {}
}
}
+33
View File
@@ -40,6 +40,7 @@ use std::{
};
use windmill_audit::audit_ee::audit_log;
use windmill_audit::ActionKind;
use windmill_worker::process_relative_imports;
use windmill_common::error::to_anyhow;
@@ -894,6 +895,38 @@ async fn create_script_internal<'c>(
.await?;
Ok((hash, new_tx))
} else {
let db2 = db.clone();
let w_id2 = w_id.clone();
let authed2 = authed.clone();
let permissioned_as2 = permissioned_as.clone();
let script_path2 = script_path.clone();
let parent_path = p_path_opt.clone();
let deployment_message = ns.deployment_message.clone();
let content = ns.content.clone();
let language = ns.language.clone();
tokio::spawn(async move {
// wait for 10 seconds to make sure the script is deployed and that the CLI sync that pushed it (f one) is complete
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
if let Err(e) = process_relative_imports(
&db2,
None,
None,
&w_id2,
&script_path2,
parent_path,
deployment_message,
&content,
&Some(language),
&authed2.email,
&authed2.username,
&permissioned_as2,
)
.await
{
tracing::error!(%e, "error processing relative imports");
}
});
handle_deployment_metadata(
&authed.email,
&authed.username,
@@ -1,6 +1,7 @@
use crate::db::DB;
use axum::Router;
use serde::{Deserialize, Serialize};
use windmill_common::auth::aws::AwsAuthResourceType;
pub fn workspaced_service() -> Router {
@@ -14,6 +15,7 @@ pub fn start_sqs(_db: DB, mut _killpill_rx: tokio::sync::broadcast::Receiver<()>
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SqsTrigger {
pub queue_url: String,
pub aws_auth_resource_type: AwsAuthResourceType,
pub aws_resource_path: String,
pub message_attributes: Option<Vec<String>>,
pub path: String,
+54 -10
View File
@@ -501,7 +501,7 @@ pub(crate) async fn tarball_workspace(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by from app, app_version
WHERE app.workspace_id = $1 AND app_version.id = app.versions[array_upper(app.versions, 1)]",
WHERE app.workspace_id = $1 AND app_version.id = app.versions[array_upper(app.versions, 1)] AND app_version.raw_app IS false",
)
.bind(&w_id)
.fetch_all(&mut *tx)
@@ -577,13 +577,35 @@ pub(crate) async fn tarball_workspace(
#[cfg(feature = "websocket")]
{
let websocket_triggers = sqlx::query_as!(
crate::websocket_triggers::WebsocketTrigger,
"SELECT workspace_id, path, url, script_path, is_flow, edited_by, email, edited_at, server_id, last_server_ping, extra_perms, error, enabled, filters as \"filters: _\", initial_messages as \"initial_messages: _\", url_runnable_args as \"url_runnable_args: _\", can_return_message FROM websocket_trigger
WHERE workspace_id = $1",
&w_id
)
.fetch_all(&mut *tx)
.await?;
crate::websocket_triggers::WebsocketTrigger,
r#"
SELECT
workspace_id,
path,
url,
script_path,
is_flow,
edited_by,
email,
edited_at,
server_id,
last_server_ping,
extra_perms,
error,
enabled,
filters AS "filters: _",
initial_messages AS "initial_messages: _",
url_runnable_args AS "url_runnable_args: _",
can_return_message
FROM
websocket_trigger
WHERE
workspace_id = $1
"#,
&w_id
)
.fetch_all(&mut *tx)
.await?;
for trigger in websocket_triggers {
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
@@ -622,8 +644,29 @@ pub(crate) async fn tarball_workspace(
{
let sqs_triggers = sqlx::query_as!(
crate::sqs_triggers_ee::SqsTrigger,
"SELECT * FROM sqs_trigger
WHERE workspace_id = $1",
r#"
SELECT
aws_auth_resource_type AS "aws_auth_resource_type: _",
aws_resource_path,
message_attributes,
queue_url,
workspace_id,
path,
script_path,
is_flow,
edited_by,
email,
edited_at,
server_id,
last_server_ping,
extra_perms,
error,
enabled
FROM
sqs_trigger
WHERE
workspace_id = $1
"#,
&w_id
)
.fetch_all(&mut *tx)
@@ -649,6 +692,7 @@ pub(crate) async fn tarball_workspace(
workspace_id,
delivery_type AS "delivery_type: _",
delivery_config AS "delivery_config: _",
subscription_mode AS "subscription_mode: _",
path,
script_path,
is_flow,
+1
View File
@@ -13,6 +13,7 @@ prometheus = ["dep:prometheus"]
loki = ["dep:tracing-loki"]
benchmark = []
parquet = ["dep:object_store", "dep:aws-config", "dep:aws-sdk-sts"]
aws_auth = ["dep:aws-sdk-sts", "dep:aws-config"]
otel = ["dep:opentelemetry-semantic-conventions", "dep:opentelemetry-otlp", "dep:opentelemetry_sdk",
"dep:opentelemetry", "dep:tracing-opentelemetry", "dep:opentelemetry-appender-tracing", "dep:tonic"]
smtp = ["dep:mail-send"]
+7
View File
@@ -6,6 +6,8 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
/// Id in the `app_script` table.
@@ -21,3 +23,8 @@ pub struct ListAppQuery {
pub include_draft_only: Option<bool>,
pub with_deployment_msg: Option<bool>,
}
#[derive(Deserialize)]
pub struct RawAppValue {
pub files: HashMap<String, String>,
}
+150
View File
@@ -1,4 +1,5 @@
use anyhow::Context;
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@@ -10,6 +11,48 @@ use crate::{
DB,
};
#[derive(Debug)]
pub struct IdToken {
token: String,
expiration: DateTime<Utc>,
}
pub fn has_expired(expiration_time: DateTime<Utc>, take: Option<Duration>) -> bool {
let now = Utc::now();
let expiration = match take {
Some(duration) => expiration_time - duration,
None => expiration_time,
};
now > expiration
}
impl From<IdToken> for String {
fn from(value: IdToken) -> Self {
value.token
}
}
impl ToString for IdToken {
fn to_string(&self) -> String {
self.token.clone()
}
}
impl IdToken {
pub fn new(token: String, expiration: DateTime<Utc>) -> Self {
Self { token, expiration }
}
pub fn token(&self) -> &str {
&self.token
}
pub fn expiration(&self) -> &DateTime<Utc> {
&self.expiration
}
}
#[derive(Deserialize, Serialize)]
pub struct JWTAuthClaims {
pub email: String,
@@ -267,3 +310,110 @@ pub async fn create_token_for_owner(
Ok(format!("jwt_{}", token))
}
#[cfg(feature = "aws_auth")]
pub mod aws {
use crate::error::to_anyhow;
use super::*;
use crate::utils::empty_string_as_none;
use aws_config::{BehaviorVersion, Region};
use aws_sdk_sts::{
config::Credentials as AwsCredentials,
operation::{
assume_role_with_saml::AssumeRoleWithSamlOutput,
assume_role_with_web_identity::AssumeRoleWithWebIdentityOutput,
},
types::Credentials,
Client,
};
pub const AWS_OIDC_AUDIENCE: &'static str = "sts.amazonaws.com";
pub trait GetAuthenticationOutput {
fn get_credentials(&self) -> Result<&Credentials>;
}
impl GetAuthenticationOutput for AssumeRoleWithSamlOutput {
fn get_credentials(&self) -> Result<&Credentials> {
let credentials = self.credentials.as_ref().ok_or(Error::BadGateway(
"Error fetching credentials from AWS STS".to_string(),
))?;
Ok(credentials)
}
}
impl GetAuthenticationOutput for AssumeRoleWithWebIdentityOutput {
fn get_credentials(&self) -> Result<&Credentials> {
let credentials = self.credentials.as_ref().ok_or(Error::BadGateway(
"Error fetching credentials from AWS STS".to_string(),
))?;
Ok(credentials)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "AWS_AUTH_RESOURCE_TYPE", rename_all = "lowercase")]
#[serde(rename_all = "lowercase")]
pub enum AwsAuthResourceType {
Credentials,
Oidc,
}
#[derive(Debug, Deserialize)]
pub struct CredentialsAuth {
#[serde(deserialize_with = "empty_string_as_none")]
pub region: Option<String>,
#[serde(rename = "awsAccessKeyId")]
pub aws_access_key_id: String,
#[serde(rename = "awsSecretAccessKey")]
pub aws_secret_access_key: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct OidcAuth {
#[serde(deserialize_with = "empty_string_as_none")]
pub region: Option<String>,
#[serde(rename = "roleArn")]
pub role_arn: String,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum AWSAuthConfig {
Credentials(CredentialsAuth),
Oidc(OidcAuth),
}
pub async fn get_oidc_authentication_data(
oidc_auth: OidcAuth,
role_session_name: Option<impl ToString>,
token: String,
) -> Result<AssumeRoleWithWebIdentityOutput> {
let region = oidc_auth.region.unwrap_or_else(|| "us-east-1".to_string());
let credentials = AwsCredentials::new("", "", None, None, "UserInput");
let config = aws_config::defaults(BehaviorVersion::latest())
.credentials_provider(credentials)
.region(Region::new(region.clone()))
.load()
.await;
let assume_role_with_web_identity_fluent_builder = Client::new(&config)
.assume_role_with_web_identity()
.set_role_arn(Some(oidc_auth.role_arn))
.set_role_session_name(role_session_name.map(|str| str.to_string()))
.set_web_identity_token(Some(token));
let resp = assume_role_with_web_identity_fluent_builder
.clone()
.send()
.await
.map_err(to_anyhow)?;
Ok(resp)
}
}
@@ -476,3 +476,7 @@ impl CredentialProvider for AwsCredentialAdapter {
pub fn bundle(w_id: &str, hash: &str) -> String {
format!("script_bundle/{}/{}", w_id, hash)
}
pub fn raw_app(w_id: &str, version: &i64) -> String {
format!("/home/rfiszel/raw_app/{}/{}", w_id, version)
}
+1 -2
View File
@@ -19,8 +19,7 @@ use git_version::git_version;
use chrono::Utc;
use croner::Cron;
use rand::distr::Alphanumeric;
use rand::{rng, Rng};
use rand::{distr::Alphanumeric, rng, Rng};
use reqwest::Client;
use semver::Version;
use serde::{Deserialize, Deserializer, Serialize};
+1
View File
@@ -4101,6 +4101,7 @@ pub async fn push<'c, 'd>(
} else if job_kind == JobKind::Dependencies
|| job_kind == JobKind::FlowDependencies
|| job_kind == JobKind::DeploymentCallback
|| job_kind == JobKind::AppDependencies
{
// using the dependency tag for deployment callback for now. We can create a separate tag when we need
"dependency".to_string()
+2
View File
@@ -49,6 +49,8 @@ mod worker_flow;
mod worker_lockfiles;
mod worker_utils;
pub use worker_lockfiles::process_relative_imports;
pub use worker::*;
pub use result_processor::handle_job_error;
+24 -10
View File
@@ -6,8 +6,10 @@ use itertools::Itertools;
use mysql_async::{
consts::ColumnType, prelude::*, FromValueError, OptsBuilder, Params, Row, SslOpts,
};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use serde_json::{json, value::RawValue, Value};
use std::str::FromStr;
use tokio::sync::Mutex;
use windmill_common::{
error::{to_anyhow, Error},
@@ -303,26 +305,38 @@ pub async fn do_mysql(
return Ok(raw_result);
}
// 2023-12-01T16:18:00.000Z
static DATE_REGEX_TZ: Lazy<regex::Regex> = Lazy::new(|| {
regex::Regex::new(r"(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.(\d+)Z").unwrap()
});
// 2025-04-21 10:08:00
static DATE_REGEX: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})").unwrap());
fn string_date_to_mysql_date(s: &str) -> mysql_async::Value {
// 2023-12-01T16:18:00.000Z
let re = regex::Regex::new(r"(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.(\d+)Z").unwrap();
let caps = re.captures(s);
let caps = DATE_REGEX_TZ.captures(s).or_else(|| DATE_REGEX.captures(s));
if let Some(caps) = caps {
mysql_async::Value::Date(
caps.get(1).unwrap().as_str().parse().unwrap_or_default(),
caps.get(2).unwrap().as_str().parse().unwrap_or_default(),
caps.get(3).unwrap().as_str().parse().unwrap_or_default(),
caps.get(4).unwrap().as_str().parse().unwrap_or_default(),
caps.get(5).unwrap().as_str().parse().unwrap_or_default(),
caps.get(6).unwrap().as_str().parse().unwrap_or_default(),
caps.get(7).unwrap().as_str().parse().unwrap_or_default(),
get_capture_by_index(&caps, 1),
get_capture_by_index(&caps, 2),
get_capture_by_index(&caps, 3),
get_capture_by_index(&caps, 4),
get_capture_by_index(&caps, 5),
get_capture_by_index(&caps, 6),
get_capture_by_index(&caps, 7),
)
} else {
mysql_async::Value::Date(0, 0, 0, 0, 0, 0, 0)
}
}
fn get_capture_by_index<T: FromStr + Default>(caps: &regex::Captures, n: usize) -> T {
caps.get(n)
.and_then(|s| s.as_str().parse::<T>().ok())
.unwrap_or_default()
}
fn convert_row_to_value(row: Row) -> serde_json::Value {
let mut map = serde_json::Map::new();
+18 -10
View File
@@ -284,6 +284,8 @@ lazy_static::lazy_static! {
let mut proxy_env = Vec::new();
if let Some(no_proxy) = NO_PROXY.as_ref() {
proxy_env.push(("NO_PROXY", no_proxy.to_string()));
} else if HTTPS_PROXY.is_some() || HTTP_PROXY.is_some() {
proxy_env.push(("NO_PROXY", "localhost,127.0.0.1".to_string()));
}
if let Some(http_proxy) = HTTP_PROXY.as_ref() {
proxy_env.push(("HTTP_PROXY", http_proxy.to_string()));
@@ -2457,16 +2459,22 @@ async fn handle_code_execution_job(
_ => None,
};
arc_data =
preview.ok_or_else(|| Error::internal_err("expected preview".to_string()))?;
metadata = ScriptMetadata {
language: job.script_lang,
codebase,
envs: None,
schema: None,
schema_validator: None,
};
(arc_data.as_ref(), &metadata)
if codebase.is_none() && job.runnable_id.is_some() {
(arc_data, arc_metadata) =
cache::script::fetch(conn, job.runnable_id.unwrap()).await?;
(arc_data.as_ref(), arc_metadata.as_ref())
} else {
arc_data =
preview.ok_or_else(|| Error::internal_err("expected preview".to_string()))?;
metadata = ScriptMetadata {
language: job.script_lang,
codebase,
envs: None,
schema: None,
schema_validator: None,
};
(arc_data.as_ref(), &metadata)
}
}
JobKind::Script_Hub => {
let ContentReqLangEnvs { content, lockfile, language, envs, codebase, schema } =
+151 -38
View File
@@ -368,44 +368,21 @@ pub async fn handle_dependency_job(
tracing::error!(%e, "error handling deployment metadata");
}
let relative_imports =
extract_relative_imports(&script_data.code, script_path, &job.script_lang);
if let Some(relative_imports) = relative_imports {
update_script_dependency_map(
&job.id,
db,
w_id,
&parent_path,
script_path,
relative_imports,
)
.await?;
let already_visited = job
.args
.as_ref()
.map(|x| {
x.get("already_visited")
.map(|v| serde_json::from_str::<Vec<String>>(v.get()).ok())
.flatten()
})
.flatten()
.unwrap_or_default();
if let Err(e) = trigger_dependents_to_recompute_dependencies(
w_id,
script_path,
deployment_message,
parent_path,
&job.permissioned_as_email,
&job.created_by,
&job.permissioned_as,
db,
already_visited,
)
.await
{
tracing::error!(%e, "error triggering dependents to recompute dependencies");
}
}
process_relative_imports(
db,
Some(job.id),
job.args.as_ref(),
&job.workspace_id,
script_path,
parent_path,
deployment_message,
&script_data.code,
&job.script_lang,
&job.permissioned_as_email,
&job.created_by,
&job.permissioned_as,
)
.await?;
Ok(to_raw_value_owned(
json!({ "status": "Successful lock file generation", "lock": content }),
@@ -443,6 +420,58 @@ fn remove_ansi_codes(s: &str) -> String {
ANSI_REGEX.replace_all(s, "").to_string()
}
pub async fn process_relative_imports(
db: &sqlx::Pool<sqlx::Postgres>,
job_id: Option<Uuid>,
args: Option<&Json<HashMap<String, Box<RawValue>>>>,
w_id: &str,
script_path: &str,
parent_path: Option<String>,
deployment_message: Option<String>,
code: &str,
script_lang: &Option<ScriptLang>,
permissioned_as_email: &str,
created_by: &str,
permissioned_as: &str,
) -> error::Result<()> {
let relative_imports = extract_relative_imports(&code, script_path, script_lang);
if let Some(relative_imports) = relative_imports {
update_script_dependency_map(
&job_id.unwrap_or_else(|| Uuid::nil()),
db,
w_id,
&parent_path,
script_path,
relative_imports,
)
.await?;
let already_visited = args
.map(|x| {
x.get("already_visited")
.map(|v| serde_json::from_str::<Vec<String>>(v.get()).ok())
.flatten()
})
.flatten()
.unwrap_or_default();
if let Err(e) = trigger_dependents_to_recompute_dependencies(
w_id,
script_path,
deployment_message,
parent_path,
permissioned_as_email,
created_by,
permissioned_as,
db,
already_visited,
)
.await
{
tracing::error!(%e, "error triggering dependents to recompute dependencies");
}
}
Ok(())
}
async fn trigger_dependents_to_recompute_dependencies(
w_id: &str,
script_path: &str,
@@ -1735,6 +1764,90 @@ pub async fn handle_app_dependency_job(
}
}
// async fn upload_raw_app(
// app_value: &RawAppValue,
// job: &QueuedJob,
// mem_peak: &mut i32,
// canceled_by: &mut Option<CanceledBy>,
// job_dir: &str,
// db: &sqlx::Pool<sqlx::Postgres>,
// worker_name: &str,
// occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
// version: i64,
// ) -> Result<()> {
// let mut entrypoint = "index.ts";
// for file in app_value.files.iter() {
// if file.0 == "/index.tsx" {
// entrypoint = "index.tsx";
// } else if file.0 == "/index.js" {
// entrypoint = "index.js";
// }
// write_file(&job_dir, file.0, &file.1)?;
// }
// let common_bun_proc_envs: HashMap<String, String> = get_common_bun_proc_envs(None).await;
// install_bun_lockfile(
// mem_peak,
// canceled_by,
// &job.id,
// &job.workspace_id,
// Some(db),
// job_dir,
// worker_name,
// common_bun_proc_envs,
// false,
// occupancy_metrics,
// )
// .await?;
// let mut cmd = tokio::process::Command::new("esbuild");
// let mut args = "--bundle --minify --outdir=dist/"
// .split(' ')
// .collect::<Vec<_>>();
// args.push(entrypoint);
// cmd.current_dir(job_dir)
// .env_clear()
// .args(args)
// .stdout(Stdio::piped())
// .stderr(Stdio::piped());
// let child = start_child_process(cmd, "esbuild").await?;
// crate::handle_child::handle_child(
// &job.id,
// db,
// mem_peak,
// canceled_by,
// child,
// false,
// worker_name,
// &job.workspace_id,
// "esbuild",
// Some(30),
// false,
// occupancy_metrics,
// )
// .await?;
// let output_dir = format!("{}/dist", job_dir);
// let target_dir = format!("/home/rfiszel/wmill/{}/{}", job.workspace_id, version);
// tokio::fs::create_dir_all(&target_dir).await?;
// tracing::info!("Copying files from {} to {}", output_dir, target_dir);
// let index_ts = format!("{}/index.js", output_dir);
// let index_css = format!("{}/index.css", output_dir);
// if tokio::fs::metadata(&index_ts).await.is_ok() {
// tokio::fs::copy(&index_ts, format!("{}/index.js", target_dir)).await?;
// }
// if tokio::fs::metadata(&index_css).await.is_ok() {
// tokio::fs::copy(&index_css, format!("{}/index.css", target_dir)).await?;
// }
// // let file_path = format!("/home/rfiszel/wmill/{}/{}", job.workspace_id, version);
// Ok(())
// }
#[cfg(feature = "python")]
async fn python_dep(
reqs: String,
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.481.0";
export const VERSION = "v1.483.2";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+1
View File
@@ -1 +1,2 @@
npm/
gen/
-21
View File
@@ -1,21 +0,0 @@
import type { ApiRequestOptions } from './ApiRequestOptions.ts';
import type { ApiResult } from './ApiResult.ts';
export class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: unknown;
public readonly request: ApiRequestOptions;
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
super(message);
this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
}
-21
View File
@@ -1,21 +0,0 @@
export type ApiRequestOptions<T = unknown> = {
readonly body?: any;
readonly cookies?: Record<string, unknown>;
readonly errors?: Record<number | string, string>;
readonly formData?: Record<string, unknown> | any[] | Blob | File;
readonly headers?: Record<string, unknown>;
readonly mediaType?: string;
readonly method:
| 'DELETE'
| 'GET'
| 'HEAD'
| 'OPTIONS'
| 'PATCH'
| 'POST'
| 'PUT';
readonly path?: Record<string, unknown>;
readonly query?: Record<string, unknown>;
readonly responseHeader?: string;
readonly responseTransformer?: (data: unknown) => Promise<T>;
readonly url: string;
};
-7
View File
@@ -1,7 +0,0 @@
export type ApiResult<TData = any> = {
readonly body: TData;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly url: string;
};
-126
View File
@@ -1,126 +0,0 @@
export class CancelError extends Error {
constructor(message: string) {
super(message);
this.name = 'CancelError';
}
public get isCancelled(): boolean {
return true;
}
}
export interface OnCancel {
readonly isResolved: boolean;
readonly isRejected: boolean;
readonly isCancelled: boolean;
(cancelHandler: () => void): void;
}
export class CancelablePromise<T> implements Promise<T> {
private _isResolved: boolean;
private _isRejected: boolean;
private _isCancelled: boolean;
readonly cancelHandlers: (() => void)[];
readonly promise: Promise<T>;
private _resolve?: (value: T | PromiseLike<T>) => void;
private _reject?: (reason?: unknown) => void;
constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: unknown) => void,
onCancel: OnCancel
) => void
) {
this._isResolved = false;
this._isRejected = false;
this._isCancelled = false;
this.cancelHandlers = [];
this.promise = new Promise<T>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this._isResolved = true;
if (this._resolve) this._resolve(value);
};
const onReject = (reason?: unknown): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this._isRejected = true;
if (this._reject) this._reject(reason);
};
const onCancel = (cancelHandler: () => void): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this.cancelHandlers.push(cancelHandler);
};
Object.defineProperty(onCancel, 'isResolved', {
get: (): boolean => this._isResolved,
});
Object.defineProperty(onCancel, 'isRejected', {
get: (): boolean => this._isRejected,
});
Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this._isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
get [Symbol.toStringTag]() {
return "Cancellable Promise";
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2> {
return this.promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null
): Promise<T | TResult> {
return this.promise.catch(onRejected);
}
public finally(onFinally?: (() => void) | null): Promise<T> {
return this.promise.finally(onFinally);
}
public cancel(): void {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this._isCancelled = true;
if (this.cancelHandlers.length) {
try {
for (const cancelHandler of this.cancelHandlers) {
cancelHandler();
}
} catch (error) {
console.warn('Cancellation threw an error', error);
return;
}
}
this.cancelHandlers.length = 0;
if (this._reject) this._reject(new CancelError('Request aborted'));
}
public get isCancelled(): boolean {
return this._isCancelled;
}
}
-63
View File
@@ -1,63 +0,0 @@
const getEnv = (key: string) => {
return Deno.env.get(key)
};
const baseUrl = getEnv("BASE_INTERNAL_URL") ?? getEnv("BASE_URL") ?? "http://localhost:8000";
const baseUrlApi = (baseUrl ?? '') + "/api";
import type { ApiRequestOptions } from './ApiRequestOptions.ts';
type Headers = Record<string, string>;
type Middleware<T> = (value: T) => T | Promise<T>;
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
export class Interceptors<T> {
_fns: Middleware<T>[];
constructor() {
this._fns = [];
}
eject(fn: Middleware<T>): void {
const index = this._fns.indexOf(fn);
if (index !== -1) {
this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)];
}
}
use(fn: Middleware<T>): void {
this._fns = [...this._fns, fn];
}
}
export type OpenAPIConfig = {
BASE: string;
CREDENTIALS: 'include' | 'omit' | 'same-origin';
ENCODE_PATH?: ((path: string) => string) | undefined;
HEADERS?: Headers | Resolver<Headers> | undefined;
PASSWORD?: string | Resolver<string> | undefined;
TOKEN?: string | Resolver<string> | undefined;
USERNAME?: string | Resolver<string> | undefined;
VERSION: string;
WITH_CREDENTIALS: boolean;
interceptors: {
request: Interceptors<RequestInit>;
response: Interceptors<Response>;
};
};
export const OpenAPI: OpenAPIConfig = {
BASE: baseUrlApi,
CREDENTIALS: 'include',
ENCODE_PATH: undefined,
HEADERS: undefined,
PASSWORD: undefined,
TOKEN: getEnv("WM_TOKEN"),
USERNAME: undefined,
VERSION: '1.481.0',
WITH_CREDENTIALS: true,
interceptors: {
request: new Interceptors(),
response: new Interceptors(),
},
};
-350
View File
@@ -1,350 +0,0 @@
import { ApiError } from './ApiError.ts';
import type { ApiRequestOptions } from './ApiRequestOptions.ts';
import type { ApiResult } from './ApiResult.ts';
import { CancelablePromise } from './CancelablePromise.ts';
import type { OnCancel } from './CancelablePromise.ts';
import type { OpenAPIConfig } from './OpenAPI.ts';
export const isString = (value: unknown): value is string => {
return typeof value === 'string';
};
export const isStringWithValue = (value: unknown): value is string => {
return isString(value) && value !== '';
};
export const isBlob = (value: any): value is Blob => {
return value instanceof Blob;
};
export const isFormData = (value: unknown): value is FormData => {
return value instanceof FormData;
};
export const base64 = (str: string): string => {
try {
return btoa(str);
} catch (err) {
// @ts-ignore
return Buffer.from(str).toString('base64');
}
};
export const getQueryString = (params: Record<string, unknown>): string => {
const qs: string[] = [];
const append = (key: string, value: unknown) => {
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
};
const encodePair = (key: string, value: unknown) => {
if (value === undefined || value === null) {
return;
}
if (value instanceof Date) {
append(key, value.toISOString());
} else if (Array.isArray(value)) {
value.forEach(v => encodePair(key, v));
} else if (typeof value === 'object') {
Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v));
} else {
append(key, value);
}
};
Object.entries(params).forEach(([key, value]) => encodePair(key, value));
return qs.length ? `?${qs.join('&')}` : '';
};
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
const encoder = config.ENCODE_PATH || encodeURI;
const path = options.url
.replace('{api-version}', config.VERSION)
.replace(/{(.*?)}/g, (substring: string, group: string) => {
if (options.path?.hasOwnProperty(group)) {
return encoder(String(options.path[group]));
}
return substring;
});
const url = config.BASE + path;
return options.query ? url + getQueryString(options.query) : url;
};
export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
if (options.formData) {
const formData = new FormData();
const process = (key: string, value: unknown) => {
if (isString(value) || isBlob(value)) {
formData.append(key, value);
} else {
formData.append(key, JSON.stringify(value));
}
};
Object.entries(options.formData)
.filter(([, value]) => value !== undefined && value !== null)
.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => process(key, v));
} else {
process(key, value);
}
});
return formData;
}
return undefined;
};
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
export const resolve = async <T>(options: ApiRequestOptions<T>, resolver?: T | Resolver<T>): Promise<T | undefined> => {
if (typeof resolver === 'function') {
return (resolver as Resolver<T>)(options);
}
return resolver;
};
export const getHeaders = async <T>(config: OpenAPIConfig, options: ApiRequestOptions<T>): Promise<Headers> => {
const [token, username, password, additionalHeaders] = await Promise.all([
// @ts-ignore
resolve(options, config.TOKEN),
// @ts-ignore
resolve(options, config.USERNAME),
// @ts-ignore
resolve(options, config.PASSWORD),
// @ts-ignore
resolve(options, config.HEADERS),
]);
const headers = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
})
.filter(([, value]) => value !== undefined && value !== null)
.reduce((headers, [key, value]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);
if (isStringWithValue(token)) {
headers['Authorization'] = `Bearer ${token}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers['Authorization'] = `Basic ${credentials}`;
}
if (options.body !== undefined) {
if (options.mediaType) {
headers['Content-Type'] = options.mediaType;
} else if (isBlob(options.body)) {
headers['Content-Type'] = options.body.type || 'application/octet-stream';
} else if (isString(options.body)) {
headers['Content-Type'] = 'text/plain';
} else if (!isFormData(options.body)) {
headers['Content-Type'] = 'application/json';
}
}
return new Headers(headers);
};
export const getRequestBody = (options: ApiRequestOptions): unknown => {
if (options.body !== undefined) {
if (options.mediaType?.includes('application/json') || options.mediaType?.includes('+json')) {
return JSON.stringify(options.body);
} else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) {
return options.body;
} else {
return JSON.stringify(options.body);
}
}
return undefined;
};
export const sendRequest = async (
config: OpenAPIConfig,
options: ApiRequestOptions,
url: string,
body: any,
formData: FormData | undefined,
headers: Headers,
onCancel: OnCancel
): Promise<Response> => {
const controller = new AbortController();
let request: RequestInit = {
headers,
body: body ?? formData,
method: options.method,
signal: controller.signal,
};
if (config.WITH_CREDENTIALS) {
request.credentials = config.CREDENTIALS;
}
for (const fn of config.interceptors.request._fns) {
request = await fn(request);
}
onCancel(() => controller.abort());
return await fetch(url, request);
};
export const getResponseHeader = (response: Response, responseHeader?: string): string | undefined => {
if (responseHeader) {
const content = response.headers.get(responseHeader);
if (isString(content)) {
return content;
}
}
return undefined;
};
export const getResponseBody = async (response: Response): Promise<unknown> => {
if (response.status !== 204) {
try {
const contentType = response.headers.get('Content-Type');
if (contentType) {
const binaryTypes = ['application/octet-stream', 'application/pdf', 'application/zip', 'audio/', 'image/', 'video/'];
if (contentType.includes('application/json') || contentType.includes('+json')) {
return await response.json();
} else if (binaryTypes.some(type => contentType.includes(type))) {
return await response.blob();
} else if (contentType.includes('multipart/form-data')) {
return await response.formData();
} else if (contentType.includes('text/')) {
return await response.text();
}
}
} catch (error) {
console.error(error);
}
}
return undefined;
};
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
const errors: Record<number, string> = {
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Timeout',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Payload Too Large',
414: 'URI Too Long',
415: 'Unsupported Media Type',
416: 'Range Not Satisfiable',
417: 'Expectation Failed',
418: 'Im a teapot',
421: 'Misdirected Request',
422: 'Unprocessable Content',
423: 'Locked',
424: 'Failed Dependency',
425: 'Too Early',
426: 'Upgrade Required',
428: 'Precondition Required',
429: 'Too Many Requests',
431: 'Request Header Fields Too Large',
451: 'Unavailable For Legal Reasons',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
505: 'HTTP Version Not Supported',
506: 'Variant Also Negotiates',
507: 'Insufficient Storage',
508: 'Loop Detected',
510: 'Not Extended',
511: 'Network Authentication Required',
...options.errors,
}
const error = errors[result.status];
if (error) {
throw new ApiError(options, result, error);
}
if (!result.ok) {
const errorStatus = result.status ?? 'unknown';
const errorStatusText = result.statusText ?? 'unknown';
const errorBody = (() => {
try {
return JSON.stringify(result.body, null, 2);
} catch (e) {
return undefined;
}
})();
throw new ApiError(options, result,
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
);
}
};
/**
* Request method
* @param config The OpenAPI configuration object
* @param options The request options from the service
* @returns CancelablePromise<T>
* @throws ApiError
*/
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions<T>): CancelablePromise<T> => {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(config, options);
if (!onCancel.isCancelled) {
let response = await sendRequest(config, options, url, body, formData, headers, onCancel);
for (const fn of config.interceptors.response._fns) {
response = await fn(response);
}
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
let transformedBody = responseBody;
if (options.responseTransformer && response.ok) {
transformedBody = await options.responseTransformer(responseBody)
}
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader ?? transformedBody,
};
catchErrorCodes(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}
});
};
-6
View File
@@ -1,6 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
export { ApiError } from './core/ApiError.ts';
export { CancelablePromise, CancelError } from './core/CancelablePromise.ts';
export { OpenAPI, type OpenAPIConfig } from './core/OpenAPI.ts';
export * from './services.gen.ts';
export * from './types.gen.ts';
File diff suppressed because one or more lines are too long
-7828
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -63,7 +63,7 @@ export {
// }
// });
export const VERSION = "1.481.0";
export const VERSION = "1.483.2";
const command = new Command()
.name("wmill")
+21 -8
View File
@@ -28,7 +28,7 @@ type Trigger = {
postgres: PostgresTrigger;
mqtt: MqttTrigger;
sqs: SqsTrigger;
gcp: GcpTrigger
gcp: GcpTrigger;
};
type TriggerFile<K extends TriggerType> = Omit<
@@ -62,7 +62,7 @@ async function getTrigger<K extends TriggerType>(
postgres: wmill.getPostgresTrigger,
mqtt: wmill.getMqttTrigger,
sqs: wmill.getSqsTrigger,
gcp: wmill.getGcpTrigger
gcp: wmill.getGcpTrigger,
};
const triggerFunction = triggerFunctions[triggerType];
@@ -90,7 +90,9 @@ async function updateTrigger<K extends TriggerType>(
postgres: wmill.updatePostgresTrigger,
mqtt: wmill.updateMqttTrigger,
sqs: wmill.updateSqsTrigger,
gcp: wmill.updateGcpTrigger
gcp: async (args) => {
throw new Error("GCP triggers are not supported yet");
},
};
const triggerFunction = triggerFunctions[triggerType];
await triggerFunction({ workspace, path, requestBody: trigger });
@@ -116,7 +118,9 @@ async function createTrigger<K extends TriggerType>(
postgres: wmill.createPostgresTrigger,
mqtt: wmill.createMqttTrigger,
sqs: wmill.createSqsTrigger,
gcp: wmill.createGcpTrigger
gcp: async (args) => {
throw new Error("GCP triggers are not supported yet");
},
};
const triggerFunction = triggerFunctions[triggerType];
await triggerFunction({ workspace, path, requestBody: trigger });
@@ -197,8 +201,8 @@ async function list(opts: GlobalOptions) {
workspace: workspace.workspaceId,
});
const gcpTriggers = await wmill.listGcpTriggers({
workspace: workspace.workspaceId
})
workspace: workspace.workspaceId,
});
const triggers = [
...httpTriggers.map((x) => ({ path: x.path, kind: "http" })),
...websocketTriggers.map((x) => ({ path: x.path, kind: "websocket" })),
@@ -207,7 +211,7 @@ async function list(opts: GlobalOptions) {
...postgresTriggers.map((x) => ({ path: x.path, kind: "postgres" })),
...mqttTriggers.map((x) => ({ path: x.path, kind: "mqtt" })),
...sqsTriggers.map((x) => ({ path: x.path, kind: "sqs" })),
...gcpTriggers.map((x) => ({ path: x.path, kind: "gcp" }))
...gcpTriggers.map((x) => ({ path: x.path, kind: "gcp" })),
];
new Table()
@@ -221,7 +225,16 @@ async function list(opts: GlobalOptions) {
function checkIfValidTrigger(kind: string | undefined): kind is TriggerType {
if (
kind &&
["http", "websocket", "kafka", "nats", "postgres", "mqtt", "sqs", "gcp"].includes(kind)
[
"http",
"websocket",
"kafka",
"nats",
"postgres",
"mqtt",
"sqs",
"gcp",
].includes(kind)
) {
return true;
} else {
@@ -0,0 +1,46 @@
# windmill-cloudformation
Cloudformation Template for Windmill on AWS EKS
## Overview
This CloudFormation template automatically deploys Windmill on AWS EKS. The deployment includes:
- An EKS cluster with configurable node types and sizes
- An RDS PostgreSQL database for Windmill data
- AWS Load Balancer Controller for handling ingress traffic
- Proper network configuration with VPC, subnets, and security groups
- A fully automated installation of Windmill via Helm
## Parameters
The template accepts various parameters to customize your deployment:
- **NodeInstanceType**: EC2 instance type for EKS worker nodes (t3.small to r5.2xlarge)
- **NodeGroupSize**: Number of EKS worker nodes
- **RdsInstanceClass**: RDS instance class for the PostgreSQL database (db.t3.micro to db.r5.2xlarge)
- **DBPassword**: Password for the PostgreSQL database
- **WorkerReplicas**: Number of Windmill worker replicas
- **NativeWorkerReplicas**: Number of Windmill native worker replicas
- **Enterprise**: Enable Windmill [Enterprise features](https://www.windmill.dev/docs/misc/plans_details#upgrading-to-enterprise-edition) (requires license key)
## Customization
To modify the Helm chart configuration or update the template, refer to the official Windmill Helm chart repository:
[https://github.com/windmill-labs/windmill-helm-charts](https://github.com/windmill-labs/windmill-helm-charts)
## Documentation
For more information about Windmill's Helm chart deployment options, see:
[https://www.windmill.dev/docs/advanced/self_host#helm-chart](https://www.windmill.dev/docs/advanced/self_host#helm-chart)
For detailed information about setting up RDS for Windmill on AWS:
[https://www.windmill.dev/docs/advanced/self_host/aws_ecs#create-a-rds-database](https://www.windmill.dev/docs/advanced/self_host/aws_ecs#create-a-rds-database)
## Deployment
1. Upload the CloudFormation template to your AWS account
2. Fill in the required parameters
3. Deploy the stack
4. Access Windmill using the URL provided in the Outputs section of the stack
After deployment, you can access Windmill via the LoadBalancer URL shown in the CloudFormation stack outputs.
@@ -0,0 +1,688 @@
AWSTemplateFormatVersion: "2010-09-09"
Description: Deploy Windmill on EKS with Helm
Parameters:
NodeInstanceType:
Type: String
Default: t3.medium
AllowedValues:
- t3.small
- t3.medium
- t3.large
- t3.xlarge
- t3.2xlarge
- m5.large
- m5.xlarge
- m5.2xlarge
- m5.4xlarge
- c5.large
- c5.xlarge
- c5.2xlarge
- r5.large
- r5.xlarge
- r5.2xlarge
Description: EC2 instance type for the EKS worker nodes
NodeGroupSize:
Type: Number
Default: 2
RdsInstanceClass:
Type: String
Default: db.t3.small
AllowedValues:
- db.t3.micro
- db.t3.small
- db.t3.medium
- db.t3.large
- db.t3.xlarge
- db.m5.large
- db.m5.xlarge
- db.m5.2xlarge
- db.r5.large
- db.r5.xlarge
- db.r5.2xlarge
Description: RDS instance class for the PostgreSQL database
DBPassword:
Type: String
NoEcho: true
WorkerReplicas:
Type: Number
Default: 2
NativeWorkerReplicas:
Type: Number
Default: 1
Enterprise:
Type: String
Default: false
AllowedValues:
- true
- false
Description: Enable Windmill Enterprise features (requires license key)
Mappings:
RegionMap:
us-east-1:
AMI: ami-0cff7528ff583bf9a
us-east-2:
AMI: ami-0cd3c7f72edd5b06d
us-west-1:
AMI: ami-0d9858aa3c6322f73
us-west-2:
AMI: ami-098e42ae54c764c35
ca-central-1:
AMI: ami-00f881f027a6d74a0
eu-west-1:
AMI: ami-04dd4500af104442f
eu-west-2:
AMI: ami-0eb260c4d5475b901
eu-west-3:
AMI: ami-05e8e20cef0eaa9d0
eu-central-1:
AMI: ami-0bad4a5e987bdebde
ap-northeast-1:
AMI: ami-0b7546e839d7ace12
ap-northeast-2:
AMI: ami-0fd0765afb77bcca7
ap-southeast-1:
AMI: ami-0c802847a7dd848c0
ap-southeast-2:
AMI: ami-07620139298af599e
ap-south-1:
AMI: ami-0851b76e8b1bce90b
sa-east-1:
AMI: ami-054a31f1b3bf90920
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub "${AWS::StackName}-vpc"
- Key: !Sub "kubernetes.io/cluster/${AWS::StackName}-cluster"
Value: shared
InternetGateway:
Type: AWS::EC2::InternetGateway
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: !Select [0, !GetAZs ""]
MapPublicIpOnLaunch: true
Tags:
- Key: kubernetes.io/role/elb
Value: "1"
- Key: !Sub "kubernetes.io/cluster/${AWS::StackName}-cluster"
Value: shared
- Key: Name
Value: !Sub ${AWS::StackName}-public-subnet-1
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.2.0/24
AvailabilityZone: !Select [1, !GetAZs ""]
MapPublicIpOnLaunch: true
Tags:
- Key: kubernetes.io/role/elb
Value: "1"
- Key: !Sub "kubernetes.io/cluster/${AWS::StackName}-cluster"
Value: shared
- Key: Name
Value: !Sub ${AWS::StackName}-public-subnet-2
RouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
PublicRoute:
Type: AWS::EC2::Route
DependsOn: AttachGateway
Properties:
RouteTableId: !Ref RouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
SubnetRouteTableAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
DependsOn: PublicRoute
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref RouteTable
SubnetRouteTableAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
DependsOn: PublicRoute
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref RouteTable
EKSClusterRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- eks.amazonaws.com
- ec2.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonEKSClusterPolicy
- arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
Policies:
- PolicyName: EKSAccess
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- eks:*
- ec2:DescribeInstances
- ec2:DescribeRouteTables
- ec2:DescribeSecurityGroups
- ec2:DescribeSubnets
- ec2:DescribeVpcs
- iam:GetRole
- iam:ListRoles
Resource: "*"
- Effect: Allow
Action:
- ssm:GetParameter
- ssm:PutParameter
Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}/*"
- PolicyName: KubernetesAccess
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- eks:DescribeCluster
- eks:ListClusters
- eks:AccessKubernetesApi
Resource: !Sub "arn:aws:eks:${AWS::Region}:${AWS::AccountId}:cluster/${AWS::StackName}-cluster"
EKSNodeRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
- arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
- arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
EKSCluster:
Type: AWS::EKS::Cluster
Properties:
Name: !Sub "${AWS::StackName}-cluster"
RoleArn: !GetAtt EKSClusterRole.Arn
ResourcesVpcConfig:
SubnetIds:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
EndpointPublicAccess: true
AccessConfig:
AuthenticationMode: API_AND_CONFIG_MAP
BootstrapClusterCreatorAdminPermissions: true
DependsOn:
- PublicRoute
- VPCCleanup
EKSClusterAccess:
Type: AWS::EKS::AccessEntry
Properties:
ClusterName: !Ref EKSCluster
PrincipalArn: !GetAtt EKSClusterRole.Arn
Type: STANDARD
Username: admin
AccessPolicies:
- PolicyArn: arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy
AccessScope:
Type: cluster
EKSNodeGroup:
Type: AWS::EKS::Nodegroup
DependsOn:
- WindmillDB
- EKSCluster
Properties:
ClusterName: !Ref EKSCluster
NodeRole: !GetAtt EKSNodeRole.Arn
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
ScalingConfig:
MinSize: 1
DesiredSize: !Ref NodeGroupSize
MaxSize: 4
InstanceTypes:
- !Ref NodeInstanceType
WindmillDBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnet group for RDS instance
SubnetIds:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
WindmillDBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow PostgreSQL access from EKS nodes
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
CidrIp: 10.0.0.0/16
WindmillDB:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceIdentifier: !Sub "${AWS::StackName}-db"
AllocatedStorage: 20
DBInstanceClass: !Ref RdsInstanceClass
Engine: postgres
EngineVersion: 17.2
MasterUsername: postgres
MasterUserPassword: !Ref DBPassword
DBName: windmill
PubliclyAccessible: false
DBSubnetGroupName: !Ref WindmillDBSubnetGroup
VPCSecurityGroups:
- !Ref WindmillDBSecurityGroup
DependsOn:
- WindmillDBSubnetGroup
WindmillInstallerInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- !Ref EKSClusterRole
WindmillInstallerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for Windmill installer instance
VpcId: !Ref VPC
SecurityGroupEgress:
- IpProtocol: -1
FromPort: -1
ToPort: -1
CidrIp: 0.0.0.0/0
WindmillInstaller:
Type: AWS::EC2::Instance
CreationPolicy:
ResourceSignal:
Timeout: PT30M # Gives 30 minutes for the installation to complete
DependsOn:
- EKSNodeGroup
- WindmillDB
Properties:
ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", AMI]
InstanceType: t3.micro
IamInstanceProfile: !Ref WindmillInstallerInstanceProfile
SubnetId: !Ref PublicSubnet1
SecurityGroupIds:
- !Ref WindmillInstallerSecurityGroup
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -e # Exit on any error
# Install required tools
yum update -y
yum install -y aws-cli jq postgresql15 aws-cfn-bootstrap
# Set up logging directory with correct permissions
mkdir -p /var/log/windmill-installer
touch /var/log/windmill-installer/install.log
# Create installation directory
mkdir -p /opt/windmill-installer
# Install kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
mv kubectl /usr/local/bin/
# Install helm
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod +x get_helm.sh
./get_helm.sh
# Create the installation script
cat << 'EOF' > /opt/windmill-installer/install.sh
#!/bin/bash
set -e
# Configure kubectl
aws sts get-caller-identity > /dev/null
export AWS_SDK_LOAD_CONFIG=1
export KUBECONFIG=/root/.kube/config
aws eks update-kubeconfig --name ${AWS::StackName}-cluster --region ${AWS::Region}
# Add debugging for each kubectl attempt
echo "HOME: $HOME"
echo "KUBECONFIG: $KUBECONFIG"
echo "User: $(whoami)"
echo "AWS Identity: $(aws sts get-caller-identity)"
echo "Trying kubectl command..."
kubectl get nodes || echo "Command failed with status $?"
echo "Waiting for EKS nodes to be ready..."
while true; do
# Force credential refresh on each attempt
aws sts get-caller-identity > /dev/null
aws eks update-kubeconfig --name ${AWS::StackName}-cluster --region ${AWS::Region}
if kubectl get nodes &>/dev/null; then
READY_NODES=$(kubectl get nodes -o json | jq -r '.items[] | select(.status.conditions[] | select(.type=="Ready" and .status=="True")) | .metadata.name' | wc -l)
DESIRED_NODES=${NodeGroupSize}
if [ "$READY_NODES" -eq "$DESIRED_NODES" ]; then
echo "All nodes are ready"
break
fi
echo "Found $READY_NODES ready nodes out of $DESIRED_NODES desired nodes"
else
echo "Waiting for cluster access..."
fi
sleep 30
done
echo "Installing AWS Load Balancer Controller..."
# Install AWS Load Balancer Controller
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName=${AWS::StackName}-cluster \
--set region=${AWS::Region} \
--set vpcId=${VPC}
echo "Waiting for AWS Load Balancer Controller to be ready..."
kubectl wait --namespace kube-system \
--for=condition=ready pod \
--selector=app.kubernetes.io/name=aws-load-balancer-controller \
--timeout=300s
echo "Waiting for RDS to be available..."
while true; do
if pg_isready -h ${WindmillDB.Endpoint.Address} -p 5432 -U postgres 2>/dev/null; then
echo "Database is ready"
break
fi
echo "Database not ready yet..."
sleep 30
done
echo "Creating namespace and installing Windmill..."
kubectl create namespace windmill
# Add helm repo and install Windmill
helm repo add windmill https://windmill-labs.github.io/windmill-helm-charts
helm repo update
helm install ${AWS::StackName} windmill/windmill \
--namespace windmill \
--set windmill.databaseUrl="postgres://postgres:${DBPassword}@${WindmillDB.Endpoint.Address}/windmill?sslmode=require" \
--set windmill.baseDomain=windmill.local \
--set windmill.baseProtocol=http \
--set windmill.appReplicas=${WorkerReplicas} \
--set windmill.lspReplicas=2 \
--set windmill.workerGroups[0].name=default \
--set windmill.workerGroups[0].mode=worker \
--set windmill.workerGroups[0].replicas=${WorkerReplicas} \
--set windmill.workerGroups[1].name=native \
--set windmill.workerGroups[1].mode=worker \
--set windmill.workerGroups[1].replicas=${NativeWorkerReplicas} \
--set windmill.app.service.spec.type=LoadBalancer \
--set windmill.app.service.spec.sessionAffinity=None \
--set windmill.app.service.port=8000 \
--set windmill.app.service.ports[0].port=8000 \
--set windmill.app.service.ports[0].targetPort=8000 \
--set windmill.app.service.ports[0].protocol=TCP \
--set postgresql.enabled=false \
--set enterprise.enabled=${Enterprise}
# Change service to LoadBalancer
echo "Changing service to LoadBalancer"
kubectl patch service windmill-app -n windmill -p '{"spec":{"type":"LoadBalancer","sessionAffinity":"None"}}'
kubectl patch service windmill-app -n windmill -p '{"spec":{"ports":[{"name":"api","port":8000,"targetPort":8000,"protocol":"TCP"},{"name":"http","port":80,"targetPort":8000,"protocol":"TCP"}]}}'
# Wait for LoadBalancer to get an address
echo "Waiting for LoadBalancer address..."
while true; do
LB_HOSTNAME=$(kubectl get svc -n windmill windmill-app -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null)
if [ ! -z "$LB_HOSTNAME" ]; then
break
fi
echo "Waiting for LoadBalancer hostname..."
sleep 30
done
# Store the LoadBalancer hostname in SSM
aws ssm put-parameter \
--region ${AWS::Region} \
--name "/${AWS::StackName}/loadbalancer-hostname" \
--value "$LB_HOSTNAME" \
--type "String" \
--overwrite
# Signal CloudFormation that installation is complete
echo "Signal CloudFormation that installation is complete"
/opt/aws/bin/cfn-signal -e $? \
--stack ${AWS::StackName} \
--resource WindmillInstaller \
--region ${AWS::Region}
# Self-terminate this instance
echo "Self-terminating instance"
aws ec2 terminate-instances --instance-ids $(curl -s http://169.254.169.254/latest/meta-data/instance-id) --region ${AWS::Region}
EOF
# Set permissions and run the installation script
chmod +x /opt/windmill-installer/install.sh
# Run the installation script directly (not as ec2-user)
cd /opt/windmill-installer && ./install.sh > /var/log/windmill-installer/install.log 2>&1
LoadBalancerHostnameLookup:
Type: Custom::SSMParameterLookup
DependsOn: WindmillInstaller
Properties:
ServiceToken: !GetAtt LookupSSMParameterFunction.Arn
ParameterName: !Sub "/${AWS::StackName}/loadbalancer-hostname"
LookupSSMParameterFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Role: !GetAtt LambdaExecutionRole.Arn
Timeout: 300
Runtime: nodejs18.x
Code:
ZipFile: !Sub |
const { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm');
const response = require('cfn-response');
exports.handler = async (event, context) => {
if (event.RequestType === 'Delete') {
return response.send(event, context, response.SUCCESS);
}
try {
const ssmClient = new SSMClient();
// Loop to check for the parameter until it's not "pending"
let tries = 0;
let paramValue = "pending";
while (paramValue === "pending" && tries < 20) {
const params = {
Name: event.ResourceProperties.ParameterName,
WithDecryption: false
};
const result = await ssmClient.send(new GetParameterCommand(params));
paramValue = result.Parameter.Value;
if (paramValue === "pending") {
await new Promise(resolve => setTimeout(resolve, 15000)); // wait 15 seconds
tries++;
}
}
return response.send(event, context, response.SUCCESS, {
HostnameValue: paramValue
});
} catch (error) {
console.error(error);
return response.send(event, context, response.FAILED, { error: error.message });
}
};
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: SSMParameterAccess
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- ssm:GetParameter
Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}/*"
VPCCleanupFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Role: !GetAtt VPCCleanupRole.Arn
Timeout: 300
Runtime: nodejs18.x
Code:
ZipFile: |
const { ElasticLoadBalancingClient, DescribeLoadBalancersCommand,
DeleteLoadBalancerCommand } = require('@aws-sdk/client-elastic-load-balancing');
const response = require('cfn-response');
exports.handler = async (event, context) => {
if (event.RequestType !== 'Delete') {
return response.send(event, context, response.SUCCESS);
}
try {
const elb = new ElasticLoadBalancingClient();
const vpcId = event.ResourceProperties.VpcId;
// Find and delete Classic Load Balancers in the VPC
const lbResponse = await elb.send(new DescribeLoadBalancersCommand({}));
let deleted = false;
for (const lb of lbResponse.LoadBalancerDescriptions || []) {
if (lb.VPCId === vpcId) {
console.log(`Deleting Classic Load Balancer: ${lb.LoadBalancerName}`);
await elb.send(new DeleteLoadBalancerCommand({
LoadBalancerName: lb.LoadBalancerName
}));
deleted = true;
}
}
if (deleted) {
// Wait for deletion to complete
console.log('Waiting 30 seconds for load balancer deletion to complete...');
await new Promise(r => setTimeout(r, 30000));
}
return response.send(event, context, response.SUCCESS);
} catch (error) {
console.error('Error deleting load balancers:', error);
return response.send(event, context, response.FAILED, {error: error.message});
}
};
VPCCleanupRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: VPCCleanupPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- ec2:DescribeAddresses
- ec2:DisassociateAddress
- ec2:DescribeNetworkInterfaces
- elasticloadbalancing:DescribeLoadBalancers
- elasticloadbalancing:DeleteLoadBalancer
- elasticloadbalancingv2:DescribeLoadBalancers
- elasticloadbalancingv2:DeleteLoadBalancer
Resource: "*"
VPCCleanup:
Type: Custom::VPCCleanup
Properties:
ServiceToken: !GetAtt VPCCleanupFunction.Arn
VpcId: !Ref VPC
Outputs:
ClusterName:
Description: EKS cluster name
Value: !Sub "${AWS::StackName}-cluster"
DatabaseEndpoint:
Description: RDS instance endpoint
Value: !GetAtt WindmillDB.Endpoint.Address
LoadBalancerHostname:
Description: Windmill LoadBalancer hostname
Value: !Sub "http://${LoadBalancerHostnameLookup.HostnameValue}"
+3 -1
View File
@@ -9,4 +9,6 @@ tests-out/
storageState.json
.env.production
dist/
static/tsdocs/
static/tsdocs/
static/ui_builder/
ui_builder.tar.gz
+217 -82
View File
@@ -1,12 +1,13 @@
{
"name": "windmill-components",
"version": "1.481.0",
"version": "1.483.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.481.0",
"version": "1.483.2",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
"@aws-crypto/sha256-js": "^4.0.0",
@@ -74,7 +75,7 @@
"windmill-parser-wasm-py": "^1.477.1",
"windmill-parser-wasm-regex": "^1.481.0",
"windmill-parser-wasm-rust": "^1.429.0",
"windmill-parser-wasm-ts": "^1.429.0",
"windmill-parser-wasm-ts": "^1.438.2",
"windmill-parser-wasm-yaml": "^1.429.0",
"windmill-sql-datatype-parser-wasm": "^1.318.0",
"y-monaco": "^0.1.4",
@@ -131,10 +132,10 @@
"svelte-splitpanes": "^8.0.9",
"svelte2tsx": "^0.6.16",
"tailwindcss": "^3.4.1",
"tar": "^7.4.3",
"tslib": "^2.6.1",
"typescript": "^5.5.0",
"vite": "^6.2.1",
"vite-plugin-circular-dependency": "^0.2.1",
"vite": "^6.3.2",
"vite-plugin-mkcert": "^1.17.5",
"yootils": "^0.3.1"
},
@@ -989,6 +990,27 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"dev": true,
"dependencies": {
"minipass": "^7.0.4"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@isaacs/fs-minipass/node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"license": "MIT",
@@ -1208,43 +1230,6 @@
"version": "0.0.1",
"license": "SEE LICENSE IN LICENSE"
},
"node_modules/@rollup/pluginutils": {
"version": "5.1.4",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils/node_modules/estree-walker": {
"version": "2.0.2",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/pluginutils/node_modules/picomatch": {
"version": "4.0.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.35.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz",
@@ -2925,11 +2910,12 @@
}
},
"node_modules/chownr": {
"version": "2.0.0",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
"node": ">=18"
}
},
"node_modules/citty": {
@@ -4434,8 +4420,9 @@
},
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"dev": true,
"license": "ISC",
"dependencies": {
"minipass": "^3.0.0"
},
@@ -4445,8 +4432,9 @@
},
"node_modules/fs-minipass/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
@@ -4554,11 +4542,74 @@
"giget": "dist/cli.mjs"
}
},
"node_modules/giget/node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/giget/node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"dev": true,
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/giget/node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/giget/node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true,
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/giget/node_modules/pathe": {
"version": "2.0.3",
"dev": true,
"license": "MIT"
},
"node_modules/giget/node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"dev": true,
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^5.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/glob": {
"version": "7.2.3",
"dev": true,
@@ -6542,37 +6593,39 @@
}
},
"node_modules/minizlib": {
"version": "2.1.2",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
"dev": true,
"license": "MIT",
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
"minipass": "^7.1.2"
},
"engines": {
"node": ">= 8"
"node": ">= 18"
}
},
"node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mkdirp": {
"version": "1.0.4",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"dev": true,
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
"mkdirp": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mlly": {
@@ -10153,19 +10206,38 @@
}
},
"node_modules/tar": {
"version": "6.2.1",
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
"dev": true,
"license": "ISC",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^5.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.0.1",
"mkdirp": "^3.0.1",
"yallist": "^5.0.0"
},
"engines": {
"node": ">=10"
"node": ">=18"
}
},
"node_modules/tar/node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/tar/node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
"dev": true,
"engines": {
"node": ">=18"
}
},
"node_modules/text-table": {
@@ -10197,6 +10269,48 @@
"dev": true,
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
"integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==",
"dev": true,
"dependencies": {
"fdir": "^6.4.3",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
"integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
"dev": true,
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"dev": true,
@@ -10505,13 +10619,17 @@
}
},
"node_modules/vite": {
"version": "6.2.1",
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz",
"integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.3",
"picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.30.1"
"rollup": "^4.34.9",
"tinyglobby": "^0.2.12"
},
"bin": {
"vite": "bin/vite.js"
@@ -10574,15 +10692,6 @@
}
}
},
"node_modules/vite-plugin-circular-dependency": {
"version": "0.2.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.2",
"chalk": "^4.1.2"
}
},
"node_modules/vite-plugin-mkcert": {
"version": "1.17.7",
"dev": true,
@@ -10599,6 +10708,32 @@
"vite": ">=3"
}
},
"node_modules/vite/node_modules/fdir": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
"integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
"dev": true,
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/vite/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/vitefu": {
"version": "1.0.6",
"dev": true,
+5 -4
View File
@@ -1,10 +1,11 @@
{
"name": "windmill-components",
"version": "1.481.0",
"version": "1.483.2",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"postinstall": "node scripts/untar_ui_builder.js",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --threshold warning",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
@@ -64,10 +65,10 @@
"svelte-splitpanes": "^8.0.9",
"svelte2tsx": "^0.6.16",
"tailwindcss": "^3.4.1",
"tar": "^7.4.3",
"tslib": "^2.6.1",
"typescript": "^5.5.0",
"vite": "^6.2.1",
"vite-plugin-circular-dependency": "^0.2.1",
"vite": "^6.3.2",
"vite-plugin-mkcert": "^1.17.5",
"yootils": "^0.3.1"
},
@@ -143,7 +144,7 @@
"windmill-parser-wasm-py": "^1.477.1",
"windmill-parser-wasm-regex": "^1.481.0",
"windmill-parser-wasm-rust": "^1.429.0",
"windmill-parser-wasm-ts": "^1.429.0",
"windmill-parser-wasm-ts": "^1.438.2",
"windmill-parser-wasm-yaml": "^1.429.0",
"windmill-sql-datatype-parser-wasm": "^1.318.0",
"y-monaco": "^0.1.4",
+48
View File
@@ -0,0 +1,48 @@
import path from 'path'
import fs from 'fs'
import { x } from 'tar'
const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-d44b577.tar.gz'
const outputTarPath = path.join(process.cwd(), 'ui_builder.tar.gz')
const extractTo = path.join(process.cwd(), 'static/ui_builder/')
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
// Download the tar file
const response = await fetch(tarUrl)
const buffer = await response.arrayBuffer()
await fs.promises.writeFile(outputTarPath, Buffer.from(buffer))
// Check if this script is being run from the package root
const isRootInstall = process.cwd() + '/scripts' === __dirname
if (isRootInstall) {
console.log('Running postinstall: direct install')
// Your postinstall logic here
} else {
console.log('Skipping postinstall: installed as dependency')
process.exit(0)
}
// Create extract directory if it doesn't exist
try {
await fs.promises.mkdir(extractTo, { recursive: true })
} catch (err) {
if (err.code !== 'EEXIST') {
throw err
}
}
await x({
file: outputTarPath,
cwd: extractTo,
sync: false,
gzip: true
})
await fs.promises.unlink(outputTarPath)
+41 -26
View File
@@ -38,6 +38,30 @@ export interface ATABootstrapConfig {
type ModuleMeta = { state: 'loading' }
export type DepsToGet = {
raw: string
module: string
version: string | undefined
}[]
function getVersionFromRaw(d: string) {
if (d.lastIndexOf('@') > 0) {
const splitted = d.split('@')
let version = splitted.pop()
if (version?.startsWith('^') || version?.startsWith('~')) {
version = version.slice(1)
}
return version
}
return 'latest'
}
export function versionRangeToVersion(version: string) {
if (version.startsWith('^') || version.startsWith('~')) {
return version.slice(1)
}
return version
}
/**
* The function which starts up type acquisition,
* returns a function which you then pass the initial
@@ -57,11 +81,11 @@ export const setupTypeAcquisition = (config: ATABootstrapConfig) => {
let resLimit = { usage: 0 }
return async (initialSourceFile: string) => {
return async (initialSourceFile: string | DepsToGet) => {
estimatedToDownload = 0
estimatedDownloaded = 0
let todo: string[] = [initialSourceFile]
let todo: (string | DepsToGet)[] = [initialSourceFile]
let next: string[] = []
let i = 0
let nb = 0
@@ -84,34 +108,25 @@ export const setupTypeAcquisition = (config: ATABootstrapConfig) => {
}
}
function getVersion(d: string) {
if (d.lastIndexOf('@') > 0) {
const splitted = d.split('@')
let version = splitted.pop()
if (version?.startsWith('^') || version?.startsWith('~')) {
version = version.slice(1)
}
return version
}
return 'latest'
}
async function resolveDeps(
initialSourceFile: string,
depsSource: string | DepsToGet,
depth: number,
resLimit: ResLimit
): Promise<string[]> {
let depsToGet = config
.depsParser(initialSourceFile)
.map((d: string) => {
let raw = mapModuleNameToModule(d)
return {
raw,
module: raw.lastIndexOf('@') > 0 ? raw.split('@').slice(0, -1).join('@') : raw,
version: getVersion(d)
}
})
.filter((f) => !moduleMap.has(f.raw))
let depsToGet =
typeof depsSource == 'object'
? depsSource
: config
.depsParser(depsSource)
.map((d: string) => {
let raw = mapModuleNameToModule(d)
return {
raw,
module: raw.lastIndexOf('@') > 0 ? raw.split('@').slice(0, -1).join('@') : raw,
version: getVersionFromRaw(raw)
}
})
.filter((f) => !moduleMap.has(f.raw))
if (depth == 0) {
const relativeDeps = depsToGet.filter((f) => isTypescriptRelativePath(f.raw))
@@ -5,7 +5,13 @@
type ResourceType,
type GetGlobalConnectedRepositoriesResponse
} from '$lib/gen'
import { workspaceStore, enterpriseLicense, userWorkspaces, userStore } from '$lib/stores'
import {
workspaceStore,
enterpriseLicense,
userWorkspaces,
userStore,
workspaceColor
} from '$lib/stores'
import { base } from '$lib/base'
import { emptySchema, emptyString } from '$lib/utils'
import SchemaForm from './SchemaForm.svelte'
@@ -147,9 +153,7 @@
viewJsonSchema = false
try {
schema = resourceTypeInfo.schema as any
schema.order = schema.order ?? Object.keys(schema.properties).sort()
notFound = false
} catch (e) {
notFound = true
@@ -516,14 +520,10 @@
<tr class="border-t border-gray-200 dark:border-gray-700">
<td class="py-2">{installation.account_id}</td>
<td class="py-2">
{#if $userWorkspaces.find((w) => w.id === installation.workspace_id)?.color}
{#if $workspaceColor}
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs"
style="background-color: {$userWorkspaces.find(
(w) => w.id === installation.workspace_id
)?.color}20; color: {$userWorkspaces.find(
(w) => w.id === installation.workspace_id
)?.color}"
style="background-color: {$workspaceColor}20; color: {$workspaceColor}"
>
{installation.workspace_id}
</span>
+5 -2
View File
@@ -375,6 +375,9 @@
(e.ctrlKey || e.metaKey) &&
(e.key == 'Enter' || e.key == 'c' || e.key == 'v' || e.key == 'x')
) {
if (e.key == 'Enter') {
dispatch('keydownCmdEnter')
}
return
}
e.stopPropagation()
@@ -846,7 +849,7 @@
}}
bind:args={value}
dndType={`nested-${title}`}
schemaSkippedValues={['label']}
hiddenArgs={['label']}
on:reorder={(e) => {
if (oneOf && oneOf[objIdx]) {
const keys = e.detail
@@ -865,7 +868,7 @@
{onlyMaskPassword}
{disablePortal}
{disabled}
schemaSkippedValues={['label']}
hiddenArgs={['label']}
schema={{
properties: obj.properties,
order: obj.order,
@@ -2,7 +2,7 @@
import { BROWSER } from 'esm-env'
import { createEventDispatcher, onMount } from 'svelte'
import '@codingame/monaco-vscode-standalone-languages'
// import '@codingame/monaco-vscode-standalone-languages'
import '@codingame/monaco-vscode-standalone-json-language-features'
import '@codingame/monaco-vscode-standalone-typescript-language-features'
import { editor as meditor } from 'monaco-editor'
@@ -27,7 +27,7 @@
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
export let schema: Schema | any
export let schemaSkippedValues: string[] = []
export let hiddenArgs: string[] = []
export let args: Record<string, any> = {}
export let shouldHideNoInputs: boolean = false
export let noVariablePicker = false
@@ -439,7 +439,7 @@
</div>
{#if opened === argName}
<div class="p-4 border-t">
{#if !schemaSkippedValues.includes(argName) && Object.keys(schema?.properties ?? {}).includes(argName)}
{#if !hiddenArgs.includes(argName) && Object.keys(schema?.properties ?? {}).includes(argName)}
{#if typeof args == 'object' && schema?.properties[argName]}
<PropertyEditor
bind:description={schema.properties[argName].description}
+254 -139
View File
@@ -7,22 +7,6 @@
languages.typescript.typescriptDefaults.addExtraLib(processStdContent, 'process.d.ts')
// languages.typescript.javascriptDefaults.setModeConfiguration({
// completionItems: true,
// hovers: true,
// documentSymbols: true,
// definitions: true,
// references: true,
// documentHighlights: true,
// rename: true,
// diagnostics: true,
// documentRangeFormattingEdits: true,
// signatureHelp: true,
// onTypeFormattingEdits: true,
// codeActions: true,
// inlayHints: true
// })
languages.typescript.typescriptDefaults.setModeConfiguration({
completionItems: true,
hovers: true,
@@ -52,8 +36,9 @@
languages.typescript.typescriptDefaults.setDiagnosticsOptions({
noSemanticValidation: false,
noSyntaxValidation: false,
noSuggestionDiagnostics: false,
diagnosticCodesToIgnore: [1108]
diagnosticCodesToIgnore: [1108, 7006, 7034, 7019, 7005]
})
languages.typescript.typescriptDefaults.setCompilerOptions({
@@ -80,23 +65,10 @@
strict: true,
noLib: false,
allowImportingTsExtensions: true,
moduleResolution: languages.typescript.ModuleResolutionKind.NodeJs
allowSyntheticDefaultImports: true,
moduleResolution: languages.typescript.ModuleResolutionKind.NodeJs,
jsx: languages.typescript.JsxEmit.React
})
// languages.typescript.javascriptDefaults.setCompilerOptions({
// target: languages.typescript.ScriptTarget.Latest,
// allowNonTsExtensions: true,
// noSemanticValidation: false,
// noSyntaxValidation: false,
// allowImportingTsExtensions: true,
// checkJs: true,
// allowJs: true,
// noUnusedParameters: true,
// noUnusedLocals: true,
// strict: true,
// noLib: true,
// moduleResolution: languages.typescript.ModuleResolutionKind.NodeJs
// })
</script>
<script lang="ts">
@@ -120,6 +92,8 @@
import { toSocket, WebSocketMessageReader, WebSocketMessageWriter } from 'vscode-ws-jsonrpc'
import { CloseAction, ErrorAction, RequestType } from 'vscode-languageclient'
import type { DocumentUri, MessageTransports } from 'vscode-languageclient'
import { MonacoBinding } from 'y-monaco'
import {
dbSchemas,
@@ -135,16 +109,18 @@
createHash as randomHash,
editorConfig,
langToExt,
updateOptions
updateOptions,
extToLang
} from '$lib/editorUtils'
import type { Disposable } from 'vscode'
import type { DocumentUri, MessageTransports } from 'vscode-languageclient'
import { workspaceStore } from '$lib/stores'
import { type Preview, ResourceService, UserService } from '$lib/gen'
import type { Text } from 'yjs'
import { initializeVscode } from '$lib/components/vscode'
import { initializeMode } from 'monaco-graphql/esm/initializeMode.js'
import type { MonacoGraphQLAPI } from 'monaco-graphql/esm/api.js'
import {
editor as meditor,
languages,
@@ -154,7 +130,6 @@
type IRange,
type IDisposable
} from 'monaco-editor'
import type { MonacoGraphQLAPI } from 'monaco-graphql/esm/api.js'
import EditorTheme from './EditorTheme.svelte'
import {
@@ -165,37 +140,27 @@
POSTGRES_TYPES,
SNOWFLAKE_TYPES
} from '$lib/consts'
import { setupTypeAcquisition } from '$lib/ata/index'
import { setupTypeAcquisition, type DepsToGet } from '$lib/ata/index'
import { initWasmTs } from '$lib/infer'
import { initVim } from './monaco_keybindings'
import { buildWorkerDefinition } from '$lib/monaco_workers/build_workers'
import { parseTypescriptDeps } from '$lib/relative_imports'
import { scriptLangToEditorLang } from '$lib/scripts'
import * as htmllang from '$lib/svelteMonarch'
import { conf, language } from '$lib/vueMonarch'
import { Autocompletor } from './copilot/autocomplete/monaco-adapter'
import { AIChatEditorHandler } from './copilot/chat/monaco-adapter'
import GlobalReviewButtons from './copilot/chat/GlobalReviewButtons.svelte'
import { writable } from 'svelte/store'
import { formatResourceTypes } from './copilot/chat/core'
import FakeMonacoPlaceHolder from './FakeMonacoPlaceHolder.svelte'
// import EditorTheme from './EditorTheme.svelte'
let divEl: HTMLDivElement | null = null
let editor: meditor.IStandaloneCodeEditor | null = null
export let lang:
| 'typescript'
| 'python'
| 'go'
| 'shell'
| 'sql'
| 'graphql'
| 'powershell'
| 'php'
| 'css'
| 'javascript'
| 'rust'
| 'yaml'
| 'csharp'
| 'nu'
| 'java'
// for related places search: ADD_NEW_LANG
export let code: string = ''
export let cmdEnterAction: (() => void) | undefined = undefined
@@ -217,22 +182,21 @@
export let args: Record<string, any> | undefined = undefined
export let useWebsockets: boolean = true
export let small = false
export let scriptLang: Preview['language'] | 'bunnative'
export let scriptLang: Preview['language'] | 'bunnative' | 'tsx' | 'jsx' | 'json' | undefined
export let disabled: boolean = false
export let lineNumbersMinChars = 3
export let files: Record<string, { code: string; readonly?: boolean }> | undefined = {}
export let extraLib: string | undefined = undefined
export let changeTimeout: number = 500
export let isAiPanelOpen: boolean = false
export let loadAsync = false
const rHash = randomHash()
let lang = scriptLangToEditorLang(scriptLang)
$: lang = scriptLangToEditorLang(scriptLang)
let filePath = computePath(path)
$: filePath = computePath(path)
function computePath(path: string | undefined): string {
if (path == '' || path == undefined || path.startsWith('/')) {
return rHash
} else {
return path as string
}
}
let initialPath: string | undefined = path
$: path != initialPath &&
@@ -251,15 +215,59 @@
let dbSchema: DBSchema | undefined = undefined
let destroyed = false
const uri =
lang != 'go' && lang != 'typescript' && lang != 'python' && lang != 'nu'
? `file:///${filePath ?? rHash}.${langToExt(lang)}`
: `file:///tmp/monaco/${randomHash()}.${langToExt(lang)}`
const uri = computeUri(filePath, scriptLang)
console.log('uri', uri)
buildWorkerDefinition()
function computeUri(filePath: string, scriptLang: string | undefined) {
let file
if (filePath.includes('.')) {
file = filePath
} else {
file = `${filePath}.${scriptLang == 'tsx' ? 'tsx' : langToExt(lang)}`
}
if (file.startsWith('/')) {
file = file.slice(1)
}
return !['deno', 'go', 'python3'].includes(scriptLang ?? '')
? `file:///${file}`
: `file:///tmp/monaco/${file}`
}
function computePath(path: string | undefined): string {
if (
['deno', 'go', 'python3'].includes(scriptLang ?? '') ||
path == '' ||
path == undefined //||path.startsWith('/')
) {
return randomHash()
} else {
console.log('path', path)
return path as string
}
}
export function switchToFile(path: string, value: string, lang: string) {
if (editor) {
const uri = mUri.parse(path)
console.log('switching to file', path, lang)
// vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(value))
let nmodel = meditor.getModel(uri)
if (nmodel) {
console.log('using existing model', path)
editor.setModel(nmodel)
} else {
console.log('creating model', path)
nmodel = meditor.createModel(value, lang, uri)
editor.setModel(nmodel)
}
model = nmodel
setTypescriptExtraLibs()
}
}
export function getCode(): string {
return editor?.getValue() ?? ''
}
@@ -716,9 +724,21 @@
}
export async function reloadWebsocket() {
console.log('reloadWebsocket')
await closeWebsockets()
if (
!useWebsockets ||
!(
(lang == 'typescript' && scriptLang === 'deno') ||
lang == 'python' ||
lang == 'go' ||
lang == 'shell'
)
) {
return
}
console.log('reloadWebsocket')
function createLanguageClient(
transports: MessageTransports,
name: string,
@@ -733,6 +753,7 @@
documentSelector: [lang],
errorHandler: {
error: () => ({ action: ErrorAction.Continue }),
closed: () => ({
action: CloseAction.Restart
})
@@ -864,11 +885,6 @@
const hostname = getHostname()
let encodedImportMap = ''
// if (lang == 'typescript') {
// let worker = await languages.typescript.getTypeScriptWorker()
// console.log(worker)
// }
if (useWebsockets) {
if (lang == 'typescript' && scriptLang === 'deno') {
@@ -1033,7 +1049,8 @@
!websocketAlive.go &&
!websocketAlive.shellcheck &&
!websocketAlive.ruff &&
scriptLang != 'bun'
scriptLang != 'bun' &&
scriptLang != 'tsx'
) {
console.log('reconnecting to language servers')
lastWsAttempt = new Date()
@@ -1108,7 +1125,7 @@
}
let initialized = false
let ata: ((s: string) => void) | undefined = undefined
let ata: ((s: string | DepsToGet) => void) | undefined = undefined
let statusDiv: Element | null = null
@@ -1130,16 +1147,93 @@
}
}
$: files && model && onFileChanges()
let svelteRegistered = false
let vueRegistered = false
function onFileChanges() {
if (files && Object.keys(files).find((x) => x.endsWith('.svelte')) != undefined) {
if (!svelteRegistered) {
svelteRegistered = true
languages.register({
id: 'svelte',
extensions: ['.svelte'],
aliases: ['Svelte', 'svelte'],
mimetypes: ['application/svelte']
})
languages.setLanguageConfiguration('svelte', htmllang.conf as any)
languages.setMonarchTokensProvider('svelte', htmllang.language as any)
}
}
if (files && Object.keys(files).find((x) => x.endsWith('.vue')) != undefined) {
if (!vueRegistered) {
vueRegistered = true
languages.register({
id: 'vue',
extensions: ['.vue'],
aliases: ['Vue', 'Vue'],
mimetypes: ['application/svelte']
})
languages.setLanguageConfiguration('vue', conf as any)
languages.setMonarchTokensProvider('vue', language as any)
}
}
if (files && model) {
for (const [path, { code, readonly }] of Object.entries(files)) {
const luri = mUri.file(path)
if (luri.toString() != model.uri.toString()) {
let nmodel = meditor.getModel(luri)
if (nmodel == undefined) {
const lmodel = meditor.createModel(code, extToLang(path?.split('.')?.pop()!), luri)
if (readonly) {
lmodel.onDidChangeContent((evt) => {
// This will effectively undo any new edits
if (lmodel.getValue() != code && code) {
lmodel.setValue(code)
}
})
}
} else {
const lmodel = meditor.getModel(luri)
if (lmodel && code) {
lmodel.setValue(code)
}
}
}
}
}
}
let timeoutModel: NodeJS.Timeout | undefined = undefined
async function loadMonaco() {
console.log('path', uri)
try {
console.log("Loading Monaco's language client")
await initializeVscode('editor')
await initializeVscode('editor', divEl!)
console.log('done loading Monaco and vscode')
} catch (e) {
console.log('error initializing services', e)
}
// vscode.languages.registerDefinitionProvider('*', {
// provideDefinition(document, position, token) {
// // Get the word under the cursor (this will be the import or function being clicked)
// const wordRange = document.getWordRangeAtPosition(position)
// const word = document.getText(wordRange)
// // Do something with the word (for example, log it or handle it)
// console.log('Clicked on import or symbol:', word)
// // Optionally, you can also return a definition location
// return null // If you don't want to override the default behavior
// }
// })
// console.log('bef ready')
// console.log('af ready')
@@ -1157,6 +1251,8 @@
}
model.updateOptions(lang == 'python' ? { tabSize: 4, insertSpaces: true } : updateOptions)
onFileChanges()
editor = meditor.create(divEl as HTMLDivElement, {
...editorConfig(code, lang, automaticLayout, fixedOverflowWidgets),
model,
@@ -1176,12 +1272,14 @@
timeoutModel = setTimeout(() => {
let ncode = getCode()
code = ncode
dispatch('change', code)
}, 500)
dispatch('change', ncode)
}, changeTimeout)
ataModel && clearTimeout(ataModel)
ataModel = setTimeout(() => {
ata?.(getCode())
if (scriptLang == 'bun') {
ata?.(getCode())
}
}, 1000)
})
@@ -1238,10 +1336,8 @@
!websocketAlive.ruff &&
!websocketAlive.shellcheck &&
!websocketAlive.go &&
!websocketInterval &&
scriptLang != 'bun'
!websocketInterval
) {
console.log('reconnecting to language servers on focus')
reloadWebsocket()
}
})
@@ -1265,6 +1361,10 @@
}
}
export async function fetchPackageDeps(deps: DepsToGet) {
ata?.(deps)
}
async function setTypescriptRTNamespace() {
if (
scriptLang &&
@@ -1287,73 +1387,78 @@
}
async function setTypescriptExtraLibs() {
if (lang === 'typescript' && scriptLang != 'deno') {
if (extraLib) {
const uri = mUri.parse('file:///extraLib.d.ts')
languages.typescript.typescriptDefaults.addExtraLib(extraLib, uri.toString())
}
if (lang === 'typescript' && (scriptLang == 'bun' || scriptLang == 'tsx') && ata == undefined) {
const hostname = getHostname()
if (scriptLang == 'bun' && ata == undefined) {
const addLibraryToRuntime = async (code: string, _path: string) => {
const path = 'file://' + _path
let uri = mUri.parse(path)
console.log('adding library to runtime', path)
languages.typescript.typescriptDefaults.addExtraLib(code, path)
try {
await vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(code))
} catch (e) {
console.log('error writing file', e)
}
const addLibraryToRuntime = async (code: string, _path: string) => {
const path = 'file://' + _path
let uri = mUri.parse(path)
console.log('adding library to runtime', path)
languages.typescript.typescriptDefaults.addExtraLib(code, path)
try {
await vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(code))
} catch (e) {
console.log('error writing file', e)
}
}
const addLocalFile = async (code: string, _path: string) => {
let p = new URL(_path, uri).href
// if (_path?.startsWith('/')) {
// p = 'file://' + p
// }
let nuri = mUri.parse(p)
console.log('adding local file', _path, nuri.toString())
if (editor) {
let localModel = meditor.getModel(nuri)
if (localModel) {
localModel.setValue(code)
} else {
meditor.createModel(code, 'typescript', nuri)
}
try {
if (model) {
model?.setValue(model.getValue())
}
} catch (e) {
console.log('error resetting model', e)
const addLocalFile = async (code: string, _path: string) => {
let p = new URL(_path, uri).href
// if (_path?.startsWith('/')) {
// p = 'file://' + p
// }
let nuri = mUri.parse(p)
console.log('adding local file', _path, nuri.toString())
if (editor) {
let localModel = meditor.getModel(nuri)
if (localModel) {
localModel.setValue(code)
} else {
meditor.createModel(code, 'typescript', nuri)
}
try {
if (model) {
model?.setValue(model.getValue())
}
} catch (e) {
console.log('error resetting model', e)
}
}
await initWasmTs()
const root = await genRoot(hostname)
console.log('SETUP TYPE ACQUISITION', { root, path })
ata = setupTypeAcquisition({
projectName: 'Windmill',
depsParser: (c) => {
return parseTypescriptDeps(c)
}
await initWasmTs()
const root = await genRoot(hostname)
console.log('SETUP TYPE ACQUISITION', { root, path })
ata = setupTypeAcquisition({
projectName: 'Windmill',
depsParser: (c) => {
return parseTypescriptDeps(c)
},
root,
scriptPath: path,
logger: console,
delegate: {
receivedFile: addLibraryToRuntime,
localFile: addLocalFile,
progress: (downloaded: number, total: number) => {
// console.log({ dl, ttl })
},
root,
scriptPath: path,
logger: console,
delegate: {
receivedFile: addLibraryToRuntime,
localFile: addLocalFile,
progress: (downloaded: number, total: number) => {
// console.log({ dl, ttl })
},
started: () => {
console.log('ATA start')
},
finished: (f) => {
console.log('ATA done')
}
started: () => {
console.log('ATA start')
},
finished: (f) => {
console.log('ATA done')
}
})
}
})
if (scriptLang == 'bun') {
ata?.('import "bun-types"')
ata?.(code)
}
dispatch('ataReady')
}
}
@@ -1374,9 +1479,14 @@
})
}
onMount(() => {
onMount(async () => {
if (BROWSER) {
loadMonaco().then((x) => (disposeMethod = x))
if (loadAsync) {
setTimeout(() => loadMonaco().then((x) => (disposeMethod = x)), 0)
} else {
let m = await loadMonaco()
disposeMethod = m
}
}
})
@@ -1408,6 +1518,11 @@
</script>
<EditorTheme />
{#if !editor}
<div class="inset-0 absolute overflow-clip">
<FakeMonacoPlaceHolder {code} />
</div>
{/if}
<div bind:this={divEl} class="{$$props.class} editor {disabled ? 'disabled' : ''}"></div>
{#if $vimMode}
<div class="fixed bottom-0 z-30" bind:this={statusDiv}></div>
@@ -361,7 +361,7 @@
<Module.default
disabled={!$enterpriseLicense || !isSlackHandler(handlerPath)}
schema={slackHandlerSchema}
schemaSkippedValues={['slack']}
hiddenArgs={['slack']}
schemaFieldTooltip={{
channel: 'Slack channel name without the "#" - example: "windmill-alerts"'
}}
@@ -0,0 +1,112 @@
<!-- Used to avoid height jitter when loading monaco asynchronously -->
<script lang="ts">
import { getOS } from '$lib/utils'
type Props = {
code?: string
autoheight?: boolean
lineNumbersWidth?: number
lineNumbersOffset?: number
class?: string
fontSize?: number
}
let {
code,
autoheight = false,
lineNumbersWidth = 51,
lineNumbersOffset = 0,
class: className = '',
fontSize = 14
}: Props = $props()
// https://github.com/microsoft/vscode/blob/baa2dad3cdacd97ac02eff0604984faf1167ff1e/src/vs/editor/common/config/editorOptions.ts#L5421
const DEFAULT_WINDOWS_FONT_FAMILY = "Consolas, 'Courier New', monospace"
const DEFAULT_MAC_FONT_FAMILY = "Menlo, Monaco, 'Courier New', monospace"
const DEFAULT_LINUX_FONT_FAMILY = "'Droid Sans Mono', 'monospace', monospace"
const fontFamily =
getOS() === 'Windows'
? DEFAULT_WINDOWS_FONT_FAMILY
: getOS() === 'macOS'
? DEFAULT_MAC_FONT_FAMILY
: DEFAULT_LINUX_FONT_FAMILY
// https://github.com/microsoft/vscode/blob/baa2dad3cdacd97ac02eff0604984faf1167ff1e/src/vs/editor/common/config/fontInfo.ts#L14
const GOLDEN_LINE_HEIGHT_RATIO = getOS() == 'macOS' ? 1.5 : 1.35
let lines = $derived(code?.split('\n') ?? [])
const charWidth = 9 // try to match as closely as possible to monaco editor
const lineHeight = fontSize * GOLDEN_LINE_HEIGHT_RATIO
let [clientWidth, clientHeight] = $state([0, 0])
let showHorizontalScrollbar = $derived(
lines.some((line) => line.length * charWidth > clientWidth - 40)
)
let [editorWidth, editorHeight] = $derived([
clientWidth,
autoheight ? lines.length * lineHeight + (showHorizontalScrollbar ? 12 : 0) : clientHeight
])
</script>
<!-- Copy pasted from actual monaco editor in the web inspector -->
<div
bind:clientWidth
bind:clientHeight
class="h-full w-full relative editor dark:bg-[#272D38] {className}"
style="--vscode-editorCodeLens-lineHeight: 18px; --vscode-editorCodeLens-fontSize: 12px; --vscode-editorCodeLens-fontFeatureSettings: 'liga' off, 'calt' off; --code-editorInlayHintsFontFamily: {fontFamily};"
>
<div
class="monaco-editor no-user-select mac standalone showUnused showDeprecated vs-dark"
role="code"
>
<div class="overflow-guard" style="width: {editorWidth}px; height: {editorHeight}px;">
<div
class="margin"
role="presentation"
aria-hidden="true"
style="position: absolute; transform: translate3d(0px, 0px, 0px); contain: strict; top: 0px; width: {editorWidth}px; height: {editorHeight}px;"
>
<div
class="margin-view-overlays"
role="presentation"
aria-hidden="true"
style="position: absolute; font-family: {fontFamily}; font-weight: normal; font-size: {fontSize}px; font-feature-settings: 'liga' 0, 'calt' 0; font-variation-settings: normal; line-height: {lineHeight}px; letter-spacing: 0px; width: {lineNumbersWidth}px; height: 4893px;"
>
{#each lines as _, i}
<div style="top:{lineHeight * i}px;height:{lineHeight}px;">
<div class="line-numbers" style="left:{lineNumbersOffset}px;width:25px;">{i + 1}</div>
</div>
{/each}
</div>
</div>
<div
class="monaco-scrollable-element editor-scrollable vs-dark mac"
style="position: absolute; overflow: hidden; left: {lineNumbersWidth}px; width: {editorWidth}px; height: {editorHeight}px"
>
<div
class="lines-content monaco-editor-background"
style="position: absolute; overflow: hidden; width: 1.67772e+07px; height: 1.67772e+07px; transform: translate3d(0px, 0px, 0px); contain: strict; top: 0px; left: 0px;"
>
<div
class="view-lines monaco-mouse-cursor-text text-tertiary/60"
style="line-height: {lineHeight}px; position: absolute; font-family: {fontFamily}; font-weight: normal; font-size: {fontSize}px; font-feature-settings: 'liga' 0, 'calt' 0; font-variation-settings: normal; line-height: {lineHeight}px; letter-spacing: 0px; width: 1143px; height: 789px;"
>
{#each lines as line, i}
<div
style="height: {lineHeight}px; top: {i * lineHeight}px;"
class="text-nowrap whitespace-pre"
>
{line}
</div>
{/each}
</div>
</div>
</div>
</div>
</div>
</div>
@@ -187,7 +187,7 @@
`path` in module.value
? module.value.path
: ($initialPathStore == '' ? $pathStore : $initialPathStore) + '/' + module.id,
jobKinds: ['preview', 'script', 'flowpreview', 'flow'].join(','),
jobKinds: ['preview', 'script', 'flowpreview', 'flow', 'flowscript'].join(','),
page: 1,
perPage: 1
})
@@ -2,7 +2,7 @@
import { BROWSER } from 'esm-env'
import { editor as meditor } from 'monaco-editor'
import '@codingame/monaco-vscode-standalone-languages'
// import '@codingame/monaco-vscode-standalone-languages'
import { onDestroy, onMount } from 'svelte'
@@ -515,6 +515,7 @@
on:change={() => {
dispatch('change', { argName, arg })
}}
loadAsync
/>
{/if}
</div>
@@ -583,6 +584,7 @@
focused = false
}}
autoHeight
loadAsync
/>
</div>
{#if !hideHelpButton}
@@ -9,6 +9,7 @@
export let error = ''
export let editor: SimpleEditor | undefined = undefined
export let small = false
export let loadAsync = false
$: tooBig = code && code?.length > 1000000
@@ -38,6 +39,7 @@
<div class="flex flex-col w-full">
<div class="border w-full">
<SimpleEditor
{loadAsync}
{small}
on:focus
on:blur
@@ -1,11 +1,12 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { ScriptService, type FlowModule, type Job, JobService } from '$lib/gen'
import { ScriptService, type FlowModule, type Job } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { getScriptByPath } from '$lib/scripts'
import { CornerDownLeft, Loader2 } from 'lucide-svelte'
import { getContext } from 'svelte'
import Button from './common/button/Button.svelte'
import type { FlowEditorContext } from './flows/types'
@@ -68,17 +69,12 @@
script.content,
script.language,
args,
$flowStore?.tag ?? (val.tag_override ? val.tag_override : script.tag)
$flowStore?.tag ?? (val.tag_override ? val.tag_override : script.tag),
script.lock,
val.hash ?? script.hash
)
} else if (val.type == 'flow') {
await testJobLoader?.abstractRun(() =>
JobService.runFlowByPath({
workspace: $workspaceStore!,
path: val.path,
requestBody: args,
skipPreprocessor: true
})
)
await testJobLoader?.runFlowByPath(val.path, args)
} else {
throw Error('Not supported module type')
}
@@ -118,6 +118,8 @@
resourceTypeInfo = resourceType
if (resourceType.schema) {
resourceSchema = resourceType.schema as Schema
resourceSchema.order =
resourceSchema.order ?? Object.keys(resourceSchema.properties).sort()
}
if (resourceTypeInfo?.format_extension) {
textFileContent = args.content
@@ -237,7 +239,7 @@
}}
/>
</div>
<div class="text-sm">
<div>
{#if loadingSchema}
<Skeleton layout={[[4]]} />
{:else if !viewJsonSchema && resourceSchema && resourceSchema?.properties}
@@ -135,7 +135,7 @@
}}
/>
<div class="flex flex-col w-full items-start">
<div class="flex flex-col w-full items-start min-h-9">
<div class="flex flex-row w-full items-center">
{#if collection?.length > 0}
<Select
@@ -0,0 +1,33 @@
<script lang="ts">
import { Button } from '$lib/components/common'
import { CornerDownLeft, Loader2 } from 'lucide-svelte'
export let isLoading
export let hideShortcut = false
export let onRun: () => Promise<void>
export let onCancel: () => Promise<void>
</script>
{#if !isLoading}
<Button
loading={isLoading}
size="sm"
color="dark"
btnClasses="!px-2 !py-1"
on:click={() => onRun()}
shortCut={{ Icon: CornerDownLeft, hide: hideShortcut }}
>
Run
</Button>
{:else}
<Button
size="sm"
color="red"
variant="border"
btnClasses="!px-2 !py-1"
on:click={() => onCancel()}
>
<Loader2 size={14} class="animate-spin mr-2" />
Cancel
</Button>
{/if}
@@ -92,7 +92,7 @@
right: 'Enabled'
}}
on:change={async (e) => {
if (!newItem) {
if (!newItem && $initialPrimarySchedule != false) {
await ScheduleService.setScheduleEnabled({
path: path,
workspace: $workspaceStore ?? '',
@@ -143,7 +143,7 @@
<p class="text-xs text-tertiary mt-10">Define a schedule frequency first</p>
{/if}
{#if $initialPrimarySchedule != false}
{#if $initialPrimarySchedule != false && !newItem}
<div class="flex">
<Button size="xs" color="light" on:click={() => scheduleEditor?.openEdit(path, isFlow)}
>Advanced</Button
@@ -18,7 +18,7 @@
import type { ComponentCustomCSS } from './apps/types'
export let schema: Schema | any
export let schemaSkippedValues: string[] = []
export let hiddenArgs: string[] = []
export let schemaFieldTooltip: Record<string, string> = {}
export let args: Record<string, any> = {}
export let disabledArgs: string[] = []
@@ -204,7 +204,7 @@
: ''}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
{#if !schemaSkippedValues.includes(argName) && keys.includes(argName)}
{#if !hiddenArgs.includes(argName) && keys.includes(argName)}
{#if typeof diff[argName] === 'object' && diff[argName].oldSchema}
{@const formerProperty = diff[argName].oldSchema}
<div class="px-2">
@@ -278,6 +278,7 @@
}}
on:acceptChange={(e) => dispatch('acceptChange', e.detail)}
on:rejectChange={(e) => dispatch('rejectChange', e.detail)}
on:keydownCmdEnter={() => dispatch('keydownCmdEnter')}
{disablePortal}
{resourceTypes}
{prettifyHeader}

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