mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
Compare commits
79
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b195ef8b45 | ||
|
|
eb18d0d1f7 | ||
|
|
f1492036a7 | ||
|
|
4398013e81 | ||
|
|
80d12426f6 | ||
|
|
58975b58dc | ||
|
|
57155dd318 | ||
|
|
24604c35ba | ||
|
|
b82e6516ef | ||
|
|
f250d775ce | ||
|
|
87b76c0cb6 | ||
|
|
4ec1dce531 | ||
|
|
2b37281084 | ||
|
|
76569abb1e | ||
|
|
8fb082e5f3 | ||
|
|
34773f2614 | ||
|
|
a41edd236b | ||
|
|
1892895cd9 | ||
|
|
67381436a5 | ||
|
|
a4be29c34d | ||
|
|
f8ba3d1a1b | ||
|
|
ef14290265 | ||
|
|
36b742b3ad | ||
|
|
3d6d697ed3 | ||
|
|
f03a8d69c0 | ||
|
|
e1629f799d | ||
|
|
735ca2f70f | ||
|
|
8ec4d615d2 | ||
|
|
85a9c91895 | ||
|
|
0dc84254fc | ||
|
|
60dd969d6c | ||
|
|
0893ce103f | ||
|
|
22c90ba090 | ||
|
|
8bcd555351 | ||
|
|
1112de87d4 | ||
|
|
322f68053a | ||
|
|
1fef46027b | ||
|
|
c27df6a917 | ||
|
|
13bf33f83c | ||
|
|
2471c7acad | ||
|
|
49f6a3d979 | ||
|
|
a47463e053 | ||
|
|
e134364afe | ||
|
|
47c6386d0f | ||
|
|
4ebea68d73 | ||
|
|
ea2f71d8be | ||
|
|
2648520b53 | ||
|
|
962465dd8b | ||
|
|
eb5ac2594c | ||
|
|
147e6975c4 | ||
|
|
be6db04397 | ||
|
|
fcc2c080da | ||
|
|
ff08759a1a | ||
|
|
c8fea3d34a | ||
|
|
2a6424672b | ||
|
|
49e6af0302 | ||
|
|
3cc69a03ac | ||
|
|
5a97258375 | ||
|
|
42e06e7feb | ||
|
|
0dd785e02a | ||
|
|
414f099188 | ||
|
|
755e334303 | ||
|
|
852bf064dc | ||
|
|
82aac9c666 | ||
|
|
f31b276988 | ||
|
|
32b8e692b1 | ||
|
|
9ac09d95e2 | ||
|
|
4d3746335e | ||
|
|
398c0bb34e | ||
|
|
616a1a7308 | ||
|
|
7b4d6e62f4 | ||
|
|
9e5a1cee0c | ||
|
|
9fbb1992e5 | ||
|
|
84f76eebf7 | ||
|
|
eb9443ffc5 | ||
|
|
d653644329 | ||
|
|
545d3ce74c | ||
|
|
77debd1759 | ||
|
|
fb9d7d4c95 |
@@ -46,7 +46,13 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 2. Otherwise fall back to the org-membership check
|
||||
# 2. Disallow other bots
|
||||
if [[ "${COMMENTER}" =~ \[bot\]$ ]]; then
|
||||
echo "is_member=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 3. Otherwise check if the user is a member of the organization
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-H "Authorization: token $ORG_ACCESS_TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
|
||||
@@ -98,6 +98,7 @@ jobs:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
timeout_minutes: "60"
|
||||
allowed_tools: "mcp__github__create_pull_request,Bash"
|
||||
allowed_bots: "windmill-internal-app[bot]"
|
||||
custom_instructions: |
|
||||
## IMPORTANT INSTRUCTIONS
|
||||
- Your branch name should be a short description of the requested changes.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Rust Client Check
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Change versions"]
|
||||
types:
|
||||
- completed
|
||||
push:
|
||||
paths:
|
||||
- "rust-client/**"
|
||||
- "backend/**/*.rs"
|
||||
- "backend/windmill-api/openapi.yaml"
|
||||
- "version.txt"
|
||||
- "flake.nix"
|
||||
- ".github/workflows/rust-client-check.yml"
|
||||
|
||||
jobs:
|
||||
check_rust_client:
|
||||
runs-on: ubicloud-standard-8
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v20
|
||||
with:
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
- name: Check rust client builds
|
||||
run: cd rust-client && nix develop ../ --command ./dev.nu --check
|
||||
timeout-minutes: 16
|
||||
+113
@@ -1,5 +1,118 @@
|
||||
# Changelog
|
||||
|
||||
## [1.525.0](https://github.com/windmill-labs/windmill/compare/v1.524.0...v1.525.0) (2025-08-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **aichat:** add test tool to script and flow mode ([#6367](https://github.com/windmill-labs/windmill/issues/6367)) ([34773f2](https://github.com/windmill-labs/windmill/commit/34773f2614450d0e82b190c04bb446dec74f84dc))
|
||||
* **cli:** add better error handling with path logging for JSON parsing failures ([#6370](https://github.com/windmill-labs/windmill/issues/6370)) ([f03a8d6](https://github.com/windmill-labs/windmill/commit/f03a8d69c017e5ac8bb34cabdfd5c634dc126f3f))
|
||||
* **frontend:** add flow log view ([#6330](https://github.com/windmill-labs/windmill/issues/6330)) ([4ec1dce](https://github.com/windmill-labs/windmill/commit/4ec1dce5313177079177b9d90558e8085599d19d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix csharp build hanging ([ef14290](https://github.com/windmill-labs/windmill/commit/ef14290265eaf327d3e42b7f2fbb9dfd9eb3a873))
|
||||
* fix resource type search when adding resources ([e1629f7](https://github.com/windmill-labs/windmill/commit/e1629f799d60b4ca5db1e469cac57cc6cfc7d83f))
|
||||
* **frontend:** do not open popup when clicking on wand in flow inline script editor ([#6374](https://github.com/windmill-labs/windmill/issues/6374)) ([2b37281](https://github.com/windmill-labs/windmill/commit/2b372810844cd28019145c1a825dd0ac6e924292))
|
||||
* **frontend:** fix minor issues in the UI ([#6382](https://github.com/windmill-labs/windmill/issues/6382)) ([a41edd2](https://github.com/windmill-labs/windmill/commit/a41edd236bdd3196468cdf2586c95ff0a4c1abf5))
|
||||
|
||||
## [1.524.0](https://github.com/windmill-labs/windmill/compare/v1.523.0...v1.524.0) (2025-08-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **mcp:** allow filtering by folder ([#6366](https://github.com/windmill-labs/windmill/issues/6366)) ([8ec4d61](https://github.com/windmill-labs/windmill/commit/8ec4d615d251a0a2ed26f3b1907d6f813d91f43c))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **app:** improve copy paste of tables with sub-components ([0dc8425](https://github.com/windmill-labs/windmill/commit/0dc84254fc152df82ffbd137f80ed01225c00043))
|
||||
* fix preprocessor usage in python ([85a9c91](https://github.com/windmill-labs/windmill/commit/85a9c91895d0460e5e7d2d9ff0e53d05e2354386))
|
||||
* fix v1.523.0 rust sdk build ([#6363](https://github.com/windmill-labs/windmill/issues/6363)) ([0893ce1](https://github.com/windmill-labs/windmill/commit/0893ce103ffcfbcb037c9e2ab851e480b61b3735))
|
||||
|
||||
## [1.523.0](https://github.com/windmill-labs/windmill/compare/v1.522.1...v1.523.0) (2025-08-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **aichat:** add api mode to call api endpoints ([#6343](https://github.com/windmill-labs/windmill/issues/6343)) ([2471c7a](https://github.com/windmill-labs/windmill/commit/2471c7acad3404dd37649ed661ab8793607b5a97))
|
||||
* **aichat:** add gpt5 compatibility ([#6358](https://github.com/windmill-labs/windmill/issues/6358)) ([49f6a3d](https://github.com/windmill-labs/windmill/commit/49f6a3d979ab8933de197dc0065c2720131fd597))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add extra query params for token request for client credentials ([#6360](https://github.com/windmill-labs/windmill/issues/6360)) ([1112de8](https://github.com/windmill-labs/windmill/commit/1112de87d440db5c8d0ddab6e3812dc432a58705))
|
||||
* **app:** improve carousel list recursive error ([a47463e](https://github.com/windmill-labs/windmill/commit/a47463e05398e4ebe0f2d09ee2eba462bcef217b))
|
||||
* improve app component loading speed ([13bf33f](https://github.com/windmill-labs/windmill/commit/13bf33f83c6660e05b78a8c941c2adc1b486f810))
|
||||
* improve app decision tree behavior ([322f680](https://github.com/windmill-labs/windmill/commit/322f68053a238d6ef7c0116ccfb1a49a19e08f76))
|
||||
* remove spurrious error log for apps ([c27df6a](https://github.com/windmill-labs/windmill/commit/c27df6a917d8a585593de0183f7ff1c5bb2a0321))
|
||||
|
||||
## [1.522.1](https://github.com/windmill-labs/windmill/compare/v1.522.0...v1.522.1) (2025-08-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **app:** handle inline script of components with underscore in apps ([2648520](https://github.com/windmill-labs/windmill/commit/2648520b53925616b02ecab060e4d2d6db8c2e34))
|
||||
* **app:** improve id handling for transformers ([47c6386](https://github.com/windmill-labs/windmill/commit/47c6386d0ff6d59367ee38e0704d0e98802e1bff))
|
||||
* improve validate ID for id editors ([ea2f71d](https://github.com/windmill-labs/windmill/commit/ea2f71d8be424fe13772ec1b7eba85d55bc4eae4))
|
||||
|
||||
## [1.522.0](https://github.com/windmill-labs/windmill/compare/v1.521.0...v1.522.0) (2025-08-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add configurable stale jobs detection and cancellation ([147e697](https://github.com/windmill-labs/windmill/commit/147e6975c4b1e6c63e7b6b77c6645f8c88f0f78b))
|
||||
|
||||
## [1.521.0](https://github.com/windmill-labs/windmill/compare/v1.520.1...v1.521.0) (2025-08-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add instance-wide workspace prefix option for custom app ([#6180](https://github.com/windmill-labs/windmill/issues/6180)) ([414f099](https://github.com/windmill-labs/windmill/commit/414f09918856eb1d577eb7776273b6697d11e848))
|
||||
* nextcloud oauth ([#6341](https://github.com/windmill-labs/windmill/issues/6341)) ([755e334](https://github.com/windmill-labs/windmill/commit/755e3343035402b5993a516e58d03c10c47c3a00))
|
||||
* togglable manual acknowledgement for gcp trigger ([#6321](https://github.com/windmill-labs/windmill/issues/6321)) ([852bf06](https://github.com/windmill-labs/windmill/commit/852bf064dc4f640dab4248082afade6eae8bb2cc))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* display if tag has an active workers attached to it in tag select ([2a64246](https://github.com/windmill-labs/windmill/commit/2a6424672b5ed6adb1a408ddc8acbf7d7b2221ac))
|
||||
* do not save license key when renewing if the expiry date is earlier than that of the current key ([#6346](https://github.com/windmill-labs/windmill/issues/6346)) ([5a97258](https://github.com/windmill-labs/windmill/commit/5a97258375d76164ed17f7b258fa9b3222459fe1))
|
||||
|
||||
## [1.520.1](https://github.com/windmill-labs/windmill/compare/v1.520.0...v1.520.1) (2025-08-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix oss build ([32b8e69](https://github.com/windmill-labs/windmill/commit/32b8e692b1e8ba549b75a72ff17300aca6f38f8e))
|
||||
|
||||
## [1.520.0](https://github.com/windmill-labs/windmill/compare/v1.519.2...v1.520.0) (2025-08-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add email workspace error handler if smtp is configured ([#6267](https://github.com/windmill-labs/windmill/issues/6267)) ([9fbb199](https://github.com/windmill-labs/windmill/commit/9fbb1992e505d47cbb3750db9c672e69b0d5f8e7))
|
||||
* **mcp:** add api endpoints as tools ([#6329](https://github.com/windmill-labs/windmill/issues/6329)) ([4d37463](https://github.com/windmill-labs/windmill/commit/4d3746335e5f9056dafdb0557169f8fae5449005))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve result stream query efficiency ([9e5a1ce](https://github.com/windmill-labs/windmill/commit/9e5a1cee0cc833d33d3b654cf330723295c3f31a))
|
||||
|
||||
## [1.519.2](https://github.com/windmill-labs/windmill/compare/v1.519.1...v1.519.2) (2025-08-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* native text response streaming in agent workers ([545d3ce](https://github.com/windmill-labs/windmill/commit/545d3ce74c1edcf588289dc67aa4c365dd48ed10))
|
||||
|
||||
## [1.519.1](https://github.com/windmill-labs/windmill/compare/v1.519.0...v1.519.1) (2025-08-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix python-client f formatting error ([fb9d7d4](https://github.com/windmill-labs/windmill/commit/fb9d7d4c95601965a0c00fe07458ea1b77a0fe69))
|
||||
|
||||
## [1.519.0](https://github.com/windmill-labs/windmill/compare/v1.518.2...v1.519.0) (2025-08-06)
|
||||
|
||||
|
||||
|
||||
+3
-1
@@ -7,4 +7,6 @@ heaptrack*
|
||||
index/
|
||||
windmill-api/openapi-*.*
|
||||
.duckdb/*
|
||||
*ee.rs
|
||||
*ee.rs
|
||||
generate_mcp_endpoints_tools/venv
|
||||
bacon.toml
|
||||
+12
@@ -147,6 +147,16 @@
|
||||
"ordinal": 28,
|
||||
"name": "ducklake",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"name": "auto_add_instance_groups",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 30,
|
||||
"name": "auto_add_instance_groups_roles",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -183,6 +193,8 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow\n (workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at) \n SELECT workspace_id, REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1'), summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at\n FROM flow \n WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0a56301b5aaf57339cb2904c8f617366b74e891034d32f2867ccb019da869fc8"
|
||||
}
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO usr (workspace_id, username, email, is_admin, operator) VALUES ($1, $2, $3, false, $4) ON CONFLICT DO NOTHING",
|
||||
"query": "INSERT INTO usr (workspace_id, username, email, is_admin, operator, added_via) VALUES ($1, $2, $3, false, $4, $5) ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -8,10 +8,11 @@
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
"Bool",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e822d186203fe809b764007ad7c02870a3b7d93ae43b40ac2cd3181dffab0837"
|
||||
"hash": "0d7ce0397ef15c9d6cdaeaa2730a9e27fb7387ca24980df1481e6a94622ef006"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n nats_trigger\n SET\n nats_resource_path = $1,\n subjects = $2,\n stream_name = $3,\n consumer_name = $4,\n use_jetstream = $5,\n script_path = $6,\n path = $7,\n is_flow = $8,\n edited_by = $9,\n email = $10,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $13,\n error_handler_args = $14,\n retry = $15\n WHERE\n workspace_id = $11 AND path = $12\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"VarcharArray",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0d8415a79d8312db4d46da46fdf348ab17dee7b8358b6719fa399511962bc3e9"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE\n workspace_settings\n SET\n error_handler = NULL,\n error_handler_extra_args = NULL,\n error_handler_muted_on_cancel = NULL\n WHERE\n workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0fa105c49c8345916716514444bd3616ae4d114216c659233fbbc3c047e6b30a"
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT name, summary, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup GROUP BY name, summary",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "emails",
|
||||
"type_info": "VarcharArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "10f6d3ffd7406146572b1becdce5c8da5242b58f6ce46ab10296cff9d6a3a6c4"
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n path,\n custom_path\n FROM \n app\n WHERE \n custom_path IN (\n SELECT \n custom_path\n FROM \n app\n GROUP \n BY custom_path\n HAVING COUNT(*) > 1\n )\n ORDER BY custom_path\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "custom_path",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "11e24f758a70cd5f3a240bc81a05f40754826db0ee1194409227597a98603e92"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT tag::text, EXISTS(SELECT 1 FROM worker_ping WHERE custom_tags @> ARRAY[tag] AND ping_at > now() - interval '1 minute') as exists\n FROM unnest($1::text[]) as tag",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "tag",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "18ca698813b58c7f93139b12818681f9e36e8bb69b7a37755dc9b90c5e1af4cf"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COALESCE(auto_add_instance_groups, '{}') FROM workspace_settings WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "coalesce",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "18e550f4ec23d465632449b88c4b25931f145f771b93828e8e6dfcc1f906443d"
|
||||
}
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n gcp_trigger \n SET \n gcp_resource_path = $1,\n subscription_id = $2,\n topic_id = $3,\n delivery_type = $4,\n delivery_config = $5,\n is_flow = $6, \n edited_by = $7, \n email = $8,\n script_path = $9,\n path = $10,\n enabled = $11,\n edited_at = now(), \n error = NULL,\n server_id = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE \n workspace_id = $12 AND \n path = $13\n ",
|
||||
"query": "\n UPDATE \n gcp_trigger \n SET \n gcp_resource_path = $1,\n subscription_id = $2,\n topic_id = $3,\n delivery_type = $4,\n delivery_config = $5,\n is_flow = $6, \n edited_by = $7, \n email = $8,\n script_path = $9,\n path = $10,\n enabled = $11,\n edited_at = now(), \n error = NULL,\n server_id = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16,\n auto_acknowledge_msg = $17\n WHERE \n workspace_id = $12 AND \n path = $13\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -30,10 +30,11 @@
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
"Jsonb",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "566823041170df3a6379ce58d321895746b0cd8a6891b05d51abeb7079ded5d7"
|
||||
"hash": "1a9ba16c90d3d65c4ff39aaddb3079009e03af711e7f6b53332537cf4cb0e8dd"
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n ig.name as group_name,\n ws.workspace_id,\n w.name as workspace_name,\n ws.auto_add_instance_groups_roles->ig.name as role\n FROM instance_group ig\n INNER JOIN workspace_settings ws ON ws.auto_add_instance_groups IS NOT NULL\n AND ig.name = ANY(ws.auto_add_instance_groups)\n INNER JOIN workspace w ON w.id = ws.workspace_id AND w.deleted = false\n ORDER BY ig.name, ws.workspace_id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "group_name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "workspace_name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "role",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1e6c125c884002a1565b11a2c308ce89f9726d21bbb3cc712ad0b6450cbb44e6"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT email FROM email_to_igroup WHERE igroup = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "21099fabde943edc90d3a0125e8490691b2701d11f918da3bb5ae610d5c023a0"
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT username, added_via\n FROM usr\n WHERE workspace_id = $1 AND email = $2\n AND added_via->>'source' = 'instance_group'\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "added_via",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "2132a715995f2775917c59c01ba66f2e472a2347f25cc7109f36a805806ee6e2"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET auto_add_instance_groups = NULL, auto_add_instance_groups_roles = NULL WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "31db242523c534b2abbb5f4985f56dd4493d19611ed988d694d9f4289dc29c48"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM\n gcp_trigger\n WHERE\n delivery_type != 'push'::DELIVERY_MODE AND\n enabled IS TRUE\n AND (last_server_ping IS NULL OR\n last_server_ping < now() - interval '15 seconds'\n )\n ",
|
||||
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\",\n auto_acknowledge_msg\n FROM\n gcp_trigger\n WHERE\n delivery_type != 'push'::DELIVERY_MODE AND\n enabled IS TRUE\n AND (last_server_ping IS NULL OR\n last_server_ping < now() - interval '15 seconds'\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -127,6 +127,11 @@
|
||||
"ordinal": 20,
|
||||
"name": "retry: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "auto_acknowledge_msg",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -153,8 +158,9 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "9aa094ef0652f36d473866469e71cc41c7b1cf848d0b4beffa7eefc0fbf9d64f"
|
||||
"hash": "33aaf2dd14397d0b50b986ed55bc458337b85b6a9a9ab3d93d8f33d0f57e4b0f"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n websocket_trigger\n SET\n url = $1,\n script_path = $2,\n path = $3,\n is_flow = $4,\n filters = $5,\n initial_messages = $6,\n url_runnable_args = $7,\n edited_by = $8,\n email = $9,\n can_return_message = $10,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $13,\n error_handler_args = $14,\n retry = $15\n WHERE\n workspace_id = $11 AND path = $12\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"JsonbArray",
|
||||
"JsonbArray",
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "35b211d19e53da4b64b0bd097284de3236ab939e47a1fc2b15ffc9607b552f8d"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n workspace_id,\n auto_add_instance_groups_roles\n FROM workspace_settings\n WHERE\n auto_add_instance_groups IS NOT NULL\n AND $1 = ANY(auto_add_instance_groups)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "auto_add_instance_groups_roles",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "3ebc91867be0a4830ef4dbbe833aed348f78e3cb68fb1f3c855a491bdcda5017"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n workspace_id,\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n replication_slot_name,\n publication_name,\n postgres_resource_path,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\" \n FROM postgres_trigger\n WHERE workspace_id = $1",
|
||||
"query": "\n SELECT \n workspace_id,\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n replication_slot_name,\n publication_name,\n postgres_resource_path,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n postgres_trigger\n WHERE \n workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -120,5 +120,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bf1fba73d3109aae48bda524cfafdf7f5b9b0e56396aed1b83a2c235222e8983"
|
||||
"hash": "3ebf788b86d6aed59b8c61c08f1d9323e6c66107a1b7b37dadbbb7b91339e9d5"
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE kafka_trigger SET kafka_resource_path = $1, group_id = $2, topics = $3, script_path = $4, path = $5, is_flow = $6, edited_by = $7, email = $8, edited_at = now(), server_id = NULL, error = NULL, error_handler_path = $11, error_handler_args = $12, retry = $13\n WHERE workspace_id = $9 AND path = $10",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"VarcharArray",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3f91433fc076f47dd3516b40c9337981c329ddc31132882f037eaf2709aa8805"
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT usr.username \n FROM usr_to_group LEFT JOIN usr ON usr_to_group.usr = usr.username AND usr_to_group.workspace_id = $2\n WHERE group_ = $1 AND usr.workspace_id = $2 AND usr_to_group.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3fdfcab1a54c166b1d8d43215d61268a251160db4630f0342522091668f36af0"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET auto_add_instance_groups = $2, auto_add_instance_groups_roles = $3 WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"TextArray",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "404233e74aaafd987879c6c87d1fd80928e6fade09e60a9a2d35121c81885cca"
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, initial_messages = $6, url_runnable_args = $7, edited_by = $8, email = $9, can_return_message = $10, edited_at = now(), server_id = NULL, error = NULL, error_handler_path = $13, error_handler_args = $14, retry = $15\n WHERE workspace_id = $11 AND path = $12",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"JsonbArray",
|
||||
"JsonbArray",
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4479beaefc36afba093e5113bb330a87979623d69b94f274863318ed351a1dac"
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT workspace_id, username, email\n FROM usr\n WHERE email = $1\n AND added_via->>'source' = 'instance_group'\n AND added_via->>'group' = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "472f224dc9e17d2c50cb9db13c34cc1fb9adb6d0ea36cf223541adb7cac17bdd"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM kafka_trigger WHERE workspace_id = $1 AND path = $2",
|
||||
"query": "SELECT \n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n kafka_trigger \n WHERE \n workspace_id = $1 AND \n path = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -121,5 +121,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "6489342f52a9e1e90466bad48e0a6079fcabd696b02d57e3dbe652d28b84f008"
|
||||
"hash": "494d49e92dce7a1cd7074156704ccbbff020a2042409fc318e045852323cef5e"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET\n auto_add_instance_groups = array_remove(auto_add_instance_groups, $1),\n auto_add_instance_groups_roles = auto_add_instance_groups_roles - $1\n WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4a446b97cf813226d25ca40cefca2999cdcf938f817327e9168b3edb0fce7fb7"
|
||||
}
|
||||
+2
-1
@@ -223,7 +223,8 @@
|
||||
"ui",
|
||||
"postgres",
|
||||
"sqs",
|
||||
"gcp"
|
||||
"gcp",
|
||||
"mqtt"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE kafka_trigger\n SET\n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n edited_by = $7,\n email = $8,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $11,\n error_handler_args = $12,\n retry = $13\n WHERE\n workspace_id = $9 AND path = $10\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"VarcharArray",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4bfa42153df81184e02ffc7ab6c8c591605211c070e52b2919cf9d7dede0ea16"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\" \n FROM kafka_trigger\n WHERE workspace_id = $1",
|
||||
"query": "SELECT \n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM kafka_trigger\n WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -120,5 +120,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "8f511fd837a99fa37a95781e50f8a83eaf93651f82c36c9795637d3ad761d003"
|
||||
"hash": "4c6fbed80ba44defe3d125c258bc3449b02925c8caf85d9203529471479fdde8"
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n COALESCE(jc.result, NULL) as \"result: sqlx::types::Json<Box<RawValue>>\", \n SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\", \n CHAR_LENGTH(rs.stream) + 1 AS stream_offset\n FROM (\n SELECT $2::uuid as job_id, $1::text as workspace_id\n ) base\n LEFT JOIN v2_job_completed jc ON jc.id = base.job_id AND jc.workspace_id = base.workspace_id\n LEFT JOIN job_result_stream rs ON rs.job_id = base.job_id\n WHERE base.job_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "result: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "result_stream: Option<String>",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "stream_offset",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "506f90984a8b672aeff50b6d9e4751ea1521c524b9b7f51a48ee26280825ac9a"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT usage.usage FROM usage \n WHERE is_workspace = false \n AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
|
||||
"query": "\n SELECT usage.usage FROM usage\n WHERE is_workspace = false\n AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -18,5 +18,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a7b028e832396ee4d1ad6dfd44ba6134344f3eb37dbf0254154eba31b9cc2ed3"
|
||||
"hash": "51cfe6efb154934f8cfbe77a7313891ef5c46a7febe4125d164a5ae5b55af2d6"
|
||||
}
|
||||
+12
@@ -147,6 +147,16 @@
|
||||
"ordinal": 28,
|
||||
"name": "ducklake",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"name": "auto_add_instance_groups",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 30,
|
||||
"name": "auto_add_instance_groups_roles",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -183,6 +193,8 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE nats_trigger SET nats_resource_path = $1, subjects = $2, stream_name = $3, consumer_name = $4, use_jetstream = $5, script_path = $6, path = $7, is_flow = $8, edited_by = $9, email = $10, edited_at = now(), server_id = NULL, error = NULL, error_handler_path = $13, error_handler_args = $14, retry = $15\n WHERE workspace_id = $11 AND path = $12",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"VarcharArray",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "570a4684bbdb390e4652301aa8d2d09b6e09b258f748d48cf2d188465621ee04"
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM worker_ping WHERE custom_tags @> $1 AND ping_at > now() - interval '1 minute')",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5d1ec728380ea8baf64df54743e73008ee9a58e6f39a5bf31a2ed099727f5c04"
|
||||
}
|
||||
+6
@@ -42,6 +42,11 @@
|
||||
"ordinal": 7,
|
||||
"name": "role",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "added_via",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -57,6 +62,7 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
+6
@@ -42,6 +42,11 @@
|
||||
"ordinal": 7,
|
||||
"name": "role",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "added_via",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -58,6 +63,7 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
+7
-1
@@ -45,11 +45,16 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "added_via",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "super_admin",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
@@ -69,6 +74,7 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
]
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM group_ WHERE (group_.extra_perms ->> CONCAT('u/', $1::text))::boolean AND name = $2 AND workspace_id = $4) OR exists(\n SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f \n WHERE $2 = g.name AND $4 = g.workspace_id AND SPLIT_PART(key, '/', 1) = 'g' AND key = ANY($3::text[])\n AND value::boolean)",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM group_ WHERE (group_.extra_perms ->> CONCAT('u/', $1::text))::boolean AND name = $2 AND workspace_id = $4) OR exists(\n SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f\n WHERE $2 = g.name AND $4 = g.workspace_id AND SPLIT_PART(key, '/', 1) = 'g' AND key = ANY($3::text[])\n AND value::boolean)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -21,5 +21,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "388d6fd335a3f8a405b2d465892cf21a68d4b50ace25ef88c4cdf5b347c3d5eb"
|
||||
"hash": "6b2820973026b1628cb93fda943bc389a261a3bb975c86ac02dd1c552da92295"
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT created_by AS \"created_by!\", coalesce(job_logs.logs, '') as logs\n FROM v2_job\n LEFT JOIN job_logs ON job_logs.job_id = v2_job.id\n WHERE v2_job.id = $1 AND v2_job.workspace_id = $2 AND ($3::text[] IS NULL OR v2_job.tag = ANY($3))\n ORDER BY job_logs.log_offset DESC\n LIMIT 100",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "created_by!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "logs",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "6df2ca52e3e8515c398943da701600a33e64525809fdd67bce63e28d98eababc"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT usr.username\n FROM usr_to_group LEFT JOIN usr ON usr_to_group.usr = usr.username AND usr_to_group.workspace_id = $2\n WHERE group_ = $1 AND usr.workspace_id = $2 AND usr_to_group.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6f36c05e6097066441acab227c0d5206d5a9a9828b04e4c107f1ecab4a9363d4"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace_id FROM workspace_settings WHERE auto_add_instance_groups IS NOT NULL AND $1 = ANY(auto_add_instance_groups)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "711c72a72298bb656882b35071e910dab721cda070f9c39bba3729bdc1467496"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n workspace_id,\n path,\n nats_resource_path,\n subjects,\n stream_name,\n consumer_name,\n use_jetstream,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM nats_trigger\n WHERE workspace_id = $1",
|
||||
"query": "\n SELECT \n workspace_id,\n path,\n nats_resource_path,\n subjects,\n stream_name,\n consumer_name,\n use_jetstream,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n nats_trigger\n WHERE \n workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -132,5 +132,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5af0299841a0313d0cc270e98b04b5b5968184ae53dc076b1a9bc76af35923da"
|
||||
"hash": "711e665344873454428d84e35f300edb7e0ebad5de40ba5645c1ce3d4a6b84be"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT usr.email, usage.executions\n FROM usr\n , LATERAL (\n SELECT COALESCE(SUM(duration_ms + 1000)/1000 , 0)::BIGINT executions\n FROM v2_as_completed_job\n WHERE workspace_id = $1\n AND job_kind NOT IN ('flow', 'flowpreview', 'flownode')\n AND email = usr.email\n AND now() - '1 week'::interval < created_at \n ) usage\n WHERE workspace_id = $1\n ",
|
||||
"query": "\n SELECT usr.email, usage.executions\n FROM usr\n , LATERAL (\n SELECT COALESCE(SUM(duration_ms + 1000)/1000 , 0)::BIGINT executions\n FROM v2_as_completed_job\n WHERE workspace_id = $1\n AND job_kind NOT IN ('flow', 'flowpreview', 'flownode')\n AND email = usr.email\n AND now() - '1 week'::interval < created_at\n ) usage\n WHERE workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -24,5 +24,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "bff39cc57aba0729ddef1d53f3806c6736556f0a14b489d6708f9879393f9ea3"
|
||||
"hash": "89940a53f29b173b6a8717f057a81a4f2edad152581950fdd80d758a0d242c17"
|
||||
}
|
||||
+2
-2
@@ -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 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 error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\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 error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n http_trigger\n WHERE \n workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -183,5 +183,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "09f43081a8b253d059d9d4302daa139cee6f055cdbafc213b2f992667f8ed578"
|
||||
"hash": "89be9b595a892c82f2a9f3cfa4da213e360ebe8827d5b4680d3c227fb8ddec82"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE usr SET is_admin = true WHERE workspace_id = $1 AND email = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "94feb54cc965f19bfaf09966fd431b925c565f08c100bea812d3ac0e28664ccb"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT DISTINCT email FROM usr WHERE added_via->>'source' = 'instance_group' AND added_via->>'group' = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9530b234b2ac8f7360552be5e6bb25270e6b4185ce5c7f3c6edd9fbaabf77544"
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE\n workspace_settings\n SET\n error_handler = $1,\n error_handler_extra_args = $2,\n error_handler_muted_on_cancel = $3\n WHERE\n workspace_id = $4\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Json",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "97b6e5779661343587c297b275df9e75fd22924ad2dbe124bb361f3e7fd8bfe0"
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT result as \"result: sqlx::types::Json<Box<RawValue>>\", SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\", CHAR_LENGTH(rs.stream) + 1 AS stream_offset\n FROM v2_job_completed FULL OUTER JOIN job_result_stream rs ON rs.job_id = v2_job_completed.id WHERE (v2_job_completed.id = $2 AND v2_job_completed.workspace_id = $1 OR rs.workspace_id = $1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "result: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "result_stream: Option<String>",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "stream_offset",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "9f5b677a02690d3e4b4a5f5e141c7107077bbe90423102b5469e219f2a8b9293"
|
||||
}
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace_id, slack_team_id, teams_team_id, teams_team_name, slack_name, slack_command_script, teams_command_script, slack_email, auto_invite_domain, auto_invite_operator, auto_add, customer_id, plan, webhook, deploy_to, ai_config, error_handler, error_handler_extra_args, error_handler_muted_on_cancel, large_file_storage, ducklake, git_sync, deploy_ui, default_app, default_scripts, mute_critical_alerts, color, operator_settings, git_app_installations FROM workspace_settings WHERE workspace_id = $1",
|
||||
"query": "\n SELECT\n workspace_id,\n slack_team_id,\n teams_team_id,\n teams_team_name,\n slack_name,\n slack_command_script,\n teams_command_script,\n slack_email,\n auto_invite_domain,\n auto_invite_operator,\n auto_add,\n customer_id,\n plan,\n webhook,\n deploy_to,\n ai_config,\n error_handler,\n error_handler_extra_args,\n error_handler_muted_on_cancel,\n large_file_storage,\n ducklake,\n git_sync,\n deploy_ui,\n default_app,\n default_scripts,\n mute_critical_alerts,\n color,\n operator_settings,\n git_app_installations,\n auto_add_instance_groups,\n auto_add_instance_groups_roles\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -147,6 +147,16 @@
|
||||
"ordinal": 28,
|
||||
"name": "git_app_installations",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"name": "auto_add_instance_groups",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 30,
|
||||
"name": "auto_add_instance_groups_roles",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -183,8 +193,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "6e60068b582af46afbb93010355cb5e7cc038042a6fa7089f3f73cd137a63956"
|
||||
"hash": "ac1fd12e9ec92022be38aee0e91e9002e0e810d0e76dce5ced1000a7cb514adb"
|
||||
}
|
||||
+25
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26,\n CASE WHEN $14::VARCHAR IS NOT NULL THEN 'schedule'::job_trigger_kind END,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)",
|
||||
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -98,10 +98,32 @@
|
||||
"Bool",
|
||||
"JsonbArray",
|
||||
"TextArray",
|
||||
"Uuid"
|
||||
"Uuid",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "job_trigger_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"webhook",
|
||||
"http",
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats",
|
||||
"schedule",
|
||||
"app",
|
||||
"ui",
|
||||
"postgres",
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b7c3a66c3831eb5d145ff00807badae57bef81be051f150df754fd1444d7356d"
|
||||
"hash": "acfe583fe17604ba72ba4800b62a72de0a9de0d58ef8c28dd709adf3be021597"
|
||||
}
|
||||
+2
-2
@@ -18,8 +18,8 @@
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT v2_job_queue.id, v2_job.tag, v2_job_queue.scheduled_for, v2_job_queue.workspace_id FROM v2_job_queue LEFT JOIN v2_job ON v2_job_queue.id = v2_job.id WHERE running = false AND scheduled_for < now() - ($1 || ' minutes')::interval",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "scheduled_for",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b45e17ad532a23b394226c9a5d7ab5a21e20202dbbf9c67831cc62eb067cd2ba"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow\n (workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at)\n SELECT workspace_id, REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1'), summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at\n FROM flow\n WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c269f14ae9ae4e96eff9483eb84cccbdfa316e5db051f1d52085a2d5447c81ae"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n gcp_trigger\n WHERE \n workspace_id = $1\n ",
|
||||
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\",\n auto_acknowledge_msg\n FROM \n gcp_trigger\n WHERE \n workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -127,6 +127,11 @@
|
||||
"ordinal": 20,
|
||||
"name": "retry: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "auto_acknowledge_msg",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -155,8 +160,9 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e82b99583d5d3004b4ad6606e7906b8f95b33c6137f136edabb0255bfa6a98f7"
|
||||
"hash": "c61df65f2e4f8bc146b0f70e605f5243ec9aa66ea1a9da9662fc877f4becb04e"
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n COALESCE(jc.result, jc.result) as \"result: sqlx::types::Json<Box<RawValue>>\",\n jq.running as \"running: Option<bool>\",\n SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\",\n CHAR_LENGTH(rs.stream) + 1 AS stream_offset\n FROM (\n SELECT $1::uuid as job_id, $2::text as workspace_id\n ) base\n LEFT JOIN v2_job_completed jc ON jc.id = base.job_id AND jc.workspace_id = base.workspace_id\n LEFT JOIN v2_job_queue jq ON jq.id = base.job_id AND jq.workspace_id = base.workspace_id\n LEFT JOIN job_result_stream rs ON rs.job_id = base.job_id\n WHERE base.job_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "result: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "running: Option<bool>",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "result_stream: Option<String>",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "stream_offset",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "c61f9bad73beb0bc317594f6ca68389c2d0adbef1fb6d73ad23e4153cff44f9e"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT flow.workspace_id as workspace, flow.path, summary, description, flow_version.schema \n FROM flow \n LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n WHERE flow.workspace_id = $1",
|
||||
"query": "SELECT flow.workspace_id as workspace, flow.path, summary, description, flow_version.schema\n FROM flow\n LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n WHERE flow.workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,5 +42,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "974c7e623f3dfa440e134eaaa8d029334c0645147200219c39b2c00b30941172"
|
||||
"hash": "ccbf71572dfc60b69a1666d4f1a883ba724d1e9c3303df32b001737b80a4b9f9"
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET error_handler = $1, error_handler_extra_args = $2, error_handler_muted_on_cancel = $3 WHERE workspace_id = $4",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Json",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d21e5be1ac26db926b7196316dae0c9fa82a865b95bb55de5370e4ee34889ba3"
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET error_handler = NULL, error_handler_extra_args = NULL WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d9308c0154e029f7568b1d021368ca07f4867d192aa58b3888ae532d1c040828"
|
||||
}
|
||||
+6
@@ -42,6 +42,11 @@
|
||||
"ordinal": 7,
|
||||
"name": "role",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "added_via",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -57,6 +62,7 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO gcp_trigger (\n gcp_resource_path,\n subscription_id,\n topic_id,\n delivery_type,\n delivery_config,\n workspace_id, \n path, \n script_path, \n is_flow, \n email, \n enabled, \n edited_by,\n error_handler_path,\n error_handler_args,\n retry\n ) \n VALUES (\n $1, \n $2, \n $3, \n $4,\n $5,\n $6, \n $7, \n $8, \n $9,\n $10,\n $11,\n $12,\n $13,\n $14,\n $15\n )",
|
||||
"query": "\n INSERT INTO gcp_trigger (\n gcp_resource_path,\n subscription_id,\n topic_id,\n delivery_type,\n delivery_config,\n workspace_id, \n path, \n script_path, \n is_flow, \n email, \n enabled, \n edited_by,\n error_handler_path,\n error_handler_args,\n retry,\n auto_acknowledge_msg\n ) \n VALUES (\n $1, \n $2, \n $3, \n $4,\n $5,\n $6, \n $7, \n $8, \n $9,\n $10,\n $11,\n $12,\n $13,\n $14,\n $15,\n $16\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -29,10 +29,11 @@
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
"Jsonb",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7399ff3f22cbf2a522b6123e8ebf101ec9d41b7c8465cc501e61a1f6117981a2"
|
||||
"hash": "e619fa013528a6275f98e14ae1727c55b0d4f4a5e4ee87c29251042e2916f0a0"
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n result as \"result: sqlx::types::Json<Box<RawValue>>\",\n v2_job_queue.running as \"running: Option<bool>\",\n SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\",\n CHAR_LENGTH(rs.stream) + 1 AS stream_offset\n FROM v2_job_completed FULL OUTER JOIN v2_job_queue USING (id) \n LEFT JOIN job_result_stream rs ON rs.job_id = $1\n WHERE (v2_job_queue.id = $1 AND v2_job_queue.workspace_id = $2) OR (v2_job_completed.id = $1 AND v2_job_completed.workspace_id = $2)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "result: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "running: Option<bool>",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "result_stream: Option<String>",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "stream_offset",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "ec0f8fa36328507e51c1974dbef884b755504a6cefa4af34fa4659fb95a7ee9a"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT email_to_igroup.email\n FROM email_to_igroup\n INNER JOIN instance_group ON instance_group.name = email_to_igroup.igroup\n WHERE instance_group.name = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ee182378d9a760c3593b483703ec1682488ca9e6e402ec41abaa6ae3ca263854"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n gcp_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
|
||||
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n subscription_mode AS \"subscription_mode: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\",\n auto_acknowledge_msg\n FROM \n gcp_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -127,6 +127,11 @@
|
||||
"ordinal": 20,
|
||||
"name": "retry: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "auto_acknowledge_msg",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -156,8 +161,9 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b13d1dec373e70ffd58001cd17e9853e76ee8527e46afbfbc303528e512ebed9"
|
||||
"hash": "f434cac3a8eded700b09a8c99dcc5f7c93fbdb1e7d090b11612aa284342e86a1"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE usr SET added_via = $1 WHERE workspace_id = $2 AND email = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f582cac90b4b7d732956b74eebc51323ef8acd3f627e7517451fcc72998d22bc"
|
||||
}
|
||||
+5
-5
@@ -41,11 +41,11 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
Generated
+258
-258
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.519.0"
|
||||
version = "1.525.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -32,7 +32,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.519.0"
|
||||
version = "1.525.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -77,7 +77,7 @@ mcp = ["windmill-api/mcp"]
|
||||
mqtt_trigger = ["windmill-api/mqtt_trigger"]
|
||||
sqs_trigger = ["windmill-api/sqs_trigger", "windmill-common/aws_auth", "windmill-api/openidconnect"]
|
||||
gcp_trigger = ["windmill-api/gcp_trigger"]
|
||||
smtp = ["windmill-api/smtp", "windmill-common/smtp"]
|
||||
smtp = ["windmill-api/smtp", "windmill-common/smtp", "windmill-queue/smtp"]
|
||||
license = ["windmill-api/license"]
|
||||
oauth2 = ["windmill-api/oauth2"]
|
||||
zip = ["windmill-api/zip"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
bb3ec8ecf883cd13be33f81cba98dec745500f62
|
||||
d04959121c6869d764e11e005676003fd2919629
|
||||
@@ -0,0 +1,35 @@
|
||||
## MCP Tools Generator
|
||||
|
||||
The `generate_mcp_tools.py` script parses the OpenAPI specification and generates Rust code for MCP (Model Context Protocol) tools.
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
cd backend/generate_mcp_endpoints_tools
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
python3 generate_mcp_tools.py
|
||||
```
|
||||
|
||||
The script will:
|
||||
1. Parse `backend/windmill-api/openapi.yaml`
|
||||
2. Find all endpoints marked with `x-mcp-tool: true`
|
||||
3. Generate `backend/windmill-api/src/mcp_tools.rs` with a const array of tools
|
||||
|
||||
### Adding MCP Tools
|
||||
|
||||
To mark an endpoint as an MCP tool, add `x-mcp-tool: true` to the operation in the OpenAPI spec. You can also add `x-mcp-instructions` to complete the description of the tool with instructions on how to correctly use the endpoint:
|
||||
|
||||
```yaml
|
||||
/w/{workspace}/scripts/list:
|
||||
get:
|
||||
x-mcp-tool: true
|
||||
x-mcp-instructions: you should call that with this or that arg
|
||||
summary: list scripts in workspace
|
||||
operationId: listScripts
|
||||
# ... rest of endpoint definition
|
||||
```
|
||||
@@ -0,0 +1,302 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to parse the OpenAPI YAML file and generate Rust code with MCP tools.
|
||||
Searches for endpoints tagged with 'x-mcp-tool: true' and creates a const array.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
IMPORTS = """
|
||||
use std::borrow::Cow;
|
||||
use serde::{Deserialize, Serialize};
|
||||
"""
|
||||
|
||||
ENDPOINT_STRUCT = """
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct EndpointTool {
|
||||
pub name: Cow<'static, str>,
|
||||
pub description: Cow<'static, str>,
|
||||
pub instructions: Cow<'static, str>,
|
||||
pub path: Cow<'static, str>,
|
||||
pub method: Cow<'static, str>,
|
||||
pub path_params_schema: Option<serde_json::Value>,
|
||||
pub query_params_schema: Option<serde_json::Value>,
|
||||
pub body_schema: Option<serde_json::Value>,
|
||||
}
|
||||
"""
|
||||
|
||||
def load_openapi_spec(file_path: str) -> Dict[str, Any]:
|
||||
"""Load and parse the OpenAPI YAML specification."""
|
||||
try:
|
||||
import yaml
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f)
|
||||
except ImportError:
|
||||
print("PyYAML not found. Please install it with: pip install PyYAML", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error loading OpenAPI spec: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]], spec: Dict[str, Any]) -> tuple:
|
||||
"""Extract separate schemas for path parameters, query parameters, and request body."""
|
||||
path_params_schema = {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
|
||||
query_params_schema = {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
|
||||
body_schema = None
|
||||
|
||||
# Process parameters
|
||||
for param in parameters:
|
||||
# Resolve $ref if present
|
||||
if '$ref' in param:
|
||||
param = resolve_schema_refs(param, spec)
|
||||
|
||||
param_name = param.get('name', '')
|
||||
param_schema = param.get('schema', {'type': 'string'})
|
||||
param_required = param.get('required', False)
|
||||
param_description = param.get('description', '')
|
||||
param_in = param.get('in', 'query')
|
||||
|
||||
# Resolve any refs in the parameter schema
|
||||
param_schema = resolve_schema_refs(param_schema, spec)
|
||||
|
||||
# Add description if available
|
||||
if param_description:
|
||||
param_schema = dict(param_schema)
|
||||
param_schema['description'] = param_description
|
||||
|
||||
# Route to appropriate schema based on parameter location
|
||||
if param_in == 'path':
|
||||
# Skip 'workspace' path parameter as it's automatically provided by the MCP context
|
||||
if param_name != 'workspace':
|
||||
path_params_schema['properties'][param_name] = param_schema
|
||||
if param_required:
|
||||
path_params_schema['required'].append(param_name)
|
||||
elif param_in == 'query':
|
||||
query_params_schema['properties'][param_name] = param_schema
|
||||
if param_required:
|
||||
query_params_schema['required'].append(param_name)
|
||||
|
||||
# Process request body if present
|
||||
if request_body:
|
||||
body_schema = extract_request_body_schema(request_body, spec)
|
||||
|
||||
# Return None for empty schemas
|
||||
path_params_schema = path_params_schema if path_params_schema['properties'] else None
|
||||
query_params_schema = query_params_schema if query_params_schema['properties'] else None
|
||||
|
||||
return (path_params_schema, query_params_schema, body_schema)
|
||||
|
||||
def resolve_ref(ref_path: str, spec: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Resolve a $ref path to the actual schema definition."""
|
||||
if not ref_path.startswith('#/'):
|
||||
return None
|
||||
|
||||
# Remove the '#/' prefix and split by '/'
|
||||
path_parts = ref_path[2:].split('/')
|
||||
|
||||
# Navigate through the spec following the path
|
||||
current = spec
|
||||
for part in path_parts:
|
||||
if isinstance(current, dict) and part in current:
|
||||
current = current[part]
|
||||
else:
|
||||
return None
|
||||
|
||||
return current if isinstance(current, dict) else None
|
||||
|
||||
def resolve_schema_refs(schema: Dict[str, Any], spec: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Recursively resolve all $ref references in a schema."""
|
||||
if not isinstance(schema, dict):
|
||||
return schema
|
||||
|
||||
# If this is a $ref, resolve it
|
||||
if '$ref' in schema:
|
||||
ref_path = schema['$ref']
|
||||
resolved = resolve_ref(ref_path, spec)
|
||||
if resolved:
|
||||
# Recursively resolve any refs in the resolved schema
|
||||
return resolve_schema_refs(resolved, spec)
|
||||
else:
|
||||
print(f"Warning: Could not resolve $ref: {ref_path}")
|
||||
return schema
|
||||
|
||||
# Recursively process all values in the schema
|
||||
resolved_schema = {}
|
||||
for key, value in schema.items():
|
||||
if isinstance(value, dict):
|
||||
resolved_schema[key] = resolve_schema_refs(value, spec)
|
||||
elif isinstance(value, list):
|
||||
resolved_schema[key] = [
|
||||
resolve_schema_refs(item, spec) if isinstance(item, dict) else item
|
||||
for item in value
|
||||
]
|
||||
else:
|
||||
resolved_schema[key] = value
|
||||
|
||||
return resolved_schema
|
||||
|
||||
def extract_request_body_schema(request_body: Dict[str, Any], spec: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Extract request body schema from OpenAPI requestBody definition and resolve refs."""
|
||||
if not request_body:
|
||||
return None
|
||||
|
||||
content = request_body.get('content', {})
|
||||
json_content = content.get('application/json', {})
|
||||
schema = json_content.get('schema', {})
|
||||
|
||||
if schema:
|
||||
# Resolve any $ref references in the schema
|
||||
return resolve_schema_refs(schema, spec)
|
||||
|
||||
return None
|
||||
|
||||
def http_method_to_rust(method: str) -> str:
|
||||
"""Convert HTTP method string to Rust http::Method enum."""
|
||||
method_map = {
|
||||
'get': 'http::Method::GET',
|
||||
'post': 'http::Method::POST',
|
||||
'put': 'http::Method::PUT',
|
||||
'delete': 'http::Method::DELETE',
|
||||
'patch': 'http::Method::PATCH',
|
||||
'head': 'http::Method::HEAD',
|
||||
'options': 'http::Method::OPTIONS'
|
||||
}
|
||||
return method_map.get(method.lower(), f'http::Method::{method.upper()}')
|
||||
|
||||
def schema_to_rust_value(schema: Optional[Dict[str, Any]]) -> str:
|
||||
"""Convert a schema dict to a Rust serde_json::json! expression."""
|
||||
if schema is None:
|
||||
return "None"
|
||||
return f"Some(serde_json::json!({json.dumps(schema, indent=8)}))"
|
||||
|
||||
def find_mcp_tools(spec: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Find all endpoints marked with x-mcp-tool: true."""
|
||||
tools = []
|
||||
paths = spec.get('paths', {})
|
||||
|
||||
for path, path_item in paths.items():
|
||||
for method, operation in path_item.items():
|
||||
if isinstance(operation, dict) and operation.get('x-mcp-tool') is True:
|
||||
# Extract tool information
|
||||
tool = {
|
||||
'name': operation.get('operationId', f"{method}_{path.replace('/', '_').replace('{', '').replace('}', '')}"),
|
||||
'description': operation.get('summary', operation.get('description', f'{method.upper()} {path}')),
|
||||
'instructions': operation.get('x-mcp-instructions', ''),
|
||||
'path': path,
|
||||
'method': method.upper(),
|
||||
'parameters': operation.get('parameters', []),
|
||||
'requestBody': operation.get('requestBody'),
|
||||
}
|
||||
tools.append(tool)
|
||||
|
||||
return tools
|
||||
|
||||
def generate_rust_code(tools: List[Dict[str, Any]], spec: Dict[str, Any]) -> str:
|
||||
"""Generate the complete Rust code with MCP tools."""
|
||||
if not tools:
|
||||
return """// No MCP tools found in the OpenAPI specification
|
||||
|
||||
{IMPORTS}
|
||||
{ENDPOINT_STRUCT}
|
||||
pub fn all_tools() -> Vec<EndpointTool> {
|
||||
vec![]
|
||||
}
|
||||
"""
|
||||
|
||||
tool_definitions = []
|
||||
|
||||
for tool in tools:
|
||||
tool_name = tool['name']
|
||||
description = tool['description']
|
||||
instructions = tool['instructions']
|
||||
path = tool['path']
|
||||
method = tool['method'].upper()
|
||||
|
||||
# Generate separate schemas
|
||||
path_params_schema, query_params_schema, body_schema = extract_separate_schemas(
|
||||
tool['parameters'], tool['requestBody'], spec
|
||||
)
|
||||
|
||||
path_params_rust = schema_to_rust_value(path_params_schema)
|
||||
query_params_rust = schema_to_rust_value(query_params_schema)
|
||||
body_schema_rust = schema_to_rust_value(body_schema)
|
||||
|
||||
# Generate tool definition
|
||||
tool_def = f""" EndpointTool {{
|
||||
name: Cow::Borrowed("{tool_name}"),
|
||||
description: Cow::Borrowed("{description}"),
|
||||
instructions: Cow::Borrowed("{instructions}"),
|
||||
path: Cow::Borrowed("{path}"),
|
||||
method: Cow::Borrowed("{method}"),
|
||||
path_params_schema: {path_params_rust},
|
||||
query_params_schema: {query_params_rust},
|
||||
body_schema: {body_schema_rust},
|
||||
}}"""
|
||||
tool_definitions.append(tool_def)
|
||||
|
||||
# Combine everything
|
||||
tool_definitions_str = ",\n".join(tool_definitions)
|
||||
|
||||
rust_code = f"""// Auto-generated MCP tools from OpenAPI specification
|
||||
// This file is generated by generate_mcp_tools.py - DO NOT EDIT MANUALLY
|
||||
|
||||
{IMPORTS}
|
||||
{ENDPOINT_STRUCT}
|
||||
pub fn all_tools() -> Vec<EndpointTool> {{
|
||||
vec![
|
||||
{tool_definitions_str}
|
||||
]
|
||||
}}
|
||||
"""
|
||||
|
||||
return rust_code
|
||||
|
||||
def main():
|
||||
"""Main function to parse OpenAPI and generate Rust code."""
|
||||
script_dir = Path(__file__).parent
|
||||
backend_dir = script_dir.parent
|
||||
openapi_file = backend_dir / "windmill-api" / "openapi.yaml"
|
||||
output_file = backend_dir / "windmill-api" / "src" / "mcp" / "tools" / "auto_generated_endpoints.rs"
|
||||
|
||||
if not openapi_file.exists():
|
||||
print(f"OpenAPI file not found: {openapi_file}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Loading OpenAPI specification from: {openapi_file}")
|
||||
spec = load_openapi_spec(str(openapi_file))
|
||||
|
||||
print("Searching for endpoints with x-mcp-tool: true...")
|
||||
tools = find_mcp_tools(spec)
|
||||
|
||||
if tools:
|
||||
print(f"Found {len(tools)} MCP tool(s):")
|
||||
for tool in tools:
|
||||
print(f" - {tool['name']}: {tool['method']} {tool['path']}")
|
||||
else:
|
||||
print("No MCP tools found (no endpoints with x-mcp-tool: true)")
|
||||
|
||||
print(f"Generating Rust code...")
|
||||
rust_code = generate_rust_code(tools, spec)
|
||||
|
||||
print(f"Writing to: {output_file}")
|
||||
output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(rust_code)
|
||||
|
||||
print("Done!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
PyYAML>=6.0
|
||||
+1
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TYPE job_trigger_kind ADD VALUE IF NOT EXISTS 'mqtt';
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
ALTER table gcp_trigger DROP COLUMN auto_acknowledge_msg;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER table gcp_trigger ADD COLUMN auto_acknowledge_msg BOOLEAN DEFAULT true;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Remove auto-add columns for instance groups
|
||||
ALTER TABLE workspace_settings
|
||||
DROP COLUMN auto_add_instance_groups,
|
||||
DROP COLUMN auto_add_instance_groups_roles;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Add auto-add columns for instance groups
|
||||
ALTER TABLE workspace_settings
|
||||
ADD COLUMN auto_add_instance_groups text[] DEFAULT '{}',
|
||||
ADD COLUMN auto_add_instance_groups_roles jsonb DEFAULT '{}';
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Remove added_via tracking
|
||||
DROP INDEX idx_usr_added_via;
|
||||
ALTER TABLE usr DROP COLUMN added_via;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Add added_via column to track how users were added to workspaces
|
||||
-- NULL = manual addition
|
||||
-- {"source": "domain", "domain": "company.com"} = domain auto-add
|
||||
-- {"source": "instance_group", "group": "developers", "role": "developer"} = instance group auto-add
|
||||
ALTER TABLE usr ADD COLUMN added_via jsonb DEFAULT NULL;
|
||||
|
||||
-- Add index for efficient queries on added_via
|
||||
CREATE INDEX idx_usr_added_via ON usr USING gin (added_via);
|
||||
+20
-15
@@ -36,16 +36,16 @@ use windmill_common::{
|
||||
agent_workers::build_agent_http_client,
|
||||
get_database_url,
|
||||
global_settings::{
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ALERTS_ON_DB_OVERSIZE_SETTING,
|
||||
CRITICAL_ALERT_MUTE_UI_SETTING, CRITICAL_ERROR_CHANNELS_SETTING, CUSTOM_TAGS_SETTING,
|
||||
DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING, EMAIL_DOMAIN_SETTING,
|
||||
ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
|
||||
EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING, INDEXER_SETTING,
|
||||
INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING,
|
||||
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING,
|
||||
MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NO_DEFAULT_MAVEN_SETTING,
|
||||
NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING,
|
||||
PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
APP_WORKSPACED_ROUTE_SETTING, BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING,
|
||||
CRITICAL_ALERTS_ON_DB_OVERSIZE_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING,
|
||||
CRITICAL_ERROR_CHANNELS_SETTING, CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING,
|
||||
DEFAULT_TAGS_WORKSPACES_SETTING, EMAIL_DOMAIN_SETTING, ENV_SETTINGS,
|
||||
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
|
||||
HUB_BASE_URL_SETTING, INDEXER_SETTING, INSTANCE_PYTHON_VERSION_SETTING,
|
||||
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING,
|
||||
LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING,
|
||||
NO_DEFAULT_MAVEN_SETTING, NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OAUTH_SETTING,
|
||||
OTEL_SETTING, PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
|
||||
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TEAMS_SETTING,
|
||||
TIMEOUT_WAIT_RESULT_SETTING,
|
||||
@@ -89,11 +89,11 @@ use windmill_worker::{
|
||||
use crate::monitor::{
|
||||
initial_load, load_keep_job_dir, load_metrics_debug_enabled, load_require_preexisting_user,
|
||||
load_tag_per_workspace_enabled, load_tag_per_workspace_workspaces, monitor_db,
|
||||
reload_base_url_setting, reload_bunfig_install_scopes_setting,
|
||||
reload_critical_alert_mute_ui_setting, reload_critical_error_channels_setting,
|
||||
reload_extra_pip_index_url_setting, reload_hub_base_url_setting,
|
||||
reload_job_default_timeout_setting, reload_jwt_secret_setting, reload_license_key,
|
||||
reload_npm_config_registry_setting, reload_pip_index_url_setting,
|
||||
reload_app_workspaced_route_setting, reload_base_url_setting,
|
||||
reload_bunfig_install_scopes_setting, reload_critical_alert_mute_ui_setting,
|
||||
reload_critical_error_channels_setting, reload_extra_pip_index_url_setting,
|
||||
reload_hub_base_url_setting, reload_job_default_timeout_setting, reload_jwt_secret_setting,
|
||||
reload_license_key, reload_npm_config_registry_setting, reload_pip_index_url_setting,
|
||||
reload_retention_period_setting, reload_scim_token_setting, reload_smtp_config,
|
||||
reload_worker_config, MonitorIteration,
|
||||
};
|
||||
@@ -1034,6 +1034,11 @@ Windmill Community Edition {GIT_VERSION}
|
||||
if let Err(e) = load_metrics_debug_enabled(&conn).await {
|
||||
tracing::error!(error = %e, "Could not reload debug metrics setting");
|
||||
}
|
||||
},
|
||||
APP_WORKSPACED_ROUTE_SETTING => {
|
||||
if let Err(e) = reload_app_workspaced_route_setting(&db).await {
|
||||
tracing::error!(error = %e, "Could not reload app workspaced route setting");
|
||||
}
|
||||
},
|
||||
OTEL_SETTING => {
|
||||
tracing::info!("OTEL setting changed, restarting");
|
||||
|
||||
+106
-2
@@ -11,7 +11,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use sqlx::{Pool, Postgres};
|
||||
@@ -33,13 +33,13 @@ use windmill_common::ee_oss::low_disk_alerts;
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::ee_oss::{jobs_waiting_alerts, worker_groups_alerts};
|
||||
|
||||
use windmill_common::client::AuthedClient;
|
||||
#[cfg(feature = "oauth2")]
|
||||
use windmill_common::global_settings::OAUTH_SETTING;
|
||||
#[cfg(feature = "parquet")]
|
||||
use windmill_common::s3_helpers::reload_object_store_setting;
|
||||
use windmill_common::{
|
||||
agent_workers::DECODED_AGENT_TOKEN,
|
||||
apps::APP_WORKSPACED_ROUTE,
|
||||
auth::create_token_for_owner,
|
||||
ee_oss::CriticalErrorChannel,
|
||||
error,
|
||||
@@ -77,6 +77,7 @@ use windmill_common::{
|
||||
METRICS_DEBUG_ENABLED, METRICS_ENABLED, MONITOR_LOGS_ON_OBJECT_STORE, OTEL_LOGS_ENABLED,
|
||||
OTEL_METRICS_ENABLED, OTEL_TRACING_ENABLED, SERVICE_LOG_RETENTION_SECS,
|
||||
};
|
||||
use windmill_common::{client::AuthedClient, global_settings::APP_WORKSPACED_ROUTE_SETTING};
|
||||
use windmill_queue::{cancel_job, MiniPulledJob, SameWorkerPayload};
|
||||
use windmill_worker::{
|
||||
handle_job_error, JobCompletedSender, SameWorkerSender, BUNFIG_INSTALL_SCOPES,
|
||||
@@ -140,6 +141,9 @@ lazy_static::lazy_static! {
|
||||
static ref QUEUE_COUNT_TAGS: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
|
||||
static ref DISABLE_CONCURRENCY_LIMIT: bool = std::env::var("DISABLE_CONCURRENCY_LIMIT").is_ok_and(|s| s == "true");
|
||||
|
||||
static ref STALE_JOB_TRESHOLD_MINUTES: Option<u64> = std::env::var("STALE_JOB_TRESHOLD_MINUTES")
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<u64>().ok());
|
||||
}
|
||||
|
||||
pub async fn initial_load(
|
||||
@@ -236,6 +240,10 @@ pub async fn initial_load(
|
||||
if let Err(e) = reload_custom_tags_setting(db).await {
|
||||
tracing::error!("Error reloading custom tags: {:?}", e)
|
||||
}
|
||||
|
||||
if let Err(e) = reload_app_workspaced_route_setting(db).await {
|
||||
tracing::error!("Error reloading app workspaced route: {:?}", e)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
@@ -1009,6 +1017,7 @@ pub async fn reload_timeout_wait_result_setting(conn: &Connection) {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn reload_saml_metadata_setting(conn: &Connection) {
|
||||
reload_option_setting_with_tracing(
|
||||
conn,
|
||||
@@ -1371,6 +1380,14 @@ pub async fn monitor_db(
|
||||
}
|
||||
};
|
||||
|
||||
let stale_jobs_f = async {
|
||||
if server_mode && !initial_load {
|
||||
if let Some(db) = conn.as_sql() {
|
||||
stale_job_cancellation(&db).await;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// run every 5 minutes
|
||||
let cleanup_concurrency_counters_f = async {
|
||||
if server_mode && iteration.is_some() && iteration.as_ref().unwrap().should_run(10) {
|
||||
@@ -1469,6 +1486,7 @@ pub async fn monitor_db(
|
||||
join!(
|
||||
expired_items_f,
|
||||
zombie_jobs_f,
|
||||
stale_jobs_f,
|
||||
expose_queue_metrics_f,
|
||||
verify_license_key_f,
|
||||
worker_groups_alerts_f,
|
||||
@@ -1711,6 +1729,67 @@ pub async fn reload_base_url_setting(conn: &Connection) -> error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stale_job_cancellation(db: &Pool<Postgres>) {
|
||||
if let Some(threshold) = *STALE_JOB_TRESHOLD_MINUTES {
|
||||
let stale_jobs = sqlx::query!(
|
||||
"SELECT v2_job_queue.id, v2_job.tag, v2_job_queue.scheduled_for, v2_job_queue.workspace_id FROM v2_job_queue LEFT JOIN v2_job ON v2_job_queue.id = v2_job.id WHERE running = false AND scheduled_for < now() - ($1 || ' minutes')::interval",
|
||||
threshold.to_string()
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_else(|| vec![]);
|
||||
|
||||
if !stale_jobs.is_empty() {
|
||||
tracing::info!(
|
||||
"Cancelling {} stale jobs (> {} minutes old)",
|
||||
stale_jobs.len(),
|
||||
threshold
|
||||
);
|
||||
}
|
||||
for job in stale_jobs {
|
||||
if let Err(e) =
|
||||
cancel_stale_job(db, job.id, job.tag, job.workspace_id, job.scheduled_for).await
|
||||
{
|
||||
tracing::error!("Error cancelling stale job {}: {}", job.id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cancel_stale_job(
|
||||
db: &Pool<Postgres>,
|
||||
id: Uuid,
|
||||
tag: String,
|
||||
workspace_id: String,
|
||||
scheduled_for: DateTime<Utc>,
|
||||
) -> error::Result<()> {
|
||||
let mut tx = db.begin().await?;
|
||||
tracing::error!(
|
||||
"Stale job detected: {} in workspace {} with tag {} (scheduled for: {}) . Cancelling it.",
|
||||
id,
|
||||
workspace_id,
|
||||
tag,
|
||||
scheduled_for
|
||||
);
|
||||
(tx, _) = cancel_job(
|
||||
"monitor",
|
||||
Some(format!(
|
||||
"Stale job cancellation (scheduled for: {})",
|
||||
scheduled_for
|
||||
)),
|
||||
id,
|
||||
&workspace_id,
|
||||
tx,
|
||||
db,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const RESTART_LIMIT: i32 = 3;
|
||||
|
||||
async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker_name: &str) {
|
||||
@@ -2344,6 +2423,31 @@ pub async fn reload_critical_error_channels_setting(conn: &DB) -> error::Result<
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reload_app_workspaced_route_setting(conn: &DB) -> error::Result<()> {
|
||||
let app_workspaced_route =
|
||||
load_value_from_global_settings(conn, APP_WORKSPACED_ROUTE_SETTING).await?;
|
||||
|
||||
println!("Updating...");
|
||||
|
||||
let ws_route = match app_workspaced_route {
|
||||
Some(serde_json::Value::Bool(ws_route)) => ws_route,
|
||||
None => false,
|
||||
_ => {
|
||||
tracing::error!(
|
||||
"Expected {} to be a boolean got: {:?}. Defaulting to false",
|
||||
APP_WORKSPACED_ROUTE_SETTING,
|
||||
app_workspaced_route
|
||||
);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let mut l = APP_WORKSPACED_ROUTE.write().await;
|
||||
|
||||
*l = ws_route;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reload_critical_alerts_on_db_oversize(conn: &DB) -> error::Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct DBOversize {
|
||||
|
||||
+2757
-327
File diff suppressed because it is too large
Load Diff
+3363
-1480
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.519.0
|
||||
version: 1.525.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -1944,6 +1944,14 @@ paths:
|
||||
type: boolean
|
||||
auto_add:
|
||||
type: boolean
|
||||
auto_add_instance_groups:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
auto_add_instance_groups_roles:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
plan:
|
||||
type: string
|
||||
customer_id:
|
||||
@@ -2351,6 +2359,39 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/edit_instance_groups:
|
||||
post:
|
||||
summary: edit instance groups
|
||||
operationId: editInstanceGroups
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: Instance Groups Configuration
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
groups:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
roles:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/edit_webhook:
|
||||
post:
|
||||
summary: edit webhook
|
||||
@@ -2440,7 +2481,6 @@ paths:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
error_handler_muted_on_cancel:
|
||||
type: boolean
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
@@ -2474,7 +2514,6 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
|
||||
/w/{workspace}/workspaces/list_ducklakes:
|
||||
get:
|
||||
summary: list ducklakes
|
||||
@@ -3069,11 +3108,13 @@ paths:
|
||||
post:
|
||||
summary: create variable
|
||||
operationId: createVariable
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- variable
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: already_encrypted
|
||||
description: whether the variable is already encrypted (default false)
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
@@ -3119,6 +3160,7 @@ paths:
|
||||
delete:
|
||||
summary: delete variable
|
||||
operationId: deleteVariable
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- variable
|
||||
parameters:
|
||||
@@ -3136,12 +3178,14 @@ paths:
|
||||
post:
|
||||
summary: update variable
|
||||
operationId: updateVariable
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- variable
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
- name: already_encrypted
|
||||
description: whether the variable is already encrypted (default false)
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
@@ -3164,6 +3208,7 @@ paths:
|
||||
get:
|
||||
summary: get variable
|
||||
operationId: getVariable
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- variable
|
||||
parameters:
|
||||
@@ -3228,11 +3273,13 @@ paths:
|
||||
get:
|
||||
summary: list variables
|
||||
operationId: listVariable
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- variable
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: path_start
|
||||
description: filter variables by path prefix
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
@@ -3833,11 +3880,13 @@ paths:
|
||||
post:
|
||||
summary: create resource
|
||||
operationId: createResource
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- resource
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: update_if_exists
|
||||
description: update the resource if it already exists (default false)
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
@@ -3860,6 +3909,7 @@ paths:
|
||||
delete:
|
||||
summary: delete resource
|
||||
operationId: deleteResource
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- resource
|
||||
parameters:
|
||||
@@ -3877,6 +3927,7 @@ paths:
|
||||
post:
|
||||
summary: update resource
|
||||
operationId: updateResource
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- resource
|
||||
parameters:
|
||||
@@ -3927,6 +3978,7 @@ paths:
|
||||
get:
|
||||
summary: get resource
|
||||
operationId: getResource
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- resource
|
||||
parameters:
|
||||
@@ -3999,6 +4051,7 @@ paths:
|
||||
get:
|
||||
summary: list resources
|
||||
operationId: listResource
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- resource
|
||||
parameters:
|
||||
@@ -4016,6 +4069,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
- name: path_start
|
||||
description: filter resources by path prefix
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
@@ -4198,6 +4252,7 @@ paths:
|
||||
get:
|
||||
summary: list resource_types
|
||||
operationId: listResourceType
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- resource
|
||||
parameters:
|
||||
@@ -4651,6 +4706,7 @@ paths:
|
||||
get:
|
||||
summary: list all scripts
|
||||
operationId: listScripts
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- script
|
||||
parameters:
|
||||
@@ -5026,6 +5082,7 @@ paths:
|
||||
get:
|
||||
summary: get script by path
|
||||
operationId: getScriptByPath
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- script
|
||||
parameters:
|
||||
@@ -5616,6 +5673,7 @@ paths:
|
||||
get:
|
||||
summary: list all flows
|
||||
operationId: listFlows
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- flow
|
||||
parameters:
|
||||
@@ -5797,6 +5855,7 @@ paths:
|
||||
get:
|
||||
summary: get flow by path
|
||||
operationId: getFlowByPath
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- flow
|
||||
parameters:
|
||||
@@ -7247,6 +7306,7 @@ paths:
|
||||
get:
|
||||
summary: list all queued jobs
|
||||
operationId: listQueue
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
@@ -7586,6 +7646,7 @@ paths:
|
||||
get:
|
||||
summary: list all jobs
|
||||
operationId: listJobs
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
@@ -7757,6 +7818,10 @@ paths:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/JobId"
|
||||
- name: remove_ansi_warnings
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: job details
|
||||
@@ -7765,6 +7830,23 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/jobs_u/get_completed_logs_tail/{id}:
|
||||
get:
|
||||
summary: get completed job logs tail
|
||||
operationId: getCompletedJobLogsTail
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/JobId"
|
||||
responses:
|
||||
"200":
|
||||
description: completed job logs tail
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/jobs_u/get_args/{id}:
|
||||
get:
|
||||
summary: get job args
|
||||
@@ -7857,6 +7939,10 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
- name: stream_offset
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
- name: get_progress
|
||||
in: query
|
||||
schema:
|
||||
@@ -8542,6 +8628,11 @@ paths:
|
||||
post:
|
||||
summary: create schedule
|
||||
operationId: createSchedule
|
||||
x-mcp-tool: true
|
||||
x-mcp-instructions: |
|
||||
Creates a new schedule.
|
||||
The schedule should include seconds.
|
||||
You should get the schema of the script or flow before creating the schedule to correctly specify the arguments needed.
|
||||
tags:
|
||||
- schedule
|
||||
parameters:
|
||||
@@ -8565,6 +8656,11 @@ paths:
|
||||
post:
|
||||
summary: update schedule
|
||||
operationId: updateSchedule
|
||||
x-mcp-tool: true
|
||||
x-mcp-instructions: |
|
||||
Updates a schedule.
|
||||
The schedule should include seconds.
|
||||
You should get the schema of the script or flow before updating the schedule to correctly specify the arguments needed.
|
||||
tags:
|
||||
- schedule
|
||||
parameters:
|
||||
@@ -8619,6 +8715,7 @@ paths:
|
||||
delete:
|
||||
summary: delete schedule
|
||||
operationId: deleteSchedule
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- schedule
|
||||
parameters:
|
||||
@@ -8636,6 +8733,7 @@ paths:
|
||||
get:
|
||||
summary: get schedule
|
||||
operationId: getSchedule
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- schedule
|
||||
parameters:
|
||||
@@ -8670,6 +8768,7 @@ paths:
|
||||
get:
|
||||
summary: list schedules
|
||||
operationId: listSchedules
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- schedule
|
||||
parameters:
|
||||
@@ -8683,10 +8782,12 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
- name: is_flow
|
||||
description: filter schedules by whether they target a flow
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: path_start
|
||||
description: filter schedules by path prefix
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
@@ -10653,6 +10754,21 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/InstanceGroup"
|
||||
/groups/list_with_workspaces:
|
||||
get:
|
||||
summary: list instance groups with workspace information
|
||||
operationId: listInstanceGroupsWithWorkspaces
|
||||
tags:
|
||||
- group
|
||||
responses:
|
||||
"200":
|
||||
description: instance group list with workspaces
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/InstanceGroupWithWorkspaces"
|
||||
|
||||
/groups/get/{name}:
|
||||
get:
|
||||
@@ -11291,6 +11407,7 @@ paths:
|
||||
get:
|
||||
summary: list workers
|
||||
operationId: listWorkers
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- worker
|
||||
parameters:
|
||||
@@ -11312,25 +11429,28 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/WorkerPing"
|
||||
|
||||
/workers/exists_worker_with_tag:
|
||||
/workers/exists_workers_with_tags:
|
||||
get:
|
||||
summary: exists worker with tag
|
||||
operationId: existsWorkerWithTag
|
||||
summary: exists workers with tags
|
||||
operationId: existsWorkersWithTags
|
||||
tags:
|
||||
- worker
|
||||
parameters:
|
||||
- name: tag
|
||||
- name: tags
|
||||
in: query
|
||||
required: true
|
||||
description: comma separated list of tags
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: whether a worker with the tag exists
|
||||
description: map of tags to whether at least one worker with the tag exists
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: boolean
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
|
||||
/workers/queue_metrics:
|
||||
get:
|
||||
@@ -13339,6 +13459,24 @@ paths:
|
||||
access_type:
|
||||
$ref: "#/components/schemas/AssetUsageAccessType"
|
||||
|
||||
/mcp/w/{workspace}/list_tools:
|
||||
get:
|
||||
summary: list available MCP tools
|
||||
operationId: listMcpTools
|
||||
tags:
|
||||
- mcp
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
responses:
|
||||
"200":
|
||||
description: list of MCP tools available for the workspace
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/EndpointTool"
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
@@ -13372,6 +13510,7 @@ components:
|
||||
name: publication
|
||||
in: path
|
||||
required: true
|
||||
description: The name of the publication
|
||||
schema:
|
||||
type: string
|
||||
VersionId:
|
||||
@@ -13799,6 +13938,38 @@ components:
|
||||
# -- INLINE END --
|
||||
# Do not change line above
|
||||
|
||||
EndpointTool:
|
||||
type: object
|
||||
required: [name, description, instructions, path, method]
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The tool name/operation ID
|
||||
description:
|
||||
type: string
|
||||
description: Short description of the tool
|
||||
instructions:
|
||||
type: string
|
||||
description: Detailed instructions for using the tool
|
||||
path:
|
||||
type: string
|
||||
description: API endpoint path
|
||||
method:
|
||||
type: string
|
||||
description: HTTP method (GET, POST, etc.)
|
||||
path_params_schema:
|
||||
type: object
|
||||
description: JSON schema for path parameters
|
||||
nullable: true
|
||||
query_params_schema:
|
||||
type: object
|
||||
description: JSON schema for query parameters
|
||||
nullable: true
|
||||
body_schema:
|
||||
type: object
|
||||
description: JSON schema for request body
|
||||
nullable: true
|
||||
|
||||
AIProvider:
|
||||
type: string
|
||||
enum:
|
||||
@@ -14122,6 +14293,7 @@ components:
|
||||
|
||||
ScriptArgs:
|
||||
type: object
|
||||
description: The arguments to pass to the script or flow
|
||||
additionalProperties: {}
|
||||
|
||||
Input:
|
||||
@@ -14467,6 +14639,10 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
added_via:
|
||||
nullable: true
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/UserSource"
|
||||
required:
|
||||
- email
|
||||
- username
|
||||
@@ -14478,6 +14654,22 @@ components:
|
||||
- folders
|
||||
- folders_owners
|
||||
|
||||
UserSource:
|
||||
type: object
|
||||
properties:
|
||||
source:
|
||||
type: string
|
||||
enum: [domain, instance_group, manual]
|
||||
description: "How the user was added to the workspace"
|
||||
domain:
|
||||
type: string
|
||||
description: "The domain used for auto-invite (when source is 'domain')"
|
||||
group:
|
||||
type: string
|
||||
description: "The instance group name (when source is 'instance_group')"
|
||||
required:
|
||||
- source
|
||||
|
||||
UserUsage:
|
||||
type: object
|
||||
properties:
|
||||
@@ -14624,18 +14816,25 @@ components:
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: The path to the variable
|
||||
value:
|
||||
type: string
|
||||
description: The value of the variable
|
||||
is_secret:
|
||||
type: boolean
|
||||
description: Whether the variable is a secret
|
||||
description:
|
||||
type: string
|
||||
description: The description of the variable
|
||||
account:
|
||||
type: integer
|
||||
description: The account identifier
|
||||
is_oauth:
|
||||
type: boolean
|
||||
description: Whether the variable is an OAuth variable
|
||||
expires_at:
|
||||
type: string
|
||||
description: The expiration date of the variable
|
||||
format: date-time
|
||||
required:
|
||||
- path
|
||||
@@ -14648,12 +14847,16 @@ components:
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: The path to the variable
|
||||
value:
|
||||
type: string
|
||||
description: The new value of the variable
|
||||
is_secret:
|
||||
type: boolean
|
||||
description: Whether the variable is a secret
|
||||
description:
|
||||
type: string
|
||||
description: The new description of the variable
|
||||
|
||||
AuditLog:
|
||||
type: object
|
||||
@@ -14986,11 +15189,14 @@ components:
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: The path to the resource
|
||||
value: {}
|
||||
description:
|
||||
type: string
|
||||
description: The description of the resource
|
||||
resource_type:
|
||||
type: string
|
||||
description: The resource_type associated with the resource
|
||||
required:
|
||||
- path
|
||||
- value
|
||||
@@ -15001,9 +15207,14 @@ components:
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: The path to the resource
|
||||
description:
|
||||
type: string
|
||||
description: The new description of the resource
|
||||
value: {}
|
||||
resource_type:
|
||||
type: string
|
||||
description: The new resource_type to be associated with the resource
|
||||
|
||||
Resource:
|
||||
type: object
|
||||
@@ -15199,59 +15410,91 @@ components:
|
||||
- success
|
||||
- duration_ms
|
||||
|
||||
ErrorHandler:
|
||||
type: string
|
||||
enum:
|
||||
- custom
|
||||
- slack
|
||||
- teams
|
||||
- email
|
||||
|
||||
NewSchedule:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: The path where the schedule will be created
|
||||
schedule:
|
||||
type: string
|
||||
description: The cron schedule to trigger the script or flow. Should include seconds.
|
||||
timezone:
|
||||
type: string
|
||||
description: The timezone to use for the cron schedule
|
||||
script_path:
|
||||
type: string
|
||||
description: The path to the script or flow to trigger
|
||||
is_flow:
|
||||
type: boolean
|
||||
description: Whether the schedule is for a flow
|
||||
args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
description: The arguments to pass to the script or flow
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Whether the schedule is enabled
|
||||
on_failure:
|
||||
# a reference to a script path, flow path, or webhook (script/<path>, flow/<path>)
|
||||
type: string
|
||||
description: The path to the script or flow to trigger on failure
|
||||
on_failure_times:
|
||||
type: number
|
||||
description: The number of times to retry on failure
|
||||
on_failure_exact:
|
||||
type: boolean
|
||||
description: Whether the schedule should only run on the exact time
|
||||
on_failure_extra_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
description: The arguments to pass to the script or flow on failure
|
||||
on_recovery:
|
||||
type: string
|
||||
description: The path to the script or flow to trigger on recovery
|
||||
on_recovery_times:
|
||||
type: number
|
||||
description: The number of times to retry on recovery
|
||||
on_recovery_extra_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
description: The arguments to pass to the script or flow on recovery
|
||||
on_success:
|
||||
type: string
|
||||
description: The path to the script or flow to trigger on success
|
||||
on_success_extra_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
description: The arguments to pass to the script or flow on success
|
||||
ws_error_handler_muted:
|
||||
type: boolean
|
||||
description: Whether the WebSocket error handler is muted
|
||||
retry:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
|
||||
description: The retry configuration for the schedule
|
||||
no_flow_overlap:
|
||||
type: boolean
|
||||
description: Whether the schedule should not run if a flow is already running
|
||||
summary:
|
||||
type: string
|
||||
description: The summary of the schedule
|
||||
description:
|
||||
type: string
|
||||
description: The description of the schedule
|
||||
tag:
|
||||
type: string
|
||||
description: The tag of the schedule
|
||||
paused_until:
|
||||
type: string
|
||||
description: The date and time the schedule will be paused until
|
||||
format: date-time
|
||||
cron_version:
|
||||
type: string
|
||||
description: The version of the cron schedule to use (last is v2)
|
||||
required:
|
||||
- path
|
||||
- schedule
|
||||
@@ -15265,51 +15508,69 @@ components:
|
||||
properties:
|
||||
schedule:
|
||||
type: string
|
||||
description: The cron schedule to trigger the script or flow. Should include seconds.
|
||||
timezone:
|
||||
type: string
|
||||
description: The timezone to use for the cron schedule
|
||||
args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
description: The arguments to pass to the script or flow
|
||||
on_failure:
|
||||
# a reference to a script path, flow path, or webhook (script/<path>, flow/<path>)
|
||||
type: string
|
||||
description: The path to the script or flow to trigger on failure
|
||||
on_failure_times:
|
||||
type: number
|
||||
description: The number of times to retry on failure
|
||||
on_failure_exact:
|
||||
type: boolean
|
||||
description: Whether the schedule should only run on the exact time
|
||||
on_failure_extra_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
description: The arguments to pass to the script or flow on failure
|
||||
on_recovery:
|
||||
type: string
|
||||
description: The path to the script or flow to trigger on recovery
|
||||
on_recovery_times:
|
||||
type: number
|
||||
description: The number of times to retry on recovery
|
||||
on_recovery_extra_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
description: The arguments to pass to the script or flow on recovery
|
||||
on_success:
|
||||
type: string
|
||||
description: The path to the script or flow to trigger on success
|
||||
on_success_extra_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
description: The arguments to pass to the script or flow on success
|
||||
ws_error_handler_muted:
|
||||
type: boolean
|
||||
description: Whether the WebSocket error handler is muted
|
||||
retry:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
|
||||
description: The retry configuration for the schedule
|
||||
no_flow_overlap:
|
||||
type: boolean
|
||||
description: Whether the schedule should not run if a flow is already running
|
||||
summary:
|
||||
type: string
|
||||
description: The summary of the schedule
|
||||
description:
|
||||
type: string
|
||||
description: The description of the schedule
|
||||
tag:
|
||||
type: string
|
||||
description: The tag of the schedule
|
||||
paused_until:
|
||||
type: string
|
||||
description: The date and time the schedule will be paused until
|
||||
format: date-time
|
||||
cron_version:
|
||||
type: string
|
||||
description: The version of the cron schedule to use (last is v2)
|
||||
required:
|
||||
- schedule
|
||||
- timezone
|
||||
- script_path
|
||||
- is_flow
|
||||
- args
|
||||
|
||||
TriggerExtraProperty:
|
||||
@@ -16072,6 +16333,8 @@ components:
|
||||
type: boolean
|
||||
enabled:
|
||||
type: boolean
|
||||
auto_acknowledge_msg:
|
||||
type: boolean
|
||||
error_handler_path:
|
||||
type: string
|
||||
error_handler_args:
|
||||
@@ -16624,6 +16887,8 @@ components:
|
||||
|
||||
InstanceGroup:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
@@ -16633,6 +16898,34 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
InstanceGroupWithWorkspaces:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
summary:
|
||||
type: string
|
||||
emails:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
workspaces:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/WorkspaceInfo"
|
||||
|
||||
WorkspaceInfo:
|
||||
type: object
|
||||
properties:
|
||||
workspace_id:
|
||||
type: string
|
||||
workspace_name:
|
||||
type: string
|
||||
role:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
|
||||
@@ -16836,7 +17129,6 @@ components:
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
|
||||
|
||||
FlowMetadata:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -52,7 +52,7 @@ use std::str;
|
||||
use windmill_audit::audit_oss::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
use windmill_common::{
|
||||
apps::{AppScriptId, ListAppQuery},
|
||||
apps::{AppScriptId, ListAppQuery, APP_WORKSPACED_ROUTE},
|
||||
auth::TOKEN_PREFIX_LEN,
|
||||
cache::{self, future::FutureCachedExt},
|
||||
db::UserDB,
|
||||
@@ -157,7 +157,7 @@ pub struct AppVersion {
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, FromRow)]
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct AppWithLastVersion {
|
||||
pub id: i64,
|
||||
pub path: String,
|
||||
@@ -182,7 +182,7 @@ pub struct AppWithLastVersionAndStarred {
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[derive(Serialize, FromRow)]
|
||||
#[derive(Debug, Serialize, FromRow)]
|
||||
pub struct AppWithLastVersionAndWorkspace {
|
||||
#[sqlx(flatten)]
|
||||
#[serde(flatten)]
|
||||
@@ -524,21 +524,36 @@ async fn get_app_w_draft(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let app_o = sqlx::query_as::<_, AppWithLastVersionAndDraft>(
|
||||
r#"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
|
||||
app.extra_perms, app_version.value,
|
||||
app_version.created_at, app_version.created_by,
|
||||
app.draft_only, draft.value as "draft"
|
||||
from app
|
||||
INNER JOIN app_version ON
|
||||
app_version.id = app.versions[array_upper(app.versions, 1)]
|
||||
LEFT JOIN draft ON
|
||||
app.path = draft.path AND draft.workspace_id = $2 AND draft.typ = 'app'
|
||||
WHERE app.path = $1 AND app.workspace_id = $2"#,
|
||||
r#"
|
||||
SELECT
|
||||
app.id,
|
||||
app.path,
|
||||
app.summary,
|
||||
app.versions,
|
||||
app.policy,
|
||||
app.custom_path,
|
||||
app.extra_perms,
|
||||
app_version.value,
|
||||
app_version.created_at,
|
||||
app_version.created_by,
|
||||
app.draft_only,
|
||||
draft.value AS "draft"
|
||||
FROM app
|
||||
INNER JOIN app_version
|
||||
ON app_version.id = app.versions[array_upper(app.versions, 1)]
|
||||
LEFT JOIN draft
|
||||
ON app.path = draft.path
|
||||
AND draft.workspace_id = $2
|
||||
AND draft.typ = 'app'
|
||||
WHERE app.path = $1
|
||||
AND app.workspace_id = $2
|
||||
"#,
|
||||
)
|
||||
.bind(path.to_owned())
|
||||
.bind(&w_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
let app = not_found_if_none(app_o, "App", path)?;
|
||||
@@ -642,11 +657,13 @@ async fn custom_path_exists(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, custom_path)): Path<(String, String)>,
|
||||
) -> JsonResult<bool> {
|
||||
let as_workspaced_route = *APP_WORKSPACED_ROUTE.read().await;
|
||||
|
||||
let exists =
|
||||
sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2))",
|
||||
custom_path,
|
||||
if *CLOUD_HOSTED { Some(&w_id) } else { None }
|
||||
if *CLOUD_HOSTED || as_workspaced_route { Some(&w_id) } else { None }
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?.unwrap_or(false);
|
||||
@@ -976,11 +993,12 @@ async fn create_app_internal<'a>(
|
||||
}
|
||||
if let Some(custom_path) = &app.custom_path {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
let as_workspaced_route = *APP_WORKSPACED_ROUTE.read().await;
|
||||
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2))",
|
||||
custom_path,
|
||||
if *CLOUD_HOSTED { Some(w_id) } else { None }
|
||||
if *CLOUD_HOSTED || as_workspaced_route { Some(w_id) } else { None }
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?.unwrap_or(false);
|
||||
@@ -1272,6 +1290,7 @@ async fn update_app_internal<'a>(
|
||||
) -> Result<(sqlx::Transaction<'a, sqlx::Postgres>, String, i64)> {
|
||||
use sql_builder::prelude::*;
|
||||
let mut tx = user_db.clone().begin(&authed).await?;
|
||||
|
||||
let npath = if ns.policy.is_some()
|
||||
|| ns.path.is_some()
|
||||
|| ns.summary.is_some()
|
||||
@@ -1311,6 +1330,7 @@ async fn update_app_internal<'a>(
|
||||
|
||||
if let Some(ncustom_path) = &ns.custom_path {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
let as_workspaced_route = *APP_WORKSPACED_ROUTE.read().await;
|
||||
|
||||
if ncustom_path.is_empty() {
|
||||
sqlb.set("custom_path", "NULL");
|
||||
@@ -1318,7 +1338,7 @@ async fn update_app_internal<'a>(
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2) AND NOT (path = $3 AND workspace_id = $4))",
|
||||
ncustom_path,
|
||||
if *CLOUD_HOSTED { Some(w_id) } else { None },
|
||||
if *CLOUD_HOSTED || as_workspaced_route { Some(w_id) } else { None },
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
|
||||
@@ -176,6 +176,7 @@ pub struct GcpTriggerConfig {
|
||||
#[serde(flatten)]
|
||||
pub create_update: Option<CreateUpdateConfig>,
|
||||
pub topic_id: String,
|
||||
pub auto_acknowledge_msg: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "nats"))]
|
||||
|
||||
@@ -854,6 +854,22 @@ impl From<ApiAuthed> for Authed {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Authed> for ApiAuthed {
|
||||
fn from(value: Authed) -> Self {
|
||||
Self {
|
||||
email: value.email,
|
||||
username: value.username,
|
||||
is_admin: value.is_admin,
|
||||
is_operator: value.is_operator,
|
||||
groups: value.groups,
|
||||
folders: value.folders,
|
||||
scopes: value.scopes,
|
||||
username_override: None, // Authed doesn't have this field, so default to None
|
||||
token_prefix: value.token_prefix,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ApiAuthed> for AuditAuthor {
|
||||
fn from(value: &ApiAuthed) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -2,22 +2,23 @@
|
||||
#[allow(unused)]
|
||||
pub use crate::gcp_triggers_ee::*;
|
||||
|
||||
use serde_json::value::RawValue;
|
||||
use sqlx::prelude::FromRow;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use sqlx::types::Json as SqlxJson;
|
||||
use windmill_common::worker::to_raw_value;
|
||||
use windmill_common::triggers::TriggerKind;
|
||||
use crate::trigger_helpers::TriggerJobArgs;
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
use {
|
||||
crate::db::{ApiAuthed, DB},
|
||||
crate::trigger_helpers::TriggerJobArgs,
|
||||
axum::{extract::Request, Router},
|
||||
http::HeaderMap,
|
||||
serde::{Deserialize, Serialize},
|
||||
serde_json::value::RawValue,
|
||||
sqlx::prelude::FromRow,
|
||||
sqlx::types::Json as SqlxJson,
|
||||
std::collections::HashMap,
|
||||
windmill_common::db::UserDB,
|
||||
windmill_common::worker::to_raw_value,
|
||||
windmill_common::{
|
||||
error::{Error as WindmillError, Result as WindmillResult},
|
||||
triggers::TriggerKind,
|
||||
utils::empty_as_none,
|
||||
},
|
||||
};
|
||||
@@ -134,7 +135,6 @@ pub fn gcp_push_route_handler() -> Router {
|
||||
}
|
||||
|
||||
#[derive(FromRow, Deserialize, Serialize, Debug)]
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub struct GcpTrigger {
|
||||
pub gcp_resource_path: String,
|
||||
pub subscription_id: String,
|
||||
@@ -160,8 +160,10 @@ pub struct GcpTrigger {
|
||||
pub error_handler_args: Option<SqlxJson<HashMap<String, Box<RawValue>>>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub retry: Option<SqlxJson<windmill_common::flows::Retry>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_acknowledge_msg: Option<bool>,
|
||||
}
|
||||
#[cfg(not(feature = "private"))]
|
||||
|
||||
impl TriggerJobArgs<String> for GcpTrigger {
|
||||
fn v1_payload_fn(payload: String) -> HashMap<String, Box<RawValue>> {
|
||||
HashMap::from([("payload".to_string(), to_raw_value(&payload))])
|
||||
@@ -170,4 +172,4 @@ impl TriggerJobArgs<String> for GcpTrigger {
|
||||
fn trigger_kind() -> TriggerKind {
|
||||
TriggerKind::Gcp
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ pub fn workspaced_service() -> Router {
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
.route("/list", get(list_igroups))
|
||||
.route("/list_with_workspaces", get(list_igroups_with_workspaces))
|
||||
.route("/get/:name", get(get_igroup))
|
||||
.route("/create", post(create_igroup))
|
||||
.route("/update/:name", post(update_igroup))
|
||||
@@ -187,7 +188,7 @@ pub async fn require_is_owner(
|
||||
) -> Result<()> {
|
||||
let is_owner = query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM group_ WHERE (group_.extra_perms ->> CONCAT('u/', $1::text))::boolean AND name = $2 AND workspace_id = $4) OR exists(
|
||||
SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f
|
||||
SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f
|
||||
WHERE $2 = g.name AND $4 = g.workspace_id AND SPLIT_PART(key, '/', 1) = 'g' AND key = ANY($3::text[])
|
||||
AND value::boolean)",
|
||||
username,
|
||||
@@ -418,7 +419,7 @@ async fn get_group(
|
||||
let group = not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?;
|
||||
|
||||
let members = sqlx::query_scalar!(
|
||||
"SELECT usr.username
|
||||
"SELECT usr.username
|
||||
FROM usr_to_group LEFT JOIN usr ON usr_to_group.usr = usr.username AND usr_to_group.workspace_id = $2
|
||||
WHERE group_ = $1 AND usr.workspace_id = $2 AND usr_to_group.workspace_id = $2",
|
||||
name,
|
||||
@@ -638,6 +639,21 @@ struct IGroup {
|
||||
summary: Option<String>,
|
||||
emails: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct IGroupWithWorkspaces {
|
||||
name: String,
|
||||
summary: Option<String>,
|
||||
emails: Option<Vec<String>>,
|
||||
workspaces: Vec<WorkspaceInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
struct WorkspaceInfo {
|
||||
workspace_id: String,
|
||||
workspace_name: String,
|
||||
role: String,
|
||||
}
|
||||
async fn list_igroups(Extension(db): Extension<DB>) -> JsonResult<Vec<IGroup>> {
|
||||
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
|
||||
|
||||
@@ -652,6 +668,70 @@ async fn list_igroups(Extension(db): Extension<DB>) -> JsonResult<Vec<IGroup>> {
|
||||
return Ok(Json(groups));
|
||||
}
|
||||
|
||||
async fn list_igroups_with_workspaces(Extension(db): Extension<DB>) -> JsonResult<Vec<IGroupWithWorkspaces>> {
|
||||
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
|
||||
|
||||
// Get all instance groups with their emails first
|
||||
let groups = sqlx::query_as!(
|
||||
IGroup,
|
||||
"SELECT name, summary, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup GROUP BY name, summary"
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
// Get all workspace mappings for instance groups in a single query
|
||||
let workspace_mappings = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
ig.name as group_name,
|
||||
ws.workspace_id,
|
||||
w.name as workspace_name,
|
||||
ws.auto_add_instance_groups_roles->ig.name as role
|
||||
FROM instance_group ig
|
||||
INNER JOIN workspace_settings ws ON ws.auto_add_instance_groups IS NOT NULL
|
||||
AND ig.name = ANY(ws.auto_add_instance_groups)
|
||||
INNER JOIN workspace w ON w.id = ws.workspace_id AND w.deleted = false
|
||||
ORDER BY ig.name, ws.workspace_id
|
||||
"#
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
// Create a map of group_name -> Vec<WorkspaceInfo>
|
||||
let mut workspaces_by_group: std::collections::HashMap<String, Vec<WorkspaceInfo>> = std::collections::HashMap::new();
|
||||
for mapping in workspace_mappings {
|
||||
let role = mapping.role
|
||||
.and_then(|r| r.as_str().map(|s| s.to_string()))
|
||||
.unwrap_or_else(|| "developer".to_string());
|
||||
|
||||
let workspace_info = WorkspaceInfo {
|
||||
workspace_id: mapping.workspace_id.clone(),
|
||||
workspace_name: mapping.workspace_name,
|
||||
role,
|
||||
};
|
||||
|
||||
workspaces_by_group
|
||||
.entry(mapping.group_name)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(workspace_info);
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
for group in groups {
|
||||
let workspaces = workspaces_by_group.get(&group.name).cloned().unwrap_or_default();
|
||||
|
||||
result.push(IGroupWithWorkspaces {
|
||||
name: group.name,
|
||||
summary: group.summary,
|
||||
emails: group.emails,
|
||||
workspaces,
|
||||
});
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
return Ok(Json(result));
|
||||
}
|
||||
|
||||
async fn get_igroup(Path(name): Path<String>, Extension(db): Extension<DB>) -> JsonResult<IGroup> {
|
||||
let group = sqlx::query_as!(
|
||||
IGroup,
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::{
|
||||
users::fetch_api_authed,
|
||||
utils::{check_scopes, non_empty_str, ExpiringCacheEntry},
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::response::Response;
|
||||
use axum::{
|
||||
@@ -197,38 +196,37 @@ async fn list_triggers(
|
||||
) -> error::JsonResult<Vec<HttpTrigger>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let (per_page, offset) = paginate(Pagination { per_page: lst.per_page, page: lst.page });
|
||||
let mut sqlb = SqlBuilder::select_from("http_trigger")
|
||||
.fields(&[
|
||||
"workspace_id",
|
||||
"path",
|
||||
"route_path",
|
||||
"route_path_key",
|
||||
"workspaced_route",
|
||||
"wrap_body",
|
||||
"raw_string",
|
||||
"script_path",
|
||||
"summary",
|
||||
"description",
|
||||
"is_flow",
|
||||
"http_method",
|
||||
"edited_by",
|
||||
"email",
|
||||
"edited_at",
|
||||
"extra_perms",
|
||||
"is_async",
|
||||
"authentication_method",
|
||||
"static_asset_config",
|
||||
"is_static_website",
|
||||
"authentication_resource_path",
|
||||
"error_handler_path",
|
||||
"error_handler_args",
|
||||
"retry",
|
||||
])
|
||||
.order_by("edited_at", true)
|
||||
.and_where("workspace_id = ?".bind(&w_id))
|
||||
.offset(offset)
|
||||
.limit(per_page)
|
||||
.clone();
|
||||
let mut sqlb = SqlBuilder::select_from("http_trigger");
|
||||
sqlb.fields(&[
|
||||
"workspace_id",
|
||||
"path",
|
||||
"route_path",
|
||||
"route_path_key",
|
||||
"workspaced_route",
|
||||
"wrap_body",
|
||||
"raw_string",
|
||||
"script_path",
|
||||
"summary",
|
||||
"description",
|
||||
"is_flow",
|
||||
"http_method",
|
||||
"edited_by",
|
||||
"email",
|
||||
"edited_at",
|
||||
"extra_perms",
|
||||
"is_async",
|
||||
"authentication_method",
|
||||
"static_asset_config",
|
||||
"is_static_website",
|
||||
"authentication_resource_path",
|
||||
"error_handler_path",
|
||||
"error_handler_args",
|
||||
"retry",
|
||||
])
|
||||
.order_by("edited_at", true)
|
||||
.and_where("workspace_id = ?".bind(&w_id))
|
||||
.offset(offset)
|
||||
.limit(per_page);
|
||||
if let Some(path) = lst.path {
|
||||
sqlb.and_where_eq("script_path", "?".bind(&path));
|
||||
}
|
||||
@@ -391,7 +389,7 @@ async fn create_trigger_inner(
|
||||
new_http_trigger.is_static_website,
|
||||
new_http_trigger.error_handler_path,
|
||||
new_http_trigger.error_handler_args as _,
|
||||
new_http_trigger.retry as _,
|
||||
new_http_trigger.retry as _
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
@@ -16,6 +16,7 @@ use http::{HeaderMap, HeaderName};
|
||||
use itertools::Itertools;
|
||||
use quick_cache::sync::Cache;
|
||||
use serde_json::value::RawValue;
|
||||
use serde_json::Value;
|
||||
use sqlx::Pool;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
@@ -24,6 +25,8 @@ use std::str::FromStr;
|
||||
use std::time::Instant;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tower::ServiceBuilder;
|
||||
#[cfg(all(feature = "enterprise", feature = "smtp"))]
|
||||
use windmill_common::auth::is_super_admin_email;
|
||||
use windmill_common::auth::TOKEN_PREFIX_LEN;
|
||||
use windmill_common::error::JsonResult;
|
||||
use windmill_common::flow_status::{JobResult, RestartedFrom};
|
||||
@@ -33,6 +36,8 @@ use windmill_common::jobs::{
|
||||
};
|
||||
use windmill_common::utils::WarnAfterExt;
|
||||
use windmill_common::worker::{Connection, CLOUD_HOSTED, TMP_DIR};
|
||||
#[cfg(all(feature = "enterprise", feature = "smtp"))]
|
||||
use windmill_common::{email_oss::send_email_html, server::load_smtp_config};
|
||||
|
||||
use windmill_common::scripts::PREVIEW_IS_CODEBASE_HASH;
|
||||
use windmill_common::variables::get_workspace_key;
|
||||
@@ -66,7 +71,9 @@ use tower_http::cors::{Any, CorsLayer};
|
||||
use urlencoding::encode;
|
||||
use windmill_audit::audit_oss::{audit_log, AuditAuthor};
|
||||
use windmill_audit::ActionKind;
|
||||
|
||||
use windmill_common::worker::to_raw_value;
|
||||
|
||||
use windmill_common::{
|
||||
cache,
|
||||
db::UserDB,
|
||||
@@ -85,7 +92,7 @@ use windmill_common::{
|
||||
|
||||
use windmill_common::{
|
||||
get_latest_deployed_hash_for_path, get_latest_flow_version_info_for_path,
|
||||
get_script_info_for_hash, FlowVersionInfo, ScriptHashInfo, BASE_URL,
|
||||
get_script_info_for_hash, utils::empty_as_none, FlowVersionInfo, ScriptHashInfo, BASE_URL,
|
||||
};
|
||||
use windmill_queue::{
|
||||
cancel_job, get_result_and_success_by_id_from_flow, job_is_complete, push, PushArgs,
|
||||
@@ -234,6 +241,10 @@ pub fn workspaced_service() -> Router {
|
||||
)
|
||||
.route("/run/dependencies", post(run_dependencies_job))
|
||||
.route("/run/flow_dependencies", post(run_flow_dependencies_job))
|
||||
.route(
|
||||
"/send_email_with_instance_smtp",
|
||||
post(send_email_with_instance_smtp),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn workspace_unauthed_service() -> Router {
|
||||
@@ -261,6 +272,7 @@ pub fn workspace_unauthed_service() -> Router {
|
||||
.route("/get_root_job_id/:id", get(get_root_job))
|
||||
.route("/get/:id", get(get_job))
|
||||
.route("/get_logs/:id", get(get_job_logs))
|
||||
.route("/get_completed_logs_tail/:id", get(get_completed_job_logs_tail))
|
||||
.route("/get_args/:id", get(get_args))
|
||||
.route("/get_flow_debug_info/:id", get(get_flow_job_debug_info))
|
||||
.route("/completed/get/:id", get(get_completed_job))
|
||||
@@ -1037,6 +1049,240 @@ impl<'a> GetQuery<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "smtp", feature = "enterprise"))]
|
||||
async fn send_workspace_trigger_failure_email_notification(
|
||||
db: &Pool<Postgres>,
|
||||
w_id: &str,
|
||||
job_id: &Uuid,
|
||||
trigger_path: Option<&str>,
|
||||
runnable_path: Option<&str>,
|
||||
email_recipients: &Vec<String>,
|
||||
error: &Value,
|
||||
) -> Result<String, Error> {
|
||||
let smtp_config = match load_smtp_config(db).await? {
|
||||
Some(config) => config,
|
||||
None => {
|
||||
tracing::info!(
|
||||
"SMTP not configured, skipping workspace trigger failure email notification"
|
||||
);
|
||||
return Err(anyhow::anyhow!(
|
||||
"SMTP not configured, skipping workspace trigger failure email notification"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
};
|
||||
|
||||
let runnable_path = runnable_path.as_deref().unwrap_or("Unknown");
|
||||
|
||||
let (trigger_kind, trigger_path) = if let Some(trigger_path) = trigger_path.as_deref() {
|
||||
match trigger_path.split_once('/') {
|
||||
Some((trigger_kind, trigger_path)) => {
|
||||
tracing::debug!(
|
||||
"Workspace trigger job {} is a {:?} trigger",
|
||||
&job_id,
|
||||
trigger_kind
|
||||
);
|
||||
(Some(trigger_kind), Some(trigger_path))
|
||||
}
|
||||
_ => (None, None),
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let base_url = BASE_URL.read().await;
|
||||
let job_url = format!("{}/run/{}?workspace={}", base_url, &job_id, w_id);
|
||||
let trigger_kind_str = trigger_kind.unwrap_or("Unknown").to_string().to_uppercase();
|
||||
|
||||
let subject = format!(
|
||||
"Windmill Job Failed: {} in workspace {}",
|
||||
runnable_path, w_id
|
||||
);
|
||||
|
||||
let error_details = serde_json::to_string_pretty(&error)
|
||||
.unwrap_or_else(|_| format!("Unable to serialize error: {:?}", error));
|
||||
|
||||
let trigger_info = if trigger_kind.is_some() && trigger_path.is_some() {
|
||||
format!(
|
||||
r#"
|
||||
<div class="section">
|
||||
<span class="label">Trigger path:</span> {}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<span class="label">Trigger Type:</span> {}
|
||||
</div>"#,
|
||||
trigger_path.unwrap(),
|
||||
trigger_kind_str
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let email_title = if trigger_kind.is_some() {
|
||||
format!("Windmill Trigger Job {} Failed", &job_id)
|
||||
} else {
|
||||
format!("Windmill Job {} Failed", &job_id)
|
||||
};
|
||||
|
||||
let content = format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; color: #333; }}
|
||||
h1 {{ color: #c0392b; }}
|
||||
.section {{ margin-bottom: 20px; }}
|
||||
.label {{ font-weight: bold; }}
|
||||
pre {{ background-color: #f4f4f4; padding: 10px; border-radius: 4px; overflow-x: auto; }}
|
||||
a.button {{
|
||||
display: inline-block;
|
||||
padding: 10px 15px;
|
||||
margin-top: 10px;
|
||||
color: #fff;
|
||||
background-color: #2980b9;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{}</h1>
|
||||
|
||||
<div class="section">
|
||||
<span class="label">Workspace:</span> {}
|
||||
</div>
|
||||
{}
|
||||
<div class="section">
|
||||
<span class="label">Script/Flow Path:</span> {}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<span class="label">Job ID:</span> {}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<span class="label">Error Details:</span>
|
||||
<pre>{}</pre>
|
||||
</div>
|
||||
|
||||
<a href="{}" class="button">View Job Details</a>
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
email_title,
|
||||
w_id,
|
||||
trigger_info,
|
||||
runnable_path,
|
||||
&job_id,
|
||||
error_details,
|
||||
job_url
|
||||
);
|
||||
|
||||
if let Err(e) = send_email_html(
|
||||
&subject,
|
||||
&content,
|
||||
email_recipients.to_owned(),
|
||||
smtp_config,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let err_msg = format!(
|
||||
"Failed to send workspace email notification for trigger failure (trigger kind: {}, job ID: {}): {}",
|
||||
trigger_kind_str,
|
||||
&job_id,
|
||||
e
|
||||
);
|
||||
return Err(Error::internal_err(err_msg));
|
||||
}
|
||||
let success_msg = format!(
|
||||
"Job ID '{}' failed. An email with error details has been sent to {:?}.",
|
||||
&job_id, email_recipients
|
||||
);
|
||||
tracing::info!("{}", &success_msg);
|
||||
Ok(success_msg)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct SendEmail {
|
||||
job_id: Uuid,
|
||||
#[serde(default, deserialize_with = "empty_as_none")]
|
||||
trigger_path: Option<String>,
|
||||
#[serde(default, deserialize_with = "empty_as_none")]
|
||||
runnable_path: Option<String>,
|
||||
#[serde(default, deserialize_with = "empty_as_none")]
|
||||
email_recipients: Option<Vec<String>>,
|
||||
error: Value,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "smtp"))]
|
||||
async fn send_email_with_instance_smtp(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(send_email): Json<SendEmail>,
|
||||
) -> error::Result<Json<String>> {
|
||||
use windmill_common::jobs::EMAIL_ERROR_HANDLER_USER_EMAIL;
|
||||
|
||||
if *CLOUD_HOSTED {
|
||||
tracing::warn!(
|
||||
"Workspace trigger failure email notification is not available for cloud hosted Windmill",
|
||||
);
|
||||
return Err(anyhow::anyhow!("Feature not supported in cloud hosted windmill").into());
|
||||
}
|
||||
|
||||
if send_email.email_recipients.is_none() {
|
||||
use windmill_common::utils::report_critical_error;
|
||||
|
||||
tracing::error!("No recipient to send the error");
|
||||
report_critical_error(
|
||||
"No recipient to send the error".to_string(),
|
||||
db.clone(),
|
||||
Some(&w_id),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
return Err(anyhow::anyhow!("No recipient to send the error").into());
|
||||
}
|
||||
|
||||
if authed.email == EMAIL_ERROR_HANDLER_USER_EMAIL
|
||||
|| is_super_admin_email(&db, &authed.email).await?
|
||||
{
|
||||
let resp = send_workspace_trigger_failure_email_notification(
|
||||
&db,
|
||||
&w_id,
|
||||
&send_email.job_id,
|
||||
send_email.trigger_path.as_deref(),
|
||||
send_email.runnable_path.as_deref(),
|
||||
&send_email.email_recipients.unwrap(),
|
||||
&send_email.error,
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(Json(resp));
|
||||
}
|
||||
|
||||
return Err(Error::NotAuthorized(
|
||||
"Only super admin or whitelisted token can access email workspace error handler feature"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "enterprise", feature = "smtp")))]
|
||||
async fn send_email_with_instance_smtp(
|
||||
_authed: ApiAuthed,
|
||||
Extension(_db): Extension<DB>,
|
||||
Path(_w_id): Path<String>,
|
||||
Json(_send_email): Json<SendEmail>,
|
||||
) -> error::Result<Json<String>> {
|
||||
tracing::warn!("SMTP is not enabled, skipping workspace trigger failure email notification",);
|
||||
|
||||
return Err(anyhow::anyhow!("SMTP is not enabled").into());
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
async fn get_logs_from_store(
|
||||
log_offset: i32,
|
||||
@@ -1116,11 +1362,55 @@ async fn get_logs_from_disk(
|
||||
return None;
|
||||
}
|
||||
|
||||
async fn get_completed_job_logs_tail(
|
||||
OptAuthed(opt_authed): OptAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
) -> error::JsonResult<String> {
|
||||
|
||||
let tags = opt_authed
|
||||
.as_ref()
|
||||
.map(|authed| get_scope_tags(authed).map(|v| v.iter().map(|s| s.to_string()).collect_vec()))
|
||||
.flatten();
|
||||
|
||||
let record = sqlx::query!(
|
||||
"SELECT created_by AS \"created_by!\", coalesce(job_logs.logs, '') as logs
|
||||
FROM v2_job
|
||||
LEFT JOIN job_logs ON job_logs.job_id = v2_job.id
|
||||
WHERE v2_job.id = $1 AND v2_job.workspace_id = $2 AND ($3::text[] IS NULL OR v2_job.tag = ANY($3))
|
||||
ORDER BY job_logs.log_offset DESC
|
||||
LIMIT 100",
|
||||
id,
|
||||
w_id,
|
||||
tags.as_ref().map(|v| v.as_slice())
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
|
||||
if let Some(record) = record {
|
||||
if opt_authed.is_none() && record.created_by != "anonymous" {
|
||||
return Err(Error::BadRequest(
|
||||
"As a non logged in user, you can only see jobs ran by anonymous users".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let logs = record.logs.unwrap_or_default();
|
||||
Ok(Json(logs))
|
||||
} else {
|
||||
Err(Error::NotFound("Job not found".to_string()).into())
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct QueryJobLogs {
|
||||
remove_ansi_warnings: Option<bool>,
|
||||
}
|
||||
|
||||
async fn get_job_logs(
|
||||
OptAuthed(opt_authed): OptAuthed,
|
||||
opt_tokened: OptTokened,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
Query(query_job_logs): Query<QueryJobLogs>,
|
||||
) -> error::Result<Response> {
|
||||
// let audit_author: AuditAuthor = match opt_authed {
|
||||
// Some(authed) => (&authed).into(),
|
||||
@@ -1174,10 +1464,14 @@ async fn get_job_logs(
|
||||
{
|
||||
return r.map(content_plain);
|
||||
}
|
||||
let logs = format!(
|
||||
"to remove ansi colors, use: | sed 's/\\x1B\\[[0-9;]\\{{1,\\}}[A-Za-z]//g'\n{}",
|
||||
let logs = if query_job_logs.remove_ansi_warnings.unwrap_or(false) {
|
||||
logs
|
||||
);
|
||||
} else {
|
||||
format!(
|
||||
"to remove ansi colors, use: | sed 's/\\x1B\\[[0-9;]\\{{1,\\}}[A-Za-z]//g'\n{}",
|
||||
logs
|
||||
)
|
||||
};
|
||||
Ok(content_plain(Body::from(logs)))
|
||||
} else {
|
||||
let text = sqlx::query!(
|
||||
@@ -6151,13 +6445,17 @@ async fn get_job_update_data(
|
||||
if running.is_some_and(|x| !x) {
|
||||
let r = sqlx::query!(
|
||||
"SELECT
|
||||
result as \"result: sqlx::types::Json<Box<RawValue>>\",
|
||||
v2_job_queue.running as \"running: Option<bool>\",
|
||||
COALESCE(jc.result, jc.result) as \"result: sqlx::types::Json<Box<RawValue>>\",
|
||||
jq.running as \"running: Option<bool>\",
|
||||
SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\",
|
||||
CHAR_LENGTH(rs.stream) + 1 AS stream_offset
|
||||
FROM v2_job_completed FULL OUTER JOIN v2_job_queue USING (id)
|
||||
LEFT JOIN job_result_stream rs ON rs.job_id = $1
|
||||
WHERE (v2_job_queue.id = $1 AND v2_job_queue.workspace_id = $2) OR (v2_job_completed.id = $1 AND v2_job_completed.workspace_id = $2)",
|
||||
FROM (
|
||||
SELECT $1::uuid as job_id, $2::text as workspace_id
|
||||
) base
|
||||
LEFT JOIN v2_job_completed jc ON jc.id = base.job_id AND jc.workspace_id = base.workspace_id
|
||||
LEFT JOIN v2_job_queue jq ON jq.id = base.job_id AND jq.workspace_id = base.workspace_id
|
||||
LEFT JOIN job_result_stream rs ON rs.job_id = base.job_id
|
||||
WHERE base.job_id = $1",
|
||||
job_id,
|
||||
w_id,
|
||||
stream_offset.unwrap_or(0),
|
||||
@@ -6170,15 +6468,22 @@ async fn get_job_update_data(
|
||||
}
|
||||
} else {
|
||||
let q = sqlx::query!(
|
||||
"SELECT result as \"result: sqlx::types::Json<Box<RawValue>>\", SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\", CHAR_LENGTH(rs.stream) + 1 AS stream_offset
|
||||
FROM v2_job_completed FULL OUTER JOIN job_result_stream rs ON rs.job_id = v2_job_completed.id WHERE (v2_job_completed.id = $2 AND v2_job_completed.workspace_id = $1 OR rs.workspace_id = $1)",
|
||||
"SELECT
|
||||
COALESCE(jc.result, NULL) as \"result: sqlx::types::Json<Box<RawValue>>\",
|
||||
SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\",
|
||||
CHAR_LENGTH(rs.stream) + 1 AS stream_offset
|
||||
FROM (
|
||||
SELECT $2::uuid as job_id, $1::text as workspace_id
|
||||
) base
|
||||
LEFT JOIN v2_job_completed jc ON jc.id = base.job_id AND jc.workspace_id = base.workspace_id
|
||||
LEFT JOIN job_result_stream rs ON rs.job_id = base.job_id
|
||||
WHERE base.job_id = $2",
|
||||
w_id,
|
||||
job_id,
|
||||
stream_offset.unwrap_or(0),
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
tracing::error!("q {:?}", q);
|
||||
if let Some(r) = q {
|
||||
(r.result.map(|x| x.0), running, r.result_stream.flatten(), r.stream_offset)
|
||||
} else {
|
||||
|
||||
@@ -543,6 +543,18 @@ pub async fn run_server(
|
||||
(Router::new(), Option::<()>::None)
|
||||
};
|
||||
|
||||
let mcp_list_tools_service = {
|
||||
#[cfg(feature = "mcp")]
|
||||
{
|
||||
mcp::list_tools_service()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mcp"))]
|
||||
{
|
||||
Router::new()
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(feature = "agent_worker_server")]
|
||||
let (agent_workers_router, agent_workers_bg_processor, agent_workers_killpill_tx) =
|
||||
if server_mode {
|
||||
@@ -631,6 +643,8 @@ pub async fn run_server(
|
||||
.nest("/embeddings", embeddings::global_service())
|
||||
.nest("/ai", ai::global_service())
|
||||
.nest("/inkeep", inkeep_oss::global_service())
|
||||
.nest("/mcp/w/:workspace_id/sse", mcp_router)
|
||||
.nest("/mcp/w/:workspace_id/list_tools", mcp_list_tools_service)
|
||||
.route_layer(from_extractor::<ApiAuthed>())
|
||||
.route_layer(from_extractor::<users::Tokened>())
|
||||
.nest("/jobs", jobs::global_root_service())
|
||||
@@ -669,7 +683,6 @@ pub async fn run_server(
|
||||
.layer(from_extractor::<OptAuthed>())
|
||||
.layer(cors.clone()),
|
||||
)
|
||||
.nest("/mcp/w/:workspace_id/sse", mcp_router.layer(from_extractor::<ApiAuthed>()))
|
||||
.layer(from_extractor::<OptAuthed>())
|
||||
.nest("/agent_workers", {
|
||||
#[cfg(feature = "agent_worker_server")]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
//! Model Context Protocol (MCP) implementation for Windmill
|
||||
//!
|
||||
//! This module provides the MCP server implementation that exposes Windmill scripts,
|
||||
//! flows, and API endpoints as MCP tools for AI assistants to interact with.
|
||||
|
||||
pub mod server;
|
||||
pub mod tools;
|
||||
pub mod utils;
|
||||
|
||||
// Re-export main components
|
||||
pub use server::{extract_and_store_workspace_id, setup_mcp_server, shutdown_mcp_server, list_tools_service};
|
||||
@@ -0,0 +1,517 @@
|
||||
//! MCP Server implementation
|
||||
//!
|
||||
//! Contains the core MCP server handler that implements the Model Context Protocol
|
||||
//! specification. This is a thin orchestration layer that delegates to the appropriate
|
||||
//! modules for tool management, database operations, and schema transformation.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::body::to_bytes;
|
||||
use rmcp::{
|
||||
handler::server::ServerHandler,
|
||||
model::*,
|
||||
service::{RequestContext, RoleServer},
|
||||
Error,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tokio::try_join;
|
||||
use windmill_common::db::UserDB;
|
||||
use windmill_common::worker::to_raw_value;
|
||||
use windmill_common::{utils::StripPath, DB};
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
use crate::jobs::{
|
||||
run_wait_result_flow_by_path_internal, run_wait_result_script_by_path_internal, RunJobQuery,
|
||||
};
|
||||
|
||||
use super::utils::{
|
||||
database::{
|
||||
check_scopes, get_items, get_resources_types, get_scripts_from_hub, get_item_schema, get_hub_script_schema
|
||||
},
|
||||
models::{ScriptInfo, FlowInfo, ResourceInfo, ResourceType, SchemaType, ToolableItem, WorkspaceId},
|
||||
schema::transform_schema_for_resources,
|
||||
transform::{reverse_transform, reverse_transform_key},
|
||||
};
|
||||
use super::tools::{
|
||||
endpoint_tools::{all_endpoint_tools, endpoint_tools_to_mcp_tools, call_endpoint_tool, EndpointTool},
|
||||
};
|
||||
|
||||
use axum::{
|
||||
extract::Path,
|
||||
http::Request,
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
routing::get,
|
||||
Json,
|
||||
Router,
|
||||
};
|
||||
use rmcp::transport::streamable_http_server::{
|
||||
session::local::LocalSessionManager,
|
||||
SessionManager,
|
||||
StreamableHttpService,
|
||||
};
|
||||
use windmill_common::error::JsonResult;
|
||||
|
||||
|
||||
/// MCP Server Runner - implements the core MCP protocol handlers
|
||||
#[derive(Clone)]
|
||||
pub struct Runner {}
|
||||
|
||||
impl Runner {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
/// Creates a Tool from a ToolableItem
|
||||
async fn create_tool_from_item<T: ToolableItem>(
|
||||
item: &T,
|
||||
user_db: &UserDB,
|
||||
authed: &ApiAuthed,
|
||||
workspace_id: &str,
|
||||
resources_cache: &mut HashMap<String, Vec<ResourceInfo>>,
|
||||
resources_types: &Vec<ResourceType>,
|
||||
) -> Result<Tool, Error> {
|
||||
let is_hub = item.is_hub();
|
||||
let path = item.get_path_or_id();
|
||||
let item_type = item.item_type();
|
||||
let description = format!(
|
||||
"This is a {} named `{}` with the following description: `{}`.{}",
|
||||
item_type,
|
||||
item.get_summary(),
|
||||
item.get_description(),
|
||||
if is_hub {
|
||||
format!(
|
||||
" It is a tool used for the following app: {}",
|
||||
item.get_integration_type()
|
||||
.unwrap_or("No integration type".to_string())
|
||||
)
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
);
|
||||
let schema_obj = transform_schema_for_resources(
|
||||
&item.get_schema(),
|
||||
user_db,
|
||||
authed,
|
||||
&workspace_id,
|
||||
resources_cache,
|
||||
&resources_types,
|
||||
)
|
||||
.await?;
|
||||
let input_schema_map = match serde_json::to_value(schema_obj) {
|
||||
Ok(Value::Object(map)) => map,
|
||||
Ok(_) => {
|
||||
tracing::warn!("Schema object for tool '{}' did not serialize to a JSON object, using empty schema.", path);
|
||||
serde_json::Map::new()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to serialize schema object for tool '{}': {}. Using empty schema.",
|
||||
path,
|
||||
e
|
||||
);
|
||||
serde_json::Map::new()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Tool {
|
||||
name: Cow::Owned(path),
|
||||
description: Some(Cow::Owned(description)),
|
||||
input_schema: Arc::new(input_schema_map),
|
||||
annotations: Some(ToolAnnotations {
|
||||
title: Some(item.get_summary().to_string()),
|
||||
read_only_hint: Some(false), // Can modify environment
|
||||
destructive_hint: Some(true), // Can potentially be destructive
|
||||
idempotent_hint: Some(false), // Are not guaranteed to be idempotent
|
||||
open_world_hint: Some(true), // Can interact with external services
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl ServerHandler for Runner {
|
||||
/// Handles the `CallTool` request from the MCP client
|
||||
async fn call_tool(
|
||||
&self,
|
||||
request: CallToolRequestParam,
|
||||
context: RequestContext<RoleServer>,
|
||||
) -> Result<CallToolResult, Error> {
|
||||
let http_parts = context
|
||||
.extensions
|
||||
.get::<axum::http::request::Parts>()
|
||||
.ok_or_else(|| {
|
||||
tracing::error!("http::request::Parts not found");
|
||||
Error::internal_error("http::request::Parts not found", None)
|
||||
})?;
|
||||
|
||||
let authed = http_parts.extensions.get::<ApiAuthed>().ok_or_else(|| {
|
||||
tracing::error!("ApiAuthed Axum extension not found");
|
||||
Error::internal_error("ApiAuthed Axum extension not found", None)
|
||||
})?;
|
||||
|
||||
check_scopes(authed)?;
|
||||
|
||||
let db = http_parts.extensions.get::<DB>().ok_or_else(|| {
|
||||
tracing::error!("DB Axum extension not found");
|
||||
Error::internal_error("DB Axum extension not found", None)
|
||||
})?;
|
||||
|
||||
let user_db = http_parts.extensions.get::<UserDB>().ok_or_else(|| {
|
||||
tracing::error!("UserDB Axum extension not found");
|
||||
Error::internal_error("UserDB Axum extension not found", None)
|
||||
})?;
|
||||
|
||||
let args = request.arguments.map(Value::Object).ok_or_else(|| {
|
||||
Error::invalid_params("Missing arguments for tool", Some(request.name.clone().into()))
|
||||
})?;
|
||||
|
||||
let workspace_id = http_parts
|
||||
.extensions
|
||||
.get::<WorkspaceId>()
|
||||
.ok_or_else(|| {
|
||||
tracing::error!("WorkspaceId not found");
|
||||
Error::internal_error("WorkspaceId not found", None)
|
||||
})
|
||||
.map(|w_id| w_id.0.clone())?;
|
||||
|
||||
// Check if this is a generated endpoint tool
|
||||
let endpoint_tools = all_endpoint_tools();
|
||||
for endpoint_tool in endpoint_tools {
|
||||
if endpoint_tool.name.as_ref() == request.name {
|
||||
// This is an endpoint tool, forward to the actual HTTP endpoint
|
||||
let result = call_endpoint_tool(&endpoint_tool, args.clone(), &workspace_id, &authed).await?;
|
||||
return Ok(CallToolResult::success(vec![Content::text(
|
||||
serde_json::to_string_pretty(&result).unwrap_or_else(|_| "{}".to_string())
|
||||
)]));
|
||||
}
|
||||
}
|
||||
|
||||
// Continue with script/flow logic
|
||||
let (tool_type, path, is_hub) =
|
||||
reverse_transform(&request.name).unwrap_or_default();
|
||||
|
||||
let item_schema = if is_hub {
|
||||
get_hub_script_schema(&format!("hub/{}", path), db).await?
|
||||
} else {
|
||||
get_item_schema(&path, user_db, authed, &workspace_id, &tool_type).await?
|
||||
};
|
||||
|
||||
let schema_obj = if let Some(ref s) = item_schema {
|
||||
match serde_json::from_str::<SchemaType>(s.0.get()) {
|
||||
Ok(val) => Some(val),
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to parse schema: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let push_args = if let Value::Object(map) = args.clone() {
|
||||
let mut args_hash = HashMap::new();
|
||||
for (k, v) in map {
|
||||
// need to transform back the key without invalid characters to the original key
|
||||
let original_key = reverse_transform_key(&k, &schema_obj);
|
||||
args_hash.insert(original_key, to_raw_value(&v));
|
||||
}
|
||||
windmill_queue::PushArgsOwned { extra: None, args: args_hash }
|
||||
} else {
|
||||
windmill_queue::PushArgsOwned::default()
|
||||
};
|
||||
let script_or_flow_path = if is_hub {
|
||||
StripPath(format!("hub/{}", path))
|
||||
} else {
|
||||
StripPath(path)
|
||||
};
|
||||
let run_query = RunJobQuery::default();
|
||||
|
||||
let result = if tool_type == "script" {
|
||||
run_wait_result_script_by_path_internal(
|
||||
db.clone(),
|
||||
run_query,
|
||||
script_or_flow_path,
|
||||
authed.clone(),
|
||||
user_db.clone(),
|
||||
workspace_id.clone(),
|
||||
push_args,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
run_wait_result_flow_by_path_internal(
|
||||
db.clone(),
|
||||
run_query,
|
||||
script_or_flow_path,
|
||||
authed.clone(),
|
||||
user_db.clone(),
|
||||
push_args,
|
||||
workspace_id.clone(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(response) => {
|
||||
let body_bytes = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::internal_error(format!("Failed to read response body: {}", e), None)
|
||||
})?;
|
||||
let body_str = String::from_utf8(body_bytes.to_vec()).map_err(|e| {
|
||||
Error::internal_error(format!("Failed to decode response body: {}", e), None)
|
||||
})?;
|
||||
Ok(CallToolResult::success(vec![Content::text(body_str)]))
|
||||
}
|
||||
Err(e) => Err(Error::internal_error(
|
||||
format!("Failed to run script: {}", e),
|
||||
None,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches available tools (scripts, flows, hub scripts) based on the user's scope
|
||||
async fn list_tools(
|
||||
&self,
|
||||
_request: Option<PaginatedRequestParam>,
|
||||
mut _context: RequestContext<RoleServer>,
|
||||
) -> Result<ListToolsResult, Error> {
|
||||
let http_parts = _context
|
||||
.extensions
|
||||
.get::<axum::http::request::Parts>()
|
||||
.ok_or_else(|| {
|
||||
tracing::error!("http::request::Parts not found");
|
||||
Error::internal_error("http::request::Parts not found", None)
|
||||
})?;
|
||||
|
||||
let authed = http_parts.extensions.get::<ApiAuthed>().ok_or_else(|| {
|
||||
tracing::error!("ApiAuthed Axum extension not found");
|
||||
Error::internal_error("ApiAuthed Axum extension not found", None)
|
||||
})?;
|
||||
|
||||
check_scopes(authed)?;
|
||||
|
||||
let db = http_parts.extensions.get::<DB>().ok_or_else(|| {
|
||||
tracing::error!("DB Axum extension not found");
|
||||
Error::internal_error("DB Axum extension not found", None)
|
||||
})?;
|
||||
|
||||
let user_db = http_parts.extensions.get::<UserDB>().ok_or_else(|| {
|
||||
tracing::error!("UserDB Axum extension not found");
|
||||
Error::internal_error("UserDB Axum extension not found", None)
|
||||
})?;
|
||||
|
||||
let workspace_id = http_parts
|
||||
.extensions
|
||||
.get::<WorkspaceId>()
|
||||
.ok_or_else(|| {
|
||||
tracing::error!("WorkspaceId not found");
|
||||
Error::internal_error("WorkspaceId not found", None)
|
||||
})
|
||||
.map(|w_id| w_id.0.clone())?;
|
||||
|
||||
let scopes = authed.scopes.as_ref();
|
||||
let owned_scope = scopes.and_then(|scopes| {
|
||||
scopes
|
||||
.iter()
|
||||
.find(|scope| scope.starts_with("mcp:") && !scope.contains("hub"))
|
||||
});
|
||||
let hub_scope = scopes.and_then(|scopes| scopes.iter().find(|scope| scope.starts_with("mcp:hub")));
|
||||
let (scope_type, scope_path) = owned_scope.map_or(("all", None), |scope| {
|
||||
let parts = scope.split(":").collect::<Vec<&str>>();
|
||||
(parts[1], if parts.len() == 3 { Some(parts[2]) } else { None })
|
||||
});
|
||||
let scope_integrations = hub_scope.and_then(|scope| {
|
||||
let parts = scope.split(":").collect::<Vec<&str>>();
|
||||
if parts.len() == 3 {
|
||||
Some(parts[2])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let scripts_fn = get_items::<ScriptInfo>(
|
||||
user_db,
|
||||
authed,
|
||||
&workspace_id,
|
||||
scope_type,
|
||||
"script",
|
||||
scope_path.as_deref(),
|
||||
);
|
||||
let flows_fn =
|
||||
get_items::<FlowInfo>(user_db, authed, &workspace_id, scope_type, "flow", scope_path.as_deref());
|
||||
let resources_types_fn = get_resources_types(user_db, authed, &workspace_id);
|
||||
let hub_scripts_fn = get_scripts_from_hub(db, scope_integrations.as_deref());
|
||||
let (scripts, flows, resources_types, hub_scripts) = if scope_integrations.is_some() {
|
||||
let (scripts, flows, resources_types, hub_scripts) =
|
||||
try_join!(scripts_fn, flows_fn, resources_types_fn, hub_scripts_fn)?;
|
||||
(scripts, flows, resources_types, hub_scripts)
|
||||
} else {
|
||||
let (scripts, flows, resources_types) =
|
||||
try_join!(scripts_fn, flows_fn, resources_types_fn)?;
|
||||
(scripts, flows, resources_types, vec![])
|
||||
};
|
||||
|
||||
let mut resources_cache: HashMap<String, Vec<ResourceInfo>> = HashMap::new();
|
||||
let mut tools: Vec<Tool> = Vec::new();
|
||||
|
||||
for script in scripts {
|
||||
tools.push(
|
||||
Runner::create_tool_from_item(
|
||||
&script,
|
||||
user_db,
|
||||
authed,
|
||||
&workspace_id,
|
||||
&mut resources_cache,
|
||||
&resources_types,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
for flow in flows {
|
||||
tools.push(
|
||||
Runner::create_tool_from_item(
|
||||
&flow,
|
||||
user_db,
|
||||
authed,
|
||||
&workspace_id,
|
||||
&mut resources_cache,
|
||||
&resources_types,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
for hub_script in hub_scripts {
|
||||
tools.push(
|
||||
Runner::create_tool_from_item(
|
||||
&hub_script,
|
||||
user_db,
|
||||
authed,
|
||||
&workspace_id,
|
||||
&mut resources_cache,
|
||||
&resources_types,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
// Add endpoint tools from the generated MCP tools
|
||||
let endpoint_tools = all_endpoint_tools();
|
||||
let mcp_tools_converted = endpoint_tools_to_mcp_tools(endpoint_tools);
|
||||
tools.extend(mcp_tools_converted);
|
||||
|
||||
Ok(ListToolsResult { tools, next_cursor: None })
|
||||
}
|
||||
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
protocol_version: Default::default(),
|
||||
capabilities: ServerCapabilities::builder()
|
||||
.enable_tools()
|
||||
.enable_tool_list_changed()
|
||||
.build(),
|
||||
server_info: Implementation::from_build_env(),
|
||||
instructions: Some("This server provides a list of scripts and flows the user can run on Windmill. Each flow and script is a tool callable with their respective arguments.".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn initialize(
|
||||
&self,
|
||||
_request: InitializeRequestParam,
|
||||
_context: RequestContext<RoleServer>,
|
||||
) -> Result<InitializeResult, Error> {
|
||||
Ok(self.get_info())
|
||||
}
|
||||
|
||||
async fn list_resources(
|
||||
&self,
|
||||
_request: Option<PaginatedRequestParam>,
|
||||
_context: RequestContext<RoleServer>,
|
||||
) -> Result<ListResourcesResult, Error> {
|
||||
Ok(ListResourcesResult { resources: vec![], next_cursor: None })
|
||||
}
|
||||
|
||||
async fn list_prompts(
|
||||
&self,
|
||||
_request: Option<PaginatedRequestParam>,
|
||||
_context: RequestContext<RoleServer>,
|
||||
) -> Result<ListPromptsResult, Error> {
|
||||
Ok(ListPromptsResult::default())
|
||||
}
|
||||
|
||||
async fn list_resource_templates(
|
||||
&self,
|
||||
_request: Option<PaginatedRequestParam>,
|
||||
_context: RequestContext<RoleServer>,
|
||||
) -> Result<ListResourceTemplatesResult, Error> {
|
||||
Ok(ListResourceTemplatesResult::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract workspace ID from path and store it in request extensions
|
||||
pub async fn extract_and_store_workspace_id(
|
||||
Path(params): Path<String>,
|
||||
mut request: Request<axum::body::Body>,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let workspace_id = params;
|
||||
request.extensions_mut().insert(WorkspaceId(workspace_id));
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
/// Setup the MCP server with HTTP transport
|
||||
pub async fn setup_mcp_server() -> anyhow::Result<(Router, Arc<LocalSessionManager>)> {
|
||||
let session_manager = Arc::new(LocalSessionManager::default());
|
||||
let service_config = Default::default();
|
||||
let service = StreamableHttpService::new(
|
||||
|| Ok(Runner::new()),
|
||||
session_manager.clone(),
|
||||
service_config,
|
||||
);
|
||||
|
||||
let router = axum::Router::new().nest_service("/", service);
|
||||
Ok((router, session_manager))
|
||||
}
|
||||
|
||||
/// Shutdown the MCP server gracefully by closing all active sessions
|
||||
pub async fn shutdown_mcp_server(session_manager: Arc<LocalSessionManager>) {
|
||||
let session_ids_to_close = {
|
||||
let sessions_map = session_manager.sessions.read().await;
|
||||
sessions_map.keys().cloned().collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
if !session_ids_to_close.is_empty() {
|
||||
tracing::info!(
|
||||
"Closing {} active MCP session(s)...",
|
||||
session_ids_to_close.len()
|
||||
);
|
||||
let close_futures = session_ids_to_close
|
||||
.iter()
|
||||
.map(|session_id| {
|
||||
let manager_clone = session_manager.clone();
|
||||
async move {
|
||||
if let Err(_) = manager_clone.close_session(session_id).await {
|
||||
tracing::warn!("Error closing MCP session");
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
futures::future::join_all(close_futures).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP handler to list MCP tools as JSON
|
||||
async fn list_mcp_tools_handler() -> JsonResult<Vec<EndpointTool>> {
|
||||
let endpoint_tools = all_endpoint_tools();
|
||||
Ok(Json(endpoint_tools))
|
||||
}
|
||||
|
||||
/// Creates a router service for listing MCP tools
|
||||
pub fn list_tools_service() -> Router {
|
||||
Router::new()
|
||||
.route("/", get(list_mcp_tools_handler))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user