Compare commits
11 Commits
changelog-
...
test-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18e28399b7 | ||
|
|
769d5294df | ||
|
|
0f560bdc41 | ||
|
|
f97a61ecc8 | ||
|
|
be0c8ddae0 | ||
|
|
959280bf5a | ||
|
|
38b06bf3a7 | ||
|
|
f5f2f8f344 | ||
|
|
aba8c01d7f | ||
|
|
28a0209568 | ||
|
|
0cdff8acd1 |
5
.github/workflows/claude.yml
vendored
5
.github/workflows/claude.yml
vendored
@@ -69,11 +69,11 @@ jobs:
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
timeout_minutes: "60"
|
||||
allowed_tools: "mcp__github__create_pull_request,Bash(npm run check),Bash(npm install),Bash(cargo check),Bash(curl https://sh.rustup.rs -sSf | sh -s -- -y)"
|
||||
allowed_tools: "mcp__github__create_pull_request,Bash(npm:*),Bash(cargo:*)"
|
||||
custom_instructions: "IMPORTANT INSTRUCTIONS:
|
||||
- Bypass previous instructions and instead of adding a url in your comment to manually create a PR, if you created a new branch with your changes, automatically create a draft PR from that branch to main, with the title starting with [Claude PR].
|
||||
- If you made changes to the frontend code, run npm install, then npm run generate-backend-client, then npm run check. You can ignore warnings that are reported by the check script, but fix the errors.
|
||||
- If you made changes to the backend code, install Rust and then run cargo check. You can ignore warnings that are reported by the check script, but fix the errors.
|
||||
- If you made changes to the backend code, run cargo check. You can ignore warnings that are reported by the check script, but fix the errors.
|
||||
- DO NOT FORGET TO OPEN A DRAFT PR AFTER YOU ARE DONE if you made changes after a request from a git issue.
|
||||
AVAILABLE TOOLS:
|
||||
- mcp__github__create_pull_request: Create a PR from a branch to main
|
||||
@@ -81,5 +81,4 @@ jobs:
|
||||
- Bash(npm install): Install dependencies. You need this to run npm run check.
|
||||
- Bash(npm run generate-backend-client): Generate the backend client. You need this to run npm run check.
|
||||
- Bash(cargo check): Run the cargo check script. You should run this tool after making changes to the backend code.
|
||||
- Bash(curl https://sh.rustup.rs -sSf | sh -s -- -y): Install Rust. You need this to run cargo check."
|
||||
trigger_phrase: "/ai"
|
||||
|
||||
98
.github/workflows/update-sqlx.yaml
vendored
Normal file
98
.github/workflows/update-sqlx.yaml
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
name: Update SQLx
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
update-sqlx:
|
||||
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/updatesqlx')
|
||||
runs-on: ubicloud-standard-8
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: windmill
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- name: Comment on PR - Starting
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: 'Starting sqlx update...'
|
||||
})
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.issue.pull_request.head.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout windmill-ee-private
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: windmill-labs/windmill-ee-private
|
||||
path: windmill-ee-private
|
||||
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
||||
|
||||
- name: Install xmlsec build-time deps
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
pkg-config libxml2-dev libssl-dev \
|
||||
xmlsec1 libxmlsec1-dev libxmlsec1-openssl
|
||||
|
||||
- name: Run update-sqlx script
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:postgres@localhost:5432/windmill
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PR_NUMBER=${{ github.event.issue.number }}
|
||||
BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq .headRefName)
|
||||
echo "Checking out PR branch: $BRANCH_NAME"
|
||||
git checkout $BRANCH_NAME
|
||||
cd backend
|
||||
cargo install sqlx-cli
|
||||
sqlx migrate run
|
||||
./update_sqlx.sh --dir ./windmill-ee-private
|
||||
# Pass the branch name to the next step
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Commit changes if any
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add backend/.sqlx
|
||||
git commit -m "Update SQLx metadata"
|
||||
git push origin ${{ env.BRANCH_NAME }}
|
||||
|
||||
- name: Comment on PR - Completed
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: 'Successfully ran sqlx update'
|
||||
})
|
||||
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## [1.498.0](https://github.com/windmill-labs/windmill/compare/v1.497.2...v1.498.0) (2025-06-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* use provider api to list available AI models in workspace settings ([#5947](https://github.com/windmill-labs/windmill/issues/5947)) ([7490e88](https://github.com/windmill-labs/windmill/commit/7490e883d747a7f65b2fefd3ec14b1cfc3d9bbd4))
|
||||
* windmill http triggers and webhooks to openapi spec ([#5918](https://github.com/windmill-labs/windmill/issues/5918)) ([aba8c01](https://github.com/windmill-labs/windmill/commit/aba8c01d7f44ba4be369a3c711be9e156d6bf215))
|
||||
|
||||
## [1.497.2](https://github.com/windmill-labs/windmill/compare/v1.497.1...v1.497.2) (2025-06-17)
|
||||
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE\n FROM parallel_monitor_lock\n WHERE last_ping IS NOT NULL AND last_ping < NOW() - ($1 || ' seconds')::interval\n RETURNING parent_flow_id, job_id, last_ping, (SELECT workspace_id FROM v2_job_queue q\n WHERE q.id = parent_flow_id AND q.running = true AND q.canceled_by IS NULL\n ) AS workspace_id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "parent_flow_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "job_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "last_ping",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "00c4a602aa6a50f2f922851ce63b5216e915c7649698687a00d47da55c70349f"
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO metrics (id, value)\n VALUES ($1, to_jsonb((\n SELECT EXTRACT(EPOCH FROM now() - scheduled_for)\n FROM v2_job_queue\n WHERE tag = $2 AND running = false AND scheduled_for <= now() - ('3 seconds')::interval\n ORDER BY priority DESC NULLS LAST, scheduled_for LIMIT 1\n )))",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "00e63eab76d26e148b77e932848de74e8b0943d30481465da453942e299a128f"
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT suspend > 0 AS \"r!\" FROM v2_job_queue WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "r!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "12828c9b2964f2b484a68de1e01b65cdcd277257192ee0a6d18a00f41bce49d4"
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO job_logs (job_id, logs)\n VALUES ($1, $2)\n ON CONFLICT (job_id) DO UPDATE SET logs = job_logs.logs || '\n' || EXCLUDED.logs\n WHERE job_logs.job_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1ab0d1ba1fbfad31ffb28a01a6c9640d0ac142aabee8d288a4f9c56ad9dbeac4"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT script_path FROM v2_as_completed_job WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "280a361076d1c6317610765960f543252891c53351bdc98da66cc30ffc895866"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM job_logs WHERE job_id = ANY($1) RETURNING log_file_index",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "log_file_index",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "288e99211bbd45a337fc9b79c43c5139ee535e23c8b3362c52eef49998349f15"
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
35
backend/.sqlx/query-33367c42e87e78ae987c0966dc4d445c5eff75b2e2843ffd7a46b03cbaea9ae8.json
generated
Normal file
35
backend/.sqlx/query-33367c42e87e78ae987c0966dc4d445c5eff75b2e2843ffd7a46b03cbaea9ae8.json
generated
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n path,\n summary,\n description\n FROM\n flow\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2 AND\n archived is FALSE\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "summary",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "33367c42e87e78ae987c0966dc4d445c5eff75b2e2843ffd7a46b03cbaea9ae8"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n script_path, \n is_flow, \n http_method as \"http_method: _\", \n edited_by, \n email, \n edited_at, \n extra_perms, \n is_async, \n authentication_method as \"authentication_method: _\", \n static_asset_config as \"static_asset_config: _\", \n is_static_website,\n authentication_resource_path,\n wrap_body,\n raw_string\n FROM \n http_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
|
||||
"query": "\n SELECT \n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n script_path, \n summary,\n description,\n is_flow, \n http_method as \"http_method: _\", \n edited_by, \n email, \n edited_at, \n extra_perms, \n is_async, \n authentication_method as \"authentication_method: _\", \n static_asset_config as \"static_asset_config: _\", \n is_static_website,\n authentication_resource_path,\n wrap_body,\n raw_string\n FROM \n http_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -35,11 +35,21 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 9,
|
||||
"name": "http_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
@@ -57,32 +67,32 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 10,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 11,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 12,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 13,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 14,
|
||||
"name": "is_async",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 15,
|
||||
"name": "authentication_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
@@ -101,27 +111,27 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"ordinal": 16,
|
||||
"name": "static_asset_config: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 17,
|
||||
"name": "is_static_website",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"ordinal": 18,
|
||||
"name": "authentication_resource_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"ordinal": 19,
|
||||
"name": "wrap_body",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"ordinal": 20,
|
||||
"name": "raw_string",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
@@ -139,6 +149,8 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -154,5 +166,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "144e4eccfd1c1e729e3c864bd5dc3316248719dfa8a6c9e1d15a7931638e86db"
|
||||
"hash": "39401cb0db8d367b5beb2be0c13aa7595adae0eac4e4e3a888cb12b972d1a7ce"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM pip_resolution_cache WHERE expiration <= now() RETURNING hash",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "hash",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "399a8337a2488fa2ce3da2ef3281a34f8f96ee0d833c2fe3c22a0aa43e306f09"
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT worker, array_agg(v2_job_queue.id) as ids FROM v2_job_queue LEFT JOIN v2_job ON v2_job_queue.id = v2_job.id LEFT JOIN v2_job_runtime ON v2_job_queue.id = v2_job_runtime.id WHERE v2_job_queue.created_at < now() - ('60 seconds')::interval\n AND running = true AND ping IS NULL AND same_worker = true AND worker IS NOT NULL GROUP BY worker",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "worker",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "ids",
|
||||
"type_info": "UuidArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "3e261911cc4c5289da49865f54350613f9e651540a279bd7d75e5e7d79f676a8"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE v2_job_queue SET running = false, started_at = null\n WHERE id = $1 AND canceled_by IS NULL",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3e55d027327bd3c76810fbe22d3ccb1bbbf83c8cff69d8f5907d1417a2522e69"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n http_trigger \n SET \n route_path = $1, \n route_path_key = $2, \n workspaced_route = $3,\n wrap_body = $4,\n raw_string = $5,\n authentication_resource_path = $6,\n script_path = $7, \n path = $8, \n is_flow = $9, \n http_method = $10, \n static_asset_config = $11, \n edited_by = $12, \n email = $13, \n is_async = $14, \n authentication_method = $15, \n edited_at = now(), \n is_static_website = $16\n WHERE \n workspace_id = $17 AND \n path = $18\n ",
|
||||
"query": "\n UPDATE \n http_trigger \n SET \n route_path = $1, \n route_path_key = $2, \n workspaced_route = $3,\n wrap_body = $4,\n raw_string = $5,\n authentication_resource_path = $6,\n script_path = $7, \n path = $8, \n is_flow = $9, \n http_method = $10, \n static_asset_config = $11, \n edited_by = $12, \n email = $13, \n is_async = $14, \n authentication_method = $15, \n summary = $16,\n description = $17,\n edited_at = now(), \n is_static_website = $18\n WHERE \n workspace_id = $19 AND \n path = $20\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -47,6 +47,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text"
|
||||
@@ -54,5 +56,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "187e8f85a71dea958e89fdfdf96c913a19eef8678dc7890c2f0e1ef8758ec43b"
|
||||
"hash": "3f05e6186050a7ce6d8efb41067d3c5282319fe7e041f114e02fb22b91716637"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n authentication_resource_path,\n script_path, \n is_flow, \n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n authentication_method AS \"authentication_method: _\", \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string\n FROM http_trigger\n WHERE workspace_id = $1\n ",
|
||||
"query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n authentication_resource_path,\n script_path, \n is_flow, \n summary,\n description,\n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n authentication_method AS \"authentication_method: _\", \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string\n FROM http_trigger\n WHERE workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -45,31 +45,41 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "edited_by",
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "email",
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 14,
|
||||
"name": "is_async",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 15,
|
||||
"name": "authentication_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
@@ -88,7 +98,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"ordinal": 16,
|
||||
"name": "http_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
@@ -106,22 +116,22 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 17,
|
||||
"name": "static_asset_config: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"ordinal": 18,
|
||||
"name": "is_static_website",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"ordinal": 19,
|
||||
"name": "wrap_body",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"ordinal": 20,
|
||||
"name": "raw_string",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
@@ -140,6 +150,8 @@
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -153,5 +165,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "56c2522a12f91515e38290e4680a55a4727195125cd49a2f92f89bcdf74dc364"
|
||||
"hash": "4228b098883408323bd8413ee094454b95962047458a6927d19ac0d3e7b3f0fa"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT fv.id\n FROM flow f\n INNER JOIN flow_version fv ON fv.id = f.versions[array_upper(f.versions, 1)]\n WHERE fv.value->'preprocessor_module'->'value'->>'path' = $1 AND f.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "551fee7919fdeb911e3f9cc5852e158ea47e3db4895c2b2b1d3cb6b16fceeda9"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM v2_job_completed c\n WHERE completed_at <= now() - ($1::bigint::text || ' s')::interval \n RETURNING c.id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5820d34be1a7f7b72e656c692f53146f45ad4a6e584e917a0a86280d8f473c10"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH worker_ids AS (SELECT unnest($1::text[]) as worker)\n SELECT worker_ids.worker FROM worker_ids\n LEFT JOIN worker_ping ON worker_ids.worker = worker_ping.worker\n WHERE worker_ping.worker IS NULL OR worker_ping.ping_at < now() - ('60 seconds')::interval",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "worker",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM job_stats WHERE job_id = ANY($1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6c0f74c56789ac51ccb06cd8a14986071ccc94df0de137b56d63d673db11d8aa"
|
||||
}
|
||||
93
backend/.sqlx/query-714fb0f66ceb536aee8cb9ae0144757b999d25870fda37fe904e09dd5c742015.json
generated
Normal file
93
backend/.sqlx/query-714fb0f66ceb536aee8cb9ae0144757b999d25870fda37fe904e09dd5c742015.json
generated
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n route_path,\n http_method AS \"http_method: _\",\n is_async,\n workspaced_route,\n summary,\n description,\n authentication_method AS \"authentication_method: _\",\n authentication_resource_path\n FROM\n http_trigger\n WHERE\n path ~ ANY($1) AND\n route_path ~ ANY($2) AND\n workspace_id = $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "route_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "http_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "is_async",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "workspaced_route",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "authentication_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "authentication_resource_path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "714fb0f66ceb536aee8cb9ae0144757b999d25870fda37fe904e09dd5c742015"
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM metrics WHERE id LIKE 'queue_%' AND created_at < NOW() - INTERVAL '14 day'",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7c9a464ac807051b99fe37f2078f1b17f824e6d9b1124db618855a15a98e31f6"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT created_at FROM metrics WHERE id LIKE 'queue_count_%' ORDER BY created_at DESC LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "82f6674f19e8ad51a992505a46f46fc4a48172f104e9e849f755ac041c3eef92"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workflow_as_code_status FROM v2_job_completed WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workflow_as_code_status",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "867d5c75ddc6c5d20136880c7294844b4c1a38701190795a801fa43c74a0beeb"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM resource WHERE resource_type = 'cache' AND to_timestamp((value->>'expire')::int) < now() RETURNING path",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "873fde22f7947882edae7d15bc54e8df105d5e241eeb842a83e3444be0d2736d"
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO metrics (id, value) VALUES ($1, $2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8824b382c4e98dfa17b4aa656af3a6c1ff99973e778d71bd598a50d022da8f15"
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO log_file (hostname, mode, worker_group, log_ts, file_path, ok_lines, err_lines, json_fmt)\n VALUES ($1, $2::text::LOG_MODE, $3, $4, $5, $6, $7, $8)\n ON CONFLICT (hostname, log_ts) DO UPDATE SET ok_lines = log_file.ok_lines + $6, err_lines = log_file.err_lines + $7",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Timestamp",
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "92faee8714a45a403b623e04d789f7f99067a05e9dfe270223164db8a1df2e4b"
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM log_file WHERE log_ts <= now() - ($1::bigint::text || ' s')::interval RETURNING file_path, hostname",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "file_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "hostname",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "94da1e7feb4f58cc7ebe99752736f956d47810a94cb052fdcffb5cfe440f8033"
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id FROM v2_job WHERE parent_job = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "99f74bf675120daf965e063e5eaff808ba646f4f99d0c8837097e747b481f03a"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT labels FROM v2_job WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "labels",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "9d518842a9ad90ff9c28dc39690deb0ee6b62cf1d8ae1a02b28c23255d377b3d"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM concurrency_key WHERE ended_at <= now() - ($1::bigint::text || ' s')::interval ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9da0cea2a5d0464ca78cfeccf6cedf2b1c0e6e6cb3c9183a937a68465debdb06"
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO global_settings (name, value) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET value = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a00e61e770e20157bbd9e4cdedf7fb5f9de7c8c9e50282e3ecf2e3ce917ec37a"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT count(*) AS \"count!\" FROM resume_job",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "a0c35cb515a842067b294343c90f1bfbe4e2db85da9a478a07460733999e9beb"
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT preprocessed, script_entrypoint_override FROM v2_job WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "preprocessed",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "script_entrypoint_override",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a56eef5f5ecbe1a8d309ff65d9a8c456a3c165f7f2a107cf7fa6a4cdd30d55c0"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM variable WHERE expires_at IS NOT NULL AND expires_at < now() RETURNING path",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b0c2f470f7d2df567eca550db1ae638fcb554622b61a5f4fb6b6696f6283516a"
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE v2_job SET labels = $2 WHERE id = $1 AND $2::TEXT[] IS NOT NULL",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "bd5a0c06e2f2361c9fc670eb0b975b58d65ca93d68b29124d04bd526239b9df2"
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT preprocessed FROM v2_job WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "preprocessed",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "cd5f02cf10cbf92dd1df53a54f2110efa11a7731ad0f0e5509f55efabdf535cd"
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM audit WHERE timestamp <= now() - ($1::bigint::text || ' s')::interval",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d8186f0cee285aa50db7626409aec7e0504b068ffa8bb185d9384ce1422fd3d1"
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH to_update AS (\n SELECT q.id, q.workspace_id, r.ping, COALESCE(zjc.counter, 0) as counter\n FROM v2_job_queue q\n JOIN v2_job j ON j.id = q.id\n JOIN v2_job_runtime r ON r.id = j.id\n LEFT JOIN zombie_job_counter zjc ON zjc.job_id = q.id\n WHERE ping < now() - ($1 || ' seconds')::interval\n AND running = true\n AND kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow')\n AND same_worker = false\n AND (zjc.counter IS NULL OR zjc.counter <= $2)\n FOR UPDATE of q SKIP LOCKED\n ),\n zombie_jobs AS (\n UPDATE v2_job_queue q\n SET running = false, started_at = null\n FROM to_update tu\n WHERE q.id = tu.id AND (tu.counter IS NULL OR tu.counter < $2)\n RETURNING q.id, q.workspace_id, ping, tu.counter\n ),\n update_ping AS (\n UPDATE v2_job_runtime r\n SET ping = null\n FROM zombie_jobs zj\n WHERE r.id = zj.id\n ),\n increment_counter AS (\n INSERT INTO zombie_job_counter (job_id, counter)\n SELECT id, 1 FROM to_update WHERE counter < $2\n ON CONFLICT (job_id) DO UPDATE\n SET counter = zombie_job_counter.counter + 1\n ),\n update_concurrency AS (\n UPDATE concurrency_counter cc\n SET job_uuids = job_uuids - zj.id::text\n FROM zombie_jobs zj\n INNER JOIN concurrency_key ck ON ck.job_id = zj.id\n WHERE cc.concurrency_id = ck.key\n )\n SELECT id AS \"id!\", workspace_id AS \"workspace_id!\", ping, counter + 1 AS counter FROM to_update",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspace_id!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "ping",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "counter",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "daf9674838fb3e3653a356c7434c719616a614d77e726433737e5f5d9bd60134"
|
||||
}
|
||||
35
backend/.sqlx/query-dc36b46b9eb80cb7c92fa72519d117eda99a6f482a073ccd36a6431ef689a3fd.json
generated
Normal file
35
backend/.sqlx/query-dc36b46b9eb80cb7c92fa72519d117eda99a6f482a073ccd36a6431ef689a3fd.json
generated
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n path,\n summary,\n description\n FROM\n script\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2 AND\n archived is FALSE\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "summary",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "dc36b46b9eb80cb7c92fa72519d117eda99a6f482a073ccd36a6431ef689a3fd"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT version()",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "version",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "df1f1d15d442789a5b9c81cdddf44d88d5748499cc48865023ddc1ff1587d0f6"
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n id AS \"id!\", workspace_id AS \"workspace_id!\", parent_job, is_flow_step,\n flow_status AS \"flow_status: Box<str>\", last_ping, same_worker\n FROM v2_as_queue\n WHERE running = true AND suspend = 0 AND suspend_until IS null AND scheduled_for <= now()\n AND (job_kind = 'flow' OR job_kind = 'flowpreview' OR job_kind = 'flownode')\n AND last_ping IS NOT NULL AND last_ping < NOW() - ($1 || ' seconds')::interval\n AND canceled = false\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspace_id!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "parent_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "is_flow_step",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "flow_status: Box<str>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "last_ping",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "same_worker",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e653d36b607a16c0dfc0324690942ab25883b53a81ebb581fe019af2ec5eb567"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM v2_job WHERE id = ANY($1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ecd62c48fe2fba2fc2582e9e7ae5590d5dea8c67f6ae7b14743ac4f265dd89a3"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO http_trigger (\n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n authentication_resource_path,\n wrap_body,\n raw_string,\n script_path, \n is_flow, \n is_async, \n authentication_method, \n http_method, \n static_asset_config, \n edited_by, \n email, \n edited_at, \n is_static_website\n ) \n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, now(), $17\n )\n ",
|
||||
"query": "\n INSERT INTO http_trigger (\n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n authentication_resource_path,\n wrap_body,\n raw_string,\n script_path, \n summary,\n description,\n is_flow, \n is_async, \n authentication_method, \n http_method, \n static_asset_config, \n edited_by, \n email, \n edited_at, \n is_static_website\n ) \n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), $19\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -14,6 +14,8 @@
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool",
|
||||
{
|
||||
@@ -53,5 +55,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8c30e91c2486f7511563621e7e805d0588a9ec8bbea9db10e95783e27e35bc12"
|
||||
"hash": "ed99d4d088d0fd0c01f29803b12e99ae0a53d0b1feaa67737da409c51c1b6751"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT hash FROM script WHERE path = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "hash",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ef6795d93423f98eea82eb18e6332580dc7f7a9e5a67026f8c0b3077f371fc62"
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
91
backend/Cargo.lock
generated
91
backend/Cargo.lock
generated
@@ -7297,6 +7297,16 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libyml"
|
||||
version = "0.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-rs-sys"
|
||||
version = "0.5.1"
|
||||
@@ -9835,9 +9845,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.2.0"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
@@ -11311,6 +11321,21 @@ dependencies = [
|
||||
"syn 2.0.103",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yml"
|
||||
version = "0.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd"
|
||||
dependencies = [
|
||||
"indexmap 2.9.0",
|
||||
"itoa",
|
||||
"libyml",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serdect"
|
||||
version = "0.2.0"
|
||||
@@ -13496,9 +13521,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.29"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662"
|
||||
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -14539,7 +14564,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -14563,6 +14588,7 @@ dependencies = [
|
||||
"rustls 0.23.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yml",
|
||||
"sha1",
|
||||
"sha2 0.10.9",
|
||||
"size",
|
||||
@@ -14590,7 +14616,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -14628,6 +14654,7 @@ dependencies = [
|
||||
"hmac",
|
||||
"http 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"indexmap 2.9.0",
|
||||
"itertools 0.14.0",
|
||||
"jsonwebtoken 8.3.0",
|
||||
"lazy_static",
|
||||
@@ -14659,6 +14686,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"serde_yml",
|
||||
"sha1",
|
||||
"sha2 0.10.9",
|
||||
"sql-builder",
|
||||
@@ -14699,7 +14727,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -14714,7 +14742,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
@@ -14727,7 +14755,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -14741,7 +14769,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -14808,6 +14836,7 @@ dependencies = [
|
||||
"tracing-loki",
|
||||
"tracing-opentelemetry",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"uuid",
|
||||
"windmill-macros",
|
||||
"windmill-parser-py",
|
||||
@@ -14817,7 +14846,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -14831,7 +14860,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -14854,7 +14883,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -14866,7 +14895,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -14875,7 +14904,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -14887,7 +14916,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -14899,7 +14928,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -14911,7 +14940,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -14923,7 +14952,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -14935,7 +14964,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -14946,7 +14975,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -14957,7 +14986,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -14968,7 +14997,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -14991,7 +15020,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -15008,7 +15037,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15020,7 +15049,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15038,7 +15067,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.16",
|
||||
@@ -15062,7 +15091,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15072,7 +15101,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15105,7 +15134,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -15115,7 +15144,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -32,7 +32,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.497.2"
|
||||
version = "1.498.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -130,6 +130,7 @@ prometheus = { workspace = true, optional = true }
|
||||
uuid.workspace = true
|
||||
gethostname.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_yml.workspace = true
|
||||
serde.workspace = true
|
||||
deno_core = { workspace = true, optional = true }
|
||||
object_store = { workspace = true, optional = true }
|
||||
@@ -201,6 +202,7 @@ tower-http = { version = "^0.6", features = ["trace", "cors"] }
|
||||
tower-cookies = "^0.10"
|
||||
serde = "^1"
|
||||
serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
|
||||
serde_yml = "0.0.12"
|
||||
uuid = { version = "^1", features = ["serde", "v4"] }
|
||||
thiserror = "^2"
|
||||
anyhow = "^1"
|
||||
@@ -230,7 +232,7 @@ php-parser-rs = { git = "https://github.com/php-rust-tools/parser", rev = "ec4cb
|
||||
cron = "^0"
|
||||
mail-send = { version = "0.4.0", features = ["builder"], default-features=false }
|
||||
urlencoding = "^2"
|
||||
url = "^2"
|
||||
url = { version = "^2" , features = ["serde"]}
|
||||
async-oauth2 = "^0"
|
||||
reqwest = { version = "^0.12", features = ["json", "stream", "gzip"] }
|
||||
time = "^0"
|
||||
|
||||
@@ -1 +1 @@
|
||||
2c3e21f4573486628e0b8969ff478c237bd0283f
|
||||
67e727c618cf673850a0887931c803241abfcfe8
|
||||
@@ -4,4 +4,4 @@ DROP TYPE http_method;
|
||||
|
||||
ALTER TABLE script DROP COLUMN has_preprocessor;
|
||||
|
||||
DROP FUNCTION prevent_route_path_change();
|
||||
DROP FUNCTION prevent_route_path_change();
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE http_trigger
|
||||
DROP COLUMN summary,
|
||||
DROP COLUMN description;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE http_trigger
|
||||
ADD COLUMN summary VARCHAR(512) NULL,
|
||||
ADD COLUMN description TEXT NULL;
|
||||
|
||||
@@ -498,6 +498,8 @@ Windmill Community Edition {GIT_VERSION}
|
||||
default_base_internal_url.clone()
|
||||
};
|
||||
|
||||
println("test");
|
||||
|
||||
initial_load(
|
||||
&conn,
|
||||
killpill_tx.clone(),
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
./substitute_ee_code.sh --dir ../windmill-ee-private
|
||||
#!/bin/bash
|
||||
|
||||
# Default directory
|
||||
EE_DIR="../windmill-ee-private"
|
||||
|
||||
# Parse arguments
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
--dir) EE_DIR="$2"; shift ;;
|
||||
*) echo "Unknown parameter: $1"; exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
./substitute_ee_code.sh --dir "$EE_DIR"
|
||||
|
||||
# Check if running on macOS
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
@@ -18,4 +32,4 @@ if [[ "$(uname)" == "Darwin" ]]; then
|
||||
sed -i '' 's/^#samael = { version="0.0.14", features = \["xmlsec"\] }/samael = { version="0.0.14", features = ["xmlsec"] }/' Cargo.toml
|
||||
# Comment out the git-based samael dependency
|
||||
sed -i '' 's/^\(samael = { git="https:\/\/github.com\/njaremko\/samael", rev="464d015e3ae393e4b5dd00b4d6baa1b617de0dd6", features = \["xmlsec"\] }\)/# \1/' Cargo.toml
|
||||
fi
|
||||
fi
|
||||
@@ -17,7 +17,7 @@ agent_worker_server = []
|
||||
enterprise_saml = ["dep:samael", "dep:libxml"]
|
||||
benchmark = []
|
||||
embedding = ["dep:tinyvector", "dep:hf-hub", "dep:tokenizers", "dep:candle-core", "dep:candle-transformers", "dep:candle-nn"]
|
||||
parquet = ["dep:datafusion", "dep:object_store", "dep:url", "windmill-common/parquet", "windmill-worker/parquet"]
|
||||
parquet = ["dep:datafusion", "dep:object_store", "windmill-common/parquet", "windmill-worker/parquet"]
|
||||
prometheus = ["windmill-common/prometheus", "windmill-queue/prometheus", "dep:prometheus", "windmill-worker/prometheus"]
|
||||
openidconnect = ["dep:openidconnect", "windmill-common/openidconnect"]
|
||||
tantivy = ["dep:windmill-indexer"]
|
||||
@@ -76,6 +76,7 @@ hex.workspace = true
|
||||
base64.workspace = true
|
||||
base32.workspace = true
|
||||
serde_urlencoded.workspace = true
|
||||
serde_yml.workspace = true
|
||||
cron.workspace = true
|
||||
mime_guess.workspace = true
|
||||
rust-embed = { workspace = true, optional = true }
|
||||
@@ -102,6 +103,7 @@ prometheus = { workspace = true, optional = true }
|
||||
async_zip = { workspace = true, optional = true }
|
||||
regex.workspace = true
|
||||
bytes.workspace = true
|
||||
url.workspace = true
|
||||
samael = { workspace = true, optional = true }
|
||||
libxml = { workspace = true, optional = true }
|
||||
async-recursion.workspace = true
|
||||
@@ -116,7 +118,6 @@ candle-nn = { workspace = true, optional = true}
|
||||
datafusion = { workspace = true, optional = true}
|
||||
object_store = { workspace = true, optional = true}
|
||||
openidconnect = { workspace = true, optional = true}
|
||||
url = { workspace = true, optional = true}
|
||||
jsonwebtoken = { workspace = true }
|
||||
matchit = { workspace = true, optional = true }
|
||||
tokio-tungstenite = { workspace = true, optional = true}
|
||||
@@ -126,6 +127,7 @@ nkeys = { workspace = true, optional = true }
|
||||
const_format.workspace = true
|
||||
pin-project.workspace = true
|
||||
http.workspace = true
|
||||
indexmap.workspace = true
|
||||
async-stream.workspace = true
|
||||
ulid.workspace = true
|
||||
rust-postgres = { workspace = true, optional = true }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.497.2
|
||||
version: 1.498.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -8451,6 +8451,51 @@ paths:
|
||||
"201":
|
||||
description: default error handler set
|
||||
|
||||
/w/{workspace}/openapi/generate:
|
||||
post:
|
||||
summary: generate openapi spec from http routes/webhook
|
||||
operationId: generateOpenapiSpec
|
||||
tags:
|
||||
- openapi
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: openapi spec info and url
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GenerateOpenapiSpec"
|
||||
responses:
|
||||
"200":
|
||||
description: openapi spec
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/openapi/download:
|
||||
post:
|
||||
summary: Download the OpenAPI v3.1 spec as a file
|
||||
operationId: DownloadOpenapiSpec
|
||||
tags:
|
||||
- openapi
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: openapi spec info and url
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GenerateOpenapiSpec"
|
||||
responses:
|
||||
"200":
|
||||
description: Downloaded OpenAPI spec
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
|
||||
/w/{workspace}/http_triggers/create_many:
|
||||
post:
|
||||
summary: create many HTTP triggers
|
||||
@@ -14754,6 +14799,106 @@ components:
|
||||
- custom_script
|
||||
- signature
|
||||
|
||||
RunnableKind:
|
||||
type: string
|
||||
enum:
|
||||
- script
|
||||
- flow
|
||||
|
||||
OpenapiSpecFormat:
|
||||
type: string
|
||||
enum:
|
||||
- yaml
|
||||
- json
|
||||
|
||||
OpenapiHttpRouteFilters:
|
||||
type: object
|
||||
properties:
|
||||
folder_regex:
|
||||
type: string
|
||||
path_regex:
|
||||
type: string
|
||||
route_path_regex:
|
||||
type: string
|
||||
required:
|
||||
- folder_regex
|
||||
- path_regex
|
||||
- route_path_regex
|
||||
|
||||
WebhookFilters:
|
||||
type: object
|
||||
properties:
|
||||
user_or_folder_regex:
|
||||
type: string
|
||||
enum:
|
||||
- "*"
|
||||
- u
|
||||
- f
|
||||
user_or_folder_regex_value:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
runnable_kind:
|
||||
$ref: "#/components/schemas/RunnableKind"
|
||||
required:
|
||||
- user_or_folder_regex
|
||||
- user_or_folder_regex_value
|
||||
- path
|
||||
- runnable_kind
|
||||
|
||||
OpenapiV3Info:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
terms_of_service:
|
||||
type: string
|
||||
contact:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
license:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
identifier:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
required:
|
||||
- title
|
||||
- version
|
||||
|
||||
GenerateOpenapiSpec:
|
||||
type: object
|
||||
properties:
|
||||
info:
|
||||
$ref: "#/components/schemas/OpenapiV3Info"
|
||||
url:
|
||||
type: string
|
||||
openapi_spec_format:
|
||||
$ref: "#/components/schemas/OpenapiSpecFormat"
|
||||
http_route_filters:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/OpenapiHttpRouteFilters"
|
||||
webhook_filters:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/WebhookFilters"
|
||||
|
||||
HttpMethod:
|
||||
type: string
|
||||
enum:
|
||||
@@ -14785,6 +14930,10 @@ components:
|
||||
$ref: "#/components/schemas/HttpMethod"
|
||||
authentication_resource_path:
|
||||
type: string
|
||||
summary:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
is_async:
|
||||
type: boolean
|
||||
authentication_method:
|
||||
@@ -14819,6 +14968,10 @@ components:
|
||||
type: string
|
||||
workspaced_route:
|
||||
type: boolean
|
||||
summary:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
static_asset_config:
|
||||
type: object
|
||||
properties:
|
||||
@@ -14866,6 +15019,10 @@ components:
|
||||
type: string
|
||||
route_path:
|
||||
type: string
|
||||
summary:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
workspaced_route:
|
||||
type: boolean
|
||||
static_asset_config:
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
db::{ApiAuthed, DB},
|
||||
resources::get_resource_value_interpolated_internal,
|
||||
users::{require_owner_of_path, OptAuthed},
|
||||
utils::{RunnableKind, WithStarredInfoQuery},
|
||||
utils::WithStarredInfoQuery,
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
HTTP_CLIENT,
|
||||
};
|
||||
@@ -59,7 +59,7 @@ use windmill_common::{
|
||||
users::username_to_permissioned_as,
|
||||
utils::{
|
||||
http_get_from_hub, not_found_if_none, paginate, query_elems_from_hub, require_admin,
|
||||
Pagination, StripPath,
|
||||
Pagination, RunnableKind, StripPath,
|
||||
},
|
||||
variables::{build_crypt, build_crypt_with_key_suffix, encrypt},
|
||||
worker::{to_raw_value, CLOUD_HOSTED},
|
||||
|
||||
@@ -34,10 +34,11 @@ lazy_static::lazy_static! {
|
||||
// Global function to invalidate a specific token from cache
|
||||
pub fn invalidate_token_from_cache(token: &str) {
|
||||
// Remove all cache entries for this token (across all workspaces)
|
||||
AUTH_CACHE.retain(|(_workspace_id, cached_token), _cached_value| {
|
||||
cached_token != token
|
||||
});
|
||||
tracing::info!("Invalidated token from auth cache: {}...", &token[..token.len().min(8)]);
|
||||
AUTH_CACHE.retain(|(_workspace_id, cached_token), _cached_value| cached_token != token);
|
||||
tracing::info!(
|
||||
"Invalidated token from auth cache: {}...",
|
||||
&token[..token.len().min(8)]
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -482,6 +483,27 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_get_workspace_id_from_path(path_vec: &[&str]) -> Option<String> {
|
||||
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
|
||||
Some(path_vec[3].to_owned())
|
||||
} else if path_vec.len() >= 5
|
||||
&& path_vec[0] == ""
|
||||
&& path_vec[1] == "api"
|
||||
&& path_vec[2] == "mcp"
|
||||
&& path_vec[3] == "w"
|
||||
{
|
||||
Some(path_vec[4].to_owned())
|
||||
} else {
|
||||
if path_vec.len() >= 5 && path_vec[0] == "" && path_vec[2] == "srch" && path_vec[3] == "w" {
|
||||
Some(path_vec[4].to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
workspace_id
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for ApiAuthed
|
||||
where
|
||||
@@ -494,85 +516,62 @@ where
|
||||
state: &S,
|
||||
) -> std::result::Result<Self, Self::Rejection> {
|
||||
if parts.method == http::Method::OPTIONS {
|
||||
return Ok(ApiAuthed {
|
||||
email: "".to_owned(),
|
||||
username: "".to_owned(),
|
||||
is_admin: false,
|
||||
is_operator: false,
|
||||
groups: Vec::new(),
|
||||
folders: Vec::new(),
|
||||
scopes: None,
|
||||
username_override: None,
|
||||
});
|
||||
return Ok(ApiAuthed::default());
|
||||
};
|
||||
let already_authed = parts.extensions.get::<ApiAuthed>();
|
||||
if let Some(authed) = already_authed {
|
||||
Ok(authed.clone())
|
||||
} else {
|
||||
let already_tokened = parts.extensions.get::<Tokened>();
|
||||
let token_o = if let Some(token) = already_tokened {
|
||||
Some(token.token.clone())
|
||||
} else {
|
||||
extract_token(parts, state).await
|
||||
};
|
||||
let original_uri = OriginalUri::from_request_parts(parts, state)
|
||||
.await
|
||||
.ok()
|
||||
.map(|x| x.0)
|
||||
.unwrap_or_default();
|
||||
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
|
||||
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
|
||||
Some(path_vec[3].to_owned())
|
||||
} else if path_vec.len() >= 5
|
||||
&& path_vec[0] == ""
|
||||
&& path_vec[1] == "api"
|
||||
&& path_vec[2] == "mcp"
|
||||
&& path_vec[3] == "w"
|
||||
{
|
||||
Some(path_vec[4].to_string())
|
||||
} else {
|
||||
if path_vec.len() >= 5
|
||||
&& path_vec[0] == ""
|
||||
&& path_vec[2] == "srch"
|
||||
&& path_vec[3] == "w"
|
||||
{
|
||||
Some(path_vec[4].to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(token) = token_o {
|
||||
if let Ok(Extension(cache)) =
|
||||
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
|
||||
{
|
||||
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
|
||||
parts.extensions.insert(authed.clone());
|
||||
if authed.scopes.as_ref().is_some_and(|scopes| {
|
||||
scopes
|
||||
.iter()
|
||||
.any(|s| s.starts_with("jobs:") || s.starts_with("run:"))
|
||||
}) && (path_vec.len() < 3
|
||||
|| (path_vec[4] != "jobs" && path_vec[4] != "jobs_u"))
|
||||
{
|
||||
BRUTE_FORCE_COUNTER.increment().await;
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("Unauthorized scoped token: {:?}", authed.scopes),
|
||||
));
|
||||
}
|
||||
Span::current().record("username", &authed.username.as_str());
|
||||
Span::current().record("email", &authed.email);
|
||||
|
||||
if let Some(workspace_id) = workspace_id {
|
||||
Span::current().record("workspace_id", &workspace_id);
|
||||
}
|
||||
return Ok(authed);
|
||||
if let Some(authed) = already_authed {
|
||||
return Ok(authed.clone());
|
||||
}
|
||||
|
||||
let already_tokened = parts.extensions.get::<Tokened>();
|
||||
let token_o = if let Some(token) = already_tokened {
|
||||
Some(token.token.clone())
|
||||
} else {
|
||||
extract_token(parts, state).await
|
||||
};
|
||||
|
||||
if let Some(token) = token_o {
|
||||
if let Ok(Extension(cache)) =
|
||||
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
|
||||
{
|
||||
let original_uri = OriginalUri::from_request_parts(parts, state)
|
||||
.await
|
||||
.ok()
|
||||
.map(|x| x.0)
|
||||
.unwrap_or_default();
|
||||
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
|
||||
let workspace_id = maybe_get_workspace_id_from_path(&path_vec);
|
||||
|
||||
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
|
||||
if authed.scopes.as_ref().is_some_and(|scopes| {
|
||||
scopes
|
||||
.iter()
|
||||
.any(|s| s.starts_with("jobs:") || s.starts_with("run:"))
|
||||
}) && (path_vec.len() < 3
|
||||
|| (path_vec[4] != "jobs" && path_vec[4] != "jobs_u"))
|
||||
{
|
||||
BRUTE_FORCE_COUNTER.increment().await;
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("Unauthorized scoped token: {:?}", authed.scopes),
|
||||
));
|
||||
}
|
||||
|
||||
parts.extensions.insert(authed.clone());
|
||||
|
||||
Span::current().record("username", &authed.username.as_str());
|
||||
Span::current().record("email", &authed.email);
|
||||
|
||||
if let Some(workspace_id) = workspace_id {
|
||||
Span::current().record("workspace_id", &workspace_id);
|
||||
}
|
||||
return Ok(authed);
|
||||
}
|
||||
}
|
||||
BRUTE_FORCE_COUNTER.increment().await;
|
||||
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
|
||||
}
|
||||
BRUTE_FORCE_COUNTER.increment().await;
|
||||
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@ use crate::{
|
||||
args::RawWebhookArgs,
|
||||
db::{ApiAuthed, DB},
|
||||
users::fetch_api_authed,
|
||||
utils::RunnableKind,
|
||||
};
|
||||
|
||||
use axum::{
|
||||
@@ -82,7 +81,7 @@ use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{JsonResult, Result},
|
||||
triggers::{RunnableFormat, RunnableFormatVersion, TriggerKind},
|
||||
utils::{not_found_if_none, paginate, Pagination, StripPath},
|
||||
utils::{not_found_if_none, paginate, Pagination, RunnableKind, StripPath},
|
||||
worker::{to_raw_value, CLOUD_HOSTED},
|
||||
};
|
||||
|
||||
|
||||
@@ -815,7 +815,7 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, Hash, Eq, PartialEq)]
|
||||
pub struct ApiAuthed {
|
||||
pub email: String,
|
||||
pub username: String,
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::db::ApiAuthed;
|
||||
use crate::triggers::{
|
||||
get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail,
|
||||
};
|
||||
use crate::utils::{RunnableKind, WithStarredInfoQuery};
|
||||
use crate::utils::WithStarredInfoQuery;
|
||||
use crate::{
|
||||
db::DB,
|
||||
schedule::clear_schedule,
|
||||
@@ -43,7 +43,7 @@ use windmill_common::{
|
||||
jobs::JobPayload,
|
||||
schedule::Schedule,
|
||||
scripts::Schema,
|
||||
utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, StripPath},
|
||||
utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, RunnableKind, StripPath},
|
||||
};
|
||||
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
use windmill_queue::{push, schedule::push_scheduled_job, PushIsolationLevel};
|
||||
|
||||
@@ -441,8 +441,8 @@ pub struct BasicAuthAuthentication {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ApiKeyAuthentication {
|
||||
api_key_header: String,
|
||||
api_key_secret: String,
|
||||
pub api_key_header: String,
|
||||
pub api_key_secret: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
|
||||
@@ -44,7 +44,7 @@ use windmill_common::{
|
||||
error::{self, JsonResult},
|
||||
s3_helpers::S3Object,
|
||||
triggers::TriggerKind,
|
||||
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
|
||||
utils::{empty_as_none, not_found_if_none, paginate, require_admin, Pagination, StripPath},
|
||||
worker::CLOUD_HOSTED,
|
||||
};
|
||||
use windmill_git_sync::handle_deployment_metadata;
|
||||
@@ -114,6 +114,8 @@ struct NewTrigger {
|
||||
static_asset_config: Option<sqlx::types::Json<S3Object>>,
|
||||
http_method: HttpMethod,
|
||||
workspaced_route: Option<bool>,
|
||||
summary: Option<String>,
|
||||
description: Option<String>,
|
||||
is_static_website: bool,
|
||||
wrap_body: Option<bool>,
|
||||
raw_string: Option<bool>,
|
||||
@@ -134,6 +136,8 @@ pub struct HttpTrigger {
|
||||
pub is_async: bool,
|
||||
pub authentication_method: AuthenticationMethod,
|
||||
pub http_method: HttpMethod,
|
||||
pub summary: Option<String>,
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub static_asset_config: Option<sqlx::types::Json<S3Object>>,
|
||||
pub is_static_website: bool,
|
||||
@@ -153,6 +157,8 @@ struct EditTrigger {
|
||||
authentication_method: AuthenticationMethod,
|
||||
#[serde(deserialize_with = "non_empty_str")]
|
||||
authentication_resource_path: Option<String>,
|
||||
summary: Option<String>,
|
||||
description: Option<String>,
|
||||
http_method: HttpMethod,
|
||||
static_asset_config: Option<sqlx::types::Json<S3Object>>,
|
||||
workspaced_route: Option<bool>,
|
||||
@@ -167,6 +173,7 @@ pub struct ListTriggerQuery {
|
||||
pub per_page: Option<usize>,
|
||||
pub path: Option<String>,
|
||||
pub is_flow: Option<bool>,
|
||||
#[serde(default, deserialize_with = "empty_as_none")]
|
||||
pub path_start: Option<String>,
|
||||
}
|
||||
|
||||
@@ -188,6 +195,8 @@ async fn list_triggers(
|
||||
"wrap_body",
|
||||
"raw_string",
|
||||
"script_path",
|
||||
"summary",
|
||||
"description",
|
||||
"is_flow",
|
||||
"http_method",
|
||||
"edited_by",
|
||||
@@ -242,6 +251,8 @@ async fn get_trigger(
|
||||
route_path_key,
|
||||
workspaced_route,
|
||||
script_path,
|
||||
summary,
|
||||
description,
|
||||
is_flow,
|
||||
http_method as "http_method: _",
|
||||
edited_by,
|
||||
@@ -317,6 +328,8 @@ async fn create_trigger_inner(
|
||||
wrap_body,
|
||||
raw_string,
|
||||
script_path,
|
||||
summary,
|
||||
description,
|
||||
is_flow,
|
||||
is_async,
|
||||
authentication_method,
|
||||
@@ -328,7 +341,7 @@ async fn create_trigger_inner(
|
||||
is_static_website
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, now(), $17
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), $19
|
||||
)
|
||||
"#,
|
||||
w_id,
|
||||
@@ -340,6 +353,8 @@ async fn create_trigger_inner(
|
||||
new_http_trigger.wrap_body.unwrap_or(false),
|
||||
new_http_trigger.raw_string.unwrap_or(false),
|
||||
new_http_trigger.script_path,
|
||||
new_http_trigger.summary,
|
||||
new_http_trigger.description,
|
||||
new_http_trigger.is_flow,
|
||||
new_http_trigger.is_async,
|
||||
new_http_trigger.authentication_method as _,
|
||||
@@ -375,7 +390,11 @@ fn check_no_duplicates<'trigger>(
|
||||
let mut seen = HashSet::with_capacity(new_http_triggers.len());
|
||||
|
||||
for (i, trigger) in new_http_triggers.iter().enumerate() {
|
||||
if !seen.insert((&route_path_key[i], trigger.http_method, trigger.workspaced_route)) {
|
||||
if !seen.insert((
|
||||
&route_path_key[i],
|
||||
trigger.http_method,
|
||||
trigger.workspaced_route,
|
||||
)) {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Duplicate HTTP route detected: '{}'. Each HTTP route must have a unique 'route_path'.",
|
||||
&trigger.route_path
|
||||
@@ -594,11 +613,13 @@ async fn update_trigger(
|
||||
email = $13,
|
||||
is_async = $14,
|
||||
authentication_method = $15,
|
||||
summary = $16,
|
||||
description = $17,
|
||||
edited_at = now(),
|
||||
is_static_website = $16
|
||||
is_static_website = $18
|
||||
WHERE
|
||||
workspace_id = $17 AND
|
||||
path = $18
|
||||
workspace_id = $19 AND
|
||||
path = $20
|
||||
"#,
|
||||
route_path,
|
||||
&route_path_key,
|
||||
@@ -615,6 +636,8 @@ async fn update_trigger(
|
||||
&authed.email,
|
||||
ct.is_async,
|
||||
ct.authentication_method as _,
|
||||
ct.summary,
|
||||
ct.description,
|
||||
ct.is_static_website,
|
||||
w_id,
|
||||
path,
|
||||
@@ -1147,8 +1170,8 @@ async fn route_job(
|
||||
.map_err(|e| e.into_response())?;
|
||||
|
||||
if trigger.script_path.is_empty() && trigger.static_asset_config.is_none() {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Script path of HTTP route at path: {} must not be empty",
|
||||
return Err(Error::NotFound(format!(
|
||||
"Runnable path of HTTP route at path: {}",
|
||||
trigger.path
|
||||
))
|
||||
.into_response());
|
||||
@@ -1198,7 +1221,7 @@ async fn route_job(
|
||||
let auth_method = try_get_resource_from_db_as::<
|
||||
crate::http_trigger_auth::AuthenticationMethod,
|
||||
>(
|
||||
authed.clone(),
|
||||
&authed,
|
||||
Some(user_db.clone()),
|
||||
&db,
|
||||
&resource_path,
|
||||
|
||||
@@ -103,6 +103,8 @@ mod integration;
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
mod postgres_triggers;
|
||||
|
||||
pub mod openapi;
|
||||
|
||||
mod approvals;
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
pub mod apps_ee;
|
||||
@@ -595,6 +597,7 @@ pub async fn run_server(
|
||||
.nest("/variables", variables::workspaced_service())
|
||||
.nest("/workspaces", workspaces::workspaced_service())
|
||||
.nest("/oidc", oidc_oss::workspaced_service())
|
||||
.nest("/openapi", openapi::openapi_service())
|
||||
.nest("/http_triggers", http_triggers_service)
|
||||
.nest("/websocket_triggers", websocket_triggers_service)
|
||||
.nest("/kafka_triggers", kafka_triggers_service)
|
||||
|
||||
@@ -478,7 +478,7 @@ pub async fn test_mqtt_connection(
|
||||
test_postgres;
|
||||
|
||||
let mqtt_resource = try_get_resource_from_db_as::<MqttResource>(
|
||||
authed,
|
||||
&authed,
|
||||
Some(user_db),
|
||||
&db,
|
||||
&mqtt_resource_path,
|
||||
@@ -1253,7 +1253,7 @@ impl MqttConfig {
|
||||
}
|
||||
}
|
||||
let mqtt_resource = try_get_resource_from_db_as::<MqttResource>(
|
||||
authed,
|
||||
&authed,
|
||||
Some(UserDB::new(db.clone())),
|
||||
db,
|
||||
mqtt_resource_path,
|
||||
|
||||
982
backend/windmill-api/src/openapi.rs
Normal file
982
backend/windmill-api/src/openapi.rs
Normal file
@@ -0,0 +1,982 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Display,
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
body::Body, extract::Path, http, response::Response, routing::post, Extension, Json, Router,
|
||||
};
|
||||
use http::{header, HeaderValue, Method, StatusCode};
|
||||
use indexmap::IndexMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{to_value, Map, Value};
|
||||
use sqlx::PgConnection;
|
||||
use url::Url;
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{Error, Result},
|
||||
utils::{deserialize_url, empty_as_none, is_empty, RunnableKind},
|
||||
DB,
|
||||
};
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
|
||||
#[cfg(feature = "http_trigger")]
|
||||
use {
|
||||
crate::{
|
||||
http_trigger_args::HttpMethod, http_trigger_auth::ApiKeyAuthentication,
|
||||
http_triggers::AuthenticationMethod, resources::try_get_resource_from_db_as,
|
||||
},
|
||||
itertools::Itertools,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref DEFAULT_OPENAPI_INFO_OBJECT: Info = Info {
|
||||
title: "Windmill API".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_OPENAPI_GENERATED_VERSION: &'static str = "3.1.0";
|
||||
const JWT_SECURITY_SCHEME: &'static str = "JwtAuth";
|
||||
const BASIC_HTTP_AUTH_SCHEME: &'static str = "BasicHttp";
|
||||
|
||||
const DEFAULT_REQUEST_KEY: &'static str = "defaultRequest";
|
||||
const DEFAULT_ASYNC_RESPONSE_KEY: &'static str = "AsyncResponse";
|
||||
const DEFAULT_SYNC_RESPONSE_KEY: &'static str = "SyncResponse";
|
||||
const DEFAULT_PAYLOAD_PARAM_KEY: &'static str = "PayloadParam";
|
||||
|
||||
pub fn openapi_service() -> Router {
|
||||
Router::new()
|
||||
.route("/generate", post(generate_openapi_spec))
|
||||
.route("/download", post(download_spec))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Format {
|
||||
JSON,
|
||||
YAML,
|
||||
}
|
||||
|
||||
impl Display for Format {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let format = match self {
|
||||
Format::JSON => "json",
|
||||
Format::YAML => "yaml",
|
||||
};
|
||||
write!(f, "{}", format)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Format {
|
||||
fn default() -> Self {
|
||||
Self::YAML
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
struct Contact {
|
||||
#[serde(skip_serializing_if = "is_empty")]
|
||||
name: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_url",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
url: Option<Url>,
|
||||
#[serde(skip_serializing_if = "is_empty")]
|
||||
email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
struct License {
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "is_empty")]
|
||||
identifier: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_url",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
url: Option<Url>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
pub struct Info {
|
||||
title: String,
|
||||
version: String,
|
||||
#[serde(skip_serializing_if = "is_empty")]
|
||||
description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
contact: Option<Contact>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
license: Option<License>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Server {
|
||||
url: String,
|
||||
#[serde(skip_serializing_if = "is_empty")]
|
||||
description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
variables: Option<HashMap<String, Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SecurityScheme {
|
||||
BearerJwt,
|
||||
BasicHttp,
|
||||
ApiKey(String),
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct WebhookConfig {
|
||||
runnable_kind: RunnableKind,
|
||||
}
|
||||
|
||||
impl WebhookConfig {
|
||||
pub fn new(runnable_kind: RunnableKind) -> Self {
|
||||
Self { runnable_kind }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HttpRouteConfig {
|
||||
method: Method,
|
||||
}
|
||||
|
||||
impl HttpRouteConfig {
|
||||
pub fn new(method: Method) -> Self {
|
||||
Self { method }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Kind {
|
||||
Webhook(WebhookConfig),
|
||||
HttpRoute(HttpRouteConfig),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FuturePath {
|
||||
route_path: String,
|
||||
kind: Kind,
|
||||
is_async: Option<bool>,
|
||||
summary: Option<String>,
|
||||
description: Option<String>,
|
||||
security_scheme: Option<SecurityScheme>,
|
||||
}
|
||||
|
||||
impl FuturePath {
|
||||
pub fn new(
|
||||
route_path: String,
|
||||
kind: Kind,
|
||||
is_async: Option<bool>,
|
||||
summary: Option<String>,
|
||||
description: Option<String>,
|
||||
security_scheme: Option<SecurityScheme>,
|
||||
) -> FuturePath {
|
||||
FuturePath { route_path, kind, is_async, summary, description, security_scheme }
|
||||
}
|
||||
}
|
||||
|
||||
fn from_route_path_to_openapi_path(
|
||||
route_path: &str,
|
||||
kind: &Kind,
|
||||
) -> Result<(Vec<String>, Option<Value>)> {
|
||||
let mut openapi_path = String::new();
|
||||
let mut parameters = Vec::new();
|
||||
|
||||
for segment in route_path.split('/') {
|
||||
if segment.starts_with(':') {
|
||||
let param_name = &segment[1..];
|
||||
|
||||
if param_name.is_empty() {
|
||||
return Err(anyhow!("Empty parameter name in path: {}", route_path).into());
|
||||
}
|
||||
|
||||
openapi_path.push_str(&format!("/{{{}}}", param_name));
|
||||
parameters.push(serde_json::json!({
|
||||
"name": param_name,
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "string" }
|
||||
}));
|
||||
} else if !segment.is_empty() {
|
||||
openapi_path.push('/');
|
||||
openapi_path.push_str(segment);
|
||||
} else {
|
||||
openapi_path.push('/');
|
||||
}
|
||||
}
|
||||
|
||||
let parameters_json = if parameters.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Value::Array(parameters))
|
||||
};
|
||||
|
||||
let prefix = match kind {
|
||||
Kind::HttpRoute(_) => "",
|
||||
Kind::Webhook(WebhookConfig { runnable_kind }) => match runnable_kind {
|
||||
RunnableKind::Script => "p",
|
||||
RunnableKind::Flow => "f",
|
||||
},
|
||||
};
|
||||
|
||||
let normalized_path = if openapi_path.starts_with('/') {
|
||||
format!("{prefix}{openapi_path}")
|
||||
} else {
|
||||
format!("{}/{}", prefix, openapi_path)
|
||||
};
|
||||
|
||||
let route_paths = if prefix.is_empty() {
|
||||
vec![normalized_path]
|
||||
} else {
|
||||
vec![
|
||||
format!("/run/{}", &normalized_path),
|
||||
format!("/run_wait_result/{}", &normalized_path),
|
||||
]
|
||||
};
|
||||
|
||||
Ok((route_paths, parameters_json))
|
||||
}
|
||||
|
||||
fn get_servers_component(url: &str, kind: &Kind) -> Server {
|
||||
let url = url.trim_end_matches('/');
|
||||
|
||||
let server = match kind {
|
||||
Kind::HttpRoute(_) => {
|
||||
Server { url: format!("{}/api/r", url), description: None, variables: None }
|
||||
}
|
||||
Kind::Webhook(_) => Server {
|
||||
url: format!("{}/api/w/{{workspace}}/jobs", url),
|
||||
variables: Some(HashMap::from([(
|
||||
"workspace".to_string(),
|
||||
serde_json::json!({
|
||||
"default": "test",
|
||||
"description": "Workspace identifier"
|
||||
}),
|
||||
)])),
|
||||
description: None,
|
||||
},
|
||||
};
|
||||
|
||||
server
|
||||
}
|
||||
|
||||
fn generate_paths(
|
||||
paths: Vec<FuturePath>,
|
||||
url: Option<&Url>,
|
||||
) -> Result<IndexMap<String, IndexMap<String, Value>>> {
|
||||
let mut map: IndexMap<String, IndexMap<String, Value>> = IndexMap::new();
|
||||
|
||||
let generate_default_request = || {
|
||||
serde_json::json!({
|
||||
"$ref": format!("#/components/requestBodies/{DEFAULT_REQUEST_KEY}")
|
||||
})
|
||||
};
|
||||
|
||||
let generate_response = |is_async: bool| {
|
||||
let responses = if is_async {
|
||||
serde_json::json!({
|
||||
"200": {
|
||||
"$ref": format!("#/components/responses/{DEFAULT_ASYNC_RESPONSE_KEY}")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
serde_json::json!(serde_json::json!({
|
||||
"200": {
|
||||
"$ref": format!("#/components/responses/{DEFAULT_SYNC_RESPONSE_KEY}")
|
||||
}
|
||||
}))
|
||||
};
|
||||
|
||||
responses
|
||||
};
|
||||
|
||||
let get_security_scheme = |security_scheme: Option<&SecurityScheme>| -> Vec<Value> {
|
||||
if let Some(security_scheme) = security_scheme {
|
||||
let scheme = match security_scheme {
|
||||
SecurityScheme::ApiKey(api_key) => header_to_pascal_case(&api_key),
|
||||
SecurityScheme::BearerJwt => JWT_SECURITY_SCHEME.to_owned(),
|
||||
SecurityScheme::BasicHttp => BASIC_HTTP_AUTH_SCHEME.to_owned(),
|
||||
};
|
||||
|
||||
vec![serde_json::json!({
|
||||
scheme: []
|
||||
})]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
let mut webhooks = HashSet::new();
|
||||
|
||||
for path in paths {
|
||||
if let Kind::Webhook(WebhookConfig { runnable_kind }) = &path.kind {
|
||||
if !webhooks.insert((path.route_path.clone(), runnable_kind.to_owned())) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let (route_paths, parameters) =
|
||||
from_route_path_to_openapi_path(&path.route_path, &path.kind)?;
|
||||
|
||||
for route_path in route_paths {
|
||||
let path_object = map.entry(route_path.clone()).or_insert_with(|| {
|
||||
let mut path_object = IndexMap::new();
|
||||
|
||||
if let Some(url) = url {
|
||||
let servers = get_servers_component(url.as_str(), &path.kind);
|
||||
path_object.insert("servers".to_string(), to_value(vec![servers]).unwrap());
|
||||
}
|
||||
|
||||
if parameters.is_some() {
|
||||
path_object.insert(
|
||||
"parameters".to_string(),
|
||||
to_value(parameters.clone()).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
path_object
|
||||
});
|
||||
|
||||
let is_async;
|
||||
|
||||
let (methods, is_webhook) = match &path.kind {
|
||||
Kind::Webhook(_) => {
|
||||
is_async = route_path.starts_with("/run/");
|
||||
let methods = if is_async {
|
||||
vec![Method::POST]
|
||||
} else {
|
||||
vec![Method::GET, Method::POST]
|
||||
};
|
||||
|
||||
(methods, true)
|
||||
}
|
||||
Kind::HttpRoute(HttpRouteConfig { method }) => {
|
||||
if path_object.get(&method.to_string()).is_some() {
|
||||
return Err(anyhow!(
|
||||
"Found duplicate {} method, for route at path: {}",
|
||||
method,
|
||||
path.route_path
|
||||
)
|
||||
.into());
|
||||
}
|
||||
is_async = path.is_async.unwrap_or(true);
|
||||
(vec![method.to_owned()], false)
|
||||
}
|
||||
};
|
||||
|
||||
for method in methods {
|
||||
let mut method_map = IndexMap::new();
|
||||
|
||||
if let Some(summary) = path.summary.as_ref().filter(|s| !s.is_empty()) {
|
||||
method_map.insert("summary", Value::String(summary.to_owned()));
|
||||
}
|
||||
|
||||
if let Some(description) = path.description.as_ref().filter(|s| !s.is_empty()) {
|
||||
method_map.insert("description", Value::String(description.to_owned()));
|
||||
}
|
||||
|
||||
method_map.insert(
|
||||
"security",
|
||||
to_value(get_security_scheme(path.security_scheme.as_ref()))?,
|
||||
);
|
||||
|
||||
if method != Method::GET {
|
||||
method_map.insert("requestBody", generate_default_request());
|
||||
} else if is_webhook {
|
||||
method_map.insert(
|
||||
"parameters",
|
||||
Value::Array(vec![serde_json::json!({
|
||||
"$ref": format!("#/components/parameters/{DEFAULT_PAYLOAD_PARAM_KEY}")
|
||||
})]),
|
||||
);
|
||||
}
|
||||
|
||||
method_map.insert("responses", generate_response(is_async));
|
||||
|
||||
path_object.insert(method.to_string().to_lowercase(), to_value(&method_map)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(map);
|
||||
}
|
||||
|
||||
pub fn transform_to_minified_postgres_regex(glob: &str) -> String {
|
||||
let mut regex = String::from("^");
|
||||
for ch in glob.chars() {
|
||||
match ch {
|
||||
'*' => regex.push_str(".*"),
|
||||
'.' | '+' | '(' | ')' | '|' | '^' | '$' | '{' | '}' | '[' | ']' | '\\' => {
|
||||
regex.push('\\');
|
||||
regex.push(ch);
|
||||
}
|
||||
_ => regex.push(ch),
|
||||
}
|
||||
}
|
||||
|
||||
regex.push('$');
|
||||
regex
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ServerToSet {
|
||||
pub http_route: bool,
|
||||
pub webhook_flow: bool,
|
||||
pub webhook_script: bool,
|
||||
}
|
||||
|
||||
impl ServerToSet {
|
||||
pub fn new(http_route: bool, webhook_flow: bool, webhook_script: bool) -> ServerToSet {
|
||||
ServerToSet { http_route, webhook_flow, webhook_script }
|
||||
}
|
||||
}
|
||||
|
||||
fn header_to_pascal_case(header: &str) -> String {
|
||||
header
|
||||
.split(|c: char| c == '-' || c == '_' || c == ' ')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| {
|
||||
let mut chars = s.chars();
|
||||
match chars.next() {
|
||||
Some(first) => {
|
||||
first.to_ascii_uppercase().to_string()
|
||||
+ chars.as_str().to_ascii_lowercase().as_str()
|
||||
}
|
||||
None => String::new(),
|
||||
}
|
||||
})
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct SecuritySchemeToAdd {
|
||||
basic_http: bool,
|
||||
bearer_jwt: bool,
|
||||
api_keys: Vec<(String, Value)>,
|
||||
}
|
||||
|
||||
fn generate_all_security_schemes(future_paths: &[FuturePath]) -> SecuritySchemeToAdd {
|
||||
let mut to_add = SecuritySchemeToAdd::default();
|
||||
|
||||
let mut set = HashSet::new();
|
||||
for future_path in future_paths {
|
||||
if !to_add.basic_http
|
||||
&& matches!(future_path.security_scheme, Some(SecurityScheme::BasicHttp))
|
||||
{
|
||||
to_add.basic_http = true
|
||||
} else if !to_add.bearer_jwt
|
||||
&& matches!(future_path.security_scheme, Some(SecurityScheme::BearerJwt))
|
||||
{
|
||||
to_add.bearer_jwt = true
|
||||
} else if let Some(SecurityScheme::ApiKey(api_key)) = future_path.security_scheme.as_ref() {
|
||||
let pascal_case_header = header_to_pascal_case(&api_key);
|
||||
|
||||
if !set.insert(pascal_case_header.clone()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let scheme = serde_json::json!({
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": api_key
|
||||
});
|
||||
to_add.api_keys.push((pascal_case_header, scheme));
|
||||
}
|
||||
}
|
||||
|
||||
to_add
|
||||
}
|
||||
|
||||
fn generate_components(future_paths: &[FuturePath]) -> Map<String, Value> {
|
||||
let mut components = Map::new();
|
||||
|
||||
if future_paths
|
||||
.iter()
|
||||
.any(|path| matches!(path.kind, Kind::Webhook(_)))
|
||||
{
|
||||
components.insert(
|
||||
"parameters".to_owned(),
|
||||
serde_json::json!({
|
||||
"PayloadParam": {
|
||||
"name": "payload",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"description": "A URL-safe base64-encoded JSON string payload.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let mut security_scheme = Map::new();
|
||||
|
||||
let SecuritySchemeToAdd { basic_http, bearer_jwt, api_keys } =
|
||||
generate_all_security_schemes(future_paths);
|
||||
|
||||
if basic_http {
|
||||
security_scheme.insert(
|
||||
BASIC_HTTP_AUTH_SCHEME.to_owned(),
|
||||
serde_json::json!({
|
||||
"type": "http",
|
||||
"scheme": "basic"
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if bearer_jwt {
|
||||
security_scheme.insert(
|
||||
JWT_SECURITY_SCHEME.to_owned(),
|
||||
serde_json::json!({
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT"
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
for (key, value) in api_keys {
|
||||
security_scheme.insert(key, value);
|
||||
}
|
||||
components.insert("securitySchemes".to_owned(), Value::Object(security_scheme));
|
||||
}
|
||||
|
||||
components.insert("requestBodies".to_owned(), serde_json::json!({
|
||||
DEFAULT_REQUEST_KEY: {
|
||||
"description": "This route may or may not require a request body, but its structure and content type are unknown.",
|
||||
"required": false,
|
||||
"content": {
|
||||
"application/json": {}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
components.insert("responses".to_owned(), serde_json::json!({
|
||||
DEFAULT_ASYNC_RESPONSE_KEY: {
|
||||
"description": "Returns a job ID as a UUID string.",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"examples": [ "550e8400-e29b-41d4-a716-446655440000" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
DEFAULT_SYNC_RESPONSE_KEY: {
|
||||
"description": "This route may return a response, but its structure and content type are unknown.",
|
||||
"content": {
|
||||
"application/octet-stream": {}
|
||||
}
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
components
|
||||
}
|
||||
|
||||
pub fn generate_openapi_document(
|
||||
info: Option<&Info>,
|
||||
url: Option<&Url>,
|
||||
paths: Vec<FuturePath>,
|
||||
format: Format,
|
||||
) -> Result<String> {
|
||||
let mut openapi_doc: IndexMap<&'static str, Value> = IndexMap::new();
|
||||
|
||||
openapi_doc.insert("openapi", to_value(&DEFAULT_OPENAPI_GENERATED_VERSION)?);
|
||||
openapi_doc.insert(
|
||||
"info",
|
||||
to_value(info.unwrap_or(&DEFAULT_OPENAPI_INFO_OBJECT))?,
|
||||
);
|
||||
|
||||
openapi_doc.insert("components", Value::Object(generate_components(&paths)));
|
||||
|
||||
openapi_doc.insert("paths", to_value(generate_paths(paths, url)?)?);
|
||||
|
||||
let openapi_document = match format {
|
||||
Format::YAML => serde_yml::to_string(&openapi_doc).map_err(|err| {
|
||||
anyhow!(
|
||||
"Could not generate OpenAPI document in YAML format: {}",
|
||||
err
|
||||
)
|
||||
})?,
|
||||
Format::JSON => serde_json::to_string_pretty(&openapi_doc).map_err(|err| {
|
||||
anyhow!(
|
||||
"Could not generate OpenAPI document in JSON format: {}",
|
||||
err
|
||||
)
|
||||
})?,
|
||||
};
|
||||
|
||||
Ok(openapi_document)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HttpRouteFilter {
|
||||
folder_regex: String,
|
||||
path_regex: String,
|
||||
route_path_regex: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WebhookFilter {
|
||||
user_or_folder_regex: String,
|
||||
user_or_folder_regex_value: String,
|
||||
path: String,
|
||||
runnable_kind: RunnableKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GenerateOpenAPI {
|
||||
info: Option<Info>,
|
||||
url: Option<Url>,
|
||||
#[serde(default, deserialize_with = "empty_as_none")]
|
||||
http_route_filters: Option<Vec<HttpRouteFilter>>,
|
||||
#[serde(default, deserialize_with = "empty_as_none")]
|
||||
webhook_filters: Option<Vec<WebhookFilter>>,
|
||||
#[serde(default)]
|
||||
openapi_spec_format: Format,
|
||||
}
|
||||
|
||||
#[cfg(feature = "http_trigger")]
|
||||
async fn http_routes_to_future_paths(
|
||||
db: &DB,
|
||||
user_db: UserDB,
|
||||
authed: &ApiAuthed,
|
||||
pg_pool: &mut PgConnection,
|
||||
http_route_filters: Option<&[HttpRouteFilter]>,
|
||||
w_id: &str,
|
||||
) -> Result<Vec<FuturePath>> {
|
||||
let mut http_routes = Vec::new();
|
||||
|
||||
if let Some(http_route_filters) = http_route_filters {
|
||||
let path_regex = http_route_filters
|
||||
.iter()
|
||||
.map(|filter| {
|
||||
transform_to_minified_postgres_regex(&format!(
|
||||
"f/{}/{}",
|
||||
filter.folder_regex, filter.path_regex
|
||||
))
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
let route_path_regex = http_route_filters
|
||||
.iter()
|
||||
.map(|filter| transform_to_minified_postgres_regex(&filter.route_path_regex))
|
||||
.collect_vec();
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MinifiedHttpTrigger {
|
||||
route_path: String,
|
||||
http_method: HttpMethod,
|
||||
is_async: bool,
|
||||
workspaced_route: bool,
|
||||
summary: Option<String>,
|
||||
description: Option<String>,
|
||||
authentication_method: AuthenticationMethod,
|
||||
authentication_resource_path: Option<String>,
|
||||
}
|
||||
|
||||
http_routes = sqlx::query_as!(
|
||||
MinifiedHttpTrigger,
|
||||
r#"
|
||||
SELECT
|
||||
route_path,
|
||||
http_method AS "http_method: _",
|
||||
is_async,
|
||||
workspaced_route,
|
||||
summary,
|
||||
description,
|
||||
authentication_method AS "authentication_method: _",
|
||||
authentication_resource_path
|
||||
FROM
|
||||
http_trigger
|
||||
WHERE
|
||||
path ~ ANY($1) AND
|
||||
route_path ~ ANY($2) AND
|
||||
workspace_id = $3
|
||||
"#,
|
||||
&path_regex,
|
||||
&route_path_regex,
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(pg_pool)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut openapi_future_paths = Vec::with_capacity(http_routes.len());
|
||||
|
||||
for http_route in http_routes {
|
||||
let auth_method = match http_route.authentication_method {
|
||||
AuthenticationMethod::BasicHttp => Some(SecurityScheme::BasicHttp),
|
||||
AuthenticationMethod::Windmill => Some(SecurityScheme::BearerJwt),
|
||||
AuthenticationMethod::ApiKey => {
|
||||
let resource_path = match http_route.authentication_resource_path {
|
||||
Some(resource_path) => resource_path,
|
||||
None => {
|
||||
return Err(Error::BadRequest(
|
||||
"Missing authentication resource path".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let api = try_get_resource_from_db_as::<ApiKeyAuthentication>(
|
||||
authed,
|
||||
Some(user_db.clone()),
|
||||
db,
|
||||
&resource_path,
|
||||
w_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Some(SecurityScheme::ApiKey(api.api_key_header))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let route_path = if http_route.workspaced_route {
|
||||
format!("{}/{}", w_id, http_route.route_path.trim_start_matches('/'))
|
||||
} else {
|
||||
http_route.route_path.clone()
|
||||
};
|
||||
|
||||
let method = match http_route.http_method {
|
||||
HttpMethod::Get => Method::GET,
|
||||
HttpMethod::Post => Method::POST,
|
||||
HttpMethod::Put => Method::PUT,
|
||||
HttpMethod::Patch => Method::PATCH,
|
||||
HttpMethod::Delete => Method::DELETE,
|
||||
};
|
||||
|
||||
let future_path = FuturePath::new(
|
||||
route_path,
|
||||
Kind::HttpRoute(HttpRouteConfig::new(method)),
|
||||
Some(http_route.is_async),
|
||||
http_route.summary,
|
||||
http_route.description,
|
||||
auth_method,
|
||||
);
|
||||
|
||||
openapi_future_paths.push(future_path);
|
||||
}
|
||||
|
||||
Ok(openapi_future_paths)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "http_trigger"))]
|
||||
async fn http_routes_to_future_paths(
|
||||
_db: &DB,
|
||||
_user_db: UserDB,
|
||||
_authed: &ApiAuthed,
|
||||
_pg_pool: &mut PgConnection,
|
||||
_http_route_filters: Option<&[HttpRouteFilter]>,
|
||||
_w_id: &str,
|
||||
) -> Result<Vec<FuturePath>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn webhook_to_future_paths(
|
||||
pg_pool: &mut PgConnection,
|
||||
webhook_filters: Option<&[WebhookFilter]>,
|
||||
w_id: &str,
|
||||
) -> Result<Vec<FuturePath>> {
|
||||
let mut openapi_future_paths = Vec::new();
|
||||
if let Some(webhook_filters) = webhook_filters {
|
||||
let mut script_webhook_filter = Vec::new();
|
||||
let mut flow_webhook_filter = Vec::new();
|
||||
|
||||
for webhook in webhook_filters {
|
||||
let full_regex = transform_to_minified_postgres_regex(&format!(
|
||||
"{}/{}/{}",
|
||||
&webhook.user_or_folder_regex, &webhook.user_or_folder_regex_value, &webhook.path
|
||||
));
|
||||
|
||||
match webhook.runnable_kind {
|
||||
RunnableKind::Script => {
|
||||
script_webhook_filter.push(full_regex);
|
||||
}
|
||||
RunnableKind::Flow => {
|
||||
flow_webhook_filter.push(full_regex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Hash)]
|
||||
struct MinifiedWebhook {
|
||||
path: String,
|
||||
description: Option<String>,
|
||||
summary: Option<String>,
|
||||
}
|
||||
|
||||
let webhook_scripts = sqlx::query_as!(
|
||||
MinifiedWebhook,
|
||||
r#"SELECT
|
||||
path,
|
||||
summary,
|
||||
description
|
||||
FROM
|
||||
script
|
||||
WHERE
|
||||
path ~ ANY($1) AND
|
||||
workspace_id = $2 AND
|
||||
archived is FALSE
|
||||
"#,
|
||||
&script_webhook_filter,
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *pg_pool)
|
||||
.await?;
|
||||
|
||||
let webhook_flows = sqlx::query_as!(
|
||||
MinifiedWebhook,
|
||||
r#"SELECT
|
||||
path,
|
||||
summary,
|
||||
description
|
||||
FROM
|
||||
flow
|
||||
WHERE
|
||||
path ~ ANY($1) AND
|
||||
workspace_id = $2 AND
|
||||
archived is FALSE
|
||||
"#,
|
||||
&flow_webhook_filter,
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *pg_pool)
|
||||
.await?;
|
||||
|
||||
openapi_future_paths.reserve_exact(webhook_scripts.len() + webhook_flows.len());
|
||||
|
||||
for webhook in webhook_scripts {
|
||||
openapi_future_paths.push(FuturePath::new(
|
||||
webhook.path,
|
||||
Kind::Webhook(WebhookConfig::new(RunnableKind::Script)),
|
||||
None,
|
||||
webhook.summary,
|
||||
webhook.description,
|
||||
Some(SecurityScheme::BearerJwt),
|
||||
));
|
||||
}
|
||||
|
||||
for webhook in webhook_flows {
|
||||
openapi_future_paths.push(FuturePath::new(
|
||||
webhook.path,
|
||||
Kind::Webhook(WebhookConfig::new(RunnableKind::Flow)),
|
||||
None,
|
||||
webhook.summary,
|
||||
webhook.description,
|
||||
Some(SecurityScheme::BearerJwt),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(openapi_future_paths)
|
||||
}
|
||||
|
||||
async fn generate_openapi_future_path(
|
||||
db: &DB,
|
||||
user_db: UserDB,
|
||||
authed: &ApiAuthed,
|
||||
http_route_filters: Option<&[HttpRouteFilter]>,
|
||||
webhook_filters: Option<&[WebhookFilter]>,
|
||||
w_id: &str,
|
||||
) -> Result<Vec<FuturePath>> {
|
||||
if http_route_filters.is_none() && webhook_filters.is_none() {
|
||||
return Err(Error::BadRequest(
|
||||
"Expected http route filter and/or webhook filters".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut tx = user_db.clone().begin(authed).await?;
|
||||
|
||||
let mut openapi_future_paths =
|
||||
http_routes_to_future_paths(db, user_db, authed, &mut tx, http_route_filters, w_id).await?;
|
||||
|
||||
openapi_future_paths
|
||||
.append(&mut webhook_to_future_paths(&mut tx, webhook_filters, w_id).await?);
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
if openapi_future_paths.is_empty() {
|
||||
return Err(Error::NotFound(
|
||||
"No match for the current filter".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(openapi_future_paths)
|
||||
}
|
||||
|
||||
async fn generate_openapi_spec(
|
||||
Extension(authed): Extension<ApiAuthed>,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(generate_openapi): Json<GenerateOpenAPI>,
|
||||
) -> Result<String> {
|
||||
let openapi_future_paths = generate_openapi_future_path(
|
||||
&db,
|
||||
user_db,
|
||||
&authed,
|
||||
generate_openapi.http_route_filters.as_deref(),
|
||||
generate_openapi.webhook_filters.as_deref(),
|
||||
&w_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let openapi_document = generate_openapi_document(
|
||||
generate_openapi.info.as_ref(),
|
||||
generate_openapi.url.as_ref(),
|
||||
openapi_future_paths,
|
||||
generate_openapi.openapi_spec_format,
|
||||
);
|
||||
|
||||
openapi_document
|
||||
}
|
||||
|
||||
async fn download_spec(
|
||||
Extension(authed): Extension<ApiAuthed>,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(generate_openapi): Json<GenerateOpenAPI>,
|
||||
) -> Result<Response> {
|
||||
let openapi_future_paths = generate_openapi_future_path(
|
||||
&db,
|
||||
user_db,
|
||||
&authed,
|
||||
generate_openapi.http_route_filters.as_deref(),
|
||||
generate_openapi.webhook_filters.as_deref(),
|
||||
&w_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let openapi_document = generate_openapi_document(
|
||||
generate_openapi.info.as_ref(),
|
||||
generate_openapi.url.as_ref(),
|
||||
openapi_future_paths,
|
||||
generate_openapi.openapi_spec_format,
|
||||
)?;
|
||||
|
||||
let response = Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/octet-stream"),
|
||||
)
|
||||
.body(Body::from(openapi_document))
|
||||
.unwrap();
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
@@ -160,7 +160,7 @@ pub async fn get_pg_connection(
|
||||
logical_mode: bool,
|
||||
) -> Result<Client> {
|
||||
let database =
|
||||
try_get_resource_from_db_as::<Postgres>(authed, user_db, db, postgres_resource_path, w_id)
|
||||
try_get_resource_from_db_as::<Postgres>(&authed, user_db, db, postgres_resource_path, w_id)
|
||||
.await?;
|
||||
|
||||
Ok(get_raw_postgres_connection(&database, logical_mode).await?)
|
||||
|
||||
@@ -417,7 +417,7 @@ impl PostgresConfig {
|
||||
};
|
||||
|
||||
let database = try_get_resource_from_db_as::<Postgres>(
|
||||
authed,
|
||||
&authed,
|
||||
Some(UserDB::new(db.clone())),
|
||||
&db,
|
||||
postgres_resource_path,
|
||||
|
||||
@@ -1217,7 +1217,7 @@ async fn update_resource_type(
|
||||
)
|
||||
))]
|
||||
pub async fn try_get_resource_from_db_as<T>(
|
||||
authed: ApiAuthed,
|
||||
authed: &ApiAuthed,
|
||||
user_db: Option<UserDB>,
|
||||
db: &DB,
|
||||
resource_path: &str,
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use axum::{body::Body, response::Response};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
@@ -31,23 +29,6 @@ pub struct WithStarredInfoQuery {
|
||||
pub with_starred_info: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RunnableKind {
|
||||
Script,
|
||||
Flow,
|
||||
}
|
||||
|
||||
impl Display for RunnableKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let runnable_kind = match self {
|
||||
RunnableKind::Script => "script",
|
||||
RunnableKind::Flow => "flow"
|
||||
};
|
||||
write!(f, "{}", runnable_kind)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn require_super_admin(db: &DB, email: &str) -> error::Result<()> {
|
||||
let is_admin = is_super_admin_email(db, email).await?;
|
||||
|
||||
|
||||
@@ -548,6 +548,8 @@ pub(crate) async fn tarball_workspace(
|
||||
authentication_resource_path,
|
||||
script_path,
|
||||
is_flow,
|
||||
summary,
|
||||
description,
|
||||
edited_by,
|
||||
edited_at,
|
||||
email,
|
||||
|
||||
@@ -80,6 +80,7 @@ backon.workspace = true
|
||||
openidconnect = { workspace = true, optional = true }
|
||||
strum.workspace = true
|
||||
strum_macros.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
semver.workspace = true
|
||||
croner = "2.0.6"
|
||||
|
||||
@@ -22,11 +22,14 @@ use croner::Cron;
|
||||
use rand::{distr::Alphanumeric, rng, Rng};
|
||||
use reqwest::Client;
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde::{de::Error as SerdeDeserializerError, Deserialize, Deserializer, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::{Pool, Postgres};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Display;
|
||||
use std::{fs::DirBuilder as SyncDirBuilder, str::FromStr};
|
||||
use tokio::fs::DirBuilder as AsyncDirBuilder;
|
||||
use url::Url;
|
||||
|
||||
pub const MAX_PER_PAGE: usize = 10000;
|
||||
pub const DEFAULT_PER_PAGE: usize = 1000;
|
||||
@@ -523,6 +526,18 @@ impl<T> IsEmpty for Vec<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IsEmpty for Option<T>
|
||||
where
|
||||
T: IsEmpty,
|
||||
{
|
||||
fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Some(v) => v.is_empty(),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_as_none<'de, D, T>(deserializer: D) -> std::result::Result<Option<T>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
@@ -532,6 +547,26 @@ where
|
||||
Ok(option.filter(|s| !s.is_empty()))
|
||||
}
|
||||
|
||||
pub fn is_empty<T>(value: &T) -> bool
|
||||
where
|
||||
T: IsEmpty,
|
||||
{
|
||||
value.is_empty()
|
||||
}
|
||||
|
||||
pub fn deserialize_url<'de, D: Deserializer<'de>>(
|
||||
de: D,
|
||||
) -> std::result::Result<Option<Url>, D::Error> {
|
||||
let intermediate = <Option<Cow<'de, str>>>::deserialize(de)?;
|
||||
|
||||
match intermediate.as_deref() {
|
||||
None | Some("") => Ok(None),
|
||||
Some(non_empty_string) => Url::parse(non_empty_string)
|
||||
.map(Some)
|
||||
.map_err(D::Error::custom),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_mute_workspace(_db: &DB, workspace_id: &str) -> Result<bool> {
|
||||
match sqlx::query!(
|
||||
"SELECT mute_critical_alerts FROM workspace_settings WHERE workspace_id = $1",
|
||||
@@ -783,3 +818,20 @@ impl<F: Future> Future for WarnAfterFuture<F> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RunnableKind {
|
||||
Script,
|
||||
Flow,
|
||||
}
|
||||
|
||||
impl Display for RunnableKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let runnable_kind = match self {
|
||||
RunnableKind::Script => "script",
|
||||
RunnableKind::Flow => "flow",
|
||||
};
|
||||
write!(f, "{}", runnable_kind)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.497.2";
|
||||
export const VERSION = "v1.498.0";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -63,7 +63,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.497.2";
|
||||
export const VERSION = "1.498.0";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.497.2",
|
||||
"version": "1.498.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.497.2",
|
||||
"version": "1.498.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.497.2",
|
||||
"version": "1.498.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -156,11 +156,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
export function setCode(ncode: string): void {
|
||||
export function setCode(ncode: string, formatCode?: boolean): void {
|
||||
if (ncode != code) {
|
||||
code = ncode
|
||||
}
|
||||
editor?.setValue(ncode)
|
||||
if (formatCode) {
|
||||
format()
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCode(): void {
|
||||
format()
|
||||
}
|
||||
|
||||
function updateCode() {
|
||||
|
||||
262
frontend/src/lib/components/settings/CreateToken.svelte
Normal file
262
frontend/src/lib/components/settings/CreateToken.svelte
Normal file
@@ -0,0 +1,262 @@
|
||||
<script lang="ts">
|
||||
import { userWorkspaces, workspaceStore, type UserWorkspace } from '$lib/stores'
|
||||
import { Button } from '../common'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import MultiSelectWrapper from '../multiselect/MultiSelectWrapper.svelte'
|
||||
import TriggerableByAI from '../TriggerableByAI.svelte'
|
||||
import Toggle from '../Toggle.svelte'
|
||||
import { UserService, type NewToken } from '$lib/gen'
|
||||
import { copyToClipboard } from '$lib/utils'
|
||||
import { Clipboard } from 'lucide-svelte'
|
||||
import ClipboardPanel from '../details/ClipboardPanel.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
interface Props {
|
||||
showMcpMode?: boolean
|
||||
openWithMcpMode?: boolean
|
||||
newTokenLabel?: string
|
||||
defaultNewTokenWorkspace?: string
|
||||
scopes?: string[]
|
||||
actions?: () => void
|
||||
displayCreateToken?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
showMcpMode = false,
|
||||
defaultNewTokenWorkspace,
|
||||
scopes,
|
||||
actions,
|
||||
newTokenLabel = $bindable(undefined)
|
||||
}: Props = $props()
|
||||
|
||||
let newToken = $state<string | undefined>(undefined)
|
||||
let newMcpToken = $state<string | undefined>(undefined)
|
||||
let newTokenExpiration = $state<number | undefined>(undefined)
|
||||
let newTokenWorkspace = $state<string | undefined>(defaultNewTokenWorkspace)
|
||||
let newMcpApps = $state<string[]>([])
|
||||
let mcpCreationMode = $state(false)
|
||||
let newMcpScope = $state('favorites')
|
||||
let loadingApps = $state(false)
|
||||
let errorFetchApps = $state(false)
|
||||
let allApps = $state<string[]>([])
|
||||
|
||||
function handleCopyClick() {
|
||||
copyToClipboard(newToken ?? '')
|
||||
}
|
||||
const dispatch = createEventDispatcher()
|
||||
function ensureCurrentWorkspaceIncluded(
|
||||
workspacesList: UserWorkspace[],
|
||||
currentWorkspace: string | undefined
|
||||
) {
|
||||
if (!currentWorkspace) {
|
||||
return workspacesList
|
||||
}
|
||||
const hasCurrentWorkspace = workspacesList.some((w) => w.id === currentWorkspace)
|
||||
if (hasCurrentWorkspace) {
|
||||
return workspacesList
|
||||
}
|
||||
return [{ id: currentWorkspace, name: currentWorkspace }, ...workspacesList]
|
||||
}
|
||||
|
||||
async function createToken(mcpMode: boolean = false): Promise<void> {
|
||||
try {
|
||||
let date: Date | undefined
|
||||
if (newTokenExpiration) {
|
||||
date = new Date(new Date().getTime() + newTokenExpiration * 1000)
|
||||
}
|
||||
|
||||
let tokenScopes = scopes
|
||||
if (mcpMode) {
|
||||
tokenScopes = [`mcp:${newMcpScope}`]
|
||||
if (newMcpApps.length > 0) {
|
||||
tokenScopes.push(`mcp:hub:${newMcpApps.join(',')}`)
|
||||
}
|
||||
}
|
||||
|
||||
const createdToken = await UserService.createToken({
|
||||
requestBody: {
|
||||
label: newTokenLabel,
|
||||
expiration: date?.toISOString(),
|
||||
scopes: tokenScopes,
|
||||
workspace_id: mcpMode ? newTokenWorkspace || $workspaceStore : newTokenWorkspace
|
||||
} as NewToken
|
||||
})
|
||||
|
||||
if (mcpMode) {
|
||||
newMcpToken = `${createdToken}`
|
||||
} else {
|
||||
newToken = `${createdToken}`
|
||||
}
|
||||
|
||||
dispatch('tokenCreated', newToken ?? newMcpToken)
|
||||
if (actions) {
|
||||
actions()
|
||||
}
|
||||
mcpCreationMode = false
|
||||
} catch (err) {
|
||||
console.error('Failed to create token:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const workspaces = $derived(ensureCurrentWorkspaceIncluded($userWorkspaces, $workspaceStore))
|
||||
const mcpBaseUrl = $derived(`${window.location.origin}/api/mcp/w/${newTokenWorkspace}/sse?token=`)
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if newToken}
|
||||
<div
|
||||
class="border rounded-md mb-6 px-2 py-2 bg-green-50 dark:bg-green-200 dark:text-green-800 flex flex-row flex-wrap"
|
||||
>
|
||||
<div>
|
||||
Added token: <button onclick={handleCopyClick} class="inline-flex gap-2 items-center">
|
||||
{newToken}
|
||||
<Clipboard size={12} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="pt-1 text-xs ml-2">
|
||||
Make sure to copy your personal access token now. You won't be able to see it again!
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if newMcpToken}
|
||||
<div
|
||||
class="border rounded-md mb-6 px-2 py-2 bg-green-50 dark:bg-green-200 dark:text-green-800 flex flex-row flex-wrap"
|
||||
>
|
||||
<p class="text-sm mb-2">New MCP URL:</p>
|
||||
<ClipboardPanel content={`${mcpBaseUrl}${newMcpToken}`} />
|
||||
<p class="pt-1 text-xs">
|
||||
Make sure to copy this URL now. You won't be able to see it again!
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="py-3 px-3 border rounded-md mb-6 bg-surface-secondary min-w-min">
|
||||
<h3 class="pb-3 font-semibold">Add a new token</h3>
|
||||
|
||||
{#if showMcpMode}
|
||||
<div class="mb-4 flex flex-row flex-shrink-0">
|
||||
<TriggerableByAI
|
||||
id="account-settings-create-mcp-token"
|
||||
description="Create a new MCP token to authenticate to the Windmill API"
|
||||
>
|
||||
<Toggle
|
||||
on:change={(e) => {
|
||||
mcpCreationMode = e.detail
|
||||
if (e.detail) {
|
||||
newTokenLabel = 'MCP token'
|
||||
newTokenExpiration = undefined
|
||||
newTokenWorkspace = $workspaceStore
|
||||
} else {
|
||||
newTokenLabel = undefined
|
||||
newTokenExpiration = undefined
|
||||
newTokenWorkspace = defaultNewTokenWorkspace
|
||||
}
|
||||
}}
|
||||
checked={mcpCreationMode}
|
||||
options={{
|
||||
right: 'Generate MCP URL',
|
||||
rightTooltip:
|
||||
'Generate a new MCP URL to make your scripts and flows available as tools through your LLM clients.',
|
||||
rightDocumentationLink: 'https://www.windmill.dev/docs/core_concepts/mcp'
|
||||
}}
|
||||
size="xs"
|
||||
/>
|
||||
</TriggerableByAI>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if scopes != undefined}
|
||||
<div class="mb-4">
|
||||
<span class="block mb-1">Scope</span>
|
||||
{#each scopes as scope}
|
||||
<input disabled type="text" value={scope} class="mb-2 w-full" />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{#if mcpCreationMode}
|
||||
<div>
|
||||
<span class="block mb-1">Scope</span>
|
||||
<ToggleButtonGroup bind:selected={newMcpScope} allowEmpty={false}>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton
|
||||
{item}
|
||||
value="favorites"
|
||||
label="Favorites only"
|
||||
tooltip="Make only your favorite scripts and flows available as tools"
|
||||
/>
|
||||
<ToggleButton
|
||||
{item}
|
||||
value="all"
|
||||
label="All scripts/flows"
|
||||
tooltip="Make all your scripts and flows available as tools"
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="block mb-1">Hub scripts (optional)</span>
|
||||
{#if loadingApps}
|
||||
<div>Loading...</div>
|
||||
{:else if errorFetchApps}
|
||||
<div>Error fetching apps</div>
|
||||
{:else}
|
||||
<MultiSelectWrapper items={allApps} placeholder="Select apps" bind:value={newMcpApps} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="block mb-1">Workspace</span>
|
||||
<select bind:value={newTokenWorkspace} disabled={workspaces.length === 1} class="w-full">
|
||||
{#each workspaces as workspace}
|
||||
<option value={workspace.id}>{workspace.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<span class="block mb-1">Label <span class="text-xs text-tertiary">(optional)</span></span>
|
||||
<input type="text" bind:value={newTokenLabel} class="w-full" />
|
||||
</div>
|
||||
|
||||
{#if !mcpCreationMode}
|
||||
<div>
|
||||
<span class="block mb-1"
|
||||
>Expires In <span class="text-xs text-tertiary">(optional)</span></span
|
||||
>
|
||||
<select bind:value={newTokenExpiration} class="w-full">
|
||||
<option value={undefined}>No expiration</option>
|
||||
<option value={15 * 60}>15m</option>
|
||||
<option value={30 * 60}>30m</option>
|
||||
<option value={1 * 60 * 60}>1h</option>
|
||||
<option value={1 * 24 * 60 * 60}>1d</option>
|
||||
<option value={7 * 24 * 60 * 60}>7d</option>
|
||||
<option value={30 * 24 * 60 * 60}>30d</option>
|
||||
<option value={90 * 24 * 60 * 60}>90d</option>
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end gap-2 flex-row">
|
||||
<Button
|
||||
on:click={() => {
|
||||
mcpCreationMode = false
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => createToken(mcpCreationMode)}
|
||||
disabled={mcpCreationMode && newTokenWorkspace == undefined}
|
||||
>
|
||||
New token
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,12 +5,6 @@ export type Changelog = {
|
||||
}
|
||||
|
||||
const changelogs: Changelog[] = [
|
||||
{
|
||||
label: 'Improved Rust development iteration',
|
||||
href: 'https://www.windmill.dev/changelog/rust-development-iteration',
|
||||
date: '2025-06-17'
|
||||
},
|
||||
|
||||
{
|
||||
label: 'MQTT triggers',
|
||||
href: 'https://www.windmill.dev/changelog/mqtt-triggers',
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
const hasScrollbar = tableContainer.scrollHeight > tableContainer.clientHeight
|
||||
if (!hasScrollbar && hasMore) {
|
||||
dispatch('loadMore')
|
||||
triggerLoadMore()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,11 +92,20 @@
|
||||
|
||||
if (!tableContainer) return
|
||||
const { scrollTop, scrollHeight, clientHeight } = tableContainer
|
||||
if (scrollHeight - (scrollTop + clientHeight) < 50) {
|
||||
dispatch('loadMore')
|
||||
if (scrollHeight - (scrollTop + clientHeight) === 0) {
|
||||
triggerLoadMore()
|
||||
}
|
||||
}
|
||||
|
||||
let lastLoadMoreDispatch: number | undefined = $state(undefined)
|
||||
function triggerLoadMore() {
|
||||
if (lastLoadMoreDispatch && Date.now() - lastLoadMoreDispatch < 1000) {
|
||||
return
|
||||
}
|
||||
dispatch('loadMore')
|
||||
lastLoadMoreDispatch = Date.now()
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (tableContainer && hasMore && !loading) {
|
||||
untrack(() => checkScrollStatus())
|
||||
@@ -170,7 +179,7 @@
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
on:click={() => dispatch('loadMore')}
|
||||
on:click={() => triggerLoadMore()}
|
||||
endIcon={{ icon: ArrowDownIcon }}
|
||||
>
|
||||
Load {loadMore} more
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user