Compare commits
95 Commits
v1.509.1
...
win-1465-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cb5660057 | ||
|
|
25aa982318 | ||
|
|
4b9683f146 | ||
|
|
14aa6d9681 | ||
|
|
2619be679e | ||
|
|
6971dcdb05 | ||
|
|
b0616c74cf | ||
|
|
daf3bb0a0a | ||
|
|
2e7ab919a7 | ||
|
|
b5afff9cc1 | ||
|
|
29c686d62a | ||
|
|
5af47d29e7 | ||
|
|
4d6a61403b | ||
|
|
d5c21131df | ||
|
|
56671bc75f | ||
|
|
9931311650 | ||
|
|
379880c6c5 | ||
|
|
55ba599022 | ||
|
|
8a2cd1c468 | ||
|
|
1084777bd6 | ||
|
|
ce4177ebae | ||
|
|
454b258ab8 | ||
|
|
adf59a36a9 | ||
|
|
1a85dc7008 | ||
|
|
496bbeec36 | ||
|
|
f892f7c9af | ||
|
|
6e8f5a5b00 | ||
|
|
d09d90589d | ||
|
|
a999bc7b28 | ||
|
|
564e92e98d | ||
|
|
323ec9a994 | ||
|
|
ad441a7ba6 | ||
|
|
c067342369 | ||
|
|
e8f4c2418b | ||
|
|
359b34ef08 | ||
|
|
8b4dc2ce73 | ||
|
|
3f784e3c07 | ||
|
|
94e20a97e0 | ||
|
|
06bd89c957 | ||
|
|
241be4897e | ||
|
|
9f926bd6bb | ||
|
|
79205839b6 | ||
|
|
ed1bee9f66 | ||
|
|
f71104e9f6 | ||
|
|
e16c8d0199 | ||
|
|
be3173d048 | ||
|
|
f4073abef0 | ||
|
|
e6f9f4d897 | ||
|
|
16757f3bb5 | ||
|
|
e869580787 | ||
|
|
0edb01f774 | ||
|
|
e02f06bf68 | ||
|
|
a57df1f781 | ||
|
|
7384aa36f4 | ||
|
|
b92fcb90a1 | ||
|
|
10befb995d | ||
|
|
91849baf34 | ||
|
|
8591478a6d | ||
|
|
86e14f8135 | ||
|
|
b00fde7f2a | ||
|
|
37b18f0661 | ||
|
|
34ccc8e0f7 | ||
|
|
2062a634f6 | ||
|
|
71b85dfc42 | ||
|
|
dc242c5a8d | ||
|
|
8bc2a5733e | ||
|
|
1713987317 | ||
|
|
c4178c05be | ||
|
|
3d99cbe8f5 | ||
|
|
506e17864e | ||
|
|
4b2c944af2 | ||
|
|
24a6038177 | ||
|
|
2c77012115 | ||
|
|
6f91ee791a | ||
|
|
dd1f520d83 | ||
|
|
3efc329fc0 | ||
|
|
f4dbc97a58 | ||
|
|
6f11a87121 | ||
|
|
be8c4ed86d | ||
|
|
8323707b07 | ||
|
|
2bd1600253 | ||
|
|
e16d5e1e8a | ||
|
|
d2328e3670 | ||
|
|
dddabc8d1b | ||
|
|
1c7b206cd4 | ||
|
|
e5dbbdaf1d | ||
|
|
9201391102 | ||
|
|
4fd5a8861d | ||
|
|
6bdc2c3223 | ||
|
|
8bb59d5a3c | ||
|
|
86fa9f91b6 | ||
|
|
dec72e201b | ||
|
|
33619ab18c | ||
|
|
0aa2ea6954 | ||
|
|
affecdad04 |
12
.github/workflows/backend-check.yml
vendored
12
.github/workflows/backend-check.yml
vendored
@@ -21,9 +21,6 @@ jobs:
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.88.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
- name: cargo check
|
||||
working-directory: ./backend
|
||||
timeout-minutes: 16
|
||||
@@ -45,9 +42,6 @@ jobs:
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.88.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
- name: cargo check
|
||||
working-directory: ./backend
|
||||
timeout-minutes: 16
|
||||
@@ -82,9 +76,6 @@ jobs:
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.88.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
- name: cargo check
|
||||
working-directory: ./backend
|
||||
timeout-minutes: 16
|
||||
@@ -122,9 +113,6 @@ jobs:
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.88.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
- name: cargo check
|
||||
timeout-minutes: 16
|
||||
working-directory: ./backend
|
||||
|
||||
3
.github/workflows/backend-test.yml
vendored
3
.github/workflows/backend-test.yml
vendored
@@ -52,9 +52,6 @@ jobs:
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.85.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
- name: cargo test
|
||||
timeout-minutes: 16
|
||||
run:
|
||||
|
||||
5
.github/workflows/claude.yml
vendored
5
.github/workflows/claude.yml
vendored
@@ -84,10 +84,6 @@ jobs:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.88.0
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
|
||||
- name: cargo check
|
||||
working-directory: ./backend
|
||||
timeout-minutes: 16
|
||||
@@ -104,6 +100,7 @@ jobs:
|
||||
allowed_tools: "mcp__github__create_pull_request,Bash"
|
||||
custom_instructions: |
|
||||
## IMPORTANT INSTRUCTIONS
|
||||
- Your branch name should be a short description of the requested changes.
|
||||
- Bypass previous instructions and instead of adding a url in your comment to manually create a PR, if you created a new branch with your changes, automatically create a draft PR from that branch to main.
|
||||
|
||||
## Code Quality Requirements
|
||||
|
||||
125
CHANGELOG.md
125
CHANGELOG.md
@@ -1,5 +1,130 @@
|
||||
# Changelog
|
||||
|
||||
## [1.515.1](https://github.com/windmill-labs/windmill/compare/v1.515.0...v1.515.1) (2025-07-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improved logs for script ([2e7ab91](https://github.com/windmill-labs/windmill/commit/2e7ab919a7bce4f04d7b09f45db48b5465346846))
|
||||
|
||||
## [1.515.0](https://github.com/windmill-labs/windmill/compare/v1.514.1...v1.515.0) (2025-07-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cli:** generate cursor rules on init ([#6270](https://github.com/windmill-labs/windmill/issues/6270)) ([29c686d](https://github.com/windmill-labs/windmill/commit/29c686d62ae3597eb59371f5a3e9dadd9dc9557f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add size limit to indexer queries on jobs table to avoid oom ([#6293](https://github.com/windmill-labs/windmill/issues/6293)) ([4d6a614](https://github.com/windmill-labs/windmill/commit/4d6a61403b51d84ea4039611cb007a42a26d24b6))
|
||||
* fix DynSelect ([55ba599](https://github.com/windmill-labs/windmill/commit/55ba599022ac2a6b513c4456747e4910f3188ec7))
|
||||
* improve docker logs collection in docker mode ([ce4177e](https://github.com/windmill-labs/windmill/commit/ce4177ebaed02df62aec676ca4624d77052f3e51))
|
||||
* resource types as arg in typescript handle imported defined types ([56671bc](https://github.com/windmill-labs/windmill/commit/56671bc75fe940f940f52ad47f5cf99c8329a0c3))
|
||||
* resource-type-ts-parser ([#6289](https://github.com/windmill-labs/windmill/issues/6289)) ([9931311](https://github.com/windmill-labs/windmill/commit/99313116509a5ebec33aa2ab813e0c189d4c4e24))
|
||||
|
||||
## [1.514.1](https://github.com/windmill-labs/windmill/compare/v1.514.0...v1.514.1) (2025-07-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* pin tokio to 1.46.1 and aws-sdks-ts ([1a85dc7](https://github.com/windmill-labs/windmill/commit/1a85dc7008d77cb4d51c488b4ba02a02cd08b05d))
|
||||
|
||||
## [1.514.0](https://github.com/windmill-labs/windmill/compare/v1.513.1...v1.514.0) (2025-07-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* migrate audit log ids to bigints (blocking migration for EE) ([6e8f5a5](https://github.com/windmill-labs/windmill/commit/6e8f5a5b00056a324fa4fc27b740290338e672c9))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix id renaming in apps ([a999bc7](https://github.com/windmill-labs/windmill/commit/a999bc7b28764e3583fdd843f13060c760627b51))
|
||||
* **mcp:** add proper check for mcp routes ([#6282](https://github.com/windmill-labs/windmill/issues/6282)) ([f892f7c](https://github.com/windmill-labs/windmill/commit/f892f7c9af1058b899c880160bd91d5e7b3ebcd6))
|
||||
|
||||
## [1.513.1](https://github.com/windmill-labs/windmill/compare/v1.513.0...v1.513.1) (2025-07-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve error handler behavior wrt to parrallel branchall & forloops ([#6273](https://github.com/windmill-labs/windmill/issues/6273)) ([3f784e3](https://github.com/windmill-labs/windmill/commit/3f784e3c07aa35ef2298bed301d581bf25300ba6))
|
||||
* jobLoader for flowstatusviewerinner work with public apps ([94e20a9](https://github.com/windmill-labs/windmill/commit/94e20a97e036fdb99e34c79b466e957b943cb26a))
|
||||
* **mcp:** allow mcp scopes to run scripts and flows ([#6278](https://github.com/windmill-labs/windmill/issues/6278)) ([e8f4c24](https://github.com/windmill-labs/windmill/commit/e8f4c2418b83af75be90b21ab2ab64537fef00eb))
|
||||
|
||||
## [1.513.0](https://github.com/windmill-labs/windmill/compare/v1.512.0...v1.513.0) (2025-07-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* enable workspace exclusion in custom tags ([#6263](https://github.com/windmill-labs/windmill/issues/6263)) ([be3173d](https://github.com/windmill-labs/windmill/commit/be3173d048f8c3ee3681840b9880f509b754d779))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add windir env var to pwsh for legacy modules like AD ([#6271](https://github.com/windmill-labs/windmill/issues/6271)) ([7920583](https://github.com/windmill-labs/windmill/commit/79205839b6c3ba51a637f634d8cfba737b4dee1c))
|
||||
* fix webhook panel reactivity ([241be48](https://github.com/windmill-labs/windmill/commit/241be4897eb3377ccce097404fcd176e746c3015))
|
||||
* improve aggrid columnDefs reactivity ([f71104e](https://github.com/windmill-labs/windmill/commit/f71104e9f64bf6cb340d99ad91977c79651780c8))
|
||||
|
||||
## [1.512.0](https://github.com/windmill-labs/windmill/compare/v1.511.0...v1.512.0) (2025-07-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* local type references parsing support for main function args ([#5995](https://github.com/windmill-labs/windmill/issues/5995)) ([10befb9](https://github.com/windmill-labs/windmill/commit/10befb995d07f50803213bcf177df8891fd920e1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix decision tree graph editor ([a57df1f](https://github.com/windmill-labs/windmill/commit/a57df1f78119b80ce764ddbff0ecb989cbb4c315))
|
||||
* include export from ts relative import tracking ([91849ba](https://github.com/windmill-labs/windmill/commit/91849baf3404c308f946f7c384ce679f2c89cbe7))
|
||||
* run autoscaling scripts with superadmin permissions ([86e14f8](https://github.com/windmill-labs/windmill/commit/86e14f8135eb8ccfad47f0340986464983019153))
|
||||
|
||||
## [1.511.0](https://github.com/windmill-labs/windmill/compare/v1.510.1...v1.511.0) (2025-07-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **aichat:** give completions to autocomplete for other languages than ts ([#6253](https://github.com/windmill-labs/windmill/issues/6253)) ([71b85df](https://github.com/windmill-labs/windmill/commit/71b85dfc42694f552a0572afd2072bbe528ac74b))
|
||||
* **aichat:** improve autocomplete with ata, editor diagnostic and deletion cues ([#6245](https://github.com/windmill-labs/windmill/issues/6245)) ([dc242c5](https://github.com/windmill-labs/windmill/commit/dc242c5a8df7d5d2ad710f3a5578dcb5fb4c9667))
|
||||
* periodic worker group script ([#6159](https://github.com/windmill-labs/windmill/issues/6159)) ([c4178c0](https://github.com/windmill-labs/windmill/commit/c4178c05be5cff4ef0617e31e0f05adc29e04f89))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add error name and message to job_postprocessing span ([34ccc8e](https://github.com/windmill-labs/windmill/commit/34ccc8e0f7487cf51370e0ff94546063ee7771ad))
|
||||
* scopes-run-and-webhook-token-display ([#6259](https://github.com/windmill-labs/windmill/issues/6259)) ([37b18f0](https://github.com/windmill-labs/windmill/commit/37b18f0661d797466423db021150c99398435c8b))
|
||||
|
||||
## [1.510.1](https://github.com/windmill-labs/windmill/compare/v1.510.0...v1.510.1) (2025-07-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* empty branches in branch one now return the previous_result ([8323707](https://github.com/windmill-labs/windmill/commit/8323707b07933eca81f0c30df2434c6409c26789))
|
||||
* improve ssh agent worker naming ([#6211](https://github.com/windmill-labs/windmill/issues/6211)) ([dd1f520](https://github.com/windmill-labs/windmill/commit/dd1f520d83d618dbf1d03855890011031da838b5))
|
||||
* in home, archived and include without main not taken into account ([f4dbc97](https://github.com/windmill-labs/windmill/commit/f4dbc97a58ea8742dc356bdad4c03bbaa7b369b7))
|
||||
* quickbooks oauth realmId ([#6232](https://github.com/windmill-labs/windmill/issues/6232)) ([be8c4ed](https://github.com/windmill-labs/windmill/commit/be8c4ed86d81013e575441f30e5a444cb4067db7))
|
||||
* update parsers to prevent assets var bug ([#6246](https://github.com/windmill-labs/windmill/issues/6246)) ([d2328e3](https://github.com/windmill-labs/windmill/commit/d2328e3670c73a5c4ce7b677339f9293604fd583))
|
||||
|
||||
## [1.510.0](https://github.com/windmill-labs/windmill/compare/v1.509.2...v1.510.0) (2025-07-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* use sse for flow status updates ([dec72e2](https://github.com/windmill-labs/windmill/commit/dec72e201bcc8242eb269826505d6338c9751f14))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* prevent loading script by hash if not permissioned ([9201391](https://github.com/windmill-labs/windmill/commit/9201391102e927b943958851ccb75bb9695833a5))
|
||||
|
||||
## [1.509.2](https://github.com/windmill-labs/windmill/compare/v1.509.1...v1.509.2) (2025-07-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add back asset kind variable ([affecda](https://github.com/windmill-labs/windmill/commit/affecdad04e95e1e1fd26bd6bcdd0ec6e3c7a28e))
|
||||
|
||||
## [1.509.1](https://github.com/windmill-labs/windmill/compare/v1.509.0...v1.509.1) (2025-07-19)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n c.id IS NOT NULL AS completed,\n CASE \n WHEN q.id IS NOT NULL THEN (CASE WHEN NOT $5 AND q.running THEN true ELSE null END)\n ELSE false\n END AS running,\n SUBSTR(logs, GREATEST($1 - log_offset, 0)) AS logs,\n COALESCE(r.memory_peak, c.memory_peak) AS mem_peak,\n CASE\n -- flow step:\n WHEN flow_step_id IS NOT NULL THEN NULL\n -- completed:\n WHEN c.id IS NOT NULL THEN COALESCE(\n c.workflow_as_code_status || c.flow_status,\n c.workflow_as_code_status,\n c.flow_status\n )\n -- not completed:\n ELSE COALESCE(\n f.workflow_as_code_status || f.flow_status,\n f.workflow_as_code_status,\n f.flow_status\n )\n END AS \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n job_logs.log_offset + CHAR_LENGTH(job_logs.logs) + 1 AS log_offset,\n created_by AS \"created_by!\",\n CASE WHEN $4::BOOLEAN THEN (\n SELECT scalar_int FROM job_stats WHERE job_id = $3 AND metric_id = 'progress_perc'\n ) END AS progress\n FROM v2_job j\n LEFT JOIN v2_job_queue q USING (id)\n LEFT JOIN v2_job_runtime r USING (id)\n LEFT JOIN v2_job_status f USING (id)\n LEFT JOIN v2_job_completed c USING (id)\n LEFT JOIN job_logs ON job_logs.job_id = $3\n WHERE j.workspace_id = $2 AND j.id = $3\n AND ($6::text[] IS NULL OR j.tag = ANY($6))",
|
||||
"query": "SELECT\n c.id IS NOT NULL AS completed,\n CASE \n WHEN q.id IS NOT NULL THEN (CASE WHEN NOT $5 AND q.running THEN true ELSE null END)\n ELSE false\n END AS running,\n SUBSTR(logs, GREATEST($1 - log_offset, 0)) AS logs,\n COALESCE(r.memory_peak, c.memory_peak) AS mem_peak,\n COALESCE(c.flow_status, f.flow_status) AS \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n COALESCE(c.workflow_as_code_status, f.workflow_as_code_status) AS \"workflow_as_code_status: sqlx::types::Json<Box<RawValue>>\",\n job_logs.log_offset + CHAR_LENGTH(job_logs.logs) + 1 AS log_offset,\n created_by AS \"created_by!\",\n CASE WHEN $4::BOOLEAN THEN (\n SELECT scalar_int FROM job_stats WHERE job_id = $3 AND metric_id = 'progress_perc'\n ) END AS progress\n FROM v2_job j\n LEFT JOIN v2_job_queue q USING (id)\n LEFT JOIN v2_job_runtime r USING (id)\n LEFT JOIN v2_job_status f USING (id)\n LEFT JOIN v2_job_completed c USING (id)\n LEFT JOIN job_logs ON job_logs.job_id = $3\n WHERE j.workspace_id = $2 AND j.id = $3\n AND ($6::text[] IS NULL OR j.tag = ANY($6))",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -30,16 +30,21 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "workflow_as_code_status: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "log_offset",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "created_by!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "progress",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
@@ -61,9 +66,10 @@
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "28258e41e95c7c86af0bc3b7469df614a9b71bc372b7994802edac93e6c2ada6"
|
||||
"hash": "278bc6b4f149f824b5db32dacfaa714ee3852dc2ddf2d661dfdd5a986a9bb62b"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]",
|
||||
"query": "\n SELECT label,\n concat(substring(token for 10)) AS token_prefix,\n expiration,\n created_at,\n last_used_at,\n scopes,\n email\n FROM token\n WHERE workspace_id = $1\n AND (\n scopes @> ARRAY['jobs:run:flows:' || $2]::text[]\n OR scopes @> ARRAY['run:flows/' || $2]::text[]\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -55,5 +55,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c7ee7ce64686cef41cebd99ad7ef31572fc1bf12e6ae473fd58fafb025989965"
|
||||
"hash": "334c9a6d770471ac53099eb5227778899a5c680b899bc6a6d1a3f89fdbf8c730"
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n tag, \n script_lang AS \"script_lang!: _\"\n FROM \n v2_job\n WHERE \n id = $1\n ",
|
||||
"query": "\n SELECT \n script_lang as \"language: _\", \n COUNT(*) as \"count!\", \n SUM(duration_ms)::BIGINT as \"total_duration!\" \n FROM \n v2_job_completed\n JOIN \n v2_job \n ON \n v2_job.id = v2_job_completed.id \n WHERE \n created_at > NOW() - INTERVAL '48 hours' \n GROUP BY \n script_lang\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "script_lang!: _",
|
||||
"name": "language: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "script_lang",
|
||||
@@ -42,17 +37,26 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "count!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "total_duration!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
true,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "4e5273b9ce05f6ee2dfd5f14c8574a0cf43682480452f7dbe23012320fe7fe25"
|
||||
"hash": "48242fc46cb8bc9e4d41c788568634619565e4ef342051af23760c11edac5ee8"
|
||||
}
|
||||
22
backend/.sqlx/query-49e2430af74ec10857e5df7f7e1ad1b53ba70bb51b0259a1f765f76db9b733ad.json
generated
Normal file
22
backend/.sqlx/query-49e2430af74ec10857e5df7f7e1ad1b53ba70bb51b0259a1f765f76db9b733ad.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n tag\n FROM \n v2_job\n WHERE \n id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "49e2430af74ec10857e5df7f7e1ad1b53ba70bb51b0259a1f765f76db9b733ad"
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"s3object",
|
||||
"resource"
|
||||
"resource",
|
||||
"variable"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO account (workspace_id, client, expires_at, refresh_token, grant_type, cc_client_id, cc_client_secret) VALUES ($1, $2, now() + ($3 || ' seconds')::interval, $4, $5, $6, $7) RETURNING id",
|
||||
"query": "INSERT INTO account (workspace_id, client, expires_at, refresh_token, grant_type, cc_client_id, cc_client_secret, cc_token_url) VALUES ($1, $2, now() + ($3 || ' seconds')::interval, $4, $5, $6, $7, $8) RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -17,6 +17,7 @@
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
@@ -24,5 +25,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "00418f2e621cbe7aed536cff357898f7250544dfcb3bd85d314af1058f3148d8"
|
||||
"hash": "bbc28b92ae8ec3d120a8976be7d3966282fe6543e0eb957fc10864dbf58de58f"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT client, refresh_token, grant_type, cc_client_id, cc_client_secret FROM account WHERE workspace_id = $1 AND id = $2",
|
||||
"query": "SELECT client, refresh_token, grant_type, cc_client_id, cc_client_secret, cc_token_url FROM account WHERE workspace_id = $1 AND id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -27,6 +27,11 @@
|
||||
"ordinal": 4,
|
||||
"name": "cc_client_secret",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "cc_token_url",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -40,8 +45,9 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b9d57959f2696f74e9d24f55fb78ff9e9041ab84b896a8b980df7b550ef27a22"
|
||||
"hash": "cc269052ffc1e613d7edc31f0f7bb84f6e6301ad1afb028813105a121a69fa7e"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
|
||||
"query": "\n SELECT label,\n concat(substring(token for 10)) AS token_prefix,\n expiration,\n created_at,\n last_used_at,\n scopes,\n email\n FROM token\n WHERE workspace_id = $1\n AND (\n scopes @> ARRAY['jobs:run:scripts:' || $2]::text[]\n OR scopes @> ARRAY['run:scripts/' || $2]::text[]\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -55,5 +55,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "eff32aeac25a75d06f73e08c26dd3fd25f6b85cbea870505751c6a82457ae1da"
|
||||
"hash": "e8b74683f994d198a051c2f211a2844241bf2a7d7702c569017ae53fca706c59"
|
||||
}
|
||||
207
backend/Cargo.lock
generated
207
backend/Cargo.lock
generated
@@ -746,9 +746,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "aws-config"
|
||||
version = "1.8.2"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebd9b83179adf8998576317ce47785948bcff399ec5b15f4dfbdedd44ddf5b92"
|
||||
checksum = "c0baa720ebadea158c5bda642ac444a2af0cdf7bb66b46d1e4533de5d1f449d0"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -788,9 +788,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.13.2"
|
||||
version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08b5d4e069cbc868041a64bd68dc8cb39a0d79585cd6c5a24caa8c2d622121be"
|
||||
checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
@@ -835,9 +835,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sqs"
|
||||
version = "1.76.0"
|
||||
version = "1.77.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "837b4b181a1306a5a67d85218fa7d7298e340041d9bd81ae6919b9ec4f86ca97"
|
||||
checksum = "32a6dcd63dffae1f7872df42ce9f1f0f785f8c9a3d3e880ef6c171d0415439df"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -857,9 +857,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sso"
|
||||
version = "1.76.0"
|
||||
version = "1.77.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64bf26698dd6d238ef1486bdda46f22a589dc813368ba868dc3d94c8d27b56ba"
|
||||
checksum = "18f2f37fea82468fe3f5a059542c05392ef680c4f7f00e0db02df8b6e5c7d0c6"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -879,9 +879,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-ssooidc"
|
||||
version = "1.77.0"
|
||||
version = "1.78.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cd07ed1edd939fae854a22054299ae3576500f4e0fadc560ca44f9c6ea1664"
|
||||
checksum = "ecb4f6eada20e0193450cd48b12ed05e1e66baac86f39160191651b932f2b7d9"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -901,9 +901,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sts"
|
||||
version = "1.78.0"
|
||||
version = "1.79.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37f7766d2344f56d10d12f3c32993da36d78217f32594fe4fb8e57a538c1cdea"
|
||||
checksum = "317377afba3498fca4948c5d32b399ef9a5ad35561a1e8a6f2ac7273dabf802d"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1034,9 +1034,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime"
|
||||
version = "1.8.4"
|
||||
version = "1.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3aaec682eb189e43c8a19c3dab2fe54590ad5f2cc2d26ab27608a20f2acf81c"
|
||||
checksum = "660f70d9d8af6876b4c9aa8dcb0dbaf0f89b04ee9a4455bea1b4ba03b15f26f6"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
@@ -1058,9 +1058,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime-api"
|
||||
version = "1.8.3"
|
||||
version = "1.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9852b9226cb60b78ce9369022c0df678af1cac231c882d5da97a0c4e03be6e67"
|
||||
checksum = "937a49ecf061895fca4a6dd8e864208ed9be7546c0527d04bc07d502ec5fba1c"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-types",
|
||||
@@ -1120,9 +1120,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-types"
|
||||
version = "1.3.7"
|
||||
version = "1.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a322fec39e4df22777ed3ad8ea868ac2f94cd15e1a55f6ee8d8d6305057689a"
|
||||
checksum = "b069d19bf01e46298eaedd7c6f283fe565a59263e53eebec945f3e6398f42390"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-async",
|
||||
@@ -1689,9 +1689,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck_derive"
|
||||
version = "1.9.3"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1"
|
||||
checksum = "441473f2b4b0459a68628c744bc61d23e730fb00128b841d30fa4bb3972257e4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3737,7 +3737,7 @@ dependencies = [
|
||||
"quinn",
|
||||
"rustls-tokio-stream",
|
||||
"serde",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
]
|
||||
@@ -4674,7 +4674,7 @@ dependencies = [
|
||||
"num-integer",
|
||||
"rust_decimal",
|
||||
"smallvec",
|
||||
"strum 0.27.1",
|
||||
"strum 0.27.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4685,9 +4685,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.19"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-iter"
|
||||
@@ -6377,7 +6377,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -6498,9 +6498,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.15"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df"
|
||||
checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -6514,7 +6514,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"socket2 0.6.0",
|
||||
"system-configuration 0.6.1",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
@@ -6820,9 +6820,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.8"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
|
||||
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"cfg-if",
|
||||
@@ -6835,7 +6835,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
|
||||
dependencies = [
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"widestring",
|
||||
"windows-sys 0.48.0",
|
||||
"winreg",
|
||||
@@ -6969,9 +6969,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jsonc-parser"
|
||||
version = "0.26.2"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b558af6b49fd918e970471374e7a798b2c9bbcda624a210ffa3901ee5614bc8e"
|
||||
checksum = "1d6d80e6d70e7911a29f3cf3f44f452df85d06f73572b494ca99a2cad3fcf8f4"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
@@ -7270,7 +7270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.53.2",
|
||||
"windows-targets 0.53.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7292,13 +7292,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.4"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638"
|
||||
checksum = "360e552c93fa0e8152ab463bc4c4837fce76a225df11dfaeea66c313de5e61f7"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"libc",
|
||||
"redox_syscall 0.5.13",
|
||||
"redox_syscall 0.5.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7380,9 +7380,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
|
||||
checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@@ -7960,7 +7960,7 @@ dependencies = [
|
||||
"rand 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
@@ -9056,7 +9056,7 @@ checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall 0.5.13",
|
||||
"redox_syscall 0.5.17",
|
||||
"smallvec",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
@@ -9507,9 +9507,9 @@ checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.35"
|
||||
version = "0.2.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a"
|
||||
checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.104",
|
||||
@@ -9841,9 +9841,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quick_cache"
|
||||
version = "0.6.14"
|
||||
version = "0.6.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b450dad8382b1b95061d5ca1eb792081fb082adf48c678791fe917509596d5f"
|
||||
checksum = "8565e62e02af316570d4b492f17af1481d6c07cea60f4e7edd71700da5052ba9"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"equivalent",
|
||||
@@ -9864,7 +9864,7 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls 0.23.29",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -9901,7 +9901,7 @@ dependencies = [
|
||||
"cfg_aliases 0.2.1",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
@@ -10156,9 +10156,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.13"
|
||||
version = "0.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
|
||||
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
]
|
||||
@@ -10663,9 +10663,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.25"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
|
||||
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
@@ -10847,7 +10847,7 @@ checksum = "22557157d7395bc30727745b365d923f1ecc230c4c80b176545f3f4f08c46e33"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"rustls 0.23.29",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -11696,6 +11696,16 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socks"
|
||||
version = "0.3.4"
|
||||
@@ -12129,11 +12139,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.1"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
|
||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||
dependencies = [
|
||||
"strum_macros 0.27.1",
|
||||
"strum_macros 0.27.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12151,14 +12161,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.27.1"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
|
||||
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
@@ -13207,7 +13216,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -13275,7 +13284,7 @@ dependencies = [
|
||||
"postgres-protocol 0.6.7",
|
||||
"postgres-types 0.2.7",
|
||||
"rand 0.8.5",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"whoami",
|
||||
@@ -13301,7 +13310,7 @@ dependencies = [
|
||||
"postgres-protocol 0.6.8",
|
||||
"postgres-types 0.2.9",
|
||||
"rand 0.9.0",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"whoami",
|
||||
@@ -13507,7 +13516,7 @@ dependencies = [
|
||||
"prost",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tokio-stream",
|
||||
@@ -14632,7 +14641,7 @@ version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7"
|
||||
dependencies = [
|
||||
"redox_syscall 0.5.13",
|
||||
"redox_syscall 0.5.17",
|
||||
"wasite",
|
||||
"web-sys",
|
||||
]
|
||||
@@ -14676,7 +14685,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -14705,7 +14714,7 @@ dependencies = [
|
||||
"sha2 0.10.9",
|
||||
"size",
|
||||
"sqlx",
|
||||
"strum 0.27.1",
|
||||
"strum 0.27.2",
|
||||
"systemstat",
|
||||
"tikv-jemalloc-ctl",
|
||||
"tikv-jemalloc-sys",
|
||||
@@ -14728,7 +14737,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -14739,8 +14748,11 @@ dependencies = [
|
||||
"async_zip",
|
||||
"aws-config",
|
||||
"aws-sdk-sqs",
|
||||
"aws-sdk-sso",
|
||||
"aws-sdk-ssooidc",
|
||||
"aws-sdk-sts",
|
||||
"axum",
|
||||
"backon",
|
||||
"base32",
|
||||
"base64 0.22.1",
|
||||
"byteorder",
|
||||
@@ -14794,6 +14806,7 @@ dependencies = [
|
||||
"rumqttc",
|
||||
"rust-embed",
|
||||
"rust_decimal",
|
||||
"rustls 0.23.29",
|
||||
"samael",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -14839,7 +14852,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -14854,7 +14867,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
@@ -14867,7 +14880,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -14881,7 +14894,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -14932,8 +14945,8 @@ dependencies = [
|
||||
"sha2 0.10.9",
|
||||
"size",
|
||||
"sqlx",
|
||||
"strum 0.27.1",
|
||||
"strum_macros 0.27.1",
|
||||
"strum 0.27.2",
|
||||
"strum_macros 0.27.2",
|
||||
"sysinfo",
|
||||
"systemstat",
|
||||
"tar",
|
||||
@@ -14960,7 +14973,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -14974,12 +14987,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"const_format",
|
||||
"flume",
|
||||
"futures",
|
||||
"lazy_static",
|
||||
"object_store",
|
||||
@@ -14997,7 +15011,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -15009,7 +15023,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -15018,7 +15032,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15030,7 +15044,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15042,7 +15056,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -15054,7 +15068,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15066,7 +15080,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15078,7 +15092,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -15089,7 +15103,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15100,7 +15114,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15112,7 +15126,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15135,7 +15149,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -15152,7 +15166,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15166,7 +15180,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15184,7 +15198,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.16",
|
||||
@@ -15208,7 +15222,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15218,7 +15232,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15251,7 +15265,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -15261,7 +15275,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15597,7 +15611,7 @@ version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.2",
|
||||
"windows-targets 0.53.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -15633,10 +15647,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.2"
|
||||
version = "0.53.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
|
||||
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm 0.53.0",
|
||||
"windows_aarch64_msvc 0.53.0",
|
||||
"windows_i686_gnu 0.53.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -28,11 +28,11 @@ members = [
|
||||
"./parsers/windmill-parser-py",
|
||||
"./parsers/windmill-parser-py-imports",
|
||||
"./parsers/windmill-sql-datatype-parser-wasm",
|
||||
"./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu",
|
||||
"./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu"
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.509.1"
|
||||
version = "1.515.1"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -190,12 +190,11 @@ windmill-api-client = { path = "./windmill-api-client" }
|
||||
reqwest-retry = "^0"
|
||||
reqwest-middleware = { version = "^0", features = ["json"] }
|
||||
|
||||
rustls = "0.23.0"
|
||||
memchr = "2.7.4"
|
||||
axum = { version = "^0.7", features = ["multipart"] }
|
||||
headers = "^0"
|
||||
hyper = { version = "^1", features = ["full"] }
|
||||
tokio = { version = "^1.42.0", features = ["full", "tracing", "time"] }
|
||||
tokio = { version = "=1.46.1", features = ["full", "tracing", "time"] }
|
||||
tokio-stream = { version = "0.1.17" }
|
||||
tower = "^0"
|
||||
tower-http = { version = "^0.6", features = ["trace", "cors"] }
|
||||
@@ -354,8 +353,14 @@ datafusion = "47.0.0"
|
||||
object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7", features = ["aws", "azure", "gcp"] }
|
||||
openidconnect = { version = "4.0.0-rc.1" }
|
||||
aws-config = "^1"
|
||||
aws-sdk-sqs = "1.57.0"
|
||||
aws-sdk-sts = "^1"
|
||||
aws-sdk-sqs = "=1.77.0"
|
||||
aws-sdk-sts = "=1.79.0"
|
||||
aws-sdk-sso = "=1.77.0"
|
||||
aws-sdk-ssooidc = "=1.78.0"
|
||||
rustls = "=0.23.29"
|
||||
|
||||
|
||||
|
||||
aws-smithy-types-convert = { version = "^0", features = ["convert-chrono"] }
|
||||
crc = "^3"
|
||||
tar = "^0"
|
||||
|
||||
@@ -1 +1 @@
|
||||
e2bbcda64a9eb783d0832bc6f764be1627046961
|
||||
cef7486dfa765f1cdfaddba59564c4bb92fe864e
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Rollback cc_token_url column addition
|
||||
ALTER TABLE account DROP COLUMN IF EXISTS cc_token_url;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add cc_token_url column for resource-level token URL override in OAuth client credentials flow
|
||||
-- This allows resources to override the token URL from instance settings when using client credentials
|
||||
ALTER TABLE account ADD COLUMN cc_token_url VARCHAR(500);
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TYPE ASSET_KIND ADD VALUE 'variable';
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE resource ALTER COLUMN created_by TYPE VARCHAR(500);
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE audit ALTER COLUMN id TYPE bigint;
|
||||
ALTER SEQUENCE audit_id_seq AS bigint;
|
||||
@@ -7,7 +7,7 @@ use anyhow::anyhow;
|
||||
use tree_sitter::Node;
|
||||
use windmill_parser::Arg;
|
||||
use windmill_parser::MainArgSignature;
|
||||
use windmill_parser::Typ;
|
||||
use windmill_parser::{ObjectType, Typ};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CsharpMainSigMeta {
|
||||
@@ -112,7 +112,7 @@ fn find_typ<'a>(typ_node: Node<'a>, code: &str) -> anyhow::Result<Typ> {
|
||||
Ok("double") | Ok("System.Double") => Ok(Typ::Float),
|
||||
Ok("bool") | Ok("System.Boolean") => Ok(Typ::Bool),
|
||||
Ok("decimal") | Ok("System.Decimal") => Ok(Typ::Float),
|
||||
Ok("object") => Ok(Typ::Object(vec![])), // TODO: Complete the object type
|
||||
Ok("object") => Ok(Typ::Object(ObjectType::new(None, Some(vec![])))), // TODO: Complete the object type
|
||||
Ok(s) => Err(anyhow!("Unknown type `{s}`")),
|
||||
Err(e) => Err(anyhow!("Error getting type name: {}", e)),
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use gosyn::{
|
||||
use itertools::Itertools;
|
||||
|
||||
use regex::Regex;
|
||||
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, Typ};
|
||||
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, ObjectType, Typ};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref REQUIRE_PARSE: Regex = Regex::new(r"//require (.*)\n").unwrap();
|
||||
@@ -142,13 +142,13 @@ fn parse_go_typ(typ: &Expression) -> (Option<String>, Typ) {
|
||||
"struct {{ {} }}",
|
||||
otyps.iter().join("; ").to_string()
|
||||
)),
|
||||
Typ::Object(typs),
|
||||
Typ::Object(ObjectType::new(None, Some(typs))),
|
||||
)
|
||||
}
|
||||
Expression::TypeInterface(_) => (Some("interface{}".to_string()), Typ::Object(vec![])),
|
||||
Expression::TypeInterface(_) => (Some("interface{}".to_string()), Typ::Object(ObjectType::new(None, Some(vec![])))),
|
||||
Expression::TypeMap(_) => (
|
||||
Some("map[string]interface{}".to_string()),
|
||||
Typ::Object(vec![]),
|
||||
Typ::Object(ObjectType::new(None, Some(vec![]))),
|
||||
),
|
||||
_ => (None, Typ::Unknown),
|
||||
}
|
||||
@@ -218,10 +218,10 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam
|
||||
Arg {
|
||||
otyp: Some("struct { Name string `json:\"name\"` }".to_string()),
|
||||
name: "o".to_string(),
|
||||
typ: Typ::Object(vec![ObjectProperty {
|
||||
typ: Typ::Object(ObjectType::new(None, Some(vec![ObjectProperty {
|
||||
key: "name".to_string(),
|
||||
typ: Box::new(Typ::Str(None))
|
||||
},]),
|
||||
},]))),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None
|
||||
@@ -229,7 +229,7 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam
|
||||
Arg {
|
||||
otyp: Some("interface{}".to_string()),
|
||||
name: "n".to_string(),
|
||||
typ: Typ::Object(vec![]),
|
||||
typ: Typ::Object(ObjectType::new(None, Some(vec![]))),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None
|
||||
@@ -237,7 +237,7 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam
|
||||
Arg {
|
||||
otyp: Some("map[string]interface{}".to_string()),
|
||||
name: "m".to_string(),
|
||||
typ: Typ::Object(vec![]),
|
||||
typ: Typ::Object(ObjectType::new(None, Some(vec![]))),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None
|
||||
|
||||
@@ -9,7 +9,7 @@ use regex_lite::Regex;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use windmill_parser::{Arg, MainArgSignature, Typ};
|
||||
use windmill_parser::{Arg, MainArgSignature, ObjectType, Typ};
|
||||
|
||||
pub fn parse_graphql_sig(code: &str) -> anyhow::Result<MainArgSignature> {
|
||||
let parsed = parse_graphql_file(&code)?;
|
||||
@@ -75,7 +75,7 @@ pub fn parse_graphql_typ(typ: &str) -> Typ {
|
||||
"Int" => Typ::Int,
|
||||
"Boolean" => Typ::Bool,
|
||||
"Float" => Typ::Float,
|
||||
_ => Typ::Object(vec![]),
|
||||
_ => Typ::Object(ObjectType::new(None, Some(vec![]))),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use serde_json::Value;
|
||||
use tree_sitter::Node;
|
||||
use windmill_parser::Arg;
|
||||
use windmill_parser::MainArgSignature;
|
||||
use windmill_parser::Typ;
|
||||
use windmill_parser::{ObjectType, Typ};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JavaMainSigMeta {
|
||||
@@ -101,7 +101,7 @@ fn find_typ<'a>(typ_node: Node<'a>, code: &str) -> anyhow::Result<(Typ, Option<V
|
||||
Ok("Double") => (Typ::Float, null),
|
||||
Ok("Boolean") => (Typ::Bool, null),
|
||||
Ok("Character") => (Typ::Str(None), null),
|
||||
Ok("Object") => (Typ::Object(vec![]),null), // TODO: Complete the object type
|
||||
Ok("Object") => (Typ::Object(ObjectType::new(None, Some(vec![]))),null), // TODO: Complete the object type
|
||||
Ok(s) => bail!("Unknown type `{s}`"),
|
||||
Err(e) => bail!("Error getting type name: {}", e),
|
||||
}
|
||||
@@ -394,7 +394,7 @@ class Main {
|
||||
Arg {
|
||||
name: "i".into(),
|
||||
otyp: Some("Object".into()),
|
||||
typ: Typ::Object(vec![]),
|
||||
typ: Typ::Object(ObjectType::new(None, Some(vec![]))),
|
||||
default: Some(json!(null)),
|
||||
has_default: true,
|
||||
oidx: None
|
||||
@@ -427,7 +427,7 @@ class Main {
|
||||
Arg {
|
||||
name: "b".into(),
|
||||
otyp: Some("Object[]".into()),
|
||||
typ: Typ::List(Box::new(Typ::Object(vec![]))),
|
||||
typ: Typ::List(Box::new(Typ::Object(ObjectType::new(None, Some(vec![]))))),
|
||||
default: Some(json!(null)),
|
||||
has_default: true,
|
||||
oidx: None
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::{anyhow, bail};
|
||||
use nu_parser::lex;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use windmill_parser::{Arg, MainArgSignature, Typ};
|
||||
use windmill_parser::{Arg, MainArgSignature, ObjectType, Typ};
|
||||
|
||||
pub fn parse_nu_signature(code: &str) -> anyhow::Result<MainArgSignature> {
|
||||
let (tokens, ..) = lex(code.as_bytes(), 0, &[], &[], true);
|
||||
@@ -162,8 +162,8 @@ pub fn parse_nu_signature(code: &str) -> anyhow::Result<MainArgSignature> {
|
||||
"int" => Typ::Int,
|
||||
"float" => Typ::Float,
|
||||
"number" => Typ::Float,
|
||||
"record" => Typ::Object(vec![]),
|
||||
"table" => Typ::List(Box::new(Typ::Object(vec![]))),
|
||||
"record" => Typ::Object(ObjectType::new(None, Some(vec![]))),
|
||||
"table" => Typ::List(Box::new(Typ::Object(ObjectType::new(None, Some(vec![]))))),
|
||||
"nothing" => Typ::Unknown,
|
||||
// TODO: needs additional work on literal parsing
|
||||
// "binary" => Typ::Bytes,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use serde_json::json;
|
||||
use windmill_parser::{Arg, MainArgSignature, Typ};
|
||||
use windmill_parser::{Arg, MainArgSignature, ObjectType, Typ};
|
||||
use windmill_parser_nu::parse_nu_signature;
|
||||
|
||||
#[test]
|
||||
@@ -200,7 +200,7 @@ mod test {
|
||||
Arg {
|
||||
name: "a7".into(),
|
||||
otyp: None,
|
||||
typ: Typ::Object(vec![]),
|
||||
typ: Typ::Object(ObjectType::new(None, Some(vec![]))),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None
|
||||
@@ -216,7 +216,7 @@ mod test {
|
||||
Arg {
|
||||
name: "a9".into(),
|
||||
otyp: None,
|
||||
typ: Typ::List(Box::new(Typ::Object(vec![]))),
|
||||
typ: Typ::List(Box::new(Typ::Object(ObjectType::new(None, Some(vec![]))))),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use serde_json::Value;
|
||||
use windmill_parser::{to_snake_case, Arg, MainArgSignature, Typ};
|
||||
use windmill_parser::{to_snake_case, Arg, MainArgSignature, ObjectType, Typ};
|
||||
|
||||
use php_parser_rs::parser::{
|
||||
self,
|
||||
@@ -18,7 +18,7 @@ fn parse_php_type(e: Type) -> Typ {
|
||||
Type::Integer(_) => Typ::Int,
|
||||
Type::String(_) => Typ::Str(None),
|
||||
Type::Array(_) => Typ::List(Box::new(Typ::Str(None))),
|
||||
Type::Object(_) => Typ::Object(vec![]),
|
||||
Type::Object(_) => Typ::Object(ObjectType::new(None, Some(vec![]))),
|
||||
Type::Named(_, name) => Typ::Resource(to_snake_case(name.to_string().as_ref())),
|
||||
_ => Typ::Unknown,
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::collections::HashMap;
|
||||
use itertools::Itertools;
|
||||
|
||||
use serde_json::json;
|
||||
use windmill_parser::{json_to_typ, Arg, MainArgSignature, Typ};
|
||||
use windmill_parser::{json_to_typ, Arg, MainArgSignature, ObjectType, Typ};
|
||||
|
||||
use rustpython_parser::{
|
||||
ast::{
|
||||
@@ -226,7 +226,7 @@ fn parse_typ(id: &str) -> Typ {
|
||||
"float" => Typ::Float,
|
||||
"int" => Typ::Int,
|
||||
"bool" => Typ::Bool,
|
||||
"dict" => Typ::Object(vec![]),
|
||||
"dict" => Typ::Object(ObjectType::new(None, Some(vec![]))),
|
||||
"list" => Typ::List(Box::new(Typ::Unknown)),
|
||||
"bytes" => Typ::Bytes,
|
||||
"datetime" => Typ::Datetime,
|
||||
|
||||
@@ -14,7 +14,7 @@ use std::{
|
||||
iter::Peekable,
|
||||
str::CharIndices,
|
||||
};
|
||||
pub use windmill_parser::{Arg, MainArgSignature, Typ};
|
||||
pub use windmill_parser::{Arg, MainArgSignature, ObjectType, Typ};
|
||||
|
||||
pub const SANITIZED_ENUM_STR: &str = "__sanitized_enum__";
|
||||
pub const SANITIZED_RAW_STRING_STR: &str = "__sanitized_raw_string__";
|
||||
@@ -231,7 +231,7 @@ lazy_static::lazy_static! {
|
||||
static ref RE_ARG_BIGQUERY: Regex = Regex::new(r#"(?m)^-- @(\w+) \((\w+(?:\[\])?)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
|
||||
|
||||
// -- $name (type) = default
|
||||
static ref RE_ARG_DUCKDB: Regex = Regex::new(r#"(?m)^-- \$(\w+) \((\w+)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
|
||||
static ref RE_ARG_DUCKDB: Regex = Regex::new(r#"(?m)^-- \$(\w+) \(([A-Za-z0-9_\[\]]+)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
|
||||
|
||||
static ref RE_ARG_SNOWFLAKE: Regex = Regex::new(r#"(?m)^-- \? (\w+) \((\w+)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
|
||||
|
||||
@@ -737,7 +737,7 @@ pub fn parse_pg_typ(typ: &str) -> Typ {
|
||||
"bigint" => Typ::Int,
|
||||
"bool" | "boolean" => Typ::Bool,
|
||||
"char" | "character" => Typ::Str(None),
|
||||
"json" | "jsonb" => Typ::Object(vec![]),
|
||||
"json" | "jsonb" => Typ::Object(ObjectType::new(None, Some(vec![]))),
|
||||
"smallint" | "int2" => Typ::Int,
|
||||
"smallserial" | "serial2" => Typ::Int,
|
||||
"serial" | "serial4" => Typ::Int,
|
||||
@@ -769,7 +769,7 @@ pub fn parse_bigquery_typ(typ: &str) -> Typ {
|
||||
match typ {
|
||||
"string" => Typ::Str(None),
|
||||
"bytes" => Typ::Bytes,
|
||||
"json" => Typ::Object(vec![]),
|
||||
"json" => Typ::Object(ObjectType::new(None, Some(vec![]))),
|
||||
"timestamp" | "date" | "time" | "datetime" => Typ::Datetime,
|
||||
"integer" | "int64" => Typ::Int,
|
||||
"float" | "float64" | "numeric" | "bignumeric" => Typ::Float,
|
||||
|
||||
@@ -7,19 +7,20 @@
|
||||
*/
|
||||
// use deno_core::{serde_v8, v8, JsRuntime, RuntimeOptions};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
|
||||
use windmill_parser::{
|
||||
json_to_typ, to_snake_case, Arg, MainArgSignature, ObjectProperty, OneOfVariant, Typ,
|
||||
json_to_typ, to_snake_case, Arg, MainArgSignature, ObjectProperty, ObjectType, OneOfVariant,
|
||||
Typ,
|
||||
};
|
||||
|
||||
use swc_common::{sync::Lrc, FileName, SourceMap, SourceMapper, Span, Spanned};
|
||||
use swc_ecma_ast::{
|
||||
ArrayLit, AssignPat, BigInt, BindingIdent, Bool, Decl, ExportDecl, Expr, FnDecl, Ident,
|
||||
IdentName, Lit, MemberExpr, MemberProp, ModuleDecl, ModuleItem, Number, ObjectLit, ObjectPat,
|
||||
Param, Pat, Str, TsArrayType, TsEntityName, TsKeywordType, TsKeywordTypeKind, TsLit, TsLitType,
|
||||
TsOptionalType, TsParenthesizedType, TsPropertySignature, TsType, TsTypeAnn, TsTypeElement,
|
||||
TsTypeLit, TsTypeRef, TsUnionOrIntersectionType, TsUnionType,
|
||||
ArrayLit, AssignPat, BigInt, BindingIdent, Bool, Decl, ExportDecl, Expr, Ident, IdentName, Lit,
|
||||
MemberExpr, MemberProp, ModuleDecl, ModuleItem, Number, ObjectLit, ObjectPat, Param, Pat, Stmt,
|
||||
Str, TsArrayType, TsEntityName, TsInterfaceDecl, TsKeywordType, TsKeywordTypeKind, TsLit,
|
||||
TsLitType, TsOptionalType, TsParenthesizedType, TsPropertySignature, TsType, TsTypeAliasDecl,
|
||||
TsTypeAnn, TsTypeElement, TsTypeLit, TsTypeRef, TsUnionOrIntersectionType, TsUnionType,
|
||||
};
|
||||
use swc_ecma_parser::{lexer::Lexer, EsSyntax, Parser, StringInput, Syntax, TsSyntax};
|
||||
|
||||
@@ -32,6 +33,19 @@ struct ImportsFinder {
|
||||
skip_type_only: bool,
|
||||
}
|
||||
|
||||
impl ImportsFinder {
|
||||
fn process_raw(&mut self, raw: Option<String>) {
|
||||
if let Some(ref s) = raw {
|
||||
let s = s.to_string();
|
||||
if s.starts_with("'") && s.ends_with("'") {
|
||||
self.imports.insert(s[1..s.len() - 1].to_string());
|
||||
} else if s.starts_with("\"") && s.ends_with("\"") {
|
||||
self.imports.insert(s[1..s.len() - 1].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visit for ImportsFinder {
|
||||
noop_visit_type!();
|
||||
|
||||
@@ -59,14 +73,47 @@ impl Visit for ImportsFinder {
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(ref s) = n.src.raw {
|
||||
let s = s.to_string();
|
||||
if s.starts_with("'") && s.ends_with("'") {
|
||||
self.imports.insert(s[1..s.len() - 1].to_string());
|
||||
} else if s.starts_with("\"") && s.ends_with("\"") {
|
||||
self.imports.insert(s[1..s.len() - 1].to_string());
|
||||
self.process_raw(n.src.raw.as_ref().map(|x| x.to_string()));
|
||||
}
|
||||
|
||||
fn visit_export_all(&mut self, node: &swc_ecma_ast::ExportAll) {
|
||||
if !self.skip_type_only || node.type_only {
|
||||
return;
|
||||
}
|
||||
|
||||
self.process_raw(node.src.raw.as_ref().map(|x| x.to_string()));
|
||||
}
|
||||
fn visit_named_export(&mut self, node: &swc_ecma_ast::NamedExport) {
|
||||
if node.src.is_none() || !self.skip_type_only || node.type_only {
|
||||
return;
|
||||
}
|
||||
if node.specifiers.len() > 0 {
|
||||
let mut is_type_only = true;
|
||||
for specifier in node.specifiers.iter() {
|
||||
match specifier {
|
||||
swc_ecma_ast::ExportSpecifier::Named(swc_ecma_ast::ExportNamedSpecifier {
|
||||
is_type_only,
|
||||
..
|
||||
}) if *is_type_only => (),
|
||||
_ => {
|
||||
is_type_only = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if is_type_only {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.process_raw(
|
||||
node.src
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.raw
|
||||
.as_ref()
|
||||
.map(|x| x.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +206,11 @@ pub fn parse_expr_for_ids(code: &str) -> anyhow::Result<Vec<(String, String)>> {
|
||||
Ok(visitor.idents.into_iter().collect())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TypeDecl {
|
||||
Interface(TsInterfaceDecl),
|
||||
Alias(TsTypeAliasDecl),
|
||||
}
|
||||
pub mod asset_parser;
|
||||
pub use asset_parser::parse_assets;
|
||||
|
||||
@@ -168,7 +220,7 @@ pub fn parse_deno_signature(
|
||||
code: &str,
|
||||
skip_dflt: bool,
|
||||
skip_params: bool,
|
||||
main_override: Option<String>,
|
||||
entrypoint_override: Option<String>,
|
||||
) -> anyhow::Result<MainArgSignature> {
|
||||
let cm: Lrc<SourceMap> = Default::default();
|
||||
let fm = cm.new_source_file(FileName::Custom("main.ts".into()).into(), code.into());
|
||||
@@ -188,6 +240,9 @@ pub fn parse_deno_signature(
|
||||
err_s += &e.into_kind().msg().to_string();
|
||||
}
|
||||
|
||||
let mut has_preprocessor = false;
|
||||
let mut entrypoint_params = None;
|
||||
|
||||
let ast = parser
|
||||
.parse_module()
|
||||
.map_err(|e| {
|
||||
@@ -195,35 +250,71 @@ pub fn parse_deno_signature(
|
||||
})?
|
||||
.body;
|
||||
|
||||
let has_preprocessor = ast.iter().any(|x| match x {
|
||||
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
|
||||
decl: Decl::Fn(FnDecl { ident: Ident { sym, .. }, .. }),
|
||||
..
|
||||
})) => &sym.to_string() == "preprocessor",
|
||||
_ => false,
|
||||
});
|
||||
let entrypoint_function = entrypoint_override.as_deref().unwrap_or("main");
|
||||
|
||||
let main_name = main_override.unwrap_or("main".to_string());
|
||||
let params = ast.into_iter().find_map(|x| match x {
|
||||
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
|
||||
decl: Decl::Fn(FnDecl { ident: Ident { sym, .. }, function, .. }),
|
||||
..
|
||||
})) if &sym.to_string() == &main_name => Some(function.params),
|
||||
_ => None,
|
||||
});
|
||||
let mut symbol_table: HashMap<String, TypeDecl> = HashMap::new();
|
||||
|
||||
for item in ast {
|
||||
if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, .. }))
|
||||
| ModuleItem::Stmt(Stmt::Decl(decl)) = item
|
||||
{
|
||||
match decl {
|
||||
Decl::TsInterface(mut iface) => match symbol_table.get_mut(iface.id.sym.as_str()) {
|
||||
Some(TypeDecl::Interface(interface)) => {
|
||||
interface.body.body.append(&mut iface.body.body);
|
||||
}
|
||||
None => {
|
||||
symbol_table.insert(
|
||||
to_snake_case(iface.id.sym.as_str()),
|
||||
TypeDecl::Interface(*iface),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Decl::TsTypeAlias(alias) => {
|
||||
symbol_table.insert(
|
||||
to_snake_case(alias.id.sym.as_str()),
|
||||
TypeDecl::Alias(*alias),
|
||||
);
|
||||
}
|
||||
Decl::Fn(fn_decl) => {
|
||||
let name = fn_decl.ident.sym.to_string();
|
||||
if name == "preprocessor" {
|
||||
has_preprocessor = true;
|
||||
}
|
||||
if name == entrypoint_function {
|
||||
entrypoint_params = Some(fn_decl.function.params.clone());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut c: u16 = 0;
|
||||
let no_main_func = params.is_none();
|
||||
|
||||
let no_main_func = entrypoint_params.is_none();
|
||||
let mut type_resolver = HashMap::new();
|
||||
let r = MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: if skip_params {
|
||||
vec![]
|
||||
} else {
|
||||
params
|
||||
.map(|x| {
|
||||
x.into_iter()
|
||||
.map(|x| parse_param(x, &cm, skip_dflt, &mut c))
|
||||
entrypoint_params
|
||||
.map(|param| {
|
||||
param
|
||||
.into_iter()
|
||||
.map(|param| {
|
||||
parse_param(
|
||||
&symbol_table,
|
||||
&mut type_resolver,
|
||||
param,
|
||||
&cm,
|
||||
skip_dflt,
|
||||
&mut c,
|
||||
)
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<Arg>>>()
|
||||
})
|
||||
.transpose()?
|
||||
@@ -236,14 +327,16 @@ pub fn parse_deno_signature(
|
||||
}
|
||||
|
||||
fn parse_param(
|
||||
x: Param,
|
||||
symbol_table: &HashMap<String, TypeDecl>,
|
||||
type_resolver: &mut HashMap<String, (Typ, bool)>,
|
||||
param: Param,
|
||||
cm: &Lrc<SourceMap>,
|
||||
skip_dflt: bool,
|
||||
counter: &mut u16,
|
||||
) -> anyhow::Result<Arg> {
|
||||
let r = match x.pat {
|
||||
let r = match param.pat {
|
||||
Pat::Ident(ident) => {
|
||||
let (name, typ, nullable) = binding_ident_to_arg(&ident);
|
||||
let (name, typ, nullable) = binding_ident_to_arg(symbol_table, type_resolver, &ident);
|
||||
Ok(Arg {
|
||||
otyp: None,
|
||||
name,
|
||||
@@ -256,9 +349,9 @@ fn parse_param(
|
||||
// Pat::Object(ObjectPat { ... }) = todo!()
|
||||
Pat::Assign(AssignPat { left, right, .. }) => {
|
||||
let (name, mut typ, _nullable) = match *left {
|
||||
Pat::Ident(ident) => binding_ident_to_arg(&ident),
|
||||
Pat::Ident(ident) => binding_ident_to_arg(symbol_table, type_resolver, &ident),
|
||||
Pat::Object(ObjectPat { type_ann, .. }) => {
|
||||
let (typ, nullable) = eval_type_ann(&type_ann);
|
||||
let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann);
|
||||
*counter += 1;
|
||||
let name = format!("anon{}", counter);
|
||||
(name, typ, nullable)
|
||||
@@ -300,16 +393,16 @@ fn parse_param(
|
||||
Ok(Arg { otyp: None, name, typ, default: dflt, has_default: true, oidx: None })
|
||||
}
|
||||
Pat::Object(ObjectPat { type_ann, .. }) => {
|
||||
let (typ, nullable) = eval_type_ann(&type_ann);
|
||||
let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann);
|
||||
*counter += 1;
|
||||
let name = format!("anon{}", counter);
|
||||
Ok(Arg { otyp: None, name, typ, default: None, has_default: nullable, oidx: None })
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"parameter syntax unsupported: `{}`: {:#?}",
|
||||
cm.span_to_snippet(x.span())
|
||||
.unwrap_or_else(|_| cm.span_to_string(x.span())),
|
||||
x.pat
|
||||
cm.span_to_snippet(param.span())
|
||||
.unwrap_or_else(|_| cm.span_to_string(param.span())),
|
||||
param.pat
|
||||
)),
|
||||
};
|
||||
r
|
||||
@@ -328,14 +421,22 @@ fn eval_span(span: Span, cm: &Lrc<SourceMap>) -> Option<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_type_ann(type_ann: &Option<Box<TsTypeAnn>>) -> (Typ, bool) {
|
||||
fn eval_type_ann(
|
||||
symbol_table: &HashMap<String, TypeDecl>,
|
||||
type_resolver: &mut HashMap<String, (Typ, bool)>,
|
||||
type_ann: &Option<Box<TsTypeAnn>>,
|
||||
) -> (Typ, bool) {
|
||||
return type_ann
|
||||
.as_ref()
|
||||
.map(|x| tstype_to_typ(&*x.type_ann))
|
||||
.map(|x| tstype_to_typ(symbol_table, type_resolver, &*x.type_ann, true))
|
||||
.unwrap_or((Typ::Unknown, false));
|
||||
}
|
||||
fn binding_ident_to_arg(BindingIdent { id, type_ann }: &BindingIdent) -> (String, Typ, bool) {
|
||||
let (typ, nullable) = eval_type_ann(type_ann);
|
||||
fn binding_ident_to_arg(
|
||||
symbol_table: &HashMap<String, TypeDecl>,
|
||||
type_resolver: &mut HashMap<String, (Typ, bool)>,
|
||||
BindingIdent { id, type_ann }: &BindingIdent,
|
||||
) -> (String, Typ, bool) {
|
||||
let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, type_ann);
|
||||
(id.sym.to_string(), typ, nullable)
|
||||
}
|
||||
|
||||
@@ -365,11 +466,193 @@ pub fn remove_pinned_imports(code: &str) -> anyhow::Result<String> {
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
|
||||
fn resolve_type_ref(type_resolver: &HashMap<String, (Typ, bool)>, typ: &mut Typ) {
|
||||
let mut visited = std::collections::HashSet::new();
|
||||
resolve_type_ref_with_visited(type_resolver, typ, &mut visited);
|
||||
}
|
||||
|
||||
fn resolve_type_ref_with_visited(
|
||||
type_resolver: &HashMap<String, (Typ, bool)>,
|
||||
typ: &mut Typ,
|
||||
visited: &mut std::collections::HashSet<String>,
|
||||
) {
|
||||
match typ {
|
||||
Typ::Object(ObjectType { props: Some(obj), .. }) => {
|
||||
for property in obj.iter_mut() {
|
||||
resolve_type_ref_with_visited(type_resolver, &mut property.typ, visited);
|
||||
}
|
||||
}
|
||||
Typ::List(list) => resolve_type_ref_with_visited(type_resolver, list, visited),
|
||||
Typ::Object(ObjectType { name: Some(name), props: None }) => {
|
||||
if visited.contains(name) {
|
||||
return;
|
||||
}
|
||||
let maybe_resolved_type = type_resolver
|
||||
.get(name)
|
||||
.map(|rs_typ| {
|
||||
let mut typ = rs_typ.0.clone();
|
||||
visited.insert(name.clone());
|
||||
resolve_type_ref_with_visited(type_resolver, &mut typ, visited);
|
||||
visited.remove(name);
|
||||
typ
|
||||
})
|
||||
.unwrap_or(Typ::Object(ObjectType::new(Some(name.to_owned()), None)));
|
||||
|
||||
*typ = maybe_resolved_type;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_interface(
|
||||
iface: &TsInterfaceDecl,
|
||||
symbol_table: &HashMap<String, TypeDecl>,
|
||||
type_resolver: &mut HashMap<String, (Typ, bool)>,
|
||||
) -> Vec<ObjectProperty> {
|
||||
let mut properties = vec![];
|
||||
|
||||
for ext in &iface.extends {
|
||||
// If the current interface extends other interfaces,
|
||||
// retrieve their properties first and add them to the current interface's object definition.
|
||||
if let Expr::Ident(Ident { sym, .. }) = &*ext.expr {
|
||||
if let Some(TypeDecl::Interface(parent_iface)) =
|
||||
symbol_table.get(&to_snake_case(sym.as_str()))
|
||||
{
|
||||
properties.extend(resolve_interface(parent_iface, symbol_table, type_resolver));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for member in &iface.body.body {
|
||||
if let TsTypeElement::TsPropertySignature(sig) = member {
|
||||
if let Expr::Ident(Ident { sym, .. }) = &*sig.key {
|
||||
let typ = sig
|
||||
.type_ann
|
||||
.as_ref()
|
||||
.map(|ta| {
|
||||
Box::new(tstype_to_typ(symbol_table, type_resolver, &ta.type_ann, false).0)
|
||||
})
|
||||
.unwrap_or(Box::new(Typ::Unknown));
|
||||
|
||||
properties.push(ObjectProperty { key: sym.to_string(), typ });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
properties
|
||||
}
|
||||
|
||||
fn resolve_type_alias(
|
||||
alias: &TsTypeAliasDecl,
|
||||
symbol_table: &HashMap<String, TypeDecl>,
|
||||
type_resolver: &mut HashMap<String, (Typ, bool)>,
|
||||
top_level_call: bool,
|
||||
) -> (Typ, bool) {
|
||||
tstype_to_typ(symbol_table, type_resolver, &alias.type_ann, top_level_call)
|
||||
}
|
||||
|
||||
fn resolve_ts_interface_and_type_alias(
|
||||
type_name: &str,
|
||||
symbol_table: &HashMap<String, TypeDecl>,
|
||||
type_resolver: &mut HashMap<String, (Typ, bool)>,
|
||||
top_level_call: bool,
|
||||
) -> Option<(Typ, bool)> {
|
||||
let Some(type_declaration) = symbol_table.get(type_name) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some(resolved_type) = type_resolver.get_mut(type_name) {
|
||||
return Some(resolved_type.to_owned());
|
||||
}
|
||||
|
||||
type_resolver.insert(
|
||||
type_name.to_owned(),
|
||||
(
|
||||
Typ::Object(ObjectType::new(Some(type_name.to_owned()), None)),
|
||||
false,
|
||||
),
|
||||
);
|
||||
|
||||
let mut resolved_type = match type_declaration {
|
||||
TypeDecl::Alias(alias) => {
|
||||
resolve_type_alias(alias, symbol_table, type_resolver, top_level_call)
|
||||
}
|
||||
TypeDecl::Interface(iface) => (
|
||||
Typ::Object(ObjectType::new(
|
||||
Some(to_snake_case(&type_name)),
|
||||
Some(resolve_interface(iface, symbol_table, type_resolver)),
|
||||
)),
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
fn update_name(typ: &mut Typ, type_name: &str) {
|
||||
match typ {
|
||||
// OBJECT TYPES:
|
||||
// Normally, when you declare something like:
|
||||
// type Postgresql = object
|
||||
// it will initially resolve to:
|
||||
// Typ::Object(ObjectType { name: None, .. })
|
||||
//
|
||||
// If the object has no name (name == None), we assign `type_name`
|
||||
// so it becomes:
|
||||
// Typ::Object(ObjectType { name: Some("postgresql"), .. })
|
||||
Typ::Object(ObjectType { name, .. }) if name.is_none() => {
|
||||
*name = Some(type_name.to_owned());
|
||||
}
|
||||
|
||||
// RESOURCE TYPES:
|
||||
// In some cases, when a type alias points to something unresolved:
|
||||
// type Postgresql = Foo
|
||||
// might initially resolve to:
|
||||
// Typ::Resource("Foo")
|
||||
//
|
||||
// This ensures backwards compatibility by updating that resource
|
||||
// name to the alias name, so it becomes:
|
||||
// Typ::Resource("postgresql")
|
||||
Typ::Resource(name) if name != type_name => {
|
||||
*name = type_name.to_owned();
|
||||
}
|
||||
|
||||
// LIST TYPES:
|
||||
// For lists, e.g. `type PostgresqlList = [object]`,
|
||||
// we recursively call `update_name` on the inner type,
|
||||
// so the same renaming logic applies inside the list.
|
||||
Typ::List(list) => {
|
||||
update_name(list, type_name);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
update_name(&mut resolved_type.0, type_name);
|
||||
|
||||
type_resolver.insert(type_name.to_owned(), resolved_type.clone());
|
||||
|
||||
// `top_level_call` indicates whether the current invocation of the function
|
||||
// is at the topmost level (e.g., the immediate parameters of the main function).
|
||||
// When true:
|
||||
// - Type references within object properties (e.g., nested interfaces) are recursively resolved
|
||||
// up to a default depth to inline and fully materialize their structure.
|
||||
if top_level_call {
|
||||
resolve_type_ref(type_resolver, &mut resolved_type.0);
|
||||
}
|
||||
|
||||
Some(resolved_type)
|
||||
}
|
||||
|
||||
fn tstype_to_typ(
|
||||
symbol_table: &HashMap<String, TypeDecl>,
|
||||
type_resolver: &mut HashMap<String, (Typ, bool)>,
|
||||
ts_type: &TsType,
|
||||
top_level_call: bool,
|
||||
) -> (Typ, bool) {
|
||||
match ts_type {
|
||||
TsType::TsKeywordType(t) => (
|
||||
match t.kind {
|
||||
TsKeywordTypeKind::TsObjectKeyword => Typ::Object(vec![]),
|
||||
TsKeywordTypeKind::TsObjectKeyword => {
|
||||
Typ::Object(ObjectType::new(None, Some(vec![])))
|
||||
}
|
||||
TsKeywordTypeKind::TsBooleanKeyword => Typ::Bool,
|
||||
TsKeywordTypeKind::TsBigIntKeyword => Typ::Int,
|
||||
TsKeywordTypeKind::TsNumberKeyword => Typ::Float,
|
||||
@@ -391,7 +674,17 @@ fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
|
||||
key: sym.to_string(),
|
||||
typ: type_ann
|
||||
.as_ref()
|
||||
.map(|typ| Box::new(tstype_to_typ(&*typ.type_ann).0))
|
||||
.map(|typ| {
|
||||
Box::new(
|
||||
tstype_to_typ(
|
||||
symbol_table,
|
||||
type_resolver,
|
||||
&*typ.type_ann,
|
||||
top_level_call,
|
||||
)
|
||||
.0,
|
||||
)
|
||||
})
|
||||
.unwrap_or(Box::new(Typ::Unknown)),
|
||||
}),
|
||||
_ => None,
|
||||
@@ -399,21 +692,25 @@ fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
(Typ::Object(properties), false)
|
||||
(Typ::Object(ObjectType::new(None, Some(properties))), false)
|
||||
}
|
||||
TsType::TsParenthesizedType(TsParenthesizedType { type_ann, .. }) => {
|
||||
tstype_to_typ(type_ann)
|
||||
tstype_to_typ(symbol_table, type_resolver, type_ann, top_level_call)
|
||||
}
|
||||
// TODO: we can do better here and extract the inner type of array
|
||||
TsType::TsArrayType(TsArrayType { elem_type, .. }) => {
|
||||
(Typ::List(Box::new(tstype_to_typ(&**elem_type).0)), false)
|
||||
}
|
||||
TsType::TsArrayType(TsArrayType { elem_type, .. }) => (
|
||||
Typ::List(Box::new(
|
||||
tstype_to_typ(symbol_table, type_resolver, &**elem_type, top_level_call).0,
|
||||
)),
|
||||
false,
|
||||
),
|
||||
TsType::TsLitType(TsLitType { lit: TsLit::Str(Str { value, .. }), .. }) => {
|
||||
(Typ::Str(Some(vec![value.to_string()])), false)
|
||||
}
|
||||
TsType::TsOptionalType(TsOptionalType { type_ann, .. }) => {
|
||||
(tstype_to_typ(type_ann).0, true)
|
||||
}
|
||||
TsType::TsOptionalType(TsOptionalType { type_ann, .. }) => (
|
||||
tstype_to_typ(symbol_table, type_resolver, type_ann, top_level_call).0,
|
||||
true,
|
||||
),
|
||||
TsType::TsUnionOrIntersectionType(TsUnionOrIntersectionType::TsUnionType(
|
||||
TsUnionType { types, .. },
|
||||
)) => {
|
||||
@@ -438,11 +735,18 @@ fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
|
||||
} else {
|
||||
0
|
||||
};
|
||||
(tstype_to_typ(&types[other_p]).0, true)
|
||||
(
|
||||
tstype_to_typ(symbol_table, type_resolver, &types[other_p], top_level_call).0,
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
if types.len() > 1 {
|
||||
let one_of_values: Vec<OneOfVariant> =
|
||||
types.into_iter().map_while(parse_one_of_type).collect();
|
||||
let one_of_values: Vec<OneOfVariant> = types
|
||||
.into_iter()
|
||||
.map_while(|t| {
|
||||
parse_one_of_type(symbol_table, type_resolver, t, top_level_call)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if one_of_values.len() == types.len() {
|
||||
return (Typ::OneOf(one_of_values), false);
|
||||
@@ -481,6 +785,7 @@ fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
|
||||
TsEntityName::Ident(Ident { sym, .. }) => sym,
|
||||
TsEntityName::TsQualifiedName(p) => &*p.right.sym,
|
||||
};
|
||||
|
||||
match sym.to_string().as_str() {
|
||||
"Resource" => (
|
||||
Typ::Resource(
|
||||
@@ -501,24 +806,68 @@ fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
|
||||
"Base64" => (Typ::Bytes, false),
|
||||
"Email" => (Typ::Email, false),
|
||||
"Sql" => (Typ::Sql, false),
|
||||
x @ _ if x.starts_with("DynSelect_") => (
|
||||
Typ::DynSelect(x.strip_prefix("DynSelect_").unwrap().to_string()),
|
||||
symbol @ _ if symbol.starts_with("DynSelect_") => (
|
||||
Typ::DynSelect(symbol.strip_prefix("DynSelect_").unwrap().to_string()),
|
||||
false,
|
||||
),
|
||||
x @ _ => (Typ::Resource(to_snake_case(x)), false),
|
||||
symbol @ _ => {
|
||||
let symbol = to_snake_case(symbol);
|
||||
resolve_ts_interface_and_type_alias(
|
||||
&symbol,
|
||||
symbol_table,
|
||||
type_resolver,
|
||||
top_level_call,
|
||||
)
|
||||
.unwrap_or_else(|| (Typ::Resource(symbol), false))
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (Typ::Unknown, false),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_one_of_type(x: &Box<TsType>) -> Option<OneOfVariant> {
|
||||
fn parse_one_of_type(
|
||||
symbol_table: &HashMap<String, TypeDecl>,
|
||||
type_resolver: &mut HashMap<String, (Typ, bool)>,
|
||||
x: &Box<TsType>,
|
||||
top_level_call: bool,
|
||||
) -> Option<OneOfVariant> {
|
||||
match &**x {
|
||||
TsType::TsTypeLit(TsTypeLit { members, .. }) => {
|
||||
let label = one_of_label(members)?;
|
||||
let properties = one_of_properties(members);
|
||||
let properties =
|
||||
one_of_properties(symbol_table, type_resolver, members, top_level_call);
|
||||
Some(OneOfVariant { label, properties })
|
||||
}
|
||||
TsType::TsTypeRef(TsTypeRef { type_name, .. }) => {
|
||||
let label = type_name.as_ident()?.sym.to_string();
|
||||
match label.as_str() {
|
||||
symbol
|
||||
if ["Resource", "Date", "Base64", "Email", "Sql"]
|
||||
.iter()
|
||||
.any(|s| *s == symbol)
|
||||
|| symbol.starts_with("DynSelect_") =>
|
||||
{
|
||||
return None
|
||||
}
|
||||
symbol @ _ => {
|
||||
let Typ::Object(ObjectType { props: Some(properties), .. }) =
|
||||
resolve_ts_interface_and_type_alias(
|
||||
symbol,
|
||||
symbol_table,
|
||||
type_resolver,
|
||||
top_level_call,
|
||||
)
|
||||
.unwrap_or_else(|| (Typ::Resource(to_snake_case(symbol)), false))
|
||||
.0
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(OneOfVariant { label, properties })
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -550,7 +899,12 @@ fn one_of_label(members: &Vec<TsTypeElement>) -> Option<String> {
|
||||
})
|
||||
}
|
||||
|
||||
fn one_of_properties(members: &Vec<TsTypeElement>) -> Vec<ObjectProperty> {
|
||||
fn one_of_properties(
|
||||
symbol_table: &HashMap<String, TypeDecl>,
|
||||
type_resolver: &mut HashMap<String, (Typ, bool)>,
|
||||
members: &Vec<TsTypeElement>,
|
||||
top_level_call: bool,
|
||||
) -> Vec<ObjectProperty> {
|
||||
members
|
||||
.iter()
|
||||
.filter_map(|x| {
|
||||
@@ -564,10 +918,12 @@ fn one_of_properties(members: &Vec<TsTypeElement>) -> Vec<ObjectProperty> {
|
||||
};
|
||||
let typ = type_ann
|
||||
.as_ref()
|
||||
.map(|typ| Box::new(tstype_to_typ(&*typ.type_ann).0))
|
||||
.unwrap_or(Box::new(Typ::Unknown));
|
||||
.map(|typ| {
|
||||
tstype_to_typ(symbol_table, type_resolver, &*typ.type_ann, top_level_call).0
|
||||
})
|
||||
.unwrap_or(Typ::Unknown);
|
||||
|
||||
Some(ObjectProperty { key: sym.to_string(), typ })
|
||||
Some(ObjectProperty { key: sym.to_string(), typ: Box::new(typ) })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use windmill_parser_ts::parse_expr_for_imports;
|
||||
use serde_json::json;
|
||||
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, ObjectType, Typ};
|
||||
use windmill_parser_ts::{parse_deno_signature, parse_expr_for_imports};
|
||||
|
||||
#[test]
|
||||
fn test_imports() {
|
||||
fn test_imports_basic() {
|
||||
let code = r#"
|
||||
import { foo } from "bar";
|
||||
import type { foo } from "bar2";
|
||||
@@ -12,8 +15,645 @@ mod tests {
|
||||
import { type foo, type bar } from "bar4";
|
||||
import * as foo from "bar5";
|
||||
import foo from "bar6";
|
||||
export * from "bar8";
|
||||
export { foo } from "bar9";
|
||||
export { type foo } from "bar10";
|
||||
export { foo, type bar } from "bar11";
|
||||
export { type foo, type bar } from "bar12";
|
||||
export { type foo, bar } from "bar13";
|
||||
export { bar, type foo } from "bar14";
|
||||
export type { foo } from "bar15";
|
||||
"#;
|
||||
let imports = parse_expr_for_imports(code, true).unwrap();
|
||||
assert_eq!(imports, vec!["bar", "bar3", "bar5", "bar6", "bar7"]);
|
||||
assert_eq!(
|
||||
imports,
|
||||
["bar", "bar11", "bar13", "bar14", "bar3", "bar5", "bar6", "bar7", "bar8", "bar9"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_empty_main_signature() {
|
||||
let code = r#"
|
||||
export async function main() {
|
||||
return "Hello World";
|
||||
}
|
||||
"#;
|
||||
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![],
|
||||
no_main_func: Some(false),
|
||||
has_preprocessor: Some(false),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_basic_types() {
|
||||
let code = r#"
|
||||
export async function main(
|
||||
str_param: string,
|
||||
num_param: number,
|
||||
bool_param: boolean,
|
||||
any_param: any
|
||||
) {
|
||||
return { str_param, num_param, bool_param, any_param };
|
||||
}
|
||||
"#;
|
||||
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![
|
||||
Arg {
|
||||
name: "str_param".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
},
|
||||
Arg {
|
||||
name: "num_param".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Float,
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
},
|
||||
Arg {
|
||||
name: "bool_param".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Bool,
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
},
|
||||
Arg {
|
||||
name: "any_param".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Unknown,
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
},
|
||||
],
|
||||
no_main_func: Some(false),
|
||||
has_preprocessor: Some(false),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_with_defaults() {
|
||||
let code = r#"
|
||||
export async function main(
|
||||
name: string = "World",
|
||||
count: number = 42,
|
||||
enabled: boolean = true
|
||||
) {
|
||||
return { name, count, enabled };
|
||||
}
|
||||
"#;
|
||||
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![
|
||||
Arg {
|
||||
name: "name".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Str(None),
|
||||
default: Some(json!("World")),
|
||||
has_default: true,
|
||||
oidx: None,
|
||||
},
|
||||
Arg {
|
||||
name: "count".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Float,
|
||||
default: Some(json!(42)),
|
||||
has_default: true,
|
||||
oidx: None,
|
||||
},
|
||||
Arg {
|
||||
name: "enabled".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Bool,
|
||||
default: Some(json!(true)),
|
||||
has_default: true,
|
||||
oidx: None,
|
||||
},
|
||||
],
|
||||
no_main_func: Some(false),
|
||||
has_preprocessor: Some(false),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_array_types() {
|
||||
let code = r#"
|
||||
export async function main(
|
||||
strings: string[],
|
||||
numbers: number[],
|
||||
items: any[]
|
||||
) {
|
||||
return { strings, numbers, items };
|
||||
}
|
||||
"#;
|
||||
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![
|
||||
Arg {
|
||||
name: "strings".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::List(Box::new(Typ::Str(None))),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
},
|
||||
Arg {
|
||||
name: "numbers".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::List(Box::new(Typ::Float)),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
},
|
||||
Arg {
|
||||
name: "items".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::List(Box::new(Typ::Unknown)),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
},
|
||||
],
|
||||
no_main_func: Some(false),
|
||||
has_preprocessor: Some(false),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_resource_types() {
|
||||
let code = r#"
|
||||
type Postgresql = Resource;
|
||||
|
||||
export async function main(
|
||||
database: Postgresql
|
||||
) {
|
||||
return { database };
|
||||
}
|
||||
"#;
|
||||
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![Arg {
|
||||
name: "database".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Resource("postgresql".to_string()),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
},],
|
||||
no_main_func: Some(false),
|
||||
has_preprocessor: Some(false),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_array_resource_types() {
|
||||
let code = r#"
|
||||
type Postgresql = Foo;
|
||||
|
||||
export async function main(
|
||||
database: Postgresql[]
|
||||
) {
|
||||
return { database };
|
||||
}
|
||||
"#;
|
||||
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![Arg {
|
||||
name: "database".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::List(Box::new(Typ::Resource("postgresql".to_string()))),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
},],
|
||||
no_main_func: Some(false),
|
||||
has_preprocessor: Some(false),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_interface_object() {
|
||||
let code = r#"
|
||||
interface UserConfig {
|
||||
name: string;
|
||||
age: number;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export async function main(config: UserConfig) {
|
||||
return config;
|
||||
}
|
||||
"#;
|
||||
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![Arg {
|
||||
name: "config".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Object(ObjectType::new(
|
||||
Some("user_config".to_string()),
|
||||
Some(vec![
|
||||
ObjectProperty::new("name".to_string(), Box::new(Typ::Str(None))),
|
||||
ObjectProperty::new("age".to_string(), Box::new(Typ::Float)),
|
||||
ObjectProperty::new("active".to_string(), Box::new(Typ::Bool)),
|
||||
])
|
||||
)),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
}],
|
||||
no_main_func: Some(false),
|
||||
has_preprocessor: Some(false),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_array_of_objects() {
|
||||
let code = r#"
|
||||
interface Item {
|
||||
id: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export async function main(items: Item[]) {
|
||||
return items;
|
||||
}
|
||||
"#;
|
||||
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![Arg {
|
||||
name: "items".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::List(Box::new(Typ::Object(ObjectType::new(
|
||||
Some("item".to_string()),
|
||||
Some(vec![
|
||||
ObjectProperty::new("id".to_string(), Box::new(Typ::Str(None))),
|
||||
ObjectProperty::new("value".to_string(), Box::new(Typ::Float)),
|
||||
])
|
||||
)))),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
}],
|
||||
no_main_func: Some(false),
|
||||
has_preprocessor: Some(false),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_nested_objects() {
|
||||
let code = r#"
|
||||
interface Address {
|
||||
street: string;
|
||||
city: string;
|
||||
}
|
||||
|
||||
interface Person {
|
||||
name: string;
|
||||
address: Address;
|
||||
}
|
||||
|
||||
export async function main(person: Person) {
|
||||
return person;
|
||||
}
|
||||
"#;
|
||||
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![Arg {
|
||||
name: "person".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Object(ObjectType::new(
|
||||
Some("person".to_string()),
|
||||
Some(vec![
|
||||
ObjectProperty::new("name".to_string(), Box::new(Typ::Str(None))),
|
||||
ObjectProperty::new(
|
||||
"address".to_string(),
|
||||
Box::new(Typ::Object(ObjectType::new(
|
||||
Some("address".to_string()),
|
||||
Some(vec![
|
||||
ObjectProperty::new(
|
||||
"street".to_string(),
|
||||
Box::new(Typ::Str(None))
|
||||
),
|
||||
ObjectProperty::new(
|
||||
"city".to_string(),
|
||||
Box::new(Typ::Str(None))
|
||||
),
|
||||
])
|
||||
)))
|
||||
),
|
||||
])
|
||||
)),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
}],
|
||||
no_main_func: Some(false),
|
||||
has_preprocessor: Some(false),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_builtin_types() {
|
||||
let code = r#"
|
||||
export async function main(
|
||||
date_param: Date,
|
||||
base64_param: Base64,
|
||||
email_param: Email,
|
||||
sql_param: Sql
|
||||
) {
|
||||
return { date_param, base64_param, email_param, sql_param };
|
||||
}
|
||||
"#;
|
||||
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![
|
||||
Arg {
|
||||
name: "date_param".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Datetime,
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
},
|
||||
Arg {
|
||||
name: "base64_param".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Bytes,
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
},
|
||||
Arg {
|
||||
name: "email_param".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Email,
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
},
|
||||
Arg {
|
||||
name: "sql_param".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Sql,
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
},
|
||||
],
|
||||
no_main_func: Some(false),
|
||||
has_preprocessor: Some(false),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_optional_parameters() {
|
||||
let code = r#"
|
||||
export async function main(
|
||||
required: string,
|
||||
optional?: number,
|
||||
with_default: boolean = false
|
||||
) {
|
||||
return { required, optional, with_default };
|
||||
}
|
||||
"#;
|
||||
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![
|
||||
Arg {
|
||||
name: "required".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
},
|
||||
Arg {
|
||||
name: "optional".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Float,
|
||||
default: None,
|
||||
has_default: true,
|
||||
oidx: None,
|
||||
},
|
||||
Arg {
|
||||
name: "with_default".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Bool,
|
||||
default: Some(json!(false)),
|
||||
has_default: true,
|
||||
oidx: None,
|
||||
},
|
||||
],
|
||||
no_main_func: Some(false),
|
||||
has_preprocessor: Some(false),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_complex_interface() {
|
||||
let code = r#"
|
||||
interface Config {
|
||||
timeout: number;
|
||||
retries: number;
|
||||
}
|
||||
|
||||
export async function main(
|
||||
config: Config,
|
||||
tags: string[]
|
||||
) {
|
||||
return { config, tags };
|
||||
}
|
||||
"#;
|
||||
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
||||
|
||||
assert_eq!(sig.args.len(), 2);
|
||||
assert_eq!(sig.args[0].name, "config");
|
||||
assert_eq!(sig.args[0].has_default, false);
|
||||
assert!(matches!(sig.args[0].typ, Typ::Object(_)));
|
||||
|
||||
assert_eq!(sig.args[1].name, "tags");
|
||||
assert_eq!(sig.args[1].has_default, false);
|
||||
assert!(matches!(sig.args[1].typ, Typ::List(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_no_main_function() {
|
||||
let code = r#"
|
||||
function helper() {
|
||||
return "helper";
|
||||
}
|
||||
|
||||
const value = 42;
|
||||
"#;
|
||||
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![],
|
||||
no_main_func: Some(true),
|
||||
has_preprocessor: Some(false),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_custom_entrypoint() {
|
||||
let code = r#"
|
||||
export async function customMain(param: string) {
|
||||
return param;
|
||||
}
|
||||
"#;
|
||||
let sig = parse_deno_signature(code, false, false, Some("customMain".to_string())).unwrap();
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![Arg {
|
||||
name: "param".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
}],
|
||||
no_main_func: Some(false),
|
||||
has_preprocessor: Some(false),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dyn_select() {
|
||||
let code = r#"
|
||||
export async function main(
|
||||
selector: DynSelect_my_options
|
||||
) {
|
||||
return selector;
|
||||
}
|
||||
"#;
|
||||
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![Arg {
|
||||
name: "selector".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::DynSelect("my_options".to_string()),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
}],
|
||||
no_main_func: Some(false),
|
||||
has_preprocessor: Some(false),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_invalid_typescript() {
|
||||
let code = r#"
|
||||
this is not valid typescript code {
|
||||
"#;
|
||||
let result = parse_deno_signature(code, false, false, None);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_with_preprocessor() {
|
||||
let code = r#"
|
||||
export async function preprocessor() {
|
||||
return "preprocessed";
|
||||
}
|
||||
|
||||
export async function main(param: string) {
|
||||
return param;
|
||||
}
|
||||
"#;
|
||||
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![Arg {
|
||||
name: "param".to_string(),
|
||||
otyp: None,
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None,
|
||||
}],
|
||||
no_main_func: Some(false),
|
||||
has_preprocessor: Some(true),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eou pipefail
|
||||
|
||||
#-# bun and deno
|
||||
OUT_DIR="../../../cli/wasm/ts"
|
||||
mkdir -p $OUT_DIR
|
||||
wasm-pack build --release --target deno --out-dir $OUT_DIR --features "ts-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
# sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-ts"/' $OUT_DIR/package.json
|
||||
rm $OUT_DIR/.gitignore
|
||||
|
||||
#-# sql languages, graphql and bash/powershell, since they all use regex
|
||||
OUT_DIR="../../../cli/wasm/regex"
|
||||
mkdir -p $OUT_DIR
|
||||
wasm-pack build --release --target deno --out-dir $OUT_DIR \
|
||||
--features "sql-parser,graphql-parser,bash-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
# sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-regex"/' $OUT_DIR/package.json
|
||||
rm $OUT_DIR/.gitignore
|
||||
|
||||
#-# python
|
||||
OUT_DIR="../../../cli/wasm/python"
|
||||
mkdir -p $OUT_DIR
|
||||
wasm-pack build --release --target deno --out-dir $OUT_DIR --features "py-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
# sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-py"/' $OUT_DIR/package.json
|
||||
rm $OUT_DIR/.gitignore
|
||||
|
||||
#-# go
|
||||
OUT_DIR="../../../cli/wasm/go"
|
||||
mkdir -p $OUT_DIR
|
||||
wasm-pack build --release --target deno --out-dir $OUT_DIR --features "go-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
# sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-go"/' $OUT_DIR/package.json
|
||||
rm $OUT_DIR/.gitignore
|
||||
|
||||
#-# php
|
||||
OUT_DIR="../../../cli/wasm/php"
|
||||
mkdir -p $OUT_DIR
|
||||
wasm-pack build --release --target deno --out-dir $OUT_DIR --features "php-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
# sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-php"/' $OUT_DIR/package.json
|
||||
rm $OUT_DIR/.gitignore
|
||||
|
||||
#-# rust
|
||||
OUT_DIR="../../../cli/wasm/rust"
|
||||
mkdir -p $OUT_DIR
|
||||
wasm-pack build --release --target deno --out-dir $OUT_DIR --features "rust-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
# sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-rust"/' $OUT_DIR/package.json
|
||||
rm $OUT_DIR/.gitignore
|
||||
|
||||
#-# ansible
|
||||
OUT_DIR="../../../cli/wasm/yaml"
|
||||
mkdir -p $OUT_DIR
|
||||
wasm-pack build --release --target deno --out-dir $OUT_DIR --features "ansible-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
# sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-yaml"/' $OUT_DIR/package.json
|
||||
rm $OUT_DIR/.gitignore
|
||||
|
||||
#-# C# (needs some more stuff to compile C tree sitter into wasm)
|
||||
OUT_DIR="../../../cli/wasm/csharp"
|
||||
mkdir -p $OUT_DIR
|
||||
CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target deno --out-dir $OUT_DIR --features "csharp-parser"
|
||||
# sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-csharp"/' $OUT_DIR/package.json
|
||||
rm $OUT_DIR/.gitignore
|
||||
|
||||
#-# Nu
|
||||
OUT_DIR="../../../cli/wasm/nu"
|
||||
mkdir -p $OUT_DIR
|
||||
wasm-pack build --release --target deno --out-dir $OUT_DIR --features "nu-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
rm $OUT_DIR/.gitignore
|
||||
|
||||
#-# Java
|
||||
OUT_DIR="../../../cli/wasm/java"
|
||||
mkdir -p $OUT_DIR
|
||||
CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target deno --out-dir $OUT_DIR --features "java-parser"
|
||||
rm $OUT_DIR/.gitignore
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eou pipefail
|
||||
|
||||
#-# full pkg
|
||||
OUT_DIR="pkg"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR --all-features \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
|
||||
#-# bun and deno
|
||||
OUT_DIR="pkg-ts"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR --features "ts-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-ts"/' $OUT_DIR/package.json
|
||||
|
||||
#-# sql languages, graphql and bash/powershell, since they all use regex
|
||||
OUT_DIR="pkg-regex"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR \
|
||||
--features "sql-parser,graphql-parser,bash-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-regex"/' $OUT_DIR/package.json
|
||||
|
||||
#-# python
|
||||
OUT_DIR="pkg-py"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR --features "py-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-py"/' $OUT_DIR/package.json
|
||||
|
||||
#-# go
|
||||
OUT_DIR="pkg-go"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR --features "go-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-go"/' $OUT_DIR/package.json
|
||||
|
||||
#-# php
|
||||
OUT_DIR="pkg-php"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR --features "php-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-php"/' $OUT_DIR/package.json
|
||||
|
||||
#-# rust
|
||||
OUT_DIR="pkg-rust"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR --features "rust-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-rust"/' $OUT_DIR/package.json
|
||||
|
||||
#-# ansible
|
||||
OUT_DIR="pkg-yaml"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR --features "ansible-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-yaml"/' $OUT_DIR/package.json
|
||||
|
||||
#-# C# (needs some more stuff to compile C tree sitter into wasm)
|
||||
# TODO: hasn't been tested on mac, might need fixing
|
||||
OUT_DIR="pkg-csharp"
|
||||
CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target web --out-dir $OUT_DIR --features "csharp-parser"
|
||||
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-csharp"/' $OUT_DIR/package.json
|
||||
|
||||
#-# nu
|
||||
# TODO: hasn't been tested on mac, might need fixing
|
||||
OUT_DIR="pkg-nu"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR --features "nu-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-nu"/' $OUT_DIR/package.json
|
||||
|
||||
#-# Java (needs some more stuff to compile C tree sitter into wasm)
|
||||
OUT_DIR="pkg-java"
|
||||
CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target web --out-dir $OUT_DIR --features "java-parser"
|
||||
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-java"/' $OUT_DIR/package.json
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eou pipefail
|
||||
|
||||
#-# full pkg
|
||||
OUT_DIR="pkg"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR --all-features \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
|
||||
#-# bun and deno
|
||||
OUT_DIR="pkg-ts"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR --features "ts-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-ts"/' $OUT_DIR/package.json
|
||||
|
||||
#-# sql languages, graphql and bash/powershell, since they all use regex
|
||||
OUT_DIR="pkg-regex"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR \
|
||||
--features "sql-parser,graphql-parser,bash-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-regex"/' $OUT_DIR/package.json
|
||||
|
||||
#-# python
|
||||
OUT_DIR="pkg-py"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR --features "py-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-py"/' $OUT_DIR/package.json
|
||||
|
||||
#-# go
|
||||
OUT_DIR="pkg-go"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR --features "go-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-go"/' $OUT_DIR/package.json
|
||||
|
||||
#-# php
|
||||
OUT_DIR="pkg-php"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR --features "php-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-php"/' $OUT_DIR/package.json
|
||||
|
||||
#-# rust
|
||||
OUT_DIR="pkg-rust"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR --features "rust-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-rust"/' $OUT_DIR/package.json
|
||||
|
||||
#-# ansible
|
||||
OUT_DIR="pkg-yaml"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR --features "ansible-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-yaml"/' $OUT_DIR/package.json
|
||||
|
||||
#-# C# (needs some more stuff to compile C tree sitter into wasm)
|
||||
OUT_DIR="pkg-csharp"
|
||||
CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target web --out-dir $OUT_DIR --features "csharp-parser"
|
||||
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-csharp"/' $OUT_DIR/package.json
|
||||
|
||||
#-# Nu
|
||||
OUT_DIR="pkg-nu"
|
||||
wasm-pack build --release --target web --out-dir $OUT_DIR --features "nu-parser" \
|
||||
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-nu"/' $OUT_DIR/package.json
|
||||
|
||||
#-# Java (needs some more stuff to compile C tree sitter into wasm)
|
||||
OUT_DIR="pkg-java"
|
||||
CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target web --out-dir $OUT_DIR --features "java-parser"
|
||||
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-java"/' $OUT_DIR/package.json
|
||||
132
backend/parsers/windmill-parser-wasm/build.nu
Executable file
132
backend/parsers/windmill-parser-wasm/build.nu
Executable file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env nu
|
||||
const targets = [
|
||||
{
|
||||
ident: "ts", # OUT_DIR inferred from this
|
||||
desc: "Bun and Deno"
|
||||
features: "ts-parser",
|
||||
env: "default", # available: default, tree-sitter
|
||||
}, {
|
||||
ident: "regex",
|
||||
desc: "Sql languages, Graphql and Bash/Powershell"
|
||||
features: "sql-parser,graphql-parser,bash-parser",
|
||||
env: "default",
|
||||
}, {
|
||||
ident: "py",
|
||||
desc: "Python",
|
||||
features: "py-parser",
|
||||
env: "default",
|
||||
}, {
|
||||
ident: "go",
|
||||
desc: "Go (Go-lang)",
|
||||
features: "go-parser",
|
||||
env: "default",
|
||||
}, {
|
||||
ident: "php",
|
||||
desc: "Php",
|
||||
features: "php-parser",
|
||||
env: "default",
|
||||
}, {
|
||||
ident: "rust",
|
||||
desc: "Rust",
|
||||
features: "rust-parser",
|
||||
env: "default",
|
||||
}, {
|
||||
ident: "yaml",
|
||||
desc: "Ansible",
|
||||
features: "ansible-parser",
|
||||
env: "default",
|
||||
}, {
|
||||
ident: "csharp",
|
||||
desc: "C#",
|
||||
features: "csharp-parser",
|
||||
env: "tree-sitter",
|
||||
}, {
|
||||
ident: "nu",
|
||||
desc: "Nu (Nushell)",
|
||||
features: "nu-parser",
|
||||
env: "tree-sitter",
|
||||
}, {
|
||||
ident: "java",
|
||||
desc: "Java",
|
||||
features: "java-parser",
|
||||
env: "tree-sitter",
|
||||
# }, {
|
||||
# ident: "ruby",
|
||||
# desc: "Ruby",
|
||||
# features: "ruby-parser",
|
||||
# env: "tree-sitter",
|
||||
},
|
||||
# ^^^ Add new entry here ^^^
|
||||
];
|
||||
# NOTE: This is legacy command for building all, but it is not more used
|
||||
# #-# full pkg
|
||||
# OUT_DIR="pkg"
|
||||
# wasm-pack build --release --target web --out-dir $OUT_DIR --all-features \
|
||||
# -Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
|
||||
# Build all separately
|
||||
def 'main all' [ --no-opt(-n), --cli] {
|
||||
if ($no_opt) {
|
||||
if ($cli) {
|
||||
$targets | each { main $in.ident -n --cli }
|
||||
} else {
|
||||
$targets | each { main $in.ident -n }
|
||||
}
|
||||
} else {
|
||||
if ($cli) {
|
||||
$targets | each { main $in.ident --cli }
|
||||
} else {
|
||||
$targets | each { main $in.ident }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Build specific language
|
||||
def main [
|
||||
target: string, # Language, e.g.: py, ts, rust
|
||||
--no-opt(-n), # Compile in debug mode for dev
|
||||
--cli # Compile and place binaries in cli.
|
||||
] {
|
||||
let t = $targets | where ident == $target;
|
||||
|
||||
if ($t | is-empty) {
|
||||
let pretty = $targets | each {|t| $"• ($t.ident) - ($t.desc)"} | str join "\n";
|
||||
panic $"Target '($target)' does not exist\n\nAvailable targets:\n($pretty)"
|
||||
} else {
|
||||
let t = $t | get 0;
|
||||
mut profile = "";
|
||||
mut tar = "";
|
||||
|
||||
if ($cli) {
|
||||
$env.OUT_DIR = $"../../../cli/wasm/($t.ident)"
|
||||
$tar = "deno"
|
||||
} else {
|
||||
$env.OUT_DIR = $"pkg-($t.ident)"
|
||||
$tar = "web"
|
||||
}
|
||||
if ($no_opt) {
|
||||
$profile = "--no-opt"
|
||||
} else {
|
||||
$profile = "--release"
|
||||
}
|
||||
print $"Building in ($profile) mode ($env.OUT_DIR)"
|
||||
match $t.env {
|
||||
"default" => {
|
||||
wasm-pack build ($profile) --target ($tar) --out-dir $env.OUT_DIR --features ($t.features) -Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
|
||||
},
|
||||
"tree-sitter" => {
|
||||
$env.CFLAGS_wasm32_unknown_unknown = $"-I(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin"
|
||||
$env.RUSTFLAGS = "-Zwasm-c-abi=spec"
|
||||
wasm-pack build ($profile) --target ($tar) --out-dir $env.OUT_DIR --features $t.features
|
||||
},
|
||||
_ => { panic $"Unknown env template: ($t.env)" },
|
||||
}
|
||||
|
||||
if ($cli) {
|
||||
rm $"($env.OUT_DIR)/.gitignore"
|
||||
} else {
|
||||
let p = $"($env.OUT_DIR)/package.json"
|
||||
open $p | update name $"windmill-parser-wasm-($t.ident)" | save -f $p
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,22 +4,9 @@
|
||||
# and perform installation to frontend
|
||||
def "main" [
|
||||
lang: string # Example: nu
|
||||
--release(-r)
|
||||
] {
|
||||
let out_dir = $'pkg-($lang)'
|
||||
if $release {
|
||||
open build-pkgs.sh
|
||||
| split row '#-'
|
||||
| find $out_dir
|
||||
| bash -c $"RUST_LOG=trace ($in.0)"
|
||||
} else {
|
||||
open build-pkgs.sh
|
||||
| split row '#-'
|
||||
| find $out_dir
|
||||
| str replace "--release" "--no-opt"
|
||||
| bash -c $"WASM_OPT=-Oz ($in.0)"
|
||||
}
|
||||
./build.nu $lang --no-opt
|
||||
(
|
||||
cd ../../../frontend; npm install ../backend/parsers/windmill-parser-wasm/($out_dir)
|
||||
cd ../../../frontend; npm install ../backend/parsers/windmill-parser-wasm/pkg-($lang)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,3 +30,6 @@ popd
|
||||
|
||||
pushd "pkg-nu" && npm publish ${args}
|
||||
popd
|
||||
|
||||
pushd "pkg-java" && npm publish ${args}
|
||||
popd
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use serde_json::json;
|
||||
use wasm_bindgen_test::wasm_bindgen_test;
|
||||
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, Typ};
|
||||
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, ObjectType, Typ};
|
||||
use windmill_parser_bash::parse_powershell_sig;
|
||||
use windmill_parser_ts::{parse_deno_signature, parse_expr_for_ids, parse_expr_for_imports};
|
||||
|
||||
@@ -115,10 +115,10 @@ export function main(test1?: string, test2: string = \"burkina\",
|
||||
Arg {
|
||||
otyp: None,
|
||||
name: "min_object".to_string(),
|
||||
typ: Typ::Object(vec![
|
||||
typ: Typ::Object(ObjectType::new(None, Some(vec![
|
||||
ObjectProperty { key: "a".to_string(), typ: Box::new(Typ::Str(None)) },
|
||||
ObjectProperty { key: "b".to_string(), typ: Box::new(Typ::Float) }
|
||||
]),
|
||||
]))),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None
|
||||
@@ -210,10 +210,10 @@ export function main(test2 = \"burkina\",
|
||||
Arg {
|
||||
otyp: None,
|
||||
name: "min_object".to_string(),
|
||||
typ: Typ::Object(vec![
|
||||
typ: Typ::Object(ObjectType::new(None, Some(vec![
|
||||
ObjectProperty { key: "a".to_string(), typ: Box::new(Typ::Str(None)) },
|
||||
ObjectProperty { key: "b".to_string(), typ: Box::new(Typ::Int) }
|
||||
]),
|
||||
]))),
|
||||
default: Some(json!({"a": "test", "b": 42})),
|
||||
has_default: true,
|
||||
oidx: None
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::HashMap;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use serde_json::json;
|
||||
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, Typ};
|
||||
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, ObjectType, Typ};
|
||||
use yaml_rust::{Yaml, YamlEmitter, YamlLoader};
|
||||
|
||||
pub fn parse_ansible_sig(inner_content: &str) -> anyhow::Result<MainArgSignature> {
|
||||
@@ -147,9 +147,9 @@ fn parse_ansible_typ(arg: &Yaml) -> Typ {
|
||||
})
|
||||
}
|
||||
}
|
||||
Typ::Object(prop_vec)
|
||||
Typ::Object(ObjectType::new(None, Some(prop_vec)))
|
||||
} else {
|
||||
Typ::Object(vec![])
|
||||
Typ::Object(ObjectType::new(None, Some(vec![])))
|
||||
}
|
||||
}
|
||||
"array" => {
|
||||
|
||||
@@ -28,6 +28,24 @@ pub struct ObjectProperty {
|
||||
pub typ: Box<Typ>,
|
||||
}
|
||||
|
||||
impl ObjectProperty {
|
||||
pub fn new(key: String, typ: Box<Typ>) -> ObjectProperty {
|
||||
ObjectProperty { key, typ }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug, PartialEq)]
|
||||
pub struct ObjectType {
|
||||
pub name: Option<String>,
|
||||
pub props: Option<Vec<ObjectProperty>>,
|
||||
}
|
||||
|
||||
impl ObjectType {
|
||||
pub fn new(name: Option<String>, props: Option<Vec<ObjectProperty>>) -> ObjectType {
|
||||
ObjectType { name, props }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug, PartialEq)]
|
||||
#[serde(rename_all(serialize = "lowercase"))]
|
||||
pub struct OneOfVariant {
|
||||
@@ -49,7 +67,7 @@ pub enum Typ {
|
||||
Email,
|
||||
Sql,
|
||||
DynSelect(String),
|
||||
Object(Vec<ObjectProperty>),
|
||||
Object(ObjectType),
|
||||
OneOf(Vec<OneOfVariant>),
|
||||
Unknown,
|
||||
}
|
||||
@@ -70,11 +88,14 @@ pub fn json_to_typ(js: &Value) -> Typ {
|
||||
Value::Number(n) if n.is_i64() => Typ::Int,
|
||||
Value::Number(_) => Typ::Float,
|
||||
Value::Bool(_) => Typ::Bool,
|
||||
Value::Object(o) => Typ::Object(
|
||||
o.iter()
|
||||
.map(|(k, v)| ObjectProperty { key: k.to_string(), typ: Box::new(json_to_typ(v)) })
|
||||
.collect(),
|
||||
),
|
||||
Value::Object(o) => Typ::Object(ObjectType::new(
|
||||
None,
|
||||
Some(
|
||||
o.iter()
|
||||
.map(|(k, v)| ObjectProperty { key: k.to_string(), typ: Box::new(json_to_typ(v)) })
|
||||
.collect(),
|
||||
),
|
||||
)),
|
||||
Value::Array(a) => Typ::List(Box::new(a.first().map(json_to_typ).unwrap_or(Typ::Unknown))),
|
||||
_ => Typ::Unknown,
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@ use windmill_common::{
|
||||
stats_oss::schedule_stats,
|
||||
triggers::TriggerKind,
|
||||
utils::{
|
||||
create_default_worker_suffix, create_ssh_agent_worker_suffix, worker_name_with_suffix,
|
||||
Mode, GIT_VERSION, HOSTNAME, MODE_AND_ADDONS,
|
||||
create_default_worker_suffix, worker_name_with_suffix, Mode, GIT_VERSION, HOSTNAME,
|
||||
MODE_AND_ADDONS,
|
||||
},
|
||||
worker::{
|
||||
reload_custom_tags_setting, Connection, HUB_CACHE_DIR, TMP_DIR, TMP_LOGS_DIR, WORKER_GROUP,
|
||||
@@ -365,7 +365,7 @@ async fn windmill_main() -> anyhow::Result<()> {
|
||||
"Creating http client for cluster using base internal url {}",
|
||||
std::env::var("BASE_INTERNAL_URL").unwrap_or_default()
|
||||
);
|
||||
let suffix = create_ssh_agent_worker_suffix(&hostname);
|
||||
let suffix = create_default_worker_suffix(&hostname);
|
||||
(
|
||||
Connection::Http(build_agent_http_client(&suffix)),
|
||||
Some(suffix),
|
||||
|
||||
@@ -65,7 +65,8 @@ use windmill_common::{
|
||||
users::truncate_token,
|
||||
utils::{empty_as_none, now_from_db, rd_string, report_critical_error, Mode},
|
||||
worker::{
|
||||
load_env_vars, load_init_bash_from_env, load_whitelist_env_vars_from_env,
|
||||
load_env_vars, load_init_bash_from_env, load_periodic_bash_script_from_env,
|
||||
load_periodic_bash_script_interval_from_env, load_whitelist_env_vars_from_env,
|
||||
load_worker_config, reload_custom_tags_setting, store_pull_query,
|
||||
store_suspended_pull_query, update_min_version, Connection, WorkerConfig,
|
||||
DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, INDEXER_CONFIG, SCRIPT_TOKEN_EXPIRY,
|
||||
@@ -213,6 +214,8 @@ pub async fn initial_load(
|
||||
priority_tags_sorted: vec![],
|
||||
dedicated_worker: None,
|
||||
init_bash: load_init_bash_from_env(),
|
||||
periodic_script_bash: load_periodic_bash_script_from_env(),
|
||||
periodic_script_interval_seconds: load_periodic_bash_script_interval_from_env(),
|
||||
cache_clear: None,
|
||||
additional_python_paths: None,
|
||||
pip_local_dependencies: None,
|
||||
@@ -1574,6 +1577,17 @@ pub async fn reload_worker_config(db: &DB, tx: KillpillSender, kill_if_change: b
|
||||
tracing::error!("Error cleaning the cache: {e:#}");
|
||||
}
|
||||
}
|
||||
|
||||
if (*wc).periodic_script_bash != config.periodic_script_bash {
|
||||
tracing::info!("Periodic script bash config changed, sending killpill. Expecting to be restarted by supervisor.");
|
||||
let _ = tx.send();
|
||||
}
|
||||
|
||||
if (*wc).periodic_script_interval_seconds != config.periodic_script_interval_seconds
|
||||
{
|
||||
tracing::info!("Periodic script interval config changed, sending killpill. Expecting to be restarted by supervisor.");
|
||||
let _ = tx.send();
|
||||
}
|
||||
}
|
||||
drop(wc);
|
||||
|
||||
@@ -1835,12 +1849,14 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
|
||||
);
|
||||
}
|
||||
|
||||
let jobs = sqlx::query_as::<_, QueuedJob>("SELECT * FROM v2_as_queue WHERE id = ANY($1)")
|
||||
.bind(&timeouts[..])
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.map_err(|e| tracing::error!("Error fetching same worker jobs: {:?}", e))
|
||||
.unwrap_or_default();
|
||||
let jobs = sqlx::query_as::<_, QueuedJob>(
|
||||
"SELECT *, null as workflow_as_code_status FROM v2_as_queue WHERE id = ANY($1)",
|
||||
)
|
||||
.bind(&timeouts[..])
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.map_err(|e| tracing::error!("Error fetching same worker jobs: {:?}", e))
|
||||
.unwrap_or_default();
|
||||
|
||||
jobs
|
||||
};
|
||||
@@ -1848,7 +1864,7 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
|
||||
let non_restartable_jobs = if *RESTART_ZOMBIE_JOBS {
|
||||
vec![]
|
||||
} else {
|
||||
sqlx::query_as::<_, QueuedJob>("SELECT * FROM v2_as_queue WHERE last_ping < now() - ($1 || ' seconds')::interval
|
||||
sqlx::query_as::<_, QueuedJob>("SELECT *, null as workflow_as_code_status FROM v2_as_queue WHERE last_ping < now() - ($1 || ' seconds')::interval
|
||||
AND running = true AND job_kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow') AND same_worker = false")
|
||||
.bind(ZOMBIE_JOB_TIMEOUT.as_str())
|
||||
.fetch_all(db)
|
||||
@@ -1873,13 +1889,14 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
|
||||
}
|
||||
}
|
||||
|
||||
let zombie_jobs_restart_limit_reached =
|
||||
sqlx::query_as::<_, QueuedJob>("SELECT * FROM v2_as_queue WHERE id = ANY($1)")
|
||||
.bind(&zombie_jobs_uuid_restart_limit_reached[..])
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_else(|| vec![]);
|
||||
let zombie_jobs_restart_limit_reached = sqlx::query_as::<_, QueuedJob>(
|
||||
"SELECT *, null as workflow_as_code_status FROM v2_as_queue WHERE id = ANY($1)",
|
||||
)
|
||||
.bind(&zombie_jobs_uuid_restart_limit_reached[..])
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_else(|| vec![]);
|
||||
|
||||
let timeouts = non_restartable_jobs
|
||||
.into_iter()
|
||||
|
||||
@@ -27,8 +27,10 @@ use serde::Serialize;
|
||||
use windmill_common::flows::InputTransform;
|
||||
use windmill_common::worker::WORKER_CONFIG;
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
use windmill_common::flow_status::{FlowStatus, FlowStatusModule, RestartedFrom};
|
||||
|
||||
use windmill_common::{
|
||||
flow_status::{FlowStatus, FlowStatusModule, RestartedFrom},
|
||||
flows::{FlowModule, FlowModuleValue, FlowValue},
|
||||
jobs::{JobKind, JobPayload, RawCode},
|
||||
jwt::JWT_SECRET,
|
||||
@@ -114,12 +116,14 @@ fn next_worker_name() -> String {
|
||||
.unwrap_or(s)
|
||||
})
|
||||
.unwrap_or("no thread name");
|
||||
format!("{id}/{thread_name}")
|
||||
format!("{id}/worker-{thread_name}")
|
||||
}
|
||||
|
||||
pub struct ApiServer {
|
||||
pub addr: std::net::SocketAddr,
|
||||
#[allow(unused)]
|
||||
tx: tokio::sync::broadcast::Sender<()>,
|
||||
#[allow(unused)]
|
||||
task: tokio::task::JoinHandle<anyhow::Result<()>>,
|
||||
}
|
||||
|
||||
@@ -153,6 +157,7 @@ impl ApiServer {
|
||||
Self { addr, tx, task }
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
async fn close(self) -> anyhow::Result<()> {
|
||||
println!("closing api server");
|
||||
let Self { tx, task, .. } = self;
|
||||
@@ -193,11 +198,13 @@ async fn set_jwt_secret() -> () {
|
||||
}
|
||||
|
||||
mod suspend_resume {
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
use serde_json::json;
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
use super::*;
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
async fn wait_until_flow_suspends(
|
||||
flow: Uuid,
|
||||
mut queue: impl Stream<Item = Uuid> + Unpin,
|
||||
@@ -218,6 +225,7 @@ mod suspend_resume {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
fn flow() -> FlowValue {
|
||||
serde_json::from_value(serde_json::json!({
|
||||
"modules": [{
|
||||
@@ -462,12 +470,12 @@ mod suspend_resume {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
mod retry {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// test helper provides some external state to help steps fail at specific points
|
||||
struct Server {
|
||||
addr: std::net::SocketAddr,
|
||||
@@ -536,6 +544,7 @@ def main(last, port):
|
||||
"#
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
fn flow_forloop_retry() -> FlowValue {
|
||||
serde_json::from_value(serde_json::json!({
|
||||
"modules": [{
|
||||
@@ -1073,6 +1082,7 @@ async fn listen_for_completed_jobs(db: &Pool<Postgres>) -> impl Stream<Item = Uu
|
||||
listen_for_uuid_on(db, "completed").await
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
async fn listen_for_queue(db: &Pool<Postgres>) -> impl Stream<Item = Uuid> + Unpin {
|
||||
listen_for_uuid_on(db, "queued").await
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ http_trigger = ["dep:matchit", "dep:thiserror", "dep:sha1", "dep:constant_time_e
|
||||
static_frontend = ["dep:rust-embed"]
|
||||
postgres_trigger = ["dep:rust-postgres", "dep:pg_escape", "dep:byteorder", "dep:thiserror", "dep:rust_decimal", "dep:rust-postgres-native-tls"]
|
||||
mqtt_trigger = ["dep:thiserror", "dep:rumqttc"]
|
||||
sqs_trigger = ["dep:aws-sdk-sqs", "dep:thiserror", "dep:aws-config"]
|
||||
sqs_trigger = ["dep:aws-sdk-sqs", "dep:aws-sdk-sts", "dep:aws-sdk-sso", "dep:aws-sdk-ssooidc", "dep:thiserror", "dep:aws-config", "dep:backon"]
|
||||
deno_core = ["dep:deno_core", "dep:deno_error"]
|
||||
gcp_trigger = ["dep:thiserror", "dep:google-cloud-pubsub", "dep:google-cloud-googleapis", "dep:tonic"]
|
||||
cloud = ["windmill-common/cloud"]
|
||||
@@ -138,13 +138,17 @@ rust_decimal = { workspace = true, optional = true }
|
||||
rust-postgres-native-tls = { workspace = true, optional = true}
|
||||
rumqttc = { workspace = true, optional = true }
|
||||
aws-sdk-sqs = { workspace = true, optional = true }
|
||||
aws-config = { workspace = true, optional = true }
|
||||
aws-sdk-sso = { workspace = true, optional = true }
|
||||
aws-sdk-ssooidc = { workspace = true, optional = true }
|
||||
aws-sdk-sts = { workspace = true, optional = true }
|
||||
rustls = { workspace = true }
|
||||
|
||||
aws-config = { workspace = true, optional = true }
|
||||
google-cloud-pubsub = { workspace = true, optional = true }
|
||||
google-cloud-googleapis = { workspace = true , optional = true }
|
||||
tonic = { workspace = true, optional = true }
|
||||
deno_error = { workspace = true, optional = true }
|
||||
deno_core = { workspace = true, optional = true }
|
||||
|
||||
backon = {workspace = true, optional = true}
|
||||
[build-dependencies]
|
||||
deno_core = { workspace = true, optional = true }
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.509.1
|
||||
version: 1.515.1
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -3413,6 +3413,9 @@ paths:
|
||||
cc_client_secret:
|
||||
type: string
|
||||
description: "OAuth client secret for resource-level credentials (client_credentials flow only)"
|
||||
cc_token_url:
|
||||
type: string
|
||||
description: "OAuth token URL override for resource-level authentication (client_credentials flow only)"
|
||||
required:
|
||||
- expires_in
|
||||
- client
|
||||
@@ -3455,6 +3458,9 @@ paths:
|
||||
cc_client_secret:
|
||||
type: string
|
||||
description: "OAuth client secret for resource-level authentication"
|
||||
cc_token_url:
|
||||
type: string
|
||||
description: "OAuth token URL override for resource-level authentication"
|
||||
required:
|
||||
- cc_client_id
|
||||
- cc_client_secret
|
||||
@@ -5090,6 +5096,10 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: authed
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: script details
|
||||
@@ -7664,7 +7674,9 @@ paths:
|
||||
progress:
|
||||
type: integer
|
||||
flow_status:
|
||||
$ref: "#/components/schemas/WorkflowStatusRecord"
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/FlowStatus"
|
||||
workflow_as_code_status:
|
||||
$ref: "#/components/schemas/WorkflowStatus"
|
||||
|
||||
/w/{workspace}/jobs_u/getupdate_sse/{id}:
|
||||
get:
|
||||
@@ -14056,6 +14068,8 @@ components:
|
||||
the execution of this script will be permissioned_as and by extension its DT_TOKEN.
|
||||
flow_status:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/FlowStatus"
|
||||
workflow_as_code_status:
|
||||
$ref: "#/components/schemas/WorkflowStatus"
|
||||
raw_flow:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/FlowValue"
|
||||
is_flow_step:
|
||||
@@ -14163,6 +14177,8 @@ components:
|
||||
the execution of this script will be permissioned_as and by extension its DT_TOKEN.
|
||||
flow_status:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/FlowStatus"
|
||||
workflow_as_code_status:
|
||||
$ref: "#/components/schemas/WorkflowStatus"
|
||||
raw_flow:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/FlowValue"
|
||||
is_flow_step:
|
||||
@@ -14625,36 +14641,41 @@ components:
|
||||
- type: object
|
||||
properties:
|
||||
object:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
typ:
|
||||
oneOf:
|
||||
- type: string
|
||||
enum:
|
||||
[
|
||||
"float",
|
||||
"int",
|
||||
"bool",
|
||||
"email",
|
||||
"unknown",
|
||||
"bytes",
|
||||
"dict",
|
||||
"datetime",
|
||||
"sql",
|
||||
]
|
||||
- type: object
|
||||
properties:
|
||||
str: {}
|
||||
required: [str]
|
||||
required:
|
||||
- key
|
||||
- typ
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
props:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
typ:
|
||||
oneOf:
|
||||
- type: string
|
||||
enum:
|
||||
[
|
||||
"float",
|
||||
"int",
|
||||
"bool",
|
||||
"email",
|
||||
"unknown",
|
||||
"bytes",
|
||||
"dict",
|
||||
"datetime",
|
||||
"sql",
|
||||
]
|
||||
- type: object
|
||||
properties:
|
||||
str: {}
|
||||
required: [str]
|
||||
required:
|
||||
- key
|
||||
- typ
|
||||
required:
|
||||
- object
|
||||
- object
|
||||
- type: object
|
||||
properties:
|
||||
list:
|
||||
|
||||
@@ -18,10 +18,11 @@ use windmill_audit::audit_oss::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
use windmill_common::{
|
||||
error::{self},
|
||||
worker::MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS,
|
||||
DB,
|
||||
};
|
||||
|
||||
use crate::{db::ApiAuthed, utils::{require_devops_role}};
|
||||
use crate::{db::ApiAuthed, utils::require_devops_role};
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
@@ -129,6 +130,42 @@ async fn update_config(
|
||||
));
|
||||
}
|
||||
|
||||
if name.starts_with("worker__") {
|
||||
let periodic_script_bash = config
|
||||
.get("periodic_script_bash")
|
||||
.filter(|v| !v.is_null())
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| !s.is_empty());
|
||||
|
||||
let periodic_script_interval = config
|
||||
.get("periodic_script_interval_seconds")
|
||||
.filter(|v| !v.is_null());
|
||||
|
||||
match (periodic_script_bash, periodic_script_interval) {
|
||||
(Some(_), Some(interval_value)) => {
|
||||
if let Some(interval) = interval_value.as_u64() {
|
||||
if interval < MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Periodic script interval must be at least {} seconds, got {} seconds",
|
||||
MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS, interval
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
return Err(error::Error::BadRequest(
|
||||
"Periodic script interval must be a valid number".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
(Some(_), None) => {
|
||||
return Err(error::Error::BadRequest(
|
||||
"Periodic script interval must be specified when periodic script is configured"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
sqlx::query!(
|
||||
"INSERT INTO config (name, config) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET config = $2",
|
||||
|
||||
@@ -18,6 +18,7 @@ use quick_cache::sync::Cache;
|
||||
use serde_json::value::RawValue;
|
||||
use sqlx::Pool;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
use std::time::Instant;
|
||||
@@ -723,7 +724,7 @@ macro_rules! get_job_query {
|
||||
CASE WHEN jsonb_typeof(args) = 'object' THEN args
|
||||
ELSE jsonb_build_object('value', args)
|
||||
END
|
||||
ELSE '{{\"reason\": \"WINDMILL_TOO_BIG\"}}'::jsonb END as args, COALESCE(flow_status, workflow_as_code_status) AS flow_status, \
|
||||
ELSE '{{\"reason\": \"WINDMILL_TOO_BIG\"}}'::jsonb END as args, flow_status, workflow_as_code_status, \
|
||||
{logs} as logs, {code} as raw_code, canceled_by is not null as canceled, canceled_by, canceled_reason, kind as job_kind, \
|
||||
CASE WHEN trigger_kind = 'schedule'::job_trigger_kind THEN trigger END AS schedule_path, permissioned_as, \
|
||||
{flow} as raw_flow, flow_step_id IS NOT NULL AS is_flow_step, script_lang as language, \
|
||||
@@ -3003,6 +3004,7 @@ impl<'a> From<UnifiedJob> for Job {
|
||||
result_columns: None,
|
||||
logs: None,
|
||||
flow_status: None,
|
||||
workflow_as_code_status: None,
|
||||
deleted: uj.deleted,
|
||||
canceled: uj.canceled,
|
||||
canceled_by: uj.canceled_by,
|
||||
@@ -3039,6 +3041,7 @@ impl<'a> From<UnifiedJob> for Job {
|
||||
scheduled_for: uj.scheduled_for.unwrap(),
|
||||
logs: None,
|
||||
flow_status: None,
|
||||
workflow_as_code_status: None,
|
||||
canceled: uj.canceled,
|
||||
canceled_by: uj.canceled_by,
|
||||
canceled_reason: None,
|
||||
@@ -3076,7 +3079,7 @@ struct CancelJob {
|
||||
reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum PreviewKind {
|
||||
Code,
|
||||
@@ -3087,7 +3090,7 @@ enum PreviewKind {
|
||||
ScriptHash,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Preview {
|
||||
content: Option<String>,
|
||||
kind: Option<PreviewKind>,
|
||||
@@ -4461,6 +4464,9 @@ pub async fn run_wait_result_flow_by_path_get(
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_license_key_valid().await?;
|
||||
|
||||
let path = flow_path.to_path();
|
||||
check_scopes(&authed, || format!("jobs:run:flows:{path}"))?;
|
||||
|
||||
if method == http::Method::HEAD {
|
||||
return Ok(Json(serde_json::json!("")).into_response());
|
||||
}
|
||||
@@ -4483,7 +4489,7 @@ pub async fn run_wait_result_flow_by_path_get(
|
||||
.to_args_from_runnable(
|
||||
&db,
|
||||
&w_id,
|
||||
RunnableId::from_flow_path(flow_path.to_path()),
|
||||
RunnableId::from_flow_path(path),
|
||||
run_query.skip_preprocessor,
|
||||
)
|
||||
.await?;
|
||||
@@ -4503,12 +4509,15 @@ pub async fn run_wait_result_script_by_path(
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_license_key_valid().await?;
|
||||
|
||||
let path = script_path.to_path();
|
||||
check_scopes(&authed, || format!("jobs:run:scripts:{path}"))?;
|
||||
|
||||
let args = args
|
||||
.to_args_from_runnable(
|
||||
&authed,
|
||||
&db,
|
||||
&w_id,
|
||||
RunnableId::from_script_path(script_path.to_path()),
|
||||
RunnableId::from_script_path(path),
|
||||
run_query.skip_preprocessor,
|
||||
)
|
||||
.await?;
|
||||
@@ -4526,14 +4535,11 @@ pub async fn run_wait_result_script_by_path_internal(
|
||||
w_id: String,
|
||||
args: PushArgsOwned,
|
||||
) -> error::Result<Response> {
|
||||
let script_path = script_path.to_path();
|
||||
check_scopes(&authed, || format!("jobs:run:scripts:{script_path}"))?;
|
||||
|
||||
check_queue_too_long(&db, QUEUE_LIMIT_WAIT_RESULT.or(run_query.queue_limit)).await?;
|
||||
|
||||
let mut tx = user_db.clone().begin(&authed).await?;
|
||||
let (job_payload, tag, delete_after_use, timeout, on_behalf_of) =
|
||||
script_path_to_payload(script_path, &mut *tx, &w_id, run_query.skip_preprocessor).await?;
|
||||
script_path_to_payload(script_path.to_path(), &mut *tx, &w_id, run_query.skip_preprocessor).await?;
|
||||
drop(tx);
|
||||
|
||||
let tag = run_query.tag.clone().or(tag);
|
||||
@@ -4716,12 +4722,15 @@ pub async fn run_wait_result_flow_by_path(
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_license_key_valid().await?;
|
||||
|
||||
let path = flow_path.to_path();
|
||||
check_scopes(&authed, || format!("jobs:run:flows:{path}"))?;
|
||||
|
||||
let args = args
|
||||
.to_args_from_runnable(
|
||||
&authed,
|
||||
&db,
|
||||
&w_id,
|
||||
RunnableId::from_flow_path(flow_path.to_path()),
|
||||
RunnableId::from_flow_path(path),
|
||||
run_query.skip_preprocessor,
|
||||
)
|
||||
.await?;
|
||||
@@ -4742,7 +4751,6 @@ pub async fn run_wait_result_flow_by_path_internal(
|
||||
check_queue_too_long(&db, run_query.queue_limit).await?;
|
||||
|
||||
let flow_path = flow_path.to_path();
|
||||
check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?;
|
||||
|
||||
let scheduled_for = run_query.get_scheduled_for(&db).await?;
|
||||
|
||||
@@ -5686,25 +5694,31 @@ pub struct JobUpdate {
|
||||
pub mem_peak: Option<i32>,
|
||||
pub progress: Option<i32>,
|
||||
pub flow_status: Option<Box<serde_json::value::RawValue>>,
|
||||
pub workflow_as_code_status: Option<Box<serde_json::value::RawValue>>,
|
||||
pub job: Option<Job>,
|
||||
pub only_result: Option<Box<serde_json::value::RawValue>>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct JobUpdateLastStatus {
|
||||
pub running: Option<bool>,
|
||||
pub completed: Option<bool>,
|
||||
pub log_offset: Option<i32>,
|
||||
pub mem_peak: Option<i32>,
|
||||
impl JobUpdate {
|
||||
pub fn hash_str(&self) -> String {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
self.hash(&mut hasher);
|
||||
format!("{:x}", hasher.finish())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&JobUpdate> for JobUpdateLastStatus {
|
||||
fn from(update: &JobUpdate) -> Self {
|
||||
Self {
|
||||
running: update.running,
|
||||
completed: update.completed,
|
||||
log_offset: update.log_offset,
|
||||
mem_peak: update.mem_peak,
|
||||
impl Hash for JobUpdate {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.running.hash(state);
|
||||
self.completed.hash(state);
|
||||
self.log_offset.hash(state);
|
||||
self.mem_peak.hash(state);
|
||||
self.progress.hash(state);
|
||||
if !self.completed.unwrap_or(false) {
|
||||
self.flow_status.as_ref().map(|x| x.get().hash(state));
|
||||
self.workflow_as_code_status
|
||||
.as_ref()
|
||||
.map(|x| x.get().hash(state));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5852,7 +5866,7 @@ fn get_job_update_sse_stream(
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut log_offset = initial_log_offset;
|
||||
let mut last_update: Option<JobUpdateLastStatus> = None;
|
||||
let mut last_update_hash: Option<String> = None;
|
||||
|
||||
// Send initial update immediately
|
||||
let mut running = running;
|
||||
@@ -5872,7 +5886,7 @@ fn get_job_update_sse_stream(
|
||||
.await
|
||||
{
|
||||
Ok(update) => {
|
||||
last_update = Some((&update).into());
|
||||
last_update_hash = Some(update.hash_str());
|
||||
let completion_sent = update.completed.unwrap_or(false);
|
||||
if running.is_some() && update.running.is_some_and(|x| x) {
|
||||
running = Some(true);
|
||||
@@ -5951,9 +5965,9 @@ fn get_job_update_sse_stream(
|
||||
// if !only_result.unwrap_or(false) {
|
||||
// tracing::error!("update {:?}", update);
|
||||
// }
|
||||
let update_last_status = (&update).into();
|
||||
let update_last_status = update.hash_str();
|
||||
// Only send if the update has changed
|
||||
if last_update.as_ref() != Some(&update_last_status) {
|
||||
if last_update_hash.as_ref() != Some(&update_last_status) {
|
||||
// Update log offset if available
|
||||
if let Some(new_offset) = update.log_offset {
|
||||
log_offset = Some(new_offset);
|
||||
@@ -5966,7 +5980,7 @@ fn get_job_update_sse_stream(
|
||||
break;
|
||||
}
|
||||
|
||||
last_update = Some(update_last_status);
|
||||
last_update_hash = Some(update_last_status);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -6068,6 +6082,7 @@ async fn get_job_update_data(
|
||||
progress: None,
|
||||
job: None,
|
||||
flow_status: None,
|
||||
workflow_as_code_status: None,
|
||||
only_result: result.0,
|
||||
})
|
||||
} else {
|
||||
@@ -6080,22 +6095,8 @@ async fn get_job_update_data(
|
||||
END AS running,
|
||||
SUBSTR(logs, GREATEST($1 - log_offset, 0)) AS logs,
|
||||
COALESCE(r.memory_peak, c.memory_peak) AS mem_peak,
|
||||
CASE
|
||||
-- flow step:
|
||||
WHEN flow_step_id IS NOT NULL THEN NULL
|
||||
-- completed:
|
||||
WHEN c.id IS NOT NULL THEN COALESCE(
|
||||
c.workflow_as_code_status || c.flow_status,
|
||||
c.workflow_as_code_status,
|
||||
c.flow_status
|
||||
)
|
||||
-- not completed:
|
||||
ELSE COALESCE(
|
||||
f.workflow_as_code_status || f.flow_status,
|
||||
f.workflow_as_code_status,
|
||||
f.flow_status
|
||||
)
|
||||
END AS \"flow_status: sqlx::types::Json<Box<RawValue>>\",
|
||||
COALESCE(c.flow_status, f.flow_status) AS \"flow_status: sqlx::types::Json<Box<RawValue>>\",
|
||||
COALESCE(c.workflow_as_code_status, f.workflow_as_code_status) AS \"workflow_as_code_status: sqlx::types::Json<Box<RawValue>>\",
|
||||
job_logs.log_offset + CHAR_LENGTH(job_logs.logs) + 1 AS log_offset,
|
||||
created_by AS \"created_by!\",
|
||||
CASE WHEN $4::BOOLEAN THEN (
|
||||
@@ -6140,6 +6141,9 @@ async fn get_job_update_data(
|
||||
new_logs: record.logs,
|
||||
mem_peak: record.mem_peak,
|
||||
progress: record.progress,
|
||||
workflow_as_code_status: record
|
||||
.workflow_as_code_status
|
||||
.map(|x: sqlx::types::Json<Box<RawValue>>| x.0),
|
||||
job,
|
||||
flow_status: record
|
||||
.flow_status
|
||||
|
||||
@@ -611,7 +611,7 @@ pub async fn run_server(
|
||||
.nest("/mqtt_triggers", mqtt_triggers_service)
|
||||
.nest("/sqs_triggers", sqs_triggers_service)
|
||||
.nest("/gcp_triggers", gcp_triggers_service)
|
||||
.nest("/postgres_triggers", postgres_triggers_service),
|
||||
.nest("/postgres_triggers", postgres_triggers_service),
|
||||
)
|
||||
.nest("/workspaces", workspaces::global_service())
|
||||
.nest(
|
||||
@@ -669,7 +669,7 @@ pub async fn run_server(
|
||||
.layer(from_extractor::<OptAuthed>())
|
||||
.layer(cors.clone()),
|
||||
)
|
||||
.nest("/mcp/w/:workspace_id/sse", mcp_router)
|
||||
.nest("/mcp/w/:workspace_id/sse", mcp_router.layer(from_extractor::<ApiAuthed>()))
|
||||
.layer(from_extractor::<OptAuthed>())
|
||||
.nest("/agent_workers", {
|
||||
#[cfg(feature = "agent_worker_server")]
|
||||
|
||||
@@ -230,6 +230,20 @@ impl Runner {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn check_scopes(authed: &ApiAuthed) -> Result<(), Error> {
|
||||
let scopes = authed.scopes.as_ref();
|
||||
if scopes.is_none()
|
||||
|| scopes
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|scope| scope != "mcp:all" && scope != "mcp:favorites" && !scope.starts_with("mcp:hub:"))
|
||||
{
|
||||
tracing::error!("Unauthorized: missing mcp scope");
|
||||
return Err(Error::internal_error("Unauthorized: missing mcp scope".to_string(), None));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_item_schema(
|
||||
path: &str,
|
||||
user_db: &UserDB,
|
||||
@@ -854,14 +868,6 @@ impl ServerHandler for Runner {
|
||||
request: CallToolRequestParam,
|
||||
context: RequestContext<RoleServer>,
|
||||
) -> Result<CallToolResult, Error> {
|
||||
let parse_args = |args_opt: Option<JsonObject>| -> Result<Value, Error> {
|
||||
args_opt.map(Value::Object).ok_or_else(|| {
|
||||
Error::invalid_params(
|
||||
"Missing arguments for tool",
|
||||
Some(request.name.clone().into()),
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
let http_parts = context
|
||||
.extensions
|
||||
@@ -875,15 +881,22 @@ impl ServerHandler for Runner {
|
||||
tracing::error!("ApiAuthed Axum extension not found");
|
||||
Error::internal_error("ApiAuthed Axum extension not found", None)
|
||||
})?;
|
||||
|
||||
Runner::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 = parse_args(request.arguments)?;
|
||||
|
||||
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
|
||||
@@ -1007,6 +1020,13 @@ impl ServerHandler for Runner {
|
||||
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)
|
||||
})?;
|
||||
|
||||
Runner::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)
|
||||
@@ -1017,11 +1037,6 @@ impl ServerHandler for Runner {
|
||||
Error::internal_error("UserDB Axum extension 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)
|
||||
})?;
|
||||
|
||||
let workspace_id = http_parts
|
||||
.extensions
|
||||
.get::<WorkspaceId>()
|
||||
@@ -1031,15 +1046,13 @@ impl ServerHandler for Runner {
|
||||
})
|
||||
.map(|w_id| w_id.0.clone())?;
|
||||
|
||||
let owned_scope = authed.scopes.as_ref().and_then(|scopes| {
|
||||
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 = authed
|
||||
.scopes
|
||||
.as_ref()
|
||||
.and_then(|scopes| scopes.iter().find(|scope| scope.starts_with("mcp:hub")));
|
||||
let hub_scope = scopes.and_then(|scopes| scopes.iter().find(|scope| scope.starts_with("mcp:hub")));
|
||||
let scope_type = owned_scope.map_or("all", |scope| {
|
||||
let parts = scope.split(":").collect::<Vec<&str>>();
|
||||
parts[1]
|
||||
|
||||
@@ -372,7 +372,7 @@ impl PostgresConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn retrieve_info(&self) -> PgInfo {
|
||||
fn retrieve_info(&self) -> PgInfo<'_> {
|
||||
let postgres_resource_path;
|
||||
let publication_name;
|
||||
let replication_slot_name;
|
||||
|
||||
@@ -283,6 +283,7 @@ pub enum ScopeDomain {
|
||||
Acls, // Granular access control lists
|
||||
RawApps, // Raw application data
|
||||
AgentWorkers, // Agent workers management
|
||||
Mcp, // MCP
|
||||
}
|
||||
|
||||
impl ScopeDomain {
|
||||
@@ -328,6 +329,7 @@ impl ScopeDomain {
|
||||
Self::Indexer => "indexer",
|
||||
Self::Teams => "teams",
|
||||
Self::GitSync => "git_sync",
|
||||
Self::Mcp => "mcp",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,6 +375,7 @@ impl ScopeDomain {
|
||||
"acls" => Some(Self::Acls),
|
||||
"raw_apps" => Some(Self::RawApps),
|
||||
"agent_workers" => Some(Self::AgentWorkers),
|
||||
"mcp" => Some(Self::Mcp),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -415,7 +418,8 @@ impl ScopeAction {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_route_access(
|
||||
pub fn
|
||||
check_route_access(
|
||||
token_scopes: &[String],
|
||||
route_path: &str,
|
||||
http_method: &str,
|
||||
@@ -424,7 +428,13 @@ pub fn check_route_access(
|
||||
let required_action = map_http_method_to_action(http_method, route_path);
|
||||
|
||||
// Find the domain and kind for this route
|
||||
let (required_domain, required_kind) = extract_domain_from_route(route_path)?;
|
||||
let (required_domain, required_kind, route_suffix) = extract_domain_from_route(route_path)?;
|
||||
|
||||
// Backward compatibility: MCP handlers expect unusual scope actions: all, favorites, hub.
|
||||
if required_domain == ScopeDomain::Mcp {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut is_scoped_token = false;
|
||||
// Check if any token scope grants the required access
|
||||
for scope_str in token_scopes {
|
||||
@@ -435,6 +445,7 @@ pub fn check_route_access(
|
||||
required_domain,
|
||||
required_action,
|
||||
required_kind.as_deref(),
|
||||
route_suffix.as_deref(),
|
||||
)? {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -527,30 +538,36 @@ fn determine_kind_from_route(route_path: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn extract_domain_from_route(route_path: &str) -> Result<(ScopeDomain, Option<String>)> {
|
||||
fn extract_domain_from_route(
|
||||
route_path: &str,
|
||||
) -> Result<(ScopeDomain, Option<String>, Option<String>)> {
|
||||
// Examples:
|
||||
// - /api/w/workspace/jobs/123 -> jobs domain (workspaced)
|
||||
// - /api/teams/sync -> teams domain (global)
|
||||
// - /api/srch/index/search -> indexer domain (global)
|
||||
let parts: Vec<&str> = route_path.split('/').collect();
|
||||
|
||||
let (domain, kind) = if parts.len() >= 5 && parts[1] == "api" && parts[2] == "w" {
|
||||
let (domain, kind, route_suffix) = if parts.len() >= 5 && parts[1] == "api" && parts[2] == "w" {
|
||||
let domain_part = parts[4];
|
||||
let route_suffix = &parts[4..].join("/");
|
||||
|
||||
let domain = ScopeDomain::from_str(domain_part);
|
||||
|
||||
let kind = determine_kind_from_route(route_suffix);
|
||||
let kind = determine_kind_from_route(&route_suffix);
|
||||
|
||||
(domain, kind)
|
||||
(domain, kind, Some(route_suffix.to_owned()))
|
||||
} else if parts.len() >= 3 && parts[1] == "api" {
|
||||
(ScopeDomain::from_str(parts[2]), None)
|
||||
(
|
||||
ScopeDomain::from_str(parts[2]),
|
||||
None,
|
||||
Some(parts[2..].join("/")),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
(None, None, None)
|
||||
};
|
||||
|
||||
if let Some(domain) = domain {
|
||||
return Ok((domain, kind));
|
||||
return Ok((domain, kind, route_suffix));
|
||||
}
|
||||
|
||||
Err(Error::BadRequest(format!(
|
||||
@@ -559,11 +576,34 @@ fn extract_domain_from_route(route_path: &str) -> Result<(ScopeDomain, Option<St
|
||||
)))
|
||||
}
|
||||
|
||||
const RUN_WHITELISTED_GET_PATHS: [&'static str; 19] = [
|
||||
"jobs_u/get_flow/",
|
||||
"jobs_u/get_root_job_id/",
|
||||
"jobs_u/get/",
|
||||
"jobs_u/get_logs/",
|
||||
"jobs_u/get_args/",
|
||||
"jobs_u/get_flow_debug_info/",
|
||||
"jobs_u/completed/get/",
|
||||
"jobs_u/completed/get_result/",
|
||||
"jobs_u/completed/get_result_maybe/",
|
||||
"jobs_u/getupdate/",
|
||||
"jobs_u/getupdate_sse/",
|
||||
"jobs_u/get_log_file/",
|
||||
"jobs/result_by_id/",
|
||||
"jobs/resume_urls/",
|
||||
"jobs/flow/user_states/",
|
||||
"jobs/job_signature/",
|
||||
"jobs/completed/get/",
|
||||
"jobs/completed/get_result/",
|
||||
"jobs/completed/get_result_maybe/",
|
||||
];
|
||||
|
||||
fn scope_grants_access(
|
||||
scope: &ScopeDefinition,
|
||||
required_domain: ScopeDomain,
|
||||
required_action: ScopeAction,
|
||||
required_kind: Option<&str>,
|
||||
route_path: Option<&str>,
|
||||
) -> Result<bool> {
|
||||
// Check domain match
|
||||
let scope_domain = ScopeDomain::from_str(&scope.domain)
|
||||
@@ -577,11 +617,19 @@ fn scope_grants_access(
|
||||
let scope_action = ScopeAction::from_str(&scope.action)
|
||||
.ok_or_else(|| Error::BadRequest(format!("Invalid scope action: {}", scope.action)))?;
|
||||
|
||||
if !scope_action.includes(&required_action) {
|
||||
if !scope_action.includes(&required_action)
|
||||
&& !(scope_domain == ScopeDomain::Jobs
|
||||
&& required_action == ScopeAction::Read
|
||||
&& route_path.is_some_and(|p| {
|
||||
RUN_WHITELISTED_GET_PATHS
|
||||
.iter()
|
||||
.any(|path| p.starts_with(path))
|
||||
}))
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if required_domain == ScopeDomain::Jobs && required_action == ScopeAction::Run {
|
||||
if scope_domain == ScopeDomain::Jobs && required_action == ScopeAction::Run {
|
||||
match (&scope.kind, required_kind) {
|
||||
(Some(scope_kind), Some(req_kind)) => {
|
||||
if scope_kind != req_kind {
|
||||
@@ -672,14 +720,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_route_domain_extraction() {
|
||||
let (domain, kind) = extract_domain_from_route("/api/w/test_workspace/jobs/123").unwrap();
|
||||
let (domain, kind, route_suffix) =
|
||||
extract_domain_from_route("/api/w/test_workspace/jobs/123").unwrap();
|
||||
assert_eq!(domain, ScopeDomain::Jobs);
|
||||
assert_eq!(kind, None);
|
||||
assert_eq!(route_suffix, Some("jobs/123".to_string()));
|
||||
|
||||
let (domain, kind) =
|
||||
let (domain, kind, route_suffix) =
|
||||
extract_domain_from_route("/api/w/test_workspace/scripts/test_script").unwrap();
|
||||
assert_eq!(domain, ScopeDomain::Scripts);
|
||||
assert_eq!(kind, None);
|
||||
assert_eq!(route_suffix, Some("scripts/test_script".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -933,6 +933,13 @@ async fn create_script_internal<'c>(
|
||||
}
|
||||
|
||||
let permissioned_as = username_to_permissioned_as(&authed.username);
|
||||
if let Some(parent_hash) = ns.parent_hash {
|
||||
tracing::info!(
|
||||
"creating script {hash:?} at path {script_path} with parent {parent_hash} on workspace {w_id}",
|
||||
);
|
||||
} else {
|
||||
tracing::info!("creating script {hash:?} at path {script_path} on workspace {w_id}",);
|
||||
}
|
||||
if needs_lock_gen {
|
||||
let tag = if ns.dedicated_worker.is_some_and(|x| x) {
|
||||
Some(format!("{}:{}", &w_id, &ns.path,))
|
||||
@@ -1490,13 +1497,23 @@ async fn get_script_by_hash_internal<'c>(
|
||||
Ok(script)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GetScriptByHashQuery {
|
||||
authed: Option<bool>,
|
||||
}
|
||||
async fn get_script_by_hash(
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, hash)): Path<(String, ScriptHash)>,
|
||||
Query(query): Query<WithStarredInfoQuery>,
|
||||
Query(query_auth): Query<GetScriptByHashQuery>,
|
||||
Extension(authed): Extension<ApiAuthed>,
|
||||
) -> JsonResult<ScriptWithStarred> {
|
||||
let mut tx = db.begin().await?;
|
||||
let mut tx = if query_auth.authed.is_some_and(|x| x) {
|
||||
user_db.begin(&authed).await?
|
||||
} else {
|
||||
db.begin().await?
|
||||
};
|
||||
let r = get_script_by_hash_internal(
|
||||
&mut tx,
|
||||
&w_id,
|
||||
|
||||
@@ -31,6 +31,7 @@ use crate::{
|
||||
check_tag_available_for_workspace, delete_job_metadata_after_use, result_to_response,
|
||||
run_flow_by_path_inner, run_script_by_path_inner, run_wait_result_internal, RunJobQuery,
|
||||
},
|
||||
utils::check_scopes,
|
||||
HTTP_CLIENT,
|
||||
};
|
||||
|
||||
@@ -703,13 +704,15 @@ async fn trigger_script_with_retry_and_error_handler(
|
||||
error_handler_args: Option<&sqlx::types::Json<HashMap<String, Box<RawValue>>>>,
|
||||
trigger_path: String,
|
||||
) -> Result<(Uuid, Option<bool>)> {
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_license_key_valid().await?;
|
||||
|
||||
check_scopes(&authed, || format!("jobs:run:scripts:{script_path}"))?;
|
||||
|
||||
let retry = retry.map(|r| r.0.clone());
|
||||
let error_handler_path = error_handler_path.map(|p| p.to_string());
|
||||
let error_handler_args = error_handler_args.map(|args| args.0.clone());
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_license_key_valid().await?;
|
||||
|
||||
let (job_payload, tag, delete_after_use, timeout, on_behalf_of) = {
|
||||
let mut tx = user_db.clone().begin(&authed).await?;
|
||||
script_path_to_payload(script_path, &mut *tx, &workspace_id, Some(false)).await?
|
||||
|
||||
@@ -199,17 +199,51 @@ pub async fn list_tokens_internal(
|
||||
) -> JsonResult<Vec<TruncatedTokenWithEmail>> {
|
||||
let tokens = if is_flow {
|
||||
sqlx::query_as!(
|
||||
TruncatedTokenWithEmail,
|
||||
"SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
|
||||
w_id, path).fetch_all(db)
|
||||
TruncatedTokenWithEmail,
|
||||
r#"
|
||||
SELECT label,
|
||||
concat(substring(token for 10)) AS token_prefix,
|
||||
expiration,
|
||||
created_at,
|
||||
last_used_at,
|
||||
scopes,
|
||||
email
|
||||
FROM token
|
||||
WHERE workspace_id = $1
|
||||
AND (
|
||||
scopes @> ARRAY['jobs:run:flows:' || $2]::text[]
|
||||
OR scopes @> ARRAY['run:flows/' || $2]::text[]
|
||||
)
|
||||
"#,
|
||||
w_id,
|
||||
path
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as!(
|
||||
TruncatedTokenWithEmail,
|
||||
"SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]",
|
||||
w_id, path)
|
||||
r#"
|
||||
SELECT label,
|
||||
concat(substring(token for 10)) AS token_prefix,
|
||||
expiration,
|
||||
created_at,
|
||||
last_used_at,
|
||||
scopes,
|
||||
email
|
||||
FROM token
|
||||
WHERE workspace_id = $1
|
||||
AND (
|
||||
scopes @> ARRAY['jobs:run:scripts:' || $2]::text[]
|
||||
OR scopes @> ARRAY['run:scripts/' || $2]::text[]
|
||||
)
|
||||
"#,
|
||||
w_id,
|
||||
path
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await?
|
||||
};
|
||||
|
||||
Ok(Json(tokens))
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
use axum::{body::Body, response::Response};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use sqlx::{Postgres, Transaction};
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::worker::CLOUD_HOSTED;
|
||||
@@ -224,7 +224,8 @@ where
|
||||
Ok(o.filter(|s| !s.trim().is_empty()))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct CriticalAlert {
|
||||
id: i32,
|
||||
alert_type: String,
|
||||
|
||||
@@ -146,33 +146,12 @@ async fn get_custom_tags(Query(query): Query<CustomTagQuery>) -> JsonResult<Vec<
|
||||
}
|
||||
if let Some(workspace) = query.workspace {
|
||||
let tags_o = CUSTOM_TAGS_PER_WORKSPACE.read().await;
|
||||
let workspace_tags = tags_o
|
||||
.1
|
||||
.iter()
|
||||
.filter(|(_, workspaces)| workspaces.contains(&workspace))
|
||||
.map(|(tag, _)| tag.clone())
|
||||
.collect::<Vec<String>>();
|
||||
let all_tags = tags_o.0.clone();
|
||||
return Ok(Json(
|
||||
all_tags
|
||||
.into_iter()
|
||||
.chain(workspace_tags.into_iter())
|
||||
.collect(),
|
||||
));
|
||||
let all_tags = tags_o.to_string_vec(Some(workspace));
|
||||
return Ok(Json(all_tags));
|
||||
} else if query.show_workspace_restriction.is_some_and(|x| x) {
|
||||
let tags_o = CUSTOM_TAGS_PER_WORKSPACE.read().await;
|
||||
let workspace_tags = tags_o
|
||||
.1
|
||||
.iter()
|
||||
.map(|(tag, workspaces)| format!("{}({})", tag, workspaces.join("+")))
|
||||
.collect::<Vec<String>>();
|
||||
let all_tags = tags_o.0.clone();
|
||||
return Ok(Json(
|
||||
all_tags
|
||||
.into_iter()
|
||||
.chain(workspace_tags.into_iter())
|
||||
.collect(),
|
||||
));
|
||||
let all_tags = tags_o.to_string_vec(None);
|
||||
return Ok(Json(all_tags));
|
||||
}
|
||||
Ok(Json(ALL_TAGS.read().await.clone().into()))
|
||||
}
|
||||
|
||||
@@ -242,15 +242,6 @@ pub struct WorkspaceSettings {
|
||||
pub git_app_installations: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Debug)]
|
||||
pub struct Usage {
|
||||
pub workspace_id: String,
|
||||
pub slack_team_id: Option<String>,
|
||||
pub slack_name: Option<String>,
|
||||
pub slack_command_script: Option<String>,
|
||||
pub slack_email: String,
|
||||
}
|
||||
|
||||
#[derive(sqlx::Type, Serialize, Deserialize, Debug)]
|
||||
#[sqlx(type_name = "WORKSPACE_KEY_KIND", rename_all = "lowercase")]
|
||||
pub enum WorkspaceKeyKind {
|
||||
|
||||
@@ -17,7 +17,7 @@ pub enum ActionKind {
|
||||
#[derive(FromRow, Serialize, Deserialize)]
|
||||
pub struct AuditLog {
|
||||
pub workspace_id: String,
|
||||
pub id: i32,
|
||||
pub id: i64,
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
pub username: String,
|
||||
pub operation: String,
|
||||
|
||||
@@ -63,6 +63,7 @@ object_store = { workspace = true, optional = true }
|
||||
prometheus = { workspace = true, optional = true }
|
||||
aws-config = { workspace = true, optional = true }
|
||||
aws-sdk-sts = { workspace = true, optional = true }
|
||||
|
||||
aws-smithy-types-convert = { workspace = true, optional = true }
|
||||
indexmap.workspace = true
|
||||
bytes.workspace = true
|
||||
|
||||
@@ -323,8 +323,6 @@ pub async fn create_token_for_owner(
|
||||
#[cfg(feature = "aws_auth")]
|
||||
pub mod aws {
|
||||
|
||||
use crate::error::to_anyhow;
|
||||
|
||||
use super::*;
|
||||
use crate::utils::empty_as_none;
|
||||
use aws_config::{BehaviorVersion, Region};
|
||||
@@ -332,7 +330,9 @@ pub mod aws {
|
||||
config::Credentials as AwsCredentials,
|
||||
operation::{
|
||||
assume_role_with_saml::AssumeRoleWithSamlOutput,
|
||||
assume_role_with_web_identity::AssumeRoleWithWebIdentityOutput,
|
||||
assume_role_with_web_identity::{
|
||||
builders::AssumeRoleWithWebIdentityFluentBuilder, AssumeRoleWithWebIdentityOutput,
|
||||
},
|
||||
},
|
||||
types::Credentials,
|
||||
Client,
|
||||
@@ -396,33 +396,27 @@ pub mod aws {
|
||||
Oidc(OidcAuth),
|
||||
}
|
||||
|
||||
pub async fn get_oidc_authentication_data(
|
||||
oidc_auth: OidcAuth,
|
||||
role_session_name: Option<impl ToString>,
|
||||
pub async fn get_assume_role_with_web_identity_fluent_builder(
|
||||
oidc_auth: &OidcAuth,
|
||||
token: String,
|
||||
) -> Result<AssumeRoleWithWebIdentityOutput> {
|
||||
let region = oidc_auth.region.unwrap_or_else(|| "us-east-1".to_string());
|
||||
role_session_name: Option<impl ToString>,
|
||||
) -> Result<AssumeRoleWithWebIdentityFluentBuilder> {
|
||||
let region = oidc_auth.region.as_deref().unwrap_or_else(|| "us-east-1");
|
||||
|
||||
let credentials = AwsCredentials::new("", "", None, None, "UserInput");
|
||||
|
||||
let config = aws_config::defaults(BehaviorVersion::latest())
|
||||
.credentials_provider(credentials)
|
||||
.region(Region::new(region.clone()))
|
||||
.region(Region::new(region.to_string()))
|
||||
.load()
|
||||
.await;
|
||||
|
||||
let assume_role_with_web_identity_fluent_builder = Client::new(&config)
|
||||
.assume_role_with_web_identity()
|
||||
.set_role_arn(Some(oidc_auth.role_arn))
|
||||
.set_role_arn(Some(oidc_auth.role_arn.to_owned()))
|
||||
.set_role_session_name(role_session_name.map(|str| str.to_string()))
|
||||
.set_web_identity_token(Some(token));
|
||||
|
||||
let resp = assume_role_with_web_identity_fluent_builder
|
||||
.clone()
|
||||
.send()
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
|
||||
Ok(resp)
|
||||
Ok(assume_role_with_web_identity_fluent_builder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ pub enum Error {
|
||||
RequireAdmin(String),
|
||||
#[error("{0}")]
|
||||
ExecutionErr(String),
|
||||
#[error("{0}")]
|
||||
ResultTooLarge(String),
|
||||
#[error("IoErr: {error:#} @{location:#}")]
|
||||
IoErr { error: io::Error, location: String },
|
||||
#[error("Utf8Err: {error:#} @{location:#}")]
|
||||
@@ -84,6 +86,44 @@ pub enum Error {
|
||||
Generic(StatusCode, String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::ExecutionErr(_) => "ExecutionErr",
|
||||
Self::ResultTooLarge(_) => "ResultTooLarge",
|
||||
Self::BadRequest(_) => "BadRequest",
|
||||
Self::QuotaExceeded(_) => "QuotaExceeded",
|
||||
Self::InternalErr(_) => "InternalErr",
|
||||
Self::InternalErrLoc { .. } => "InternalErr",
|
||||
Self::InternalErrAt(_, _) => "InternalErr",
|
||||
Self::Anyhow { .. } => "Anyhow",
|
||||
Self::JsonErr(_) => "JsonErr",
|
||||
Self::AIError(_) => "AIError",
|
||||
Self::AlreadyCompleted(_) => "AlreadyCompleted",
|
||||
Self::FindPythonError(_) => "FindPythonError",
|
||||
Self::ArgumentErr(_) => "ArgumentErr",
|
||||
Self::Generic(_, _) => "Generic",
|
||||
Self::IoErr { .. } => "IoErr",
|
||||
Self::Utf8Err { .. } => "Utf8Err",
|
||||
Self::UuidErr { .. } => "UuidErr",
|
||||
Self::SqlErr { .. } => "SqlErr",
|
||||
Self::SerdeJson { .. } => "SerdeJson",
|
||||
Self::HexErr { .. } => "HexErr",
|
||||
Self::DatabaseMigration(_) => "DatabaseMigration",
|
||||
Self::ExitStatus(_, _) => "ExitStatus",
|
||||
Self::ExecutionRawError(_) => "ExecutionRawError",
|
||||
Self::BadGateway(_) => "BadGateway",
|
||||
Self::BadConfig(_) => "BadConfig",
|
||||
Self::ConnectingToDatabase(_) => "ConnectingToDatabase",
|
||||
Self::NotFound(_) => "NotFound",
|
||||
Self::NotAuthorized(_) => "NotAuthorized",
|
||||
Self::MetricNotFound(_) => "MetricNotFound",
|
||||
Self::PermissionDenied(_) => "PermissionDenied",
|
||||
_ => "InternalErr",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prettify_location(location: &'static Location<'static>) -> String {
|
||||
location
|
||||
.to_string()
|
||||
|
||||
@@ -97,6 +97,8 @@ pub struct QueuedJob {
|
||||
pub permissioned_as: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub flow_status: Option<Json<Box<RawValue>>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub workflow_as_code_status: Option<Json<Box<RawValue>>>,
|
||||
pub is_flow_step: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub language: Option<ScriptLang>,
|
||||
@@ -179,6 +181,7 @@ impl Default for QueuedJob {
|
||||
job_kind: JobKind::Identity,
|
||||
schedule_path: None,
|
||||
permissioned_as: "".to_string(),
|
||||
workflow_as_code_status: None,
|
||||
flow_status: None,
|
||||
is_flow_step: false,
|
||||
language: None,
|
||||
@@ -236,6 +239,8 @@ pub struct CompletedJob {
|
||||
pub permissioned_as: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub flow_status: Option<sqlx::types::Json<Box<RawValue>>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub workflow_as_code_status: Option<sqlx::types::Json<Box<RawValue>>>,
|
||||
pub is_flow_step: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub language: Option<ScriptLang>,
|
||||
@@ -658,16 +663,10 @@ pub async fn check_tag_available_for_workspace_internal(
|
||||
}
|
||||
|
||||
let custom_tags_per_w = CUSTOM_TAGS_PER_WORKSPACE.read().await;
|
||||
if custom_tags_per_w.0.contains(&tag.to_string()) {
|
||||
is_tag_in_workspace_custom_tags = true;
|
||||
} else if custom_tags_per_w.1.contains_key(tag)
|
||||
&& custom_tags_per_w
|
||||
.1
|
||||
.get(tag)
|
||||
.unwrap()
|
||||
.contains(&w_id.to_string())
|
||||
{
|
||||
if custom_tags_per_w.global.contains(&tag.to_string()) {
|
||||
is_tag_in_workspace_custom_tags = true;
|
||||
} else if let Some(specific_tag) = custom_tags_per_w.specific.get(tag) {
|
||||
is_tag_in_workspace_custom_tags = specific_tag.applies_to_workspace(w_id);
|
||||
}
|
||||
|
||||
match is_tag_in_scope_tags {
|
||||
|
||||
@@ -82,6 +82,7 @@ pub mod variables;
|
||||
pub mod worker;
|
||||
pub mod workspaces;
|
||||
pub mod triggers;
|
||||
pub mod stream;
|
||||
|
||||
pub const DEFAULT_MAX_CONNECTIONS_SERVER: u32 = 50;
|
||||
pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 5;
|
||||
|
||||
@@ -6,7 +6,7 @@ pub use crate::stats_ee::*;
|
||||
use sqlx::Postgres;
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
use crate::{error::Result, scripts::ScriptLang, DB};
|
||||
use crate::{error::Result, DB};
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub async fn get_disable_stats_setting(_db: &DB) -> bool {
|
||||
@@ -20,14 +20,6 @@ pub async fn schedule_stats(_db: &DB, _http_client: &reqwest::Client) -> () {
|
||||
// stats details are closed source
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
#[derive(Debug, sqlx::FromRow, serde::Serialize)]
|
||||
struct JobsUsage {
|
||||
language: Option<ScriptLang>,
|
||||
total_duration: i64,
|
||||
count: i64,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub enum SendStatsReason {
|
||||
Manual,
|
||||
|
||||
42
backend/windmill-common/src/stream.rs
Normal file
42
backend/windmill-common/src/stream.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use futures::{Stream, TryStreamExt};
|
||||
|
||||
pub async fn collect_stream_with_limits<T, S>(
|
||||
mut row_stream: S,
|
||||
max_size_megabytes: usize,
|
||||
estimate_row_size: fn(&T) -> anyhow::Result<usize>,
|
||||
cutoff_time: chrono::TimeDelta,
|
||||
) -> Result<Vec<T>, anyhow::Error>
|
||||
where
|
||||
S: Stream<Item = Result<T, sqlx::Error>> + Unpin,
|
||||
{
|
||||
let started_time = chrono::Utc::now();
|
||||
let mut results = Vec::new();
|
||||
let mut total_size = 0;
|
||||
let max_size_bytes = max_size_megabytes * 1024 * 1024;
|
||||
|
||||
while let Some(row) = row_stream.try_next().await? {
|
||||
let row_size = estimate_row_size(&row)?;
|
||||
|
||||
results.push(row);
|
||||
total_size += row_size;
|
||||
if total_size + row_size > max_size_bytes {
|
||||
tracing::info!(
|
||||
"Stream was cutoff at {} elements because the collected size reached the treshold of {} MB",
|
||||
results.len(),
|
||||
max_size_megabytes
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if chrono::Utc::now() - started_time > cutoff_time {
|
||||
tracing::info!(
|
||||
"Stream was cutoff at {} elements because collecting it was taking longer than {}s",
|
||||
results.len(),
|
||||
cutoff_time.num_seconds()
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
@@ -198,20 +198,13 @@ fn instance_name(hostname: &str) -> String {
|
||||
const DEFAULT_WORKER_SUFFIX_LEN: usize = 5;
|
||||
pub const SSH_AGENT_WORKER_SUFFIX: &'static str = "/ssh";
|
||||
|
||||
pub fn create_worker_suffix(hostname: &str, rd_string_len: usize, ssh_ag_worker: bool) -> String {
|
||||
let mut wk_suffix = format!("{}-{}", instance_name(hostname), rd_string(rd_string_len));
|
||||
if ssh_ag_worker {
|
||||
wk_suffix.push_str(SSH_AGENT_WORKER_SUFFIX);
|
||||
}
|
||||
pub fn create_worker_suffix(hostname: &str, rd_string_len: usize) -> String {
|
||||
let wk_suffix = format!("{}-{}", instance_name(hostname), rd_string(rd_string_len));
|
||||
wk_suffix
|
||||
}
|
||||
|
||||
pub fn create_ssh_agent_worker_suffix(hostname: &str) -> String {
|
||||
create_worker_suffix(hostname, DEFAULT_WORKER_SUFFIX_LEN, true)
|
||||
}
|
||||
|
||||
pub fn create_default_worker_suffix(hostname: &str) -> String {
|
||||
create_worker_suffix(hostname, DEFAULT_WORKER_SUFFIX_LEN, false)
|
||||
create_worker_suffix(hostname, DEFAULT_WORKER_SUFFIX_LEN)
|
||||
}
|
||||
|
||||
pub fn worker_name_with_suffix(is_agent: bool, worker_group: &str, suffix: &str) -> String {
|
||||
@@ -222,6 +215,14 @@ pub fn worker_name_with_suffix(is_agent: bool, worker_group: &str, suffix: &str)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn retrieve_common_worker_prefix(worker_name: &str) -> String {
|
||||
let (prefix, _) = worker_name.rsplit_once('-').unzip();
|
||||
|
||||
prefix
|
||||
.expect("Invalid worker_name: expected at least one '-' in the name")
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub fn paginate(pagination: Pagination) -> (usize, usize) {
|
||||
let per_page = pagination
|
||||
.per_page
|
||||
|
||||
@@ -35,8 +35,105 @@ use crate::{
|
||||
KillpillSender, DB,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct CustomTags {
|
||||
pub global: Vec<String>,
|
||||
pub specific: HashMap<String, SpecificTagData>,
|
||||
}
|
||||
|
||||
impl CustomTags {
|
||||
pub fn from(tags: Vec<String>) -> Self {
|
||||
let mut global = vec![];
|
||||
let mut specific: HashMap<String, SpecificTagData> = HashMap::new();
|
||||
for e in tags {
|
||||
if let Some(cap) = CUSTOM_TAG_REGEX.captures(&e) {
|
||||
let tag_name = cap.get(1).unwrap().as_str().to_string();
|
||||
let workspace_str = cap.get(2).unwrap().as_str();
|
||||
let tag_type = SpecificTagType::from_regex_string(workspace_str);
|
||||
let workspaces: Vec<String> = workspace_str
|
||||
.split(tag_type.corresponding_separator())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(str::to_string)
|
||||
.collect();
|
||||
if workspaces.is_empty() {
|
||||
tracing::warn!("Ignoring tag `{}` with empty exclusion/inclusion list", e);
|
||||
global.push(e);
|
||||
continue;
|
||||
}
|
||||
specific.insert(tag_name, SpecificTagData { tag_type, workspaces });
|
||||
} else {
|
||||
global.push(e.to_string());
|
||||
}
|
||||
}
|
||||
Self { global, specific }
|
||||
}
|
||||
|
||||
pub fn to_string_vec(&self, filter_with_workspace: Option<String>) -> Vec<String> {
|
||||
let specific = if let Some(workspace) = filter_with_workspace {
|
||||
self.specific
|
||||
.iter()
|
||||
.filter(|(_, tag_data)| tag_data.applies_to_workspace(&workspace))
|
||||
.map(|(tag, _)| tag.clone())
|
||||
.collect::<Vec<String>>()
|
||||
} else {
|
||||
self.specific
|
||||
.iter()
|
||||
.map(|(tag, tag_data)| {
|
||||
let separator = tag_data.tag_type.corresponding_separator();
|
||||
let mut workspaces = tag_data.workspaces.join(&*separator.to_string());
|
||||
if tag_data.tag_type == SpecificTagType::AllExcluding {
|
||||
// the AllExcluding tag syntax has a leading separator
|
||||
workspaces.insert(0, separator);
|
||||
}
|
||||
format!("{}({})", tag, workspaces)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
};
|
||||
let all_tags = self.global.clone();
|
||||
all_tags.into_iter().chain(specific.into_iter()).collect()
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SpecificTagData {
|
||||
pub tag_type: SpecificTagType,
|
||||
pub workspaces: Vec<String>,
|
||||
}
|
||||
|
||||
impl SpecificTagData {
|
||||
pub fn applies_to_workspace(&self, workspace_id: &str) -> bool {
|
||||
match self.tag_type {
|
||||
SpecificTagType::AllExcluding => !self.workspaces.contains(&workspace_id.to_string()),
|
||||
SpecificTagType::NoneExcept => self.workspaces.contains(&workspace_id.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum SpecificTagType {
|
||||
AllExcluding,
|
||||
NoneExcept,
|
||||
}
|
||||
|
||||
impl SpecificTagType {
|
||||
pub fn corresponding_separator(&self) -> char {
|
||||
match self {
|
||||
SpecificTagType::AllExcluding => '^',
|
||||
SpecificTagType::NoneExcept => '+',
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_regex_string(workspaces_str: &str) -> Self {
|
||||
if workspaces_str.contains(SpecificTagType::AllExcluding.corresponding_separator()) {
|
||||
SpecificTagType::AllExcluding
|
||||
} else {
|
||||
// Regex match ensures the second branch is correct
|
||||
SpecificTagType::NoneExcept
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const DEFAULT_CLOUD_TIMEOUT: u64 = 900;
|
||||
pub const DEFAULT_SELFHOSTED_TIMEOUT: u64 = 604800; // 7 days
|
||||
pub const MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS: u64 = 60;
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref WORKER_GROUP: String = std::env::var("WORKER_GROUP").unwrap_or_else(|_| {
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
@@ -107,6 +204,8 @@ lazy_static::lazy_static! {
|
||||
dedicated_worker: Default::default(),
|
||||
cache_clear: Default::default(),
|
||||
init_bash: Default::default(),
|
||||
periodic_script_bash: Default::default(),
|
||||
periodic_script_interval_seconds: Default::default(),
|
||||
additional_python_paths: Default::default(),
|
||||
pip_local_dependencies: Default::default(),
|
||||
env_vars: Default::default(),
|
||||
@@ -127,11 +226,21 @@ lazy_static::lazy_static! {
|
||||
.map(|x| x.split(',').map(|x| x.to_string()).collect::<Vec<_>>()).unwrap_or_default();
|
||||
|
||||
|
||||
pub static ref CUSTOM_TAGS_PER_WORKSPACE: Arc<RwLock<(Vec<String>, HashMap<String, Vec<String>>)>> = Arc::new(RwLock::new((vec![], HashMap::new())));
|
||||
pub static ref CUSTOM_TAGS_PER_WORKSPACE: Arc<RwLock<CustomTags>> = Arc::new(RwLock::new(CustomTags::default()));
|
||||
|
||||
pub static ref ALL_TAGS: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(vec![]));
|
||||
|
||||
static ref CUSTOM_TAG_REGEX: Regex = Regex::new(r"^([\w-]+)\(((?:[\w-]+\+)*[\w-]+)\)$").unwrap();
|
||||
|
||||
|
||||
// ^([\w-]+) # Group 1: tag name
|
||||
// \( # Literal '('
|
||||
// ( # Group 2: the full workspace list
|
||||
// (?:[\w-]+\+)*[\w-]+ # NoneExcept pattern: ws1+ws2
|
||||
// | # OR
|
||||
// (?:\^[\w-]+)+ # AllExcluding pattern: ^ws1^ws2
|
||||
// )
|
||||
// \)$ # Closing ')'
|
||||
static ref CUSTOM_TAG_REGEX: Regex = Regex::new(r"^([\w-]+)\(((?:[\w-]+\+)*[\w-]+|(?:\^[\w-]+)+)\)$").unwrap();
|
||||
|
||||
pub static ref DISABLE_BUNDLING: bool = std::env::var("DISABLE_BUNDLING")
|
||||
.ok()
|
||||
@@ -470,12 +579,12 @@ pub async fn reload_custom_tags_setting(db: &DB) -> error::Result<()> {
|
||||
CUSTOM_TAGS.clone()
|
||||
};
|
||||
|
||||
let custom_tags = process_custom_tags(tags);
|
||||
let custom_tags = CustomTags::from(tags);
|
||||
|
||||
tracing::info!(
|
||||
"Loaded setting custom_tags, common: {:?}, per-workspace: {:?}",
|
||||
custom_tags.0,
|
||||
custom_tags.1,
|
||||
custom_tags.global,
|
||||
custom_tags.specific,
|
||||
);
|
||||
|
||||
{
|
||||
@@ -485,29 +594,18 @@ pub async fn reload_custom_tags_setting(db: &DB) -> error::Result<()> {
|
||||
{
|
||||
let mut l = ALL_TAGS.write().await;
|
||||
*l = [
|
||||
custom_tags.0.clone(),
|
||||
custom_tags.1.keys().map(|x| x.to_string()).collect_vec(),
|
||||
custom_tags.global.clone(),
|
||||
custom_tags
|
||||
.specific
|
||||
.keys()
|
||||
.map(|x| x.to_string())
|
||||
.collect_vec(),
|
||||
]
|
||||
.concat();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_custom_tags(tags: Vec<String>) -> (Vec<String>, HashMap<String, Vec<String>>) {
|
||||
let mut global = vec![];
|
||||
let mut specific: HashMap<String, Vec<String>> = HashMap::new();
|
||||
for e in tags {
|
||||
if let Some(cap) = CUSTOM_TAG_REGEX.captures(&e) {
|
||||
let tag = cap.get(1).unwrap().as_str().to_string();
|
||||
let workspaces = cap.get(2).unwrap().as_str().split("+");
|
||||
specific.insert(tag, workspaces.map(|x| x.to_string()).collect_vec());
|
||||
} else {
|
||||
global.push(e.to_string());
|
||||
}
|
||||
}
|
||||
(global, specific)
|
||||
}
|
||||
|
||||
fn parse_file<T: FromStr>(path: &str) -> Option<T> {
|
||||
std::process::Command::new("cat")
|
||||
.args([path])
|
||||
@@ -1433,6 +1531,34 @@ pub async fn load_worker_config(
|
||||
&env_vars_static,
|
||||
);
|
||||
|
||||
let periodic_script_bash = config
|
||||
.periodic_script_bash
|
||||
.or_else(|| load_periodic_bash_script_from_env())
|
||||
.and_then(|x| if x.is_empty() { None } else { Some(x) });
|
||||
let periodic_script_interval_seconds = config
|
||||
.periodic_script_interval_seconds
|
||||
.or_else(|| load_periodic_bash_script_interval_from_env());
|
||||
|
||||
if periodic_script_bash.is_some() {
|
||||
if let Some(interval) = periodic_script_interval_seconds {
|
||||
if interval < MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS {
|
||||
killpill_tx.send();
|
||||
return Err(anyhow::anyhow!(
|
||||
"Periodic script interval must be at least {} seconds, got {} seconds",
|
||||
MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS,
|
||||
interval
|
||||
)
|
||||
.into());
|
||||
}
|
||||
} else {
|
||||
killpill_tx.send();
|
||||
return Err(anyhow::anyhow!(
|
||||
"Periodic script interval must be specified when periodic script is configured"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(WorkerConfig {
|
||||
worker_tags,
|
||||
priority_tags_sorted,
|
||||
@@ -1441,6 +1567,8 @@ pub async fn load_worker_config(
|
||||
.init_bash
|
||||
.or_else(|| load_init_bash_from_env())
|
||||
.and_then(|x| if x.is_empty() { None } else { Some(x) }),
|
||||
periodic_script_bash,
|
||||
periodic_script_interval_seconds,
|
||||
cache_clear: config.cache_clear,
|
||||
pip_local_dependencies: config
|
||||
.pip_local_dependencies
|
||||
@@ -1458,6 +1586,18 @@ pub fn load_init_bash_from_env() -> Option<String> {
|
||||
.and_then(|x| if x.is_empty() { None } else { Some(x) })
|
||||
}
|
||||
|
||||
pub fn load_periodic_bash_script_from_env() -> Option<String> {
|
||||
std::env::var("PERIODIC_BASH_SCRIPT")
|
||||
.ok()
|
||||
.and_then(|x| if x.is_empty() { None } else { Some(x) })
|
||||
}
|
||||
|
||||
pub fn load_periodic_bash_script_interval_from_env() -> Option<u64> {
|
||||
std::env::var("PERIODIC_BASH_SCRIPT_INTERVAL_SECONDS")
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<u64>().ok())
|
||||
}
|
||||
|
||||
pub fn load_pip_local_dependencies_from_env() -> Option<Vec<String>> {
|
||||
std::env::var("PIP_LOCAL_DEPENDENCIES")
|
||||
.ok()
|
||||
@@ -1510,6 +1650,8 @@ pub struct WorkerConfigOpt {
|
||||
pub priority_tags: Option<HashMap<String, u8>>,
|
||||
pub dedicated_worker: Option<String>,
|
||||
pub init_bash: Option<String>,
|
||||
pub periodic_script_bash: Option<String>,
|
||||
pub periodic_script_interval_seconds: Option<u64>,
|
||||
pub cache_clear: Option<u32>,
|
||||
pub additional_python_paths: Option<Vec<String>>,
|
||||
pub pip_local_dependencies: Option<Vec<String>>,
|
||||
@@ -1524,6 +1666,8 @@ impl Default for WorkerConfigOpt {
|
||||
priority_tags: Default::default(),
|
||||
dedicated_worker: Default::default(),
|
||||
init_bash: Default::default(),
|
||||
periodic_script_bash: Default::default(),
|
||||
periodic_script_interval_seconds: Default::default(),
|
||||
cache_clear: Default::default(),
|
||||
additional_python_paths: Default::default(),
|
||||
pip_local_dependencies: Default::default(),
|
||||
@@ -1539,6 +1683,8 @@ pub struct WorkerConfig {
|
||||
pub priority_tags_sorted: Vec<PriorityTags>,
|
||||
pub dedicated_worker: Option<WorkspacedPath>,
|
||||
pub init_bash: Option<String>,
|
||||
pub periodic_script_bash: Option<String>,
|
||||
pub periodic_script_interval_seconds: Option<u64>,
|
||||
pub cache_clear: Option<u32>,
|
||||
pub additional_python_paths: Option<Vec<String>>,
|
||||
pub pip_local_dependencies: Option<Vec<String>>,
|
||||
@@ -1547,8 +1693,8 @@ pub struct WorkerConfig {
|
||||
|
||||
impl std::fmt::Debug for WorkerConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "WorkerConfig {{ worker_tags: {:?}, priority_tags_sorted: {:?}, dedicated_worker: {:?}, init_bash: {:?}, cache_clear: {:?}, additional_python_paths: {:?}, pip_local_dependencies: {:?}, env_vars: {:?} }}",
|
||||
self.worker_tags, self.priority_tags_sorted, self.dedicated_worker, self.init_bash, self.cache_clear, self.additional_python_paths, self.pip_local_dependencies, self.env_vars.iter().map(|(k, v)| format!("{}: {}{} ({} chars)", k, &v[..3.min(v.len())], "***", v.len())).collect::<Vec<String>>().join(", "))
|
||||
write!(f, "WorkerConfig {{ worker_tags: {:?}, priority_tags_sorted: {:?}, dedicated_worker: {:?}, init_bash: {:?}, periodic_script_bash: {:?}, periodic_script_interval_seconds: {:?}, cache_clear: {:?}, additional_python_paths: {:?}, pip_local_dependencies: {:?}, env_vars: {:?} }}",
|
||||
self.worker_tags, self.priority_tags_sorted, self.dedicated_worker, self.init_bash, self.periodic_script_bash, self.periodic_script_interval_seconds, self.cache_clear, self.additional_python_paths, self.pip_local_dependencies, self.env_vars.iter().map(|(k, v)| format!("{}: {}{} ({} chars)", k, &v[..3.min(v.len())], "***", v.len())).collect::<Vec<String>>().join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1567,3 +1713,138 @@ pub fn to_raw_value_owned(result: serde_json::Value) -> Box<RawValue> {
|
||||
serde_json::value::to_raw_value(&result)
|
||||
.unwrap_or_else(|_| RawValue::from_string("{}".to_string()).unwrap())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn test_mixed_tags() {
|
||||
let input = vec![
|
||||
"global".to_string(),
|
||||
"feat(ws1+ws2)".to_string(),
|
||||
"hotfix(^ws3^ws4)".to_string(),
|
||||
];
|
||||
let result = CustomTags::from(input);
|
||||
|
||||
assert_eq!(result.global, vec!["global"]);
|
||||
|
||||
let mut expected = HashMap::new();
|
||||
expected.insert(
|
||||
"feat".to_string(),
|
||||
SpecificTagData {
|
||||
tag_type: SpecificTagType::NoneExcept,
|
||||
workspaces: vec!["ws1".to_string(), "ws2".to_string()],
|
||||
},
|
||||
);
|
||||
expected.insert(
|
||||
"hotfix".to_string(),
|
||||
SpecificTagData {
|
||||
tag_type: SpecificTagType::AllExcluding,
|
||||
workspaces: vec!["ws3".to_string(), "ws4".to_string()],
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(result.specific, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_specific_tag_format() {
|
||||
let input = vec!["invalid(custom+format".to_string()];
|
||||
let result = CustomTags::from(input);
|
||||
|
||||
// Regex does not match, so it's treated as a global tag.
|
||||
assert_eq!(result.global, vec!["invalid(custom+format"]);
|
||||
assert!(result.specific.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_input() {
|
||||
let input: Vec<String> = vec![];
|
||||
let result = CustomTags::from(input);
|
||||
assert!(result.global.is_empty());
|
||||
assert!(result.specific.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_tags_from_parses_global_tags_correctly() {
|
||||
let input = vec!["frontend".to_string(), "urgent".to_string()];
|
||||
let tags = CustomTags::from(input.clone());
|
||||
assert_eq!(tags.global, input);
|
||||
assert!(tags.specific.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_tags_from_parses_specific_tags_none_except() {
|
||||
let input = vec!["urgent(ws1+ws2)".to_string()];
|
||||
let tags = CustomTags::from(input.clone());
|
||||
|
||||
assert!(tags.global.is_empty());
|
||||
assert_eq!(tags.specific.len(), 1);
|
||||
|
||||
let data = tags.specific.get("urgent").unwrap();
|
||||
assert_eq!(data.tag_type, SpecificTagType::NoneExcept);
|
||||
assert_eq!(data.workspaces, vec!["ws1", "ws2"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_tags_from_parses_specific_tags_all_excluding() {
|
||||
let input = vec!["legacy(^ws1^ws2)".to_string()];
|
||||
let tags = CustomTags::from(input.clone());
|
||||
|
||||
assert!(tags.global.is_empty());
|
||||
assert_eq!(tags.specific.len(), 1);
|
||||
|
||||
let data = tags.specific.get("legacy").unwrap();
|
||||
assert_eq!(data.tag_type, SpecificTagType::AllExcluding);
|
||||
assert_eq!(data.workspaces, vec!["ws1", "ws2"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_tags_from_ignores_empty_workspace_list_as_global() {
|
||||
let input = vec!["foo()".to_string(), "bar(^)".to_string()];
|
||||
let tags = CustomTags::from(input.clone());
|
||||
|
||||
assert_eq!(tags.global, input);
|
||||
assert!(tags.specific.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_tags_from_filters_specific_tags_by_workspace_none_except() {
|
||||
let input = vec!["urgent(ws1+ws2)".to_string()];
|
||||
let tags = CustomTags::from(input);
|
||||
|
||||
let output = tags.to_string_vec(Some("ws1".to_string()));
|
||||
assert_eq!(output, vec!["urgent"]);
|
||||
|
||||
let output_none = tags.to_string_vec(Some("ws3".to_string()));
|
||||
assert!(output_none.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_tags_from_filters_specific_tags_by_workspace_all_excluding() {
|
||||
let input = vec!["legacy(^ws1^ws2)".to_string()];
|
||||
let tags = CustomTags::from(input);
|
||||
|
||||
let output = tags.to_string_vec(Some("ws3".to_string()));
|
||||
assert_eq!(output, vec!["legacy"]);
|
||||
|
||||
let output_excluded = tags.to_string_vec(Some("ws1".to_string()));
|
||||
assert!(output_excluded.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_tags_from_reconstructs_all_tags_when_no_filter() {
|
||||
let input = vec![
|
||||
"foo".to_string(),
|
||||
"urgent(ws1+ws2)".to_string(),
|
||||
"legacy(^ws1^ws2)".to_string(),
|
||||
];
|
||||
let tags = CustomTags::from(input);
|
||||
|
||||
let mut result = tags.to_string_vec(None);
|
||||
result.sort();
|
||||
assert_eq!(result, vec!["foo", "legacy(^ws1^ws2)", "urgent(ws1+ws2)"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,12 @@ edition.workspace = true
|
||||
name = "windmill_indexer"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "windmill_indexer_debug"
|
||||
path = "./src/main.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["enterprise", "private", "parquet"]
|
||||
parquet = ["dep:object_store"]
|
||||
private = []
|
||||
enterprise = []
|
||||
@@ -32,3 +36,5 @@ object_store = { workspace = true, optional = true}
|
||||
tokio-tar.workspace = true
|
||||
lazy_static.workspace = true
|
||||
const_format.workspace = true
|
||||
flume.workspace = true
|
||||
gethostname.workspace = true
|
||||
|
||||
131
backend/windmill-indexer/src/main.rs
Normal file
131
backend/windmill-indexer/src/main.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use anyhow::anyhow;
|
||||
use const_format::concatcp;
|
||||
use gethostname::gethostname;
|
||||
use std::{str::FromStr, sync::{Arc, Mutex}};
|
||||
use tantivy::{directory::MmapDirectory, schema::Schema};
|
||||
use windmill_common::{
|
||||
indexer::TantivyIndexerSettings, utils::{rd_string, Mode}, worker::TMP_DIR,
|
||||
};
|
||||
use windmill_indexer::{
|
||||
completed_runs_ee::{
|
||||
fill_schema, IndexWriter, IndexedJobTrackerData, INDEXED_JOB_TRACKER_FILENAME,
|
||||
},
|
||||
indexer_ee::{clean_dir_all_if_exists, IndexedDBTracker, S3BackedMMapDirectory},
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref HOSTNAME :String = std::env::var("FORCE_HOSTNAME").unwrap_or_else(|_| {
|
||||
gethostname()
|
||||
.to_str()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| rd_string(5))
|
||||
});
|
||||
}
|
||||
|
||||
const S3_JOB_INDEX_PATH: &str = "search-dbg/completed_job_index";
|
||||
const JOB_INDEX_DIRECTORY_PATH: &str = concatcp!(TMP_DIR, "/search-dbg/completed_jobs_index");
|
||||
|
||||
fn init_index() -> anyhow::Result<IndexWriter> {
|
||||
let mut schema_builder = Schema::builder();
|
||||
let fields = fill_schema(&mut schema_builder);
|
||||
let schema = schema_builder.build();
|
||||
|
||||
std::fs::create_dir_all(JOB_INDEX_DIRECTORY_PATH)?;
|
||||
|
||||
let idx_tantivy_dir = MmapDirectory::open(JOB_INDEX_DIRECTORY_PATH)
|
||||
.map_err(|e| anyhow!("Failed to create MMapDirectory: {e}"))?;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
let idx_tantivy_dir = S3BackedMMapDirectory::new(
|
||||
std::path::PathBuf::from_str(JOB_INDEX_DIRECTORY_PATH).map_err(|e| anyhow!("{}", e))?,
|
||||
idx_tantivy_dir,
|
||||
S3_JOB_INDEX_PATH.to_string(),
|
||||
tokio::runtime::Handle::current(),
|
||||
);
|
||||
|
||||
clean_dir_all_if_exists(JOB_INDEX_DIRECTORY_PATH)?;
|
||||
let index = tantivy::Index::open_or_create(idx_tantivy_dir, schema)
|
||||
.map_err(|e| anyhow!("Failed to open index {e}"))?;
|
||||
|
||||
let job_tracker: IndexedDBTracker<IndexedJobTrackerData> = IndexedDBTracker::new(
|
||||
std::path::PathBuf::from_str(INDEXED_JOB_TRACKER_FILENAME)
|
||||
.map_err(|e| anyhow!("Failed to read {}: {e}", INDEXED_JOB_TRACKER_FILENAME))?,
|
||||
index.directory().clone(),
|
||||
);
|
||||
|
||||
Ok(IndexWriter { index, fields, job_tracker })
|
||||
}
|
||||
#[tokio::main]
|
||||
pub async fn main() -> anyhow::Result<()> {
|
||||
let hostname = HOSTNAME.to_owned();
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info")
|
||||
}
|
||||
|
||||
let (_guard, _jaja) = windmill_common::tracing_init::initialize_tracing(&hostname, &Mode::Indexer, "");
|
||||
|
||||
let db = windmill_common::initial_connection().await?;
|
||||
|
||||
let mut idx_writer = init_index()?;
|
||||
|
||||
let max_created_at = std::env::var("INDEX_STARTING_FROM")
|
||||
.ok()
|
||||
.map(|start_from| chrono::DateTime::parse_from_rfc3339(&start_from))
|
||||
.transpose()
|
||||
.map_err(|e| anyhow!("Failed to parse date for INDEX_STARTING_FROM"))?;
|
||||
|
||||
if let Some(max_created_at) = max_created_at {
|
||||
tracing::info!("Date to start indexing aka INDEX_STARTING_FROM is set to {:?}", max_created_at);
|
||||
idx_writer.job_tracker.data =
|
||||
Some(IndexedJobTrackerData { max_created_at: max_created_at.into(), queued_uuids_remainder: vec![] });
|
||||
} else {
|
||||
tracing::info!("INDEX_STARTING_FROM env var not found, indexing from the very first job");
|
||||
}
|
||||
|
||||
let writer = idx_writer
|
||||
.index
|
||||
.writer(300 * 1024 * 1024)?;
|
||||
|
||||
let w = Arc::new(Mutex::new(writer));
|
||||
idx_writer.refresh_jobs(w.clone(), &db, &TantivyIndexerSettings::default()).await?;
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if let Err(e) = Arc::try_unwrap(w)
|
||||
.map_err(|_| {
|
||||
anyhow!(
|
||||
"There was more than 1 refrence to the writer. This should not be possible."
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
.into_inner()
|
||||
.unwrap()
|
||||
.wait_merging_threads()
|
||||
{
|
||||
tracing::error!("Error while waiting for index writer merging threads: {e}");
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
tracing::info!("All merging threads completed, releasing lock");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// let (index_reader, index_writer) = {
|
||||
// let mut indexer_rx = killpill_rx.resubscribe();
|
||||
//
|
||||
// let (mut reader, mut writer) = (None, None);
|
||||
// tokio::select! {
|
||||
// _ = indexer_rx.recv() => {
|
||||
// tracing::info!("Received killpill, aborting index initialization");
|
||||
// },
|
||||
// res = windmill_indexer::completed_runs_oss::init_index(&db) => {
|
||||
// let res = res?;
|
||||
// reader = Some(res.0);
|
||||
// writer = Some(res.1);
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// (reader, writer)
|
||||
// };
|
||||
@@ -20,8 +20,8 @@ use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use serde::{ser::SerializeMap, Serialize};
|
||||
use serde_json::{json, value::RawValue};
|
||||
use sqlx::PgExecutor;
|
||||
use sqlx::{types::Json, FromRow, Pool, Postgres, Transaction};
|
||||
use sqlx::{Encode, PgExecutor};
|
||||
use sqlx::{types::Json, Pool, Postgres, Transaction};
|
||||
use tokio::{sync::RwLock, time::sleep};
|
||||
use ulid::Ulid;
|
||||
use uuid::Uuid;
|
||||
@@ -401,6 +401,13 @@ pub async fn append_logs(
|
||||
}
|
||||
}
|
||||
|
||||
pub const PERIODIC_SCRIPT_TAG: &str = "periodic_bash_script";
|
||||
pub const INIT_SCRIPT_TAG: &str = "init_script";
|
||||
pub const INIT_SCRIPT_PATH_PREFIX: &str = "init_script_";
|
||||
pub const PERIODIC_SCRIPT_PATH_PREFIX: &str = "periodic_script_";
|
||||
|
||||
|
||||
|
||||
pub async fn push_init_job<'c>(
|
||||
db: &Pool<Postgres>,
|
||||
content: String,
|
||||
@@ -415,7 +422,7 @@ pub async fn push_init_job<'c>(
|
||||
windmill_common::jobs::JobPayload::Code(windmill_common::jobs::RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: Some(format!("init_script_{worker_name}")),
|
||||
path: Some(format!("{INIT_SCRIPT_PATH_PREFIX}{worker_name}")),
|
||||
language: ScriptLang::Bash,
|
||||
lock: None,
|
||||
custom_concurrency_key: None,
|
||||
@@ -438,7 +445,56 @@ pub async fn push_init_job<'c>(
|
||||
true,
|
||||
None,
|
||||
true,
|
||||
Some("init_script".to_string()),
|
||||
Some(INIT_SCRIPT_TAG.to_string()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
inner_tx.commit().await?;
|
||||
Ok(uuid)
|
||||
}
|
||||
|
||||
pub async fn push_periodic_bash_job<'c>(
|
||||
db: &Pool<Postgres>,
|
||||
content: String,
|
||||
worker_name: &str,
|
||||
) -> error::Result<Uuid> {
|
||||
let tx = PushIsolationLevel::IsolatedRoot(db.clone());
|
||||
let ehm = HashMap::new();
|
||||
let timestamp = chrono::Utc::now().timestamp();
|
||||
let (uuid, inner_tx) = push(
|
||||
&db,
|
||||
tx,
|
||||
"admins",
|
||||
windmill_common::jobs::JobPayload::Code(windmill_common::jobs::RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: Some(format!("{PERIODIC_SCRIPT_PATH_PREFIX}{}_{}", worker_name, timestamp)),
|
||||
language: ScriptLang::Bash,
|
||||
lock: None,
|
||||
custom_concurrency_key: None,
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
cache_ttl: None,
|
||||
dedicated_worker: None,
|
||||
}),
|
||||
PushArgs::from(&ehm),
|
||||
worker_name,
|
||||
"worker@windmill.dev",
|
||||
SUPERADMIN_SECRET_EMAIL.to_string(),
|
||||
Some("worker_periodic_script_job"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
None,
|
||||
true,
|
||||
Some(PERIODIC_SCRIPT_TAG.to_string()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -517,6 +573,7 @@ pub struct WrappedError {
|
||||
pub trait ValidableJson {
|
||||
fn is_valid_json(&self) -> bool;
|
||||
fn wm_labels(&self) -> Option<Vec<String>>;
|
||||
fn size(&self) -> usize;
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
@@ -532,6 +589,10 @@ impl ValidableJson for WrappedError {
|
||||
fn wm_labels(&self) -> Option<Vec<String>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl ValidableJson for Box<RawValue> {
|
||||
@@ -544,6 +605,10 @@ impl ValidableJson for Box<RawValue> {
|
||||
.ok()
|
||||
.map(|r| r.wm_labels)
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
self.get().len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ValidableJson> ValidableJson for Arc<T> {
|
||||
@@ -554,6 +619,10 @@ impl<T: ValidableJson> ValidableJson for Arc<T> {
|
||||
fn wm_labels(&self) -> Option<Vec<String>> {
|
||||
T::wm_labels(&self)
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
T::size(&self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ValidableJson for serde_json::Value {
|
||||
@@ -566,6 +635,10 @@ impl ValidableJson for serde_json::Value {
|
||||
.ok()
|
||||
.map(|r| r.wm_labels)
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
self.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ValidableJson> ValidableJson for Json<T> {
|
||||
@@ -576,6 +649,10 @@ impl<T: ValidableJson> ValidableJson for Json<T> {
|
||||
fn wm_labels(&self) -> Option<Vec<String>> {
|
||||
self.0.wm_labels()
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
self.0.size()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn register_metric<T, F, F2, R>(
|
||||
@@ -662,6 +739,7 @@ pub async fn add_completed_job_error(
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE: Option<String> = std::env::var("GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE").ok();
|
||||
pub static ref MAX_RESULT_SIZE_MB: usize = std::env::var("MAX_RESULT_SIZE_MB").unwrap_or("500".to_string()).parse().unwrap_or(500);
|
||||
}
|
||||
|
||||
pub async fn add_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
@@ -697,15 +775,34 @@ pub async fn add_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
let job_id = queued_job.id;
|
||||
// tracing::error!("1 {:?}", start.elapsed());
|
||||
|
||||
tracing::debug!(
|
||||
"completed job {} {}",
|
||||
queued_job.id,
|
||||
serde_json::to_string(&result).unwrap_or_else(|_| "".to_string())
|
||||
);
|
||||
// tracing::debug!(
|
||||
// "completed job {} {}",
|
||||
// queued_job.id,
|
||||
// serde_json::to_string(&result).unwrap_or_else(|_| "".to_string())
|
||||
// );
|
||||
|
||||
let mem_peak = mem_peak;
|
||||
// add_time!(bench, "add_completed_job query START");
|
||||
|
||||
let result_size = result.size() / 1024 / 1024;
|
||||
if result_size > 2 {
|
||||
if result_size > *MAX_RESULT_SIZE_MB {
|
||||
tracing::error!("Result of job {} is too large: {}MB > MAX_RESULT_SIZE_MB={}MB", queued_job.id, result_size, *MAX_RESULT_SIZE_MB);
|
||||
return Err(Error::ResultTooLarge(format!("Result of job {} is too large: {}MB > MAX_RESULT_SIZE_MB={}MB.\nUse external storages such as the Windmill Object Storage to store large results: https://www.windmill.dev/docs/core_concepts/object_storage_in_windmill", queued_job.id, result_size, *MAX_RESULT_SIZE_MB)));
|
||||
}
|
||||
append_logs(
|
||||
&queued_job.id,
|
||||
&queued_job.workspace_id,
|
||||
format!("Warning: Result of job {} is large: {}MB.\nRecommended max size is 2MB.\nPrefer using external storages such as the Windmill Object Storage to store large results: https://www.windmill.dev/docs/core_concepts/object_storage_in_windmill", queued_job.id, result_size),
|
||||
&db.into(),
|
||||
)
|
||||
.await;
|
||||
if *CLOUD_HOSTED {
|
||||
return Err(Error::ResultTooLarge(format!("Result of job {} is too large for multi-tenant cloud: {}MB (max 2MB).\nUse external storages such as the Windmill Object Storage to store large results: https://www.windmill.dev/docs/core_concepts/object_storage_in_windmill", queued_job.id, result_size)));
|
||||
} else {
|
||||
tracing::warn!("Result of job {} is larger than 2MB: {}MB. Not recommended.", queued_job.id, result_size);
|
||||
}
|
||||
}
|
||||
let _duration = sqlx::query_scalar!(
|
||||
"INSERT INTO v2_job_completed AS cj
|
||||
( workspace_id
|
||||
@@ -996,7 +1093,7 @@ pub async fn add_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
.with_max_times(5)
|
||||
.build(),
|
||||
)
|
||||
.when(|err| !matches!(err, Error::QuotaExceeded(_)))
|
||||
.when(|err| !matches!(err, Error::QuotaExceeded(_)) && !matches!(err, Error::ResultTooLarge(_)))
|
||||
.notify(|err, dur| {
|
||||
tracing::error!(
|
||||
"Could not insert completed job, retrying in {dur:#?}, err: {err:#?}"
|
||||
@@ -1487,12 +1584,6 @@ pub async fn handle_maybe_scheduled_job<'c>(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, FromRow)]
|
||||
struct CompletedJobSubset {
|
||||
success: bool,
|
||||
result: Option<sqlx::types::Json<Box<RawValue>>>,
|
||||
started_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn apply_schedule_handlers<'a, 'c, T: Serialize + Send + Sync>(
|
||||
@@ -3145,7 +3236,7 @@ async fn get_queued_job_tx<'c>(
|
||||
tx: &mut Transaction<'c, Postgres>,
|
||||
) -> error::Result<Option<QueuedJob>> {
|
||||
sqlx::query_as::<_, QueuedJob>(
|
||||
"SELECT *
|
||||
"SELECT *, null as workflow_as_code_status
|
||||
FROM v2_as_queue WHERE id = $1 AND workspace_id = $2",
|
||||
)
|
||||
.bind(id)
|
||||
@@ -3157,7 +3248,7 @@ async fn get_queued_job_tx<'c>(
|
||||
|
||||
pub async fn get_queued_job(id: &Uuid, w_id: &str, db: &DB) -> error::Result<Option<QueuedJob>> {
|
||||
sqlx::query_as::<_, QueuedJob>(
|
||||
"SELECT *
|
||||
"SELECT *, null as workflow_as_code_status
|
||||
FROM v2_as_queue WHERE id = $1 AND workspace_id = $2",
|
||||
)
|
||||
.bind(id)
|
||||
@@ -4694,7 +4785,7 @@ async fn restarted_flows_resolution(
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SameWorkerPayload {
|
||||
pub job_id: Uuid,
|
||||
pub recoverable: bool,
|
||||
|
||||
@@ -14,6 +14,17 @@ pub async fn queue_init_job(client: &HttpClient, content: &str) -> anyhow::Resul
|
||||
.and_then(|x: String| Uuid::parse_str(&x).map_err(|e| anyhow::anyhow!(e)))
|
||||
}
|
||||
|
||||
pub async fn queue_periodic_job(client: &HttpClient, content: &str) -> anyhow::Result<Uuid> {
|
||||
client
|
||||
.post(
|
||||
"/api/agent_workers/queue_periodic_job",
|
||||
None,
|
||||
&QueueInitJob { content: content.to_string() },
|
||||
)
|
||||
.await
|
||||
.and_then(|x: String| Uuid::parse_str(&x).map_err(|e| anyhow::anyhow!(e)))
|
||||
}
|
||||
|
||||
pub async fn pull_job(
|
||||
client: &HttpClient,
|
||||
headers: Option<HeaderMap>,
|
||||
|
||||
@@ -21,7 +21,9 @@ use windmill_common::{
|
||||
#[cfg(feature = "dind")]
|
||||
use windmill_common::error::to_anyhow;
|
||||
|
||||
use windmill_queue::{append_logs, CanceledBy, MiniPulledJob};
|
||||
use windmill_queue::{
|
||||
append_logs, CanceledBy, MiniPulledJob, INIT_SCRIPT_PATH_PREFIX, PERIODIC_SCRIPT_PATH_PREFIX,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref BIN_BASH: String = std::env::var("BASH_PATH").unwrap_or_else(|_| "/bin/bash".to_string());
|
||||
@@ -163,7 +165,10 @@ exit $exit_status
|
||||
&& job
|
||||
.runnable_path
|
||||
.as_ref()
|
||||
.map(|x| !x.starts_with("init_script_"))
|
||||
.map(|x| {
|
||||
!x.starts_with(INIT_SCRIPT_PATH_PREFIX)
|
||||
&& !x.starts_with(PERIODIC_SCRIPT_PATH_PREFIX)
|
||||
})
|
||||
.unwrap_or(true);
|
||||
let child = if nsjail {
|
||||
let _ = write_file(
|
||||
@@ -367,6 +372,7 @@ async fn handle_docker_job(
|
||||
.await;
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
log = log_stream.next() => {
|
||||
match log {
|
||||
Some(Ok(log)) => {
|
||||
@@ -461,21 +467,22 @@ async fn handle_docker_job(
|
||||
rm_container(&client, &container_id).await;
|
||||
|
||||
return Err(e);
|
||||
} else {
|
||||
let _ = tokio::time::timeout(tokio::time::Duration::from_secs(5), logs).await;
|
||||
rm_container(&client, &container_id).await;
|
||||
|
||||
let result = result.unwrap();
|
||||
|
||||
if result.is_some_and(|x| x > 0) {
|
||||
return Err(Error::ExecutionErr(format!(
|
||||
"Docker job completed with unsuccessful exit status: {}",
|
||||
result.unwrap()
|
||||
)));
|
||||
}
|
||||
return Ok(to_raw_value(&json!(format!(
|
||||
"Docker job completed with success exit status"
|
||||
))));
|
||||
}
|
||||
|
||||
rm_container(&client, &container_id).await;
|
||||
|
||||
let result = result.unwrap();
|
||||
|
||||
if result.is_some_and(|x| x > 0) {
|
||||
return Err(Error::ExecutionErr(format!(
|
||||
"Docker job completed with unsuccessful exit status: {}",
|
||||
result.unwrap()
|
||||
)));
|
||||
}
|
||||
return Ok(to_raw_value(&json!(format!(
|
||||
"Docker job completed with success exit status"
|
||||
))));
|
||||
}
|
||||
|
||||
#[cfg(feature = "dind")]
|
||||
@@ -733,7 +740,16 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
|
||||
let _ = write_file(job_dir, "result.out", "")?;
|
||||
let _ = write_file(job_dir, "result2.out", "")?;
|
||||
|
||||
let child = if !*DISABLE_NSJAIL {
|
||||
let nsjail = !*DISABLE_NSJAIL
|
||||
&& job
|
||||
.runnable_path
|
||||
.as_ref()
|
||||
.map(|x| {
|
||||
!x.starts_with(INIT_SCRIPT_PATH_PREFIX)
|
||||
&& !x.starts_with(PERIODIC_SCRIPT_PATH_PREFIX)
|
||||
})
|
||||
.unwrap_or(true);
|
||||
let child = if nsjail {
|
||||
let _ = write_file(
|
||||
job_dir,
|
||||
"run.config.proto",
|
||||
@@ -797,6 +813,7 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
|
||||
#[cfg(windows)]
|
||||
{
|
||||
cmd.env("SystemRoot", SYSTEM_ROOT.as_str())
|
||||
.env("WINDIR", SYSTEM_ROOT.as_str())
|
||||
.env(
|
||||
"LOCALAPPDATA",
|
||||
std::env::var("LOCALAPPDATA")
|
||||
|
||||
@@ -524,7 +524,7 @@ pub async fn update_worker_ping_for_failed_init_script(
|
||||
pub fn error_to_value(err: Error) -> serde_json::Value {
|
||||
match err {
|
||||
Error::JsonErr(err) => err,
|
||||
_ => json!({"message": err.to_string(), "name": "InternalErr"}),
|
||||
_ => json!({"message": err.to_string(), "name": err.name()}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -149,7 +149,6 @@ pub async fn do_duckdb(
|
||||
.clone()
|
||||
.unwrap_or_else(|| "text".to_string())
|
||||
.as_str(),
|
||||
client,
|
||||
)?;
|
||||
m.insert(sig_arg.name, duckdb_value);
|
||||
}
|
||||
@@ -314,7 +313,6 @@ fn row_to_value(row: &Row<'_>, column_names: &[String]) -> Result<Box<RawValue>>
|
||||
fn json_value_to_duckdb_value(
|
||||
json_value: &serde_json::Value,
|
||||
arg_type: &str,
|
||||
client: &AuthedClient,
|
||||
) -> Result<duckdb::types::Value> {
|
||||
let arg_type = arg_type.to_lowercase();
|
||||
let duckdb_value = match json_value {
|
||||
@@ -372,22 +370,12 @@ fn json_value_to_duckdb_value(
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::Value::Array(arr) => duckdb::types::Value::Array(
|
||||
arr.iter()
|
||||
.map(|val| json_value_to_duckdb_value(val, arg_type.as_str(), client))
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
),
|
||||
serde_json::Value::Object(map) => duckdb::types::Value::Struct(
|
||||
map.iter()
|
||||
.map(|(k, v)| {
|
||||
Ok::<_, Error>((
|
||||
k.clone(),
|
||||
json_value_to_duckdb_value(v, arg_type.as_str(), client)?,
|
||||
))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.into(),
|
||||
),
|
||||
serde_json::Value::Array(arr) => {
|
||||
duckdb::types::Value::Text(serde_json::to_string(arr).map_err(to_anyhow)?)
|
||||
}
|
||||
serde_json::Value::Object(map) => {
|
||||
duckdb::types::Value::Text(serde_json::to_string(map).map_err(to_anyhow)?)
|
||||
}
|
||||
|
||||
value @ _ => {
|
||||
return Err(Error::ExecutionErr(format!(
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#[cfg(feature = "otel")]
|
||||
use opentelemetry::trace::FutureExt;
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::types::Json;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU16, Ordering},
|
||||
Arc,
|
||||
}
|
||||
},
|
||||
};
|
||||
use tracing::{field, Instrument};
|
||||
#[cfg(not(feature = "otel"))]
|
||||
@@ -17,7 +17,12 @@ use windmill_common::otel_oss::FutureExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
use windmill_common::{
|
||||
add_time, error::{self, Error}, jobs::JobKind, utils::WarnAfterExt, worker::{to_raw_value, Connection, WORKER_GROUP}, KillpillSender, DB
|
||||
add_time,
|
||||
error::{self, Error},
|
||||
jobs::JobKind,
|
||||
utils::WarnAfterExt,
|
||||
worker::{to_raw_value, Connection, WORKER_GROUP},
|
||||
KillpillSender, DB,
|
||||
};
|
||||
|
||||
#[cfg(feature = "benchmark")]
|
||||
@@ -25,6 +30,7 @@ use windmill_common::bench::{BenchmarkInfo, BenchmarkIter};
|
||||
|
||||
use windmill_queue::{
|
||||
append_logs, get_queued_job, CanceledBy, JobCompleted, MiniPulledJob, WrappedError,
|
||||
INIT_SCRIPT_TAG,
|
||||
};
|
||||
|
||||
use serde_json::{json, value::RawValue, Value};
|
||||
@@ -34,10 +40,21 @@ use tokio::{sync::Notify, task::JoinHandle};
|
||||
use windmill_queue::{add_completed_job, add_completed_job_error};
|
||||
|
||||
use crate::{
|
||||
bash_executor::ANSI_ESCAPE_RE, common::{error_to_value, read_result, save_in_cache}, otel_oss::add_root_flow_job_to_otlp, worker_flow::update_flow_status_after_job_completion, JobCompletedReceiver, JobCompletedSender, SameWorkerSender, SendResult, SendResultPayload, UpdateFlow, INIT_SCRIPT_TAG, SAME_WORKER_REQUIREMENTS
|
||||
bash_executor::ANSI_ESCAPE_RE,
|
||||
common::{error_to_value, read_result, save_in_cache},
|
||||
otel_oss::add_root_flow_job_to_otlp,
|
||||
worker_flow::update_flow_status_after_job_completion,
|
||||
JobCompletedReceiver, JobCompletedSender, SameWorkerSender, SendResult, SendResultPayload,
|
||||
UpdateFlow, SAME_WORKER_REQUIREMENTS,
|
||||
};
|
||||
use windmill_common::client::AuthedClient;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ErrorMessage {
|
||||
message: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
async fn process_jc(
|
||||
jc: JobCompleted,
|
||||
worker_name: &str,
|
||||
@@ -50,17 +67,35 @@ async fn process_jc(
|
||||
) {
|
||||
let success: bool = jc.success;
|
||||
|
||||
let span = tracing::span!(
|
||||
tracing::Level::INFO,
|
||||
"job_postprocessing",
|
||||
job_id = %jc.job.id, root_job = field::Empty, workspace_id = %jc.job.workspace_id, worker = %worker_name,tag = %jc.job.tag,
|
||||
// hostname = %hostname,
|
||||
language = field::Empty,
|
||||
script_path = field::Empty,
|
||||
flow_step_id = field::Empty,
|
||||
parent_job = field::Empty,
|
||||
otel.name = field::Empty
|
||||
);
|
||||
let span = if success {
|
||||
tracing::span!(
|
||||
tracing::Level::INFO,
|
||||
"job_postprocessing",
|
||||
job_id = %jc.job.id, root_job = field::Empty, workspace_id = %jc.job.workspace_id, worker = %worker_name,tag = %jc.job.tag,
|
||||
// hostname = %hostname,
|
||||
language = field::Empty,
|
||||
script_path = field::Empty,
|
||||
flow_step_id = field::Empty,
|
||||
parent_job = field::Empty,
|
||||
otel.name = field::Empty,
|
||||
success = %success,
|
||||
)
|
||||
} else {
|
||||
tracing::span!(
|
||||
tracing::Level::INFO,
|
||||
"job_postprocessing",
|
||||
job_id = %jc.job.id, root_job = field::Empty, workspace_id = %jc.job.workspace_id, worker = %worker_name,tag = %jc.job.tag,
|
||||
// hostname = %hostname,
|
||||
language = field::Empty,
|
||||
script_path = field::Empty,
|
||||
flow_step_id = field::Empty,
|
||||
parent_job = field::Empty,
|
||||
otel.name = field::Empty,
|
||||
success = %success,
|
||||
error.message = field::Empty,
|
||||
error.name = field::Empty,
|
||||
)
|
||||
};
|
||||
let rj = if let Some(root_job) = jc.job.flow_innermost_root_job {
|
||||
root_job
|
||||
} else {
|
||||
@@ -89,6 +124,12 @@ async fn process_jc(
|
||||
if let Some(root_job) = jc.job.flow_innermost_root_job.as_ref() {
|
||||
span.record("root_job", root_job.to_string().as_str());
|
||||
}
|
||||
if !success {
|
||||
if let Ok(result_error) = serde_json::from_str::<ErrorMessage>(jc.result.get()) {
|
||||
span.record("error.message", result_error.message.as_str());
|
||||
span.record("error.name", result_error.name.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
let root_job = handle_receive_completed_job(
|
||||
jc,
|
||||
@@ -173,7 +214,10 @@ pub fn start_background_processor(
|
||||
let mut bench = BenchmarkIter::new();
|
||||
|
||||
match sr {
|
||||
JobCompletedRx::JobCompleted(SendResult { result: SendResultPayload::JobCompleted(jc), time }) => {
|
||||
JobCompletedRx::JobCompleted(SendResult {
|
||||
result: SendResultPayload::JobCompleted(jc),
|
||||
time,
|
||||
}) => {
|
||||
let is_init_script_and_failure =
|
||||
!jc.success && jc.job.tag.as_str() == INIT_SCRIPT_TAG;
|
||||
let is_dependency_job = matches!(
|
||||
@@ -216,19 +260,24 @@ pub fn start_background_processor(
|
||||
{
|
||||
infos.add_iter(bench, true);
|
||||
}
|
||||
last_processing_duration.store(time.elapsed().as_secs() as u16, Ordering::SeqCst);
|
||||
last_processing_duration
|
||||
.store(time.elapsed().as_secs() as u16, Ordering::SeqCst);
|
||||
}
|
||||
JobCompletedRx::JobCompleted(SendResult { result: SendResultPayload::UpdateFlow(UpdateFlow {
|
||||
flow,
|
||||
w_id,
|
||||
success,
|
||||
result,
|
||||
worker_dir,
|
||||
stop_early_override,
|
||||
token,
|
||||
}), time}) => {
|
||||
JobCompletedRx::JobCompleted(SendResult {
|
||||
result:
|
||||
SendResultPayload::UpdateFlow(UpdateFlow {
|
||||
flow,
|
||||
w_id,
|
||||
success,
|
||||
result,
|
||||
worker_dir,
|
||||
stop_early_override,
|
||||
token,
|
||||
}),
|
||||
time,
|
||||
}) => {
|
||||
// let r;
|
||||
tracing::info!(parent_flow = %flow, "updating flow status");
|
||||
tracing::info!(parent_flow = %flow, "updating flow status after job completion");
|
||||
if let Err(e) = update_flow_status_after_job_completion(
|
||||
&db,
|
||||
&AuthedClient::new(
|
||||
@@ -255,14 +304,14 @@ pub fn start_background_processor(
|
||||
{
|
||||
tracing::error!("Error updating flow status after job completion for {flow} on {worker_name}: {e:#}");
|
||||
}
|
||||
last_processing_duration.store(time.elapsed().as_secs() as u16, Ordering::SeqCst);
|
||||
last_processing_duration
|
||||
.store(time.elapsed().as_secs() as u16, Ordering::SeqCst);
|
||||
}
|
||||
JobCompletedRx::Killpill => {
|
||||
tracing::info!("killpill job received, processing only same worker jobs");
|
||||
has_been_killed = true;
|
||||
},
|
||||
JobCompletedRx::WakeUp => {
|
||||
}
|
||||
JobCompletedRx::WakeUp => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,13 +644,14 @@ async fn handle_non_flow_job_error(
|
||||
job: &MiniPulledJob,
|
||||
mem_peak: i32,
|
||||
canceled_by: Option<CanceledBy>,
|
||||
err: Value,
|
||||
err_string: String,
|
||||
err_json: Value,
|
||||
worker_name: &str,
|
||||
) -> Result<WrappedError, Error> {
|
||||
append_logs(
|
||||
&job.id,
|
||||
&job.workspace_id,
|
||||
format!("Unexpected error during job execution:\n{err:#?}"),
|
||||
format!("Unexpected error during job execution:\n{err_string}"),
|
||||
&db.into(),
|
||||
)
|
||||
.await;
|
||||
@@ -610,7 +660,7 @@ async fn handle_non_flow_job_error(
|
||||
job,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
err,
|
||||
err_json,
|
||||
worker_name,
|
||||
false,
|
||||
None,
|
||||
@@ -633,7 +683,9 @@ pub async fn handle_job_error(
|
||||
job_completed_tx: JobCompletedSender,
|
||||
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
|
||||
) {
|
||||
let err = error_to_value(err);
|
||||
let err_string = format!("{}: {}", err.name(), err.to_string());
|
||||
let err_json = error_to_value(err);
|
||||
|
||||
|
||||
let update_job_future = || async {
|
||||
handle_non_flow_job_error(
|
||||
@@ -641,7 +693,8 @@ pub async fn handle_job_error(
|
||||
job,
|
||||
mem_peak,
|
||||
canceled_by.clone(),
|
||||
err.clone(),
|
||||
err_string,
|
||||
err_json.clone(),
|
||||
worker_name,
|
||||
)
|
||||
.await
|
||||
@@ -660,8 +713,8 @@ pub async fn handle_job_error(
|
||||
(job.id, Uuid::nil())
|
||||
};
|
||||
|
||||
let wrapped_error = WrappedError { error: err.clone() };
|
||||
tracing::error!(parent_flow = %flow, subflow = %job_status_to_update, "handle job error, updating flow status: {err:?}");
|
||||
let wrapped_error = WrappedError { error: err_json.clone() };
|
||||
tracing::error!(parent_flow = %flow, subflow = %job_status_to_update, "handle job error, updating flow status: {err_json:?}");
|
||||
let updated_flow = update_flow_status_after_job_completion(
|
||||
db,
|
||||
client,
|
||||
|
||||
@@ -46,11 +46,13 @@ fn make_rules_for_arg_typ(typ: &Typ) -> Vec<SchemaValidationRule> {
|
||||
Typ::Sql => {
|
||||
rules.push(SchemaValidationRule::IsString);
|
||||
}
|
||||
Typ::Object(props) => {
|
||||
Typ::Object(object_type) => {
|
||||
let mut obj_rules = vec![];
|
||||
|
||||
for prop in props {
|
||||
obj_rules.push((prop.key.to_string(), make_rules_for_arg_typ(&prop.typ)));
|
||||
if let Some(props) = &object_type.props {
|
||||
for prop in props {
|
||||
obj_rules.push((prop.key.to_string(), make_rules_for_arg_typ(&prop.typ)));
|
||||
}
|
||||
}
|
||||
|
||||
rules.push(SchemaValidationRule::IsObject(obj_rules))
|
||||
|
||||
@@ -13,6 +13,8 @@ use anyhow::anyhow;
|
||||
use futures::TryFutureExt;
|
||||
use tokio::time::timeout;
|
||||
use windmill_common::client::AuthedClient;
|
||||
use windmill_common::utils::report_critical_error;
|
||||
use windmill_common::utils::retrieve_common_worker_prefix;
|
||||
use windmill_common::{
|
||||
agent_workers::DECODED_AGENT_TOKEN,
|
||||
apps::AppScriptId,
|
||||
@@ -21,8 +23,8 @@ use windmill_common::{
|
||||
scripts::PREVIEW_IS_TAR_CODEBASE_HASH,
|
||||
utils::{create_directory_async, WarnAfterExt},
|
||||
worker::{
|
||||
make_pull_query, write_file, Connection, HttpClient, MAX_TIMEOUT, ROOT_CACHE_DIR,
|
||||
ROOT_CACHE_NOMOUNT_DIR, TMP_DIR,
|
||||
make_pull_query, write_file, Connection, HttpClient, MAX_TIMEOUT,
|
||||
MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS, ROOT_CACHE_DIR, ROOT_CACHE_NOMOUNT_DIR, TMP_DIR,
|
||||
},
|
||||
KillpillSender,
|
||||
};
|
||||
@@ -69,8 +71,9 @@ use windmill_common::{
|
||||
|
||||
use windmill_queue::{
|
||||
append_logs, canceled_job_to_result, empty_result, get_same_worker_job, pull, push_init_job,
|
||||
CanceledBy, JobAndPerms, JobCompleted, MiniPulledJob, PrecomputedAgentInfo, PulledJob,
|
||||
SameWorkerPayload, HTTP_CLIENT,
|
||||
push_periodic_bash_job, CanceledBy, JobAndPerms, JobCompleted, MiniPulledJob,
|
||||
PrecomputedAgentInfo, PulledJob, SameWorkerPayload, HTTP_CLIENT, INIT_SCRIPT_TAG,
|
||||
PERIODIC_SCRIPT_TAG,
|
||||
};
|
||||
|
||||
#[cfg(feature = "prometheus")]
|
||||
@@ -97,7 +100,7 @@ use tokio::{
|
||||
use rand::Rng;
|
||||
|
||||
use crate::{
|
||||
agent_workers::queue_init_job,
|
||||
agent_workers::{queue_init_job, queue_periodic_job},
|
||||
bash_executor::{handle_bash_job, handle_powershell_job},
|
||||
bun_executor::handle_bun_job,
|
||||
common::{
|
||||
@@ -424,8 +427,6 @@ impl std::ops::Deref for NextJob {
|
||||
//only matter if CLOUD_HOSTED
|
||||
pub const MAX_RESULT_SIZE: usize = 1024 * 1024 * 2; // 2MB
|
||||
|
||||
pub const INIT_SCRIPT_TAG: &str = "init_script";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SameWorkerSender(pub Sender<SameWorkerPayload>, pub Arc<AtomicU16>);
|
||||
|
||||
@@ -779,7 +780,8 @@ pub fn start_interactive_worker_shell(
|
||||
} else {
|
||||
let pulled_job = match &conn {
|
||||
Connection::Sql(db) => {
|
||||
let query = ("".to_string(), make_pull_query(&[hostname.to_owned()]));
|
||||
let common_worker_prefix = retrieve_common_worker_prefix(&worker_name);
|
||||
let query = ("".to_string(), make_pull_query(&[common_worker_prefix]));
|
||||
|
||||
#[cfg(feature = "benchmark")]
|
||||
let mut bench = windmill_common::bench::BenchmarkIter::new();
|
||||
@@ -1221,10 +1223,9 @@ pub async fn run_worker(
|
||||
)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// If we're the first worker to run, we start another background process that listens for a specific tag.
|
||||
// This tag is associated only with jobs using Bash as the script language.
|
||||
// For agent workers, the expected tag format is the worker name suffixed with "-ssh".
|
||||
// For regular workers, the tag is simply the machine's hostname and if not found the randomly generated hostname.
|
||||
// The tag itself is simply the worker’s common name (for example, wk-{worker_group}-{instance_name}).
|
||||
let interactive_shell = if i_worker == 1 {
|
||||
let it_shell = start_interactive_worker_shell(
|
||||
conn.clone(),
|
||||
@@ -1305,6 +1306,13 @@ pub async fn run_worker(
|
||||
tracing::error!(worker = %worker_name, hostname = %hostname, "Error queuing init bash script for worker {worker_name}: {e:#}");
|
||||
return;
|
||||
}
|
||||
|
||||
spawn_periodic_script_task(
|
||||
worker_name.clone(),
|
||||
conn.clone(),
|
||||
same_worker_tx.clone(),
|
||||
killpill_rx.resubscribe(),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "prometheus")]
|
||||
@@ -1780,6 +1788,7 @@ pub async fn run_worker(
|
||||
let tag = job.tag.clone();
|
||||
|
||||
let is_init_script: bool = job.tag.as_str() == INIT_SCRIPT_TAG;
|
||||
let is_periodic_bash_script: bool = job.tag.as_str() == PERIODIC_SCRIPT_TAG;
|
||||
let is_flow = job.is_flow();
|
||||
let job_id = job.id;
|
||||
|
||||
@@ -1835,7 +1844,43 @@ pub async fn run_worker(
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
Ok(false) if is_periodic_bash_script => {
|
||||
tracing::error!(
|
||||
"periodic script job failed. Check logs for job ID {} for details.",
|
||||
job_id
|
||||
);
|
||||
|
||||
if let Connection::Sql(db) = conn {
|
||||
report_critical_error(
|
||||
format!(
|
||||
"Periodic script job {} returned false (failed). Check logs for job ID {} for details.",
|
||||
job_id, job_id
|
||||
),
|
||||
db.clone(),
|
||||
Some(&arc_job.workspace_id),
|
||||
Some("periodic_script_job_failed"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
if is_periodic_bash_script {
|
||||
tracing::error!("periodic script job failed");
|
||||
|
||||
// Report critical error for periodic script failures
|
||||
if let Connection::Sql(db) = conn {
|
||||
report_critical_error(
|
||||
format!(
|
||||
"Periodic script job {} failed in worker {}: {}",
|
||||
job_id, worker_name, &err
|
||||
),
|
||||
db.clone(),
|
||||
Some(&arc_job.workspace_id),
|
||||
Some("periodic_script_job_failed"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
handle_all_job_kind_error(
|
||||
&conn,
|
||||
&authed_client,
|
||||
@@ -2013,10 +2058,126 @@ async fn queue_init_bash_maybe<'c>(
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_periodic_script_task(
|
||||
worker_name: String,
|
||||
conn: Connection,
|
||||
same_worker_tx: SameWorkerSender,
|
||||
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let config = WORKER_CONFIG.read().await;
|
||||
|
||||
match (
|
||||
&config.periodic_script_bash,
|
||||
&config.periodic_script_interval_seconds,
|
||||
) {
|
||||
(Some(_), None) => {
|
||||
tracing::error!(
|
||||
worker = %worker_name,
|
||||
"periodic_script_bash is set but periodic_script_interval_seconds is not set. Both must be configured together."
|
||||
);
|
||||
return;
|
||||
}
|
||||
(None, Some(_)) => {
|
||||
tracing::error!(
|
||||
worker = %worker_name,
|
||||
"periodic_script_interval_seconds is set but periodic_script_bash is not set. Both must be configured together."
|
||||
);
|
||||
return;
|
||||
}
|
||||
(Some(content), Some(interval_seconds)) => {
|
||||
let interval_seconds = *interval_seconds;
|
||||
if interval_seconds < MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS {
|
||||
tracing::error!(
|
||||
worker = %worker_name,
|
||||
"Periodic script interval {} seconds is below minimum of {} seconds. Periodic script task will not start.",
|
||||
interval_seconds,
|
||||
MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let content = content.clone();
|
||||
let interval_duration = Duration::from_secs(interval_seconds);
|
||||
|
||||
tracing::info!(
|
||||
worker = %worker_name,
|
||||
"Starting periodic script task (interval: {}s)",
|
||||
interval_seconds
|
||||
);
|
||||
|
||||
loop {
|
||||
tracing::info!(
|
||||
worker = %worker_name,
|
||||
"Triggering periodic script execution"
|
||||
);
|
||||
|
||||
match queue_periodic_script_bash_maybe(
|
||||
&conn,
|
||||
same_worker_tx.clone(),
|
||||
&worker_name,
|
||||
&content,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
tracing::debug!(
|
||||
worker = %worker_name,
|
||||
"Successfully queued periodic script"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
worker = %worker_name,
|
||||
"Error queuing periodic script: {e:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
_ = killpill_rx.recv() => {
|
||||
tracing::info!("Periodic init script task shutting down for worker {}", worker_name);
|
||||
break;
|
||||
}
|
||||
_ = tokio::time::sleep(interval_duration) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, None) => {
|
||||
tracing::debug!(
|
||||
worker = %worker_name,
|
||||
"No periodic script configured"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn queue_periodic_script_bash_maybe<'c>(
|
||||
conn: &Connection,
|
||||
same_worker_tx: SameWorkerSender,
|
||||
worker_name: &str,
|
||||
content: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let uuid = match conn {
|
||||
Connection::Sql(db) => push_periodic_bash_job(db, content.to_owned(), worker_name).await?,
|
||||
Connection::Http(client) => queue_periodic_job(client, &content).await?,
|
||||
};
|
||||
|
||||
same_worker_tx
|
||||
.send(SameWorkerPayload { job_id: uuid, recoverable: false })
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
tracing::info!("Creating periodic script job {uuid} from periodic script: {content}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct SendResult {
|
||||
pub result: SendResultPayload,
|
||||
pub time: Instant,
|
||||
}
|
||||
|
||||
pub enum SendResultPayload {
|
||||
JobCompleted(JobCompleted),
|
||||
UpdateFlow(UpdateFlow),
|
||||
|
||||
@@ -98,7 +98,7 @@ pub async fn update_flow_status_after_job_completion(
|
||||
success,
|
||||
result,
|
||||
stop_early_override,
|
||||
skip_error_handler: false,
|
||||
has_triggered_error_handler: false,
|
||||
};
|
||||
let mut unrecoverable = unrecoverable;
|
||||
loop {
|
||||
@@ -115,7 +115,7 @@ pub async fn update_flow_status_after_job_completion(
|
||||
same_worker_tx,
|
||||
worker_dir,
|
||||
rec.stop_early_override,
|
||||
rec.skip_error_handler,
|
||||
rec.has_triggered_error_handler,
|
||||
worker_name,
|
||||
job_completed_tx.clone(),
|
||||
#[cfg(feature = "benchmark")]
|
||||
@@ -140,7 +140,7 @@ pub async fn update_flow_status_after_job_completion(
|
||||
same_worker_tx,
|
||||
worker_dir,
|
||||
rec.stop_early_override,
|
||||
rec.skip_error_handler,
|
||||
rec.has_triggered_error_handler,
|
||||
worker_name,
|
||||
job_completed_tx.clone(),
|
||||
#[cfg(feature = "benchmark")]
|
||||
@@ -188,7 +188,7 @@ pub struct RecUpdateFlowStatusAfterJobCompletion {
|
||||
success: bool,
|
||||
result: Arc<Box<RawValue>>,
|
||||
stop_early_override: Option<bool>,
|
||||
skip_error_handler: bool,
|
||||
has_triggered_error_handler: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -220,6 +220,10 @@ fn get_stop_after_if_data(
|
||||
return (false, None);
|
||||
}
|
||||
|
||||
fn result_has_recover_true(nresult: Arc<Box<RawValue>>) -> bool {
|
||||
let recover = serde_json::from_str::<RecoveryObject>(nresult.get());
|
||||
return recover.map(|r| r.recover.unwrap_or(false)).unwrap_or(false);
|
||||
}
|
||||
// #[instrument(level = "trace", skip_all)]
|
||||
pub async fn update_flow_status_after_job_completion_internal(
|
||||
db: &DB,
|
||||
@@ -233,11 +237,12 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
same_worker_tx: &SameWorkerSender,
|
||||
worker_dir: &str,
|
||||
stop_early_override: Option<bool>,
|
||||
skip_error_handler: bool,
|
||||
has_triggered_error_handler: bool,
|
||||
worker_name: &str,
|
||||
job_completed_tx: JobCompletedSender,
|
||||
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
|
||||
) -> error::Result<UpdateFlowStatusAfterJobCompletion> {
|
||||
let mut has_triggered_error_handler = has_triggered_error_handler;
|
||||
add_time!(bench, "update flow status internal START");
|
||||
let (
|
||||
should_continue_flow,
|
||||
@@ -292,6 +297,11 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
Step::Step(i) => flow_value.modules.get(i),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if current_module.is_some_and(|x| x.is_flow()) {
|
||||
has_triggered_error_handler = false;
|
||||
}
|
||||
|
||||
let module_status = match module_step {
|
||||
Step::PreprocessorStep => old_status
|
||||
.preprocessor_module
|
||||
@@ -565,10 +575,39 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let branches = current_module
|
||||
.and_then(|x| x.get_branches_skip_failures().ok())
|
||||
.map(|x| {
|
||||
x.branches
|
||||
.iter()
|
||||
.map(|b| b.skip_failure.unwrap_or(false))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let mut njobs = Vec::new();
|
||||
|
||||
let jobs_filtered = if branchall.is_some() {
|
||||
if let Some(branches) = branches {
|
||||
for (branch, job) in branches.iter().zip(jobs.iter()) {
|
||||
if !branch {
|
||||
njobs.push(job.clone());
|
||||
}
|
||||
}
|
||||
njobs.as_slice()
|
||||
} else {
|
||||
jobs.as_slice()
|
||||
}
|
||||
} else {
|
||||
jobs.as_slice()
|
||||
};
|
||||
|
||||
|
||||
|
||||
let new_status = if skip_loop_failures
|
||||
|| sqlx::query_scalar!(
|
||||
"SELECT status = 'success' OR status = 'skipped' AS \"success!\" FROM v2_job_completed WHERE id = ANY($1)",
|
||||
jobs.as_slice()
|
||||
jobs_filtered
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await
|
||||
@@ -620,7 +659,15 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
tracing::info!(
|
||||
"parallel iteration {job_id_for_status} of flow {flow} has finished",
|
||||
);
|
||||
(true, Some(new_status))
|
||||
|
||||
// for parallel branchall and forloop, we do not want to trigger the error handler again at the forloop/branchall node since it was already triggered at the leaf level
|
||||
// so we want to ignore the has_triggered_error_handler flag at the forloop/branchall node and reset it based on if the node is a success or failure
|
||||
if !success && flow_value.failure_module.is_some() {
|
||||
has_triggered_error_handler = true;
|
||||
} else {
|
||||
has_triggered_error_handler = false;
|
||||
}
|
||||
(success, Some(new_status))
|
||||
} else {
|
||||
add_time!(bench, "handle parallel flow start");
|
||||
tx.commit().await?;
|
||||
@@ -771,7 +818,7 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
.and_then(|x| x.retry.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
tracing::info!("update flow status on rety: {retry:#?} ");
|
||||
tracing::info!("update flow status on retry: {retry:#?} ");
|
||||
next_retry(&retry, &old_status.retry).is_none()
|
||||
} else {
|
||||
false
|
||||
@@ -791,17 +838,9 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
}
|
||||
};
|
||||
|
||||
let skip_parallel_branchall_failure = match (module_status, new_status.as_ref()) {
|
||||
(
|
||||
FlowStatusModule::InProgress { branchall: Some(_), parallel: true, .. },
|
||||
Some(FlowStatusModule::Success { flow_jobs_success, .. }),
|
||||
) => compute_skip_branchall_failure(0, true, current_module, flow_jobs_success).await?,
|
||||
(
|
||||
FlowStatusModule::InProgress { branchall: Some(_), parallel: true, .. },
|
||||
Some(FlowStatusModule::Failure { flow_jobs_success, .. }),
|
||||
) => compute_skip_branchall_failure(0, true, current_module, flow_jobs_success).await?,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
|
||||
|
||||
let step_counter = if inc_step_counter {
|
||||
sqlx::query!(
|
||||
"UPDATE v2_job_status
|
||||
@@ -1112,7 +1151,7 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
.flow_innermost_root_job
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| "none".to_string());
|
||||
tracing::info!(id = %flow_job.id, root_id = %job_root, "update flow status");
|
||||
|
||||
|
||||
let should_continue_flow = match success {
|
||||
_ if stop_early => false,
|
||||
@@ -1121,10 +1160,10 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
false if unrecoverable => false,
|
||||
false
|
||||
if skip_seq_branch_failure
|
||||
|| skip_parallel_branchall_failure
|
||||
|| skip_loop_failures
|
||||
|| continue_on_error =>
|
||||
{
|
||||
|
||||
!is_last_step
|
||||
}
|
||||
false
|
||||
@@ -1153,7 +1192,7 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
}
|
||||
false
|
||||
if !is_failure_step
|
||||
&& !skip_error_handler
|
||||
&& !has_triggered_error_handler
|
||||
&& flow_value.failure_module.is_some() =>
|
||||
{
|
||||
true
|
||||
@@ -1161,7 +1200,10 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
false => false,
|
||||
};
|
||||
|
||||
tracing::debug!(id = %flow_job.id, root_id = %job_root, "flow status updated");
|
||||
tracing::info!(id = %flow_job.id, root_id = %job_root, success = %success, stop_early = %stop_early, is_last_step = %is_last_step, unrecoverable = %unrecoverable,
|
||||
skip_seq_branch_failure = %skip_seq_branch_failure, skip_loop_failures = %skip_loop_failures,
|
||||
current_module_id = %current_module.map(|x| x.id.clone()).unwrap_or_default(),
|
||||
continue_on_error = %continue_on_error, should_continue_flow = %should_continue_flow, "computed if flow should continue");
|
||||
|
||||
(
|
||||
should_continue_flow,
|
||||
@@ -1184,10 +1226,10 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
"Flow job canceled\n".to_string()
|
||||
} else if stop_early {
|
||||
format!("Flow job stopped early because of a stop early predicate returning true\n")
|
||||
} else if success {
|
||||
"Flow job completed with success\n".to_string()
|
||||
} else if is_failure_step {
|
||||
format!("Flow job completed with error, and error handler was triggered.\n It completed with {}, and with recover: {}\n", if success { "success" } else { "error" }, result_has_recover_true(nresult.clone()))
|
||||
} else {
|
||||
"Flow job completed with error\n".to_string()
|
||||
format!("Flow job completed with {}\n", if success { "success" } else { "error" })
|
||||
};
|
||||
append_logs(&flow_job.id, w_id, logs, &db.into()).await;
|
||||
}
|
||||
@@ -1237,6 +1279,7 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
if flow_job.is_canceled() {
|
||||
add_completed_job_error(
|
||||
db,
|
||||
@@ -1259,13 +1302,9 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
|
||||
save_in_cache(db, client, &flow_job, cached_res_path, nresult.clone()).await;
|
||||
}
|
||||
fn result_has_recover_true(nresult: Arc<Box<RawValue>>) -> bool {
|
||||
let recover = serde_json::from_str::<RecoveryObject>(nresult.get());
|
||||
return recover.map(|r| r.recover.unwrap_or(false)).unwrap_or(false);
|
||||
}
|
||||
|
||||
let success = success
|
||||
&& (!is_failure_step || result_has_recover_true(nresult.clone()))
|
||||
&& !skip_error_handler
|
||||
&& stop_early_err_msg.is_none();
|
||||
|
||||
add_time!(bench, "flow status update 1");
|
||||
@@ -1357,7 +1396,7 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
} else {
|
||||
None
|
||||
},
|
||||
skip_error_handler: skip_error_handler || is_failure_step,
|
||||
has_triggered_error_handler: has_triggered_error_handler || is_failure_step,
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -1780,10 +1819,11 @@ async fn push_next_flow_job(
|
||||
.flow_innermost_root_job
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| "none".to_string());
|
||||
tracing::info!(id = %flow_job.id, root_id = %job_root, "pushing next flow job");
|
||||
|
||||
let mut step = Step::from_i32_and_len(status.step, flow.modules.len());
|
||||
|
||||
tracing::info!(id = %flow_job.id, root_id = %job_root, step = ?step, "pushing next flow job");
|
||||
|
||||
let mut status_module = match step {
|
||||
Step::Step(i) => status
|
||||
.modules
|
||||
@@ -1797,6 +1837,8 @@ async fn push_next_flow_job(
|
||||
Step::FailureStep => status.failure_module.module_status.clone(),
|
||||
};
|
||||
|
||||
// tracing::error!("status_module: {status_module:#?}");
|
||||
|
||||
let fj: mappable_rc::Marc<MiniPulledJob> = flow_job.clone().into();
|
||||
let arc_flow_job_args: Marc<HashMap<String, Box<RawValue>>> = Marc::map(fj, |x| {
|
||||
if let Some(args) = &x.args {
|
||||
@@ -1813,6 +1855,14 @@ async fn push_next_flow_job(
|
||||
success: true,
|
||||
result: if flow.modules.is_empty() {
|
||||
to_raw_value(arc_flow_job_args.as_ref())
|
||||
} else if matches!(
|
||||
status_module,
|
||||
FlowStatusModule::Success { branch_chosen: Some(_), .. }
|
||||
) {
|
||||
last_job_result
|
||||
.as_ref()
|
||||
.map(|x| x.as_ref().clone())
|
||||
.unwrap_or_else(|| to_raw_value(&json!("{}")))
|
||||
} else {
|
||||
// it has to be an empty for loop event
|
||||
serde_json::from_str("[]").unwrap()
|
||||
@@ -2550,8 +2600,16 @@ async fn push_next_flow_job(
|
||||
json!(FlowStatusModule::Success {
|
||||
id: status_module.id(),
|
||||
job: Uuid::nil(),
|
||||
flow_jobs: Some(vec![]),
|
||||
flow_jobs_success: Some(vec![]),
|
||||
flow_jobs: if branch_chosen.is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(vec![])
|
||||
},
|
||||
flow_jobs_success: if branch_chosen.is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(vec![])
|
||||
},
|
||||
branch_chosen: branch_chosen,
|
||||
approvers: vec![],
|
||||
failed_retries: vec![],
|
||||
|
||||
@@ -203,7 +203,7 @@ fn parse_ts_relative_imports(raw_code: &str, script_path: &str) -> error::Result
|
||||
let normalized = normalized.to_str().unwrap().to_string();
|
||||
relative_imports.push(normalized);
|
||||
} else {
|
||||
tracing::error!("error canonicalizing path: {:?}", normalized);
|
||||
tracing::error!("error canonicalizing path: {script_path} with import {import}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.509.1";
|
||||
export const VERSION = "v1.515.1";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -5,11 +5,6 @@ import { GlobalUserInfo } from "./gen/types.gen.ts";
|
||||
|
||||
import { loginInteractive, tryGetLoginInfo } from "./login.ts";
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
import {
|
||||
addWorkspace,
|
||||
removeWorkspace,
|
||||
Workspace
|
||||
} from "./workspace.ts";
|
||||
|
||||
/**
|
||||
* Main authentication function - moved from context.ts to break circular dependencies
|
||||
@@ -48,6 +43,7 @@ export async function requireLogin(
|
||||
}
|
||||
|
||||
// Update workspace token
|
||||
const { removeWorkspace, addWorkspace } = await import("./workspace.ts");
|
||||
removeWorkspace(workspace.name, false, opts);
|
||||
workspace.token = newToken;
|
||||
addWorkspace(workspace, opts);
|
||||
|
||||
@@ -64,7 +64,7 @@ await build({
|
||||
"nu",
|
||||
"ts",
|
||||
"regex",
|
||||
"python",
|
||||
"py",
|
||||
"go",
|
||||
"php",
|
||||
"rust",
|
||||
|
||||
430
cli/flow_guidance.ts
Normal file
430
cli/flow_guidance.ts
Normal file
@@ -0,0 +1,430 @@
|
||||
export const FLOW_GUIDANCE = `
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# System Prompt: OpenFlow Workflow Generator
|
||||
|
||||
You are an expert at creating OpenFlow YAML specifications for Windmill workflows.
|
||||
OpenFlow is an open standard for defining workflows as directed acyclic graphs where each node represents a computation step.
|
||||
When asked to create a flow, ask the user in which folder he wants to put it if not specified. Then create a new folder in the specified folder, that ends with \`.flow\`. It should contain a \`.yaml\` file that contains the flow definition.
|
||||
For rawscript type module in the flow, the content key should start with "!inline" followed by the path of the script containing the code. It should be put in the same folder as the flow.
|
||||
For script type module, path should be the path of the script in the whole repository (not constrained to the flow folder).
|
||||
You do not need to create .lock and .yaml files manually. Instead, you should run \`wmill flow generate-locks --yes\` to create them.
|
||||
After writing the flow, you can ask the user if he wants to push the flow with \`wmill sync push\`. Both should be run at the root of the repository.
|
||||
|
||||
## OpenFlow Structure
|
||||
|
||||
Every OpenFlow workflow must follow this root structure:
|
||||
|
||||
\`\`\`yaml
|
||||
summary: "Brief one-line description"
|
||||
description: "Optional detailed description"
|
||||
value:
|
||||
modules: [] # Array of workflow steps
|
||||
# Optional properties:
|
||||
failure_module: {} # Error handler
|
||||
preprocessor_module: {} # Runs before first step
|
||||
same_worker: false # Force same worker execution
|
||||
concurrent_limit: 0 # Limit concurrent executions
|
||||
concurrency_key: "string" # Custom concurrency grouping
|
||||
concurrency_time_window_s: 0
|
||||
skip_expr: "javascript_expression" # Skip workflow condition
|
||||
cache_ttl: 0 # Cache results duration
|
||||
priority: 0 # Execution priority
|
||||
early_return: "javascript_expression" # Early termination condition
|
||||
schema: # JSON Schema for workflow inputs
|
||||
type: object
|
||||
properties: {}
|
||||
required: []
|
||||
\`\`\`
|
||||
|
||||
## Module Types
|
||||
|
||||
### 1. RawScript (Inline Code)
|
||||
\`\`\`yaml
|
||||
id: unique_step_id
|
||||
value:
|
||||
type: rawscript
|
||||
content: '!inline inline_script_1.inline_script.ts'
|
||||
language: bun|deno|python3|go|bash|powershell|postgresql|mysql|bigquery|snowflake|mssql|oracledb|graphql|nativets|php
|
||||
input_transforms:
|
||||
param1:
|
||||
type: javascript|static
|
||||
expr: "flow_input.name" # or for static: value: "fixed_value"
|
||||
# Optional properties:
|
||||
path: "optional/path"
|
||||
lock: "dependency_lock_content"
|
||||
tag: "version_tag"
|
||||
concurrent_limit: 0
|
||||
concurrency_time_window_s: 0
|
||||
custom_concurrency_key: "key"
|
||||
is_trigger: false
|
||||
assets: []
|
||||
\`\`\`
|
||||
|
||||
### 2. PathScript (Reference to Existing Script)
|
||||
\`\`\`yaml
|
||||
id: step_id
|
||||
value:
|
||||
type: script
|
||||
path: "u/user/script_name" # or "f/folder/script_name" or "hub/script_path"
|
||||
input_transforms:
|
||||
param_name:
|
||||
type: javascript
|
||||
expr: "results.previous_step"
|
||||
# Optional:
|
||||
hash: "specific_version_hash"
|
||||
tag_override: "version_tag"
|
||||
is_trigger: false
|
||||
\`\`\`
|
||||
|
||||
### 3. PathFlow (Sub-workflow)
|
||||
\`\`\`yaml
|
||||
id: step_id
|
||||
value:
|
||||
type: flow
|
||||
path: "f/folder/flow_name"
|
||||
input_transforms:
|
||||
param_name:
|
||||
type: static
|
||||
value: "fixed_value"
|
||||
\`\`\`
|
||||
|
||||
### 4. ForLoop
|
||||
\`\`\`yaml
|
||||
id: loop_step
|
||||
value:
|
||||
type: forloopflow
|
||||
iterator:
|
||||
type: javascript
|
||||
expr: "flow_input.items" # Must evaluate to array
|
||||
skip_failures: true|false
|
||||
parallel: true|false # Run iterations in parallel
|
||||
parallelism: 4 # Max parallel iterations (if parallel: true)
|
||||
modules:
|
||||
- id: loop_body_step
|
||||
value:
|
||||
type: rawscript
|
||||
content: |
|
||||
export async function main(iter: any) {
|
||||
// iter.value contains current item
|
||||
// iter.index contains current index
|
||||
return iter.value;
|
||||
}
|
||||
language: bun
|
||||
input_transforms:
|
||||
iter:
|
||||
type: javascript
|
||||
expr: "flow_input.iter"
|
||||
\`\`\`
|
||||
|
||||
### 5. WhileLoop
|
||||
\`\`\`yaml
|
||||
id: while_step
|
||||
value:
|
||||
type: whileloopflow
|
||||
skip_failures: false
|
||||
parallel: false
|
||||
parallelism: 1
|
||||
modules:
|
||||
- id: condition_check
|
||||
value:
|
||||
type: rawscript
|
||||
content: |
|
||||
export async function main() {
|
||||
return Math.random() > 0.5; // Continue condition
|
||||
}
|
||||
language: bun
|
||||
input_transforms: {}
|
||||
\`\`\`
|
||||
|
||||
### 6. Conditional Branch (BranchOne)
|
||||
\`\`\`yaml
|
||||
id: branch_step
|
||||
value:
|
||||
type: branchone
|
||||
branches:
|
||||
- summary: "Condition 1"
|
||||
expr: "results.previous_step > 10"
|
||||
modules:
|
||||
- id: branch1_step
|
||||
value:
|
||||
type: rawscript
|
||||
content: "export async function main() { return 'branch1'; }"
|
||||
language: bun
|
||||
input_transforms: {}
|
||||
- summary: "Condition 2"
|
||||
expr: "results.previous_step <= 10"
|
||||
modules:
|
||||
- id: branch2_step
|
||||
value:
|
||||
type: rawscript
|
||||
content: "export async function main() { return 'branch2'; }"
|
||||
language: bun
|
||||
input_transforms: {}
|
||||
default: # Runs if no branch condition matches
|
||||
- id: default_step
|
||||
value:
|
||||
type: rawscript
|
||||
content: "export async function main() { return 'default'; }"
|
||||
language: bun
|
||||
input_transforms: {}
|
||||
\`\`\`
|
||||
|
||||
### 7. Parallel Branches (BranchAll)
|
||||
\`\`\`yaml
|
||||
id: parallel_step
|
||||
value:
|
||||
type: branchall
|
||||
parallel: true # Run branches in parallel
|
||||
branches:
|
||||
- summary: "Branch A"
|
||||
skip_failure: false # Continue if this branch fails
|
||||
modules:
|
||||
- id: branch_a_step
|
||||
value:
|
||||
type: rawscript
|
||||
content: "export async function main() { return 'A'; }"
|
||||
language: bun
|
||||
input_transforms: {}
|
||||
- summary: "Branch B"
|
||||
skip_failure: true
|
||||
modules:
|
||||
- id: branch_b_step
|
||||
value:
|
||||
type: rawscript
|
||||
content: "export async function main() { return 'B'; }"
|
||||
language: bun
|
||||
input_transforms: {}
|
||||
\`\`\`
|
||||
|
||||
### 8. Identity (Pass-through)
|
||||
\`\`\`yaml
|
||||
id: identity_step
|
||||
value:
|
||||
type: identity
|
||||
flow: false # Set to true if this represents a sub-flow
|
||||
\`\`\`
|
||||
|
||||
## Input Transforms & Data Flow
|
||||
|
||||
### JavaScript Expressions
|
||||
Reference data using these variables in \`expr\` fields:
|
||||
- \`flow_input.property_name\` - Access workflow inputs
|
||||
- \`results.step_id\` - Access outputs from previous steps
|
||||
- \`results.step_id.property\` - Access specific properties
|
||||
- \`flow_input.iter.value\` - Current iteration value (in loops)
|
||||
- \`flow_input.iter.index\` - Current iteration index (in loops)
|
||||
|
||||
### Static Values
|
||||
\`\`\`yaml
|
||||
input_transforms:
|
||||
param_name:
|
||||
type: static
|
||||
value: "fixed_string" # Can be string, number, boolean, object, array
|
||||
\`\`\`
|
||||
|
||||
### Resource References
|
||||
\`\`\`yaml
|
||||
input_transforms:
|
||||
database:
|
||||
type: static
|
||||
value: "$res:f/folder/my_database" # Reference to stored resource
|
||||
\`\`\`
|
||||
|
||||
## Advanced Module Properties
|
||||
|
||||
### Error Handling & Control Flow
|
||||
\`\`\`yaml
|
||||
id: step_id
|
||||
value: # ... module definition
|
||||
# Control flow options:
|
||||
stop_after_if:
|
||||
expr: "results.step_id.should_stop"
|
||||
skip_if_stopped: true
|
||||
error_message: "Custom stop message"
|
||||
stop_after_all_iters_if: # For loops only
|
||||
expr: "results.step_id.should_stop_loop"
|
||||
skip_if_stopped: false
|
||||
skip_if:
|
||||
expr: "results.step_id.should_skip"
|
||||
sleep:
|
||||
type: javascript
|
||||
expr: "flow_input.delay_seconds"
|
||||
continue_on_error: false # Continue workflow if this step fails
|
||||
delete_after_use: false # Clean up results after use
|
||||
|
||||
# Execution control:
|
||||
cache_ttl: 3600 # Cache results for 1 hour
|
||||
timeout: 300 # Step timeout in seconds
|
||||
priority: 0 # Higher numbers = higher priority
|
||||
mock:
|
||||
enabled: false
|
||||
return_value: "mocked_result"
|
||||
|
||||
# Suspend/Approval:
|
||||
suspend:
|
||||
required_events: 1 # Number of resume events needed
|
||||
timeout: 86400 # Timeout in seconds
|
||||
resume_form:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
approved:
|
||||
type: boolean
|
||||
user_auth_required: true
|
||||
user_groups_required:
|
||||
type: static
|
||||
value: ["admin"]
|
||||
self_approval_disabled: false
|
||||
hide_cancel: false
|
||||
continue_on_disapprove_timeout: false
|
||||
|
||||
# Retry configuration:
|
||||
retry:
|
||||
constant:
|
||||
attempts: 3
|
||||
seconds: 5
|
||||
# OR exponential backoff:
|
||||
# exponential:
|
||||
# attempts: 3
|
||||
# multiplier: 2
|
||||
# seconds: 1
|
||||
# random_factor: 10 # 0-100% jitter
|
||||
\`\`\`
|
||||
|
||||
## Special Modules
|
||||
|
||||
### Failure Handler (Error Handler)
|
||||
\`\`\`yaml
|
||||
value:
|
||||
failure_module:
|
||||
id: failure
|
||||
value:
|
||||
type: rawscript
|
||||
content: |
|
||||
export async function main(error: any) {
|
||||
// error.message, error.step_id, error.name, error.stack
|
||||
console.log("Flow failed:", error.message);
|
||||
return error;
|
||||
}
|
||||
language: bun
|
||||
input_transforms: {}
|
||||
\`\`\`
|
||||
|
||||
### Preprocessor
|
||||
\`\`\`yaml
|
||||
value:
|
||||
preprocessor_module:
|
||||
id: preprocessor
|
||||
value:
|
||||
type: rawscript
|
||||
content: |
|
||||
export async function main() {
|
||||
console.log("Flow starting...");
|
||||
return "preprocessed";
|
||||
}
|
||||
language: bun
|
||||
input_transforms: {}
|
||||
\`\`\`
|
||||
|
||||
## Schema Definition
|
||||
\`\`\`yaml
|
||||
schema:
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema"
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "User name"
|
||||
default: ""
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
count:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
database:
|
||||
type: object
|
||||
format: "resource-postgresql" # Resource type reference
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required: ["name", "email"]
|
||||
order: ["name", "email", "count"] # UI field order
|
||||
\`\`\`
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Step IDs**: Use descriptive, unique identifiers (alphanumeric + underscores)
|
||||
2. **Data Flow**: Chain steps using \`results.step_id\` references
|
||||
3. **Error Handling**: Add failure_module for critical workflows
|
||||
4. **Languages**: Use \`bun\` for TypeScript (fastest), \`python3\` for Python
|
||||
5. **Resources**: Store credentials/configs as resources, reference with \`$res:path\`
|
||||
6. **Loops**: Prefer \`parallel: true\` for independent iterations
|
||||
7. **Branching**: Use \`branchone\` for if/else logic, \`branchall\` for parallel processing
|
||||
8. **Schemas**: Always define input schemas for better UX and validation
|
||||
|
||||
## Example Complete Workflow
|
||||
\`\`\`yaml
|
||||
summary: "Process user data"
|
||||
description: "Validates user input, processes data, and sends notifications"
|
||||
value:
|
||||
modules:
|
||||
- id: validate_input
|
||||
value:
|
||||
type: rawscript
|
||||
content: '!inline inline_script_0.inline_script.ts'
|
||||
# script at path inline_script_0.inline_script.ts will contain
|
||||
# export async function main(email: string, name: string) {
|
||||
# if (!email.includes('@')) throw new Error('Invalid email');
|
||||
# return { email, name, valid: true };
|
||||
# }
|
||||
language: bun
|
||||
input_transforms:
|
||||
email:
|
||||
type: javascript
|
||||
expr: "flow_input.email"
|
||||
name:
|
||||
type: javascript
|
||||
expr: "flow_input.name"
|
||||
- id: process_data
|
||||
value:
|
||||
type: script
|
||||
path: "f/shared/data_processor"
|
||||
input_transforms:
|
||||
user_data:
|
||||
type: javascript
|
||||
expr: "results.validate_input"
|
||||
- id: send_notification
|
||||
value:
|
||||
type: rawscript
|
||||
content: '!inline inline_script_1.inline_script.ts'
|
||||
# script at path inline_script_1.inline_script.ts will contain
|
||||
# export async function main(processed_data: any) {
|
||||
# console.log("Sending notification for:", processed_data.name);
|
||||
# return "notification_sent";
|
||||
# }
|
||||
language: bun
|
||||
input_transforms:
|
||||
processed_data:
|
||||
type: javascript
|
||||
expr: "results.process_data"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: "User email address"
|
||||
name:
|
||||
type: string
|
||||
description: "User full name"
|
||||
required: ["email", "name"]
|
||||
\`\`\`
|
||||
|
||||
When generating OpenFlow YAML, ensure proper indentation, valid YAML syntax, and logical step dependencies. Always include meaningful summaries and proper input transforms to connect workflow steps.
|
||||
`;
|
||||
51
cli/main.ts
51
cli/main.ts
@@ -12,6 +12,7 @@ import app from "./apps.ts";
|
||||
import script from "./script.ts";
|
||||
import workspace, { getActiveWorkspace } from "./workspace.ts";
|
||||
import resource from "./resource.ts";
|
||||
import resourceType from "./resource-type.ts";
|
||||
import user from "./user.ts";
|
||||
import variable from "./variable.ts";
|
||||
import hub from "./hub.ts";
|
||||
@@ -22,6 +23,7 @@ import sync from "./sync.ts";
|
||||
import gitsyncSettings from "./gitsync-settings.ts";
|
||||
import instance from "./instance.ts";
|
||||
import workerGroups from "./worker_groups.ts";
|
||||
import { SCRIPT_GUIDANCE } from "./script_guidance.ts";
|
||||
|
||||
import dev from "./dev.ts";
|
||||
import { fetchVersion } from "./context.ts";
|
||||
@@ -35,6 +37,7 @@ import { add as workspaceAdd } from "./workspace.ts";
|
||||
import workers from "./workers.ts";
|
||||
import queues from "./queues.ts";
|
||||
import { readLockfile } from "./metadata.ts";
|
||||
import { FLOW_GUIDANCE } from "./flow_guidance.ts";
|
||||
|
||||
export {
|
||||
flow,
|
||||
@@ -42,6 +45,7 @@ export {
|
||||
script,
|
||||
workspace,
|
||||
resource,
|
||||
resourceType,
|
||||
user,
|
||||
variable,
|
||||
hub,
|
||||
@@ -65,7 +69,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.509.1";
|
||||
export const VERSION = "1.515.1";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
@@ -268,6 +272,50 @@ const command = new Command()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create .cursor/rules directory and files with SCRIPT_GUIDANCE content
|
||||
try {
|
||||
const scriptGuidanceContent = SCRIPT_GUIDANCE;
|
||||
const flowGuidanceContent = FLOW_GUIDANCE;
|
||||
|
||||
// Create .cursor/rules directory
|
||||
await Deno.mkdir(".cursor/rules", { recursive: true });
|
||||
|
||||
// Create windmill.mdc file
|
||||
if (!await Deno.stat(".cursor/rules/script.mdc").catch(() => null)) {
|
||||
await Deno.writeTextFile(".cursor/rules/script.mdc", scriptGuidanceContent);
|
||||
log.info(colors.green("Created .cursor/rules/script.mdc"));
|
||||
}
|
||||
|
||||
if (!await Deno.stat(".cursor/rules/flow.mdc").catch(() => null)) {
|
||||
await Deno.writeTextFile(".cursor/rules/flow.mdc", flowGuidanceContent);
|
||||
log.info(colors.green("Created .cursor/rules/flow.mdc"));
|
||||
}
|
||||
|
||||
// Create CLAUDE.md file
|
||||
if (!await Deno.stat("CLAUDE.md").catch(() => null)) {
|
||||
await Deno.writeTextFile("CLAUDE.md", `
|
||||
# Claude
|
||||
|
||||
You are a helpful assistant that can help with Windmill scripts and flows creation.
|
||||
|
||||
## Script Guidance
|
||||
${scriptGuidanceContent}
|
||||
|
||||
## Flow Guidance
|
||||
${flowGuidanceContent}
|
||||
`);
|
||||
log.info(colors.green("Created CLAUDE.md"));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
log.warn(`Could not create guidance files: ${error.message}`);
|
||||
} else {
|
||||
log.warn(`Could not create guidance files: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
)
|
||||
.command("app", app)
|
||||
@@ -275,6 +323,7 @@ const command = new Command()
|
||||
.command("script", script)
|
||||
.command("workspace", workspace)
|
||||
.command("resource", resource)
|
||||
.command("resource-type", resourceType)
|
||||
.command("user", user)
|
||||
.command("variable", variable)
|
||||
.command("hub", hub)
|
||||
|
||||
248
cli/metadata.ts
248
cli/metadata.ts
@@ -14,7 +14,11 @@ import {
|
||||
} from "./bootstrap/script_bootstrap.ts";
|
||||
import { Workspace } from "./workspace.ts";
|
||||
import { SchemaProperty } from "./bootstrap/common.ts";
|
||||
import { languagesWithRawReqsSupport, LanguageWithRawReqsSupport, ScriptLanguage } from "./script_common.ts";
|
||||
import {
|
||||
languagesWithRawReqsSupport,
|
||||
LanguageWithRawReqsSupport,
|
||||
ScriptLanguage,
|
||||
} from "./script_common.ts";
|
||||
import { inferContentTypeFromFilePath } from "./script_common.ts";
|
||||
import { GlobalDeps, exts, findGlobalDeps } from "./script.ts";
|
||||
import {
|
||||
@@ -37,12 +41,12 @@ export class LockfileGenerationError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateAllMetadata() { }
|
||||
export async function generateAllMetadata() {}
|
||||
|
||||
function findClosestRawReqs(
|
||||
lang: LanguageWithRawReqsSupport | undefined,
|
||||
remotePath: string,
|
||||
globalDeps: GlobalDeps,
|
||||
globalDeps: GlobalDeps
|
||||
): string | undefined {
|
||||
let bestCandidate: { k: string; v: string } | undefined = undefined;
|
||||
if (lang) {
|
||||
@@ -74,11 +78,14 @@ async function generateFlowHash(
|
||||
// Get language name from path
|
||||
const lang = inferContentTypeFromFilePath(f.path, defaultTs);
|
||||
// Get lock for that language
|
||||
[, reqs] = Object.entries(rawReqs).find(([lang2, _]) => lang == lang2) ?? [];
|
||||
[, reqs] =
|
||||
Object.entries(rawReqs).find(([lang2, _]) => lang == lang2) ?? [];
|
||||
}
|
||||
|
||||
// Embed lock into hash
|
||||
hashes[f.path] = await generateHash(await f.getContentText() + (reqs ?? ""));
|
||||
hashes[f.path] = await generateHash(
|
||||
(await f.getContentText()) + (reqs ?? "")
|
||||
);
|
||||
}
|
||||
}
|
||||
return { ...hashes, [TOP_HASH]: await generateHash(JSON.stringify(hashes)) };
|
||||
@@ -92,7 +99,7 @@ export async function generateFlowLockInternal(
|
||||
},
|
||||
justUpdateMetadataLock?: boolean,
|
||||
noStaleMessage?: boolean,
|
||||
useRawReqs?: boolean,
|
||||
useRawReqs?: boolean
|
||||
): Promise<string | void> {
|
||||
if (folder.endsWith(SEP)) {
|
||||
folder = folder.substring(0, folder.length - 1);
|
||||
@@ -136,16 +143,20 @@ export async function generateFlowLockInternal(
|
||||
}
|
||||
|
||||
if (useRawReqs) {
|
||||
log.warn("If using local lockfiles, following redeployments from Web App will inevitably override generated lockfiles by CLI. To maintain your script's lockfiles you will need to redeploy only from CLI. (Behavior is subject to change)")
|
||||
log.warn(
|
||||
"If using local lockfiles, following redeployments from Web App will inevitably override generated lockfiles by CLI. To maintain your script's lockfiles you will need to redeploy only from CLI. (Behavior is subject to change)"
|
||||
);
|
||||
log.info(
|
||||
(await blueColor())(
|
||||
`Found raw requirements (${languagesWithRawReqsSupport.map((l) => l.rrFilename).join("/")}) for ${folder}, using it`,
|
||||
),
|
||||
`Found raw requirements (${languagesWithRawReqsSupport
|
||||
.map((l) => l.rrFilename)
|
||||
.join("/")}) for ${folder}, using it`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const flowValue = (await yamlParseFile(
|
||||
folder! + SEP + "flow.yaml",
|
||||
folder! + SEP + "flow.yaml"
|
||||
)) as FlowFile;
|
||||
|
||||
if (!justUpdateMetadataLock) {
|
||||
@@ -164,7 +175,7 @@ export async function generateFlowLockInternal(
|
||||
replaceInlineScripts(
|
||||
flowValue.value.modules,
|
||||
folder + SEP!,
|
||||
changedScripts,
|
||||
changedScripts
|
||||
);
|
||||
|
||||
//removeChangedLocks
|
||||
@@ -172,28 +183,26 @@ export async function generateFlowLockInternal(
|
||||
workspace,
|
||||
flowValue.value,
|
||||
remote_path,
|
||||
rawReqs,
|
||||
rawReqs
|
||||
);
|
||||
|
||||
const inlineScripts = extractInlineScriptsForFlows(
|
||||
flowValue.value.modules,
|
||||
newPathAssigner(opts.defaultTs ?? "bun"),
|
||||
newPathAssigner(opts.defaultTs ?? "bun")
|
||||
);
|
||||
inlineScripts
|
||||
.filter((s) => s.path.endsWith(".lock"))
|
||||
.forEach((s) => {
|
||||
Deno.writeTextFileSync(
|
||||
Deno.cwd() + SEP + folder + SEP + s.path,
|
||||
s.content,
|
||||
s.content
|
||||
);
|
||||
});
|
||||
|
||||
// Overwrite `flow.yaml` with the new lockfile references
|
||||
await Deno.writeTextFile(
|
||||
Deno.cwd() + SEP + folder + SEP + "flow.yaml",
|
||||
yamlStringify(
|
||||
flowValue as Record<string, any>
|
||||
)
|
||||
yamlStringify(flowValue as Record<string, any>)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -223,7 +232,7 @@ export async function generateScriptMetadataInternal(
|
||||
noStaleMessage: boolean,
|
||||
globalDeps: GlobalDeps,
|
||||
codebases: SyncCodebase[],
|
||||
justUpdateMetadataLock?: boolean,
|
||||
justUpdateMetadataLock?: boolean
|
||||
): Promise<string | undefined> {
|
||||
const remotePath = scriptPath
|
||||
.substring(0, scriptPath.indexOf("."))
|
||||
@@ -231,26 +240,24 @@ export async function generateScriptMetadataInternal(
|
||||
|
||||
const language = inferContentTypeFromFilePath(scriptPath, opts.defaultTs);
|
||||
|
||||
const rrLang = languagesWithRawReqsSupport.find((l) => language == l.language);
|
||||
|
||||
const rawReqs = findClosestRawReqs(
|
||||
rrLang,
|
||||
scriptPath,
|
||||
globalDeps,
|
||||
const rrLang = languagesWithRawReqsSupport.find(
|
||||
(l) => language == l.language
|
||||
);
|
||||
|
||||
const rawReqs = findClosestRawReqs(rrLang, scriptPath, globalDeps);
|
||||
|
||||
if (rawReqs && rrLang) {
|
||||
log.info(
|
||||
(await blueColor())(
|
||||
`Found raw requirements (${rrLang.rrFilename}) for ${scriptPath}, using it`,
|
||||
),
|
||||
`Found raw requirements (${rrLang.rrFilename}) for ${scriptPath}, using it`
|
||||
)
|
||||
);
|
||||
}
|
||||
const metadataWithType = await parseMetadataFile(
|
||||
remotePath,
|
||||
undefined,
|
||||
globalDeps,
|
||||
codebases,
|
||||
codebases
|
||||
);
|
||||
|
||||
// read script content
|
||||
@@ -262,7 +269,7 @@ export async function generateScriptMetadataInternal(
|
||||
if (await checkifMetadataUptodate(remotePath, hash, undefined)) {
|
||||
if (!noStaleMessage) {
|
||||
log.info(
|
||||
colors.green(`Script ${remotePath} metadata is up-to-date, skipping`),
|
||||
colors.green(`Script ${remotePath} metadata is up-to-date, skipping`)
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -284,7 +291,7 @@ export async function generateScriptMetadataInternal(
|
||||
scriptContent,
|
||||
language,
|
||||
metadataParsedContent,
|
||||
scriptPath,
|
||||
scriptPath
|
||||
);
|
||||
}
|
||||
|
||||
@@ -298,7 +305,7 @@ export async function generateScriptMetadataInternal(
|
||||
language,
|
||||
remotePath,
|
||||
metadataParsedContent,
|
||||
rawReqs,
|
||||
rawReqs
|
||||
);
|
||||
} else {
|
||||
metadataParsedContent.lock = "";
|
||||
@@ -319,7 +326,7 @@ export async function generateScriptMetadataInternal(
|
||||
hash = await generateScriptHash(
|
||||
rawReqs,
|
||||
scriptContent,
|
||||
metadataContentUsedForHash,
|
||||
metadataContentUsedForHash
|
||||
);
|
||||
await updateMetadataGlobalLock(remotePath, hash);
|
||||
if (!justUpdateMetadataLock) {
|
||||
@@ -332,14 +339,14 @@ export async function updateScriptSchema(
|
||||
scriptContent: string,
|
||||
language: ScriptLanguage,
|
||||
metadataContent: Record<string, any>,
|
||||
path: string,
|
||||
path: string
|
||||
): Promise<void> {
|
||||
// infer schema from script content and update it inplace
|
||||
const result = await inferSchema(
|
||||
language,
|
||||
scriptContent,
|
||||
metadataContent.schema,
|
||||
path,
|
||||
path
|
||||
);
|
||||
metadataContent.schema = result.schema;
|
||||
if (result.has_preprocessor) {
|
||||
@@ -360,7 +367,7 @@ async function updateScriptLock(
|
||||
language: ScriptLanguage,
|
||||
remotePath: string,
|
||||
metadataContent: Record<string, any>,
|
||||
rawDeps: string | undefined,
|
||||
rawDeps: string | undefined
|
||||
): Promise<void> {
|
||||
if (
|
||||
!(
|
||||
@@ -393,7 +400,7 @@ async function updateScriptLock(
|
||||
raw_deps: rawDeps,
|
||||
entrypoint: remotePath,
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
let responseText = "reading response failed";
|
||||
@@ -404,11 +411,11 @@ async function updateScriptLock(
|
||||
if (lock === undefined) {
|
||||
if (response?.["error"]?.["message"]) {
|
||||
throw new LockfileGenerationError(
|
||||
`Failed to generate lockfile: ${response?.["error"]?.["message"]}`,
|
||||
`Failed to generate lockfile: ${response?.["error"]?.["message"]}`
|
||||
);
|
||||
}
|
||||
throw new LockfileGenerationError(
|
||||
`Failed to generate lockfile: ${JSON.stringify(response, null, 2)}`,
|
||||
`Failed to generate lockfile: ${JSON.stringify(response, null, 2)}`
|
||||
);
|
||||
}
|
||||
const lockPath = remotePath + ".script.lock";
|
||||
@@ -420,7 +427,7 @@ async function updateScriptLock(
|
||||
if (await Deno.stat(lockPath)) {
|
||||
await Deno.remove(lockPath);
|
||||
}
|
||||
} catch { }
|
||||
} catch {}
|
||||
metadataContent.lock = "";
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -428,7 +435,7 @@ async function updateScriptLock(
|
||||
throw e;
|
||||
}
|
||||
throw new LockfileGenerationError(
|
||||
`Failed to generate lockfile:${rawResponse.statusText}, ${responseText}, ${e}`,
|
||||
`Failed to generate lockfile:${rawResponse.statusText}, ${responseText}, ${e}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -459,7 +466,7 @@ export async function updateFlow(
|
||||
use_local_lockfiles: true,
|
||||
raw_deps: rawDeps,
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Standard dependency resolution on the server
|
||||
@@ -475,7 +482,7 @@ export async function updateFlow(
|
||||
flow_value,
|
||||
path: remotePath,
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -489,20 +496,20 @@ export async function updateFlow(
|
||||
const msg = (res as any)?.["error"]?.["message"];
|
||||
if (msg) {
|
||||
throw new LockfileGenerationError(
|
||||
`Failed to generate lockfile: ${msg}`,
|
||||
`Failed to generate lockfile: ${msg}`
|
||||
);
|
||||
}
|
||||
throw new LockfileGenerationError(
|
||||
`Failed to generate lockfile: ${rawResponse.statusText}, ${responseText}`,
|
||||
`Failed to generate lockfile: ${rawResponse.statusText}, ${responseText}`
|
||||
);
|
||||
}
|
||||
return (res as any).updated_flow_value;
|
||||
} catch (e) {
|
||||
try {
|
||||
responseText = await rawResponse.text();
|
||||
} catch { }
|
||||
} catch {}
|
||||
throw new Error(
|
||||
`Failed to generate lockfile. Status was: ${rawResponse.statusText}, ${responseText}, ${e}`,
|
||||
`Failed to generate lockfile. Status was: ${rawResponse.statusText}, ${responseText}, ${e}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -514,7 +521,7 @@ export async function inferSchema(
|
||||
language: ScriptLanguage,
|
||||
content: string,
|
||||
currentSchema: any,
|
||||
path: string,
|
||||
path: string
|
||||
): Promise<{
|
||||
schema: any;
|
||||
has_preprocessor: boolean | undefined;
|
||||
@@ -523,7 +530,7 @@ export async function inferSchema(
|
||||
let inferedSchema: any;
|
||||
if (language === "python3") {
|
||||
const { parse_python } = await import(
|
||||
"./wasm/python/windmill_parser_wasm.js"
|
||||
"./wasm/py/windmill_parser_wasm.js"
|
||||
);
|
||||
inferedSchema = JSON.parse(parse_python(content));
|
||||
} else if (language === "nativets") {
|
||||
@@ -592,8 +599,8 @@ export async function inferSchema(
|
||||
...inferedSchema.args,
|
||||
];
|
||||
} else if (language === "duckdb") {
|
||||
const { parse_sql } = await import("./wasm/regex/windmill_parser_wasm.js");
|
||||
inferedSchema = JSON.parse(parse_sql(content));
|
||||
const { parse_duckdb } = await import("./wasm/regex/windmill_parser_wasm.js");
|
||||
inferedSchema = JSON.parse(parse_duckdb(content));
|
||||
} else if (language === "graphql") {
|
||||
const { parse_graphql } = await import(
|
||||
"./wasm/regex/windmill_parser_wasm.js"
|
||||
@@ -640,8 +647,8 @@ export async function inferSchema(
|
||||
if (inferedSchema.type == "Invalid") {
|
||||
log.info(
|
||||
colors.yellow(
|
||||
`Script ${path} invalid, it cannot be parsed to infer schema.`,
|
||||
),
|
||||
`Script ${path} invalid, it cannot be parsed to infer schema.`
|
||||
)
|
||||
);
|
||||
return {
|
||||
schema: defaultScriptMetadata().schema,
|
||||
@@ -655,7 +662,7 @@ export async function inferSchema(
|
||||
}
|
||||
currentSchema.required = [];
|
||||
const oldProperties = JSON.parse(
|
||||
JSON.stringify(currentSchema?.properties ?? {}),
|
||||
JSON.stringify(currentSchema?.properties ?? {})
|
||||
);
|
||||
currentSchema.properties = {};
|
||||
|
||||
@@ -666,7 +673,7 @@ export async function inferSchema(
|
||||
currentSchema.properties[arg.name] = oldProperties[arg.name];
|
||||
}
|
||||
currentSchema.properties[arg.name] = sortObject(
|
||||
currentSchema.properties[arg.name],
|
||||
currentSchema.properties[arg.name]
|
||||
);
|
||||
|
||||
argSigToJsonSchemaType(arg.typ, currentSchema.properties[arg.name]);
|
||||
@@ -693,7 +700,7 @@ function sortObject(obj: any): any {
|
||||
...acc,
|
||||
[key]: obj[key],
|
||||
}),
|
||||
{},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -703,24 +710,30 @@ export function argSigToJsonSchemaType(
|
||||
| string
|
||||
| { resource: string | null }
|
||||
| {
|
||||
list:
|
||||
| (string | { object: { key: string; typ: any }[] })
|
||||
| { str: any }
|
||||
| { object: { key: string; typ: any }[] }
|
||||
| null;
|
||||
}
|
||||
list:
|
||||
| (
|
||||
| string
|
||||
| {
|
||||
object: {
|
||||
name?: string;
|
||||
props?: { key: string; typ: any }[];
|
||||
};
|
||||
}
|
||||
)
|
||||
| { str: any }
|
||||
| { object: { name?: string; props?: { key: string; typ: any }[] } }
|
||||
| null;
|
||||
}
|
||||
| { dynselect: string }
|
||||
| { str: string[] | null }
|
||||
| { object: { key: string; typ: any }[] }
|
||||
| { object: { name?: string; props?: { key: string; typ: any }[] } }
|
||||
| {
|
||||
oneof: [
|
||||
{
|
||||
oneof: {
|
||||
label: string;
|
||||
properties: { key: string; typ: any }[];
|
||||
},
|
||||
];
|
||||
},
|
||||
oldS: SchemaProperty,
|
||||
}[];
|
||||
},
|
||||
oldS: SchemaProperty
|
||||
): void {
|
||||
const newS: SchemaProperty = { type: "" };
|
||||
if (t === "int") {
|
||||
@@ -770,9 +783,12 @@ export function argSigToJsonSchemaType(
|
||||
}
|
||||
} else if (typeof t !== "string" && `object` in t) {
|
||||
newS.type = "object";
|
||||
if (t.object) {
|
||||
if (t.object.name) {
|
||||
newS.format = `resource-${t.object.name}`;
|
||||
}
|
||||
if (t.object.props) {
|
||||
const properties: Record<string, any> = {};
|
||||
for (const prop of t.object) {
|
||||
for (const prop of t.object.props) {
|
||||
if (oldS.properties && prop.key in oldS.properties) {
|
||||
properties[prop.key] = oldS.properties[prop.key];
|
||||
} else {
|
||||
@@ -804,12 +820,24 @@ export function argSigToJsonSchemaType(
|
||||
newS.type = "array";
|
||||
if (t.list === "int" || t.list === "float") {
|
||||
newS.items = { type: "number" };
|
||||
newS.originalType = "number[]";
|
||||
} else if (t.list === "bytes") {
|
||||
newS.items = { type: "string", contentEncoding: "base64" };
|
||||
} else if (t.list == "string") {
|
||||
newS.items = { type: "string" };
|
||||
} else if (t.list && typeof t.list == "object" && "str" in t.list) {
|
||||
newS.originalType = "bytes[]";
|
||||
} else if (
|
||||
t.list &&
|
||||
typeof t.list == "object" &&
|
||||
"str" in t.list &&
|
||||
t.list.str
|
||||
) {
|
||||
newS.items = { type: "string", enum: t.list.str };
|
||||
newS.originalType = "enum[]";
|
||||
} else if (
|
||||
t.list == "string" ||
|
||||
(t.list && typeof t.list == "object" && "str" in t.list)
|
||||
) {
|
||||
newS.items = { type: "string", enum: oldS.items?.enum };
|
||||
newS.originalType = "string[]";
|
||||
} else if (
|
||||
t.list &&
|
||||
typeof t.list == "object" &&
|
||||
@@ -820,23 +848,30 @@ export function argSigToJsonSchemaType(
|
||||
type: "resource",
|
||||
resourceType: t.list.resource as string,
|
||||
};
|
||||
newS.originalType = "resource[]";
|
||||
} else if (
|
||||
t.list &&
|
||||
typeof t.list == "object" &&
|
||||
"object" in t.list &&
|
||||
t.list.object &&
|
||||
t.list.object.length > 0
|
||||
t.list.object
|
||||
) {
|
||||
const properties: Record<string, any> = {};
|
||||
for (const prop of t.list.object) {
|
||||
properties[prop.key] = { description: "", type: "" };
|
||||
|
||||
argSigToJsonSchemaType(prop.typ, properties[prop.key]);
|
||||
if (t.list.object.name) {
|
||||
newS.format = `resource-${t.list.object.name}`;
|
||||
}
|
||||
|
||||
newS.items = { type: "object", properties: properties };
|
||||
if (t.list.object.props && t.list.object.props.length > 0) {
|
||||
const properties: Record<string, any> = {};
|
||||
for (const prop of t.list.object.props) {
|
||||
properties[prop.key] = { description: "", type: "" };
|
||||
argSigToJsonSchemaType(prop.typ, properties[prop.key]);
|
||||
}
|
||||
newS.items = { type: "object", properties: properties };
|
||||
} else {
|
||||
newS.items = { type: "object" };
|
||||
}
|
||||
newS.originalType = "record[]";
|
||||
} else {
|
||||
newS.items = { type: "object" };
|
||||
newS.originalType = "object[]";
|
||||
}
|
||||
} else {
|
||||
newS.type = "object";
|
||||
@@ -886,16 +921,15 @@ export function argSigToJsonSchemaType(
|
||||
delete oldS.items;
|
||||
}
|
||||
|
||||
Object.assign(oldS, newS);
|
||||
if (oldS.format && !newS.format) {
|
||||
oldS.format = undefined
|
||||
}
|
||||
|
||||
Object.assign(oldS, newS);
|
||||
// if (sameItems && savedItems != undefined && savedItems.enum != undefined) {
|
||||
// sendUserToast(JSON.stringify(savedItems))
|
||||
// oldS.items = savedItems
|
||||
// }
|
||||
|
||||
if (oldS.format?.startsWith("resource-") && newS.type != "object") {
|
||||
oldS.format = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -912,9 +946,7 @@ export function replaceLock(o?: { lock?: string | string[] }) {
|
||||
o.lock = readInlinePathSync(lockPath);
|
||||
} catch (e) {
|
||||
log.info(
|
||||
colors.yellow(
|
||||
`Failed to read lockfile, doing as if it was empty: ${e}`,
|
||||
),
|
||||
colors.yellow(`Failed to read lockfile, doing as if it was empty: ${e}`)
|
||||
);
|
||||
o.lock = "";
|
||||
}
|
||||
@@ -924,13 +956,13 @@ export async function parseMetadataFile(
|
||||
scriptPath: string,
|
||||
generateMetadataIfMissing:
|
||||
| (GlobalOptions & {
|
||||
path: string;
|
||||
workspaceRemote: Workspace;
|
||||
schemaOnly?: boolean;
|
||||
})
|
||||
path: string;
|
||||
workspaceRemote: Workspace;
|
||||
schemaOnly?: boolean;
|
||||
})
|
||||
| undefined,
|
||||
globalDeps: GlobalDeps,
|
||||
codebases: SyncCodebase[],
|
||||
codebases: SyncCodebase[]
|
||||
): Promise<{ isJson: boolean; payload: any; path: string }> {
|
||||
let metadataFilePath = scriptPath + ".script.json";
|
||||
try {
|
||||
@@ -956,14 +988,14 @@ export async function parseMetadataFile(
|
||||
// no metadata file at all. Create it
|
||||
log.info(
|
||||
(await blueColor())(
|
||||
`Creating script metadata file for ${metadataFilePath}`,
|
||||
),
|
||||
`Creating script metadata file for ${metadataFilePath}`
|
||||
)
|
||||
);
|
||||
metadataFilePath = scriptPath + ".script.yaml";
|
||||
let scriptInitialMetadata = defaultScriptMetadata();
|
||||
const scriptInitialMetadataYaml = yamlStringify(
|
||||
scriptInitialMetadata as Record<string, any>,
|
||||
yamlOptions,
|
||||
yamlOptions
|
||||
);
|
||||
await Deno.writeTextFile(metadataFilePath, scriptInitialMetadataYaml, {
|
||||
createNew: true,
|
||||
@@ -972,8 +1004,8 @@ export async function parseMetadataFile(
|
||||
if (generateMetadataIfMissing) {
|
||||
log.info(
|
||||
(await blueColor())(
|
||||
`Generating lockfile and schema for ${metadataFilePath}`,
|
||||
),
|
||||
`Generating lockfile and schema for ${metadataFilePath}`
|
||||
)
|
||||
);
|
||||
try {
|
||||
await generateScriptMetadataInternal(
|
||||
@@ -984,10 +1016,10 @@ export async function parseMetadataFile(
|
||||
false,
|
||||
globalDeps,
|
||||
codebases,
|
||||
false,
|
||||
false
|
||||
);
|
||||
scriptInitialMetadata = (await yamlParseFile(
|
||||
metadataFilePath,
|
||||
metadataFilePath
|
||||
)) as ScriptMetadata;
|
||||
if (!generateMetadataIfMissing.schemaOnly) {
|
||||
replaceLock(scriptInitialMetadata);
|
||||
@@ -995,8 +1027,8 @@ export async function parseMetadataFile(
|
||||
} catch (e) {
|
||||
log.info(
|
||||
colors.yellow(
|
||||
`Failed to generate lockfile and schema for ${metadataFilePath}: ${e}`,
|
||||
),
|
||||
`Failed to generate lockfile and schema for ${metadataFilePath}: ${e}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1035,7 +1067,7 @@ export async function checkifMetadataUptodate(
|
||||
path: string,
|
||||
hash: string,
|
||||
conf: Lock | undefined,
|
||||
subpath?: string,
|
||||
subpath?: string
|
||||
) {
|
||||
if (!conf) {
|
||||
conf = await readLockfile();
|
||||
@@ -1051,17 +1083,17 @@ export async function checkifMetadataUptodate(
|
||||
export async function generateScriptHash(
|
||||
rawReqs: string | undefined,
|
||||
scriptContent: string,
|
||||
newMetadataContent: string,
|
||||
newMetadataContent: string
|
||||
) {
|
||||
return await generateHash(
|
||||
(rawReqs ?? "") + scriptContent + newMetadataContent,
|
||||
(rawReqs ?? "") + scriptContent + newMetadataContent
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateMetadataGlobalLock(
|
||||
path: string,
|
||||
hash: string,
|
||||
subpath?: string,
|
||||
subpath?: string
|
||||
): Promise<void> {
|
||||
const conf = await readLockfile();
|
||||
if (!conf?.locks) {
|
||||
@@ -1080,6 +1112,6 @@ export async function updateMetadataGlobalLock(
|
||||
}
|
||||
await Deno.writeTextFile(
|
||||
WMILL_LOCKFILE,
|
||||
yamlStringify(conf as Record<string, any>, yamlOptions),
|
||||
yamlStringify(conf as Record<string, any>, yamlOptions)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,23 +76,37 @@ async function push(opts: PushOptions, filePath: string, name: string) {
|
||||
log.info(colors.bold.underline.green("Resource pushed"));
|
||||
}
|
||||
|
||||
async function list(opts: GlobalOptions) {
|
||||
async function list(opts: GlobalOptions & { schema?: boolean }) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const res = await wmill.listResourceType({
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
|
||||
new Table()
|
||||
.header(["Workspace", "Name"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(res.map((x) => [x.workspace_id ?? "Global", x.name]))
|
||||
.render();
|
||||
if (opts.schema) {
|
||||
new Table()
|
||||
.header(["Workspace", "Name", "Schema"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(res.map((x) => [x.workspace_id ?? "Global", x.name, JSON.stringify(x.schema, null, 2)]))
|
||||
.render();
|
||||
} else {
|
||||
new Table()
|
||||
.header(["Workspace", "Name"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(res.map((x) => [x.workspace_id ?? "Global", x.name]))
|
||||
.render();
|
||||
}
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
.description("resource type related commands")
|
||||
.action(() =>
|
||||
log.info("2 actions available, list and push.")
|
||||
)
|
||||
.command("list", "list all resource types")
|
||||
.option("--schema", "Show schema in the output")
|
||||
.action(list as any)
|
||||
.command(
|
||||
"push",
|
||||
|
||||
439
cli/script_guidance.ts
Normal file
439
cli/script_guidance.ts
Normal file
@@ -0,0 +1,439 @@
|
||||
export const SCRIPT_GUIDANCE = `
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Windmill Script Writing Guide - Universal System Prompt
|
||||
|
||||
You are a coding assistant for the Windmill platform. You help users write scripts in various languages that run on Windmill's execution environment. Each script should be placed in a folder. Ask the user in which folder he wants the script to be located at before starting coding.
|
||||
After writing a script, you do not need to create .lock and .yaml files manually. Instead, you can run \`wmill script generate-metadata\` bash command. This command takes no arguments. After writing the script, you can ask the user if he wants to push the script with \`wmill sync push\`. Both should be run at the root of the repository.
|
||||
|
||||
## General Principles
|
||||
|
||||
On Windmill, scripts are executed in isolated environments with specific conventions:
|
||||
|
||||
- Scripts must export a main function
|
||||
- Do not call the main function
|
||||
- Libraries are installed automatically - do not show installation instructions
|
||||
- Credentials and configuration are stored in resources and passed as parameters
|
||||
- The windmill client (wmill) provides APIs for interacting with the platform
|
||||
- You can use \`wmill resource-type list --schema\` to list all resource types available. You should use that to know the type of the resource you need to use in your script. You can use grep if the output is too long.
|
||||
|
||||
## Language-Specific Instructions
|
||||
|
||||
### TypeScript Variants
|
||||
|
||||
#### Bun Runtime (\`bun\`)
|
||||
|
||||
- Export a single **async** function called \`main\`
|
||||
- Libraries are installed automatically
|
||||
- Full npm ecosystem available
|
||||
|
||||
#### Deno Runtime (\`deno\`)
|
||||
|
||||
- Export a single **async** function called \`main\`
|
||||
- Import npm libraries: \`import ... from "npm:{package}";\`
|
||||
- Import deno libraries normally
|
||||
- Libraries are installed automatically
|
||||
|
||||
#### TypeScript Resource Types & Windmill Client
|
||||
|
||||
**Resource Types:**
|
||||
On Windmill, credentials and configuration are stored in resources and passed as parameters to main.
|
||||
If you need credentials, add a parameter to \`main\` with the corresponding resource type inside the \`RT\` namespace: \`RT.Stripe\`.
|
||||
Only use them if needed to satisfy instructions. Always use the RT namespace.
|
||||
|
||||
**Windmill Client (\`import * as wmill from "windmill-client"\`):**
|
||||
|
||||
\`\`\`typescript
|
||||
// Resource operations
|
||||
wmill.getResource(path?: string, undefinedIfEmpty?: boolean): Promise<any>
|
||||
wmill.setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise<void>
|
||||
|
||||
// State management (persistent across executions)
|
||||
wmill.getState(): Promise<any>
|
||||
wmill.setState(state: any): Promise<void>
|
||||
|
||||
// Variables
|
||||
wmill.getVariable(path: string): Promise<string>
|
||||
wmill.setVariable(path: string, value: string, isSecretIfNotExist?: boolean, descriptionIfNotExist?: string): Promise<void>
|
||||
|
||||
// Script execution
|
||||
wmill.runScript(path?: string | null, hash_?: string | null, args?: Record<string, any> | null, verbose?: boolean): Promise<any>
|
||||
wmill.runScriptAsync(path: string | null, hash_: string | null, args: Record<string, any> | null, scheduledInSeconds?: number | null): Promise<string>
|
||||
wmill.waitJob(jobId: string, verbose?: boolean): Promise<any>
|
||||
wmill.getResult(jobId: string): Promise<any>
|
||||
wmill.getRootJobId(jobId?: string): Promise<string>
|
||||
|
||||
// S3 file operations (if S3 is configured)
|
||||
wmill.loadS3File(s3object: S3Object, s3ResourcePath?: string | undefined): Promise<Uint8Array | undefined>
|
||||
wmill.writeS3File(s3object: S3Object | undefined, fileContent: string | Blob, s3ResourcePath?: string | undefined): Promise<S3Object>
|
||||
|
||||
// Flow operations
|
||||
wmill.setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): Promise<void>
|
||||
wmill.getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise<any>
|
||||
wmill.getResumeUrls(approver?: string): Promise<{approvalPage: string, resume: string, cancel: string}>
|
||||
\`\`\`
|
||||
|
||||
### Python (\`python3\`)
|
||||
|
||||
- Script contains at least one function called \`main\`
|
||||
- Libraries are installed automatically
|
||||
- Do not call the main function
|
||||
|
||||
**Resource Types:**
|
||||
If you need credentials, add a parameter to \`main\` with the corresponding resource type.
|
||||
**Redefine** the type of needed resources before the main function as TypedDict (only include if actually needed).
|
||||
Resource type name must be **IN LOWERCASE**.
|
||||
If an import conflicts with a resource type name, **rename the imported object, not the type name**.
|
||||
Import TypedDict from typing **if using it**.
|
||||
|
||||
**Windmill Client (\`import wmill\`):**
|
||||
|
||||
\`\`\`python
|
||||
# Resource operations
|
||||
wmill.get_resource(path: str, none_if_undefined: bool = False) -> dict | None
|
||||
wmill.set_resource(path: str, value: Any, resource_type: str = "any") -> None
|
||||
|
||||
# State management
|
||||
wmill.get_state() -> Any
|
||||
wmill.set_state(value: Any) -> None
|
||||
wmill.get_flow_user_state(key: str) -> Any
|
||||
wmill.set_flow_user_state(key: str, value: Any) -> None
|
||||
|
||||
# Variables
|
||||
wmill.get_variable(path: str) -> str
|
||||
wmill.set_variable(path: str, value: str, is_secret: bool = False) -> None
|
||||
|
||||
# Script execution
|
||||
wmill.run_script(path: str = None, hash_: str = None, args: dict = None, timeout = None, verbose: bool = False) -> Any
|
||||
wmill.run_script_async(path: str = None, hash_: str = None, args: dict = None, scheduled_in_secs: int = None) -> str
|
||||
wmill.wait_job(job_id: str, timeout = None, verbose: bool = False) -> Any
|
||||
wmill.get_result(job_id: str) -> Any
|
||||
|
||||
# S3 operations
|
||||
wmill.load_s3_file(s3object: S3Object | str, s3_resource_path: str | None = None) -> bytes
|
||||
wmill.write_s3_file(s3object: S3Object | str | None, file_content: BufferedReader | bytes, s3_resource_path: str | None = None) -> S3Object
|
||||
|
||||
# Utilities
|
||||
wmill.get_workspace() -> str
|
||||
wmill.whoami() -> dict
|
||||
wmill.set_progress(value: int, job_id: Optional[str] = None) -> None
|
||||
\`\`\`
|
||||
|
||||
### PHP (\`php\`)
|
||||
|
||||
- Script must start with \`<?php\`
|
||||
- Contains at least one function called \`main\`
|
||||
- **Redefine** resource types before main function (only if needed)
|
||||
- Check if class exists using \`class_exists\` before defining types
|
||||
- Resource type name must be exactly as specified
|
||||
|
||||
**Resource Types:**
|
||||
If you need credentials, add a parameter to \`main\` with the corresponding resource type.
|
||||
**Redefine** the type of needed resources before the main function.
|
||||
Before defining each type, check if the class already exists using class_exists.
|
||||
The resource type name has to be exactly as specified.
|
||||
|
||||
**Library Dependencies:**
|
||||
|
||||
\`\`\`php
|
||||
// require:
|
||||
// mylibrary/mylibrary
|
||||
// myotherlibrary/myotherlibrary@optionalversion
|
||||
\`\`\`
|
||||
|
||||
One per line before main function. Autoload already included.
|
||||
|
||||
### Rust (\`rust\`)
|
||||
|
||||
\`\`\`rust
|
||||
use anyhow::anyhow;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct ReturnType {
|
||||
// ...
|
||||
}
|
||||
|
||||
fn main(...) -> anyhow::Result<ReturnType>
|
||||
\`\`\`
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
\`\`\`\`rust
|
||||
//! \`\`\`cargo
|
||||
//! [dependencies]
|
||||
//! anyhow = "1.0.86"
|
||||
//! \`\`\`
|
||||
\`\`\`\`
|
||||
|
||||
Serde already included. For async functions, keep main sync and create runtime inside.
|
||||
|
||||
### Go (\`go\`)
|
||||
|
||||
- File package must be "inner"
|
||||
- Export single function called \`main\`
|
||||
- Return type: \`({return_type}, error)\`
|
||||
|
||||
### Bash (\`bash\`)
|
||||
|
||||
- Do not include "#!/bin/bash"
|
||||
- Arguments: \`var1="$1"\`, \`var2="$2"\`, etc.
|
||||
|
||||
### SQL Variants
|
||||
|
||||
#### PostgreSQL (\`postgresql\`)
|
||||
|
||||
- Arguments: \`$1::{type}\`, \`$2::{type}\`, etc.
|
||||
- Name parameters: \`-- $1 name1\` or \`-- $2 name = default\`
|
||||
|
||||
#### MySQL (\`mysql\`)
|
||||
|
||||
- Arguments: \`?\` placeholders
|
||||
- Name parameters: \`-- ? name1 ({type})\` or \`-- ? name2 ({type}) = default\`
|
||||
|
||||
#### BigQuery (\`bigquery\`)
|
||||
|
||||
- Arguments: \`@name1\`, \`@name2\`, etc.
|
||||
- Name parameters: \`-- @name1 ({type})\` or \`-- @name2 ({type}) = default\`
|
||||
|
||||
#### Snowflake (\`snowflake\`)
|
||||
|
||||
- Arguments: \`?\` placeholders
|
||||
- Name parameters: \`-- ? name1 ({type})\` or \`-- ? name2 ({type}) = default\`
|
||||
|
||||
#### Microsoft SQL Server (\`mssql\`)
|
||||
|
||||
- Arguments: \`@P1\`, \`@P2\`, etc.
|
||||
- Name parameters: \`-- @P1 name1 ({type})\` or \`-- @P2 name2 ({type}) = default\`
|
||||
|
||||
### GraphQL (\`graphql\`)
|
||||
|
||||
- Add needed arguments as query parameters
|
||||
|
||||
### PowerShell (\`powershell\`)
|
||||
|
||||
- Arguments via param function on first line:
|
||||
|
||||
\`\`\`powershell
|
||||
param($ParamName1, $ParamName2 = "default value", [{type}]$ParamName3, ...)
|
||||
\`\`\`
|
||||
|
||||
### C# (\`csharp\`)
|
||||
|
||||
- Public static Main method inside a class
|
||||
- NuGet packages: \`#r "nuget: PackageName, Version"\` at top
|
||||
- Method signature: \`public static ReturnType Main(parameter types...)\`
|
||||
|
||||
### Java (\`java\`)
|
||||
|
||||
- Main public class with \`public static main()\` method
|
||||
- Dependencies: \`//requirements://groupId:artifactId:version\` at top
|
||||
- Method signature: \`public static Object main(parameter types...)\`
|
||||
|
||||
## Supported Languages
|
||||
|
||||
\`bunnative\`, \`nativets\`, \`bun\`, \`deno\`, \`python3\`, \`php\`, \`rust\`, \`go\`, \`bash\`, \`postgresql\`, \`mysql\`, \`bigquery\`, \`snowflake\`, \`mssql\`, \`graphql\`, \`powershell\`, \`csharp\`, \`java\`
|
||||
|
||||
Always follow the specific conventions for the language being used and include only necessary dependencies and resource types.
|
||||
|
||||
# Windmill CLI Commands Summary
|
||||
|
||||
## Core Commands
|
||||
|
||||
### \`wmill init\`
|
||||
|
||||
Bootstrap a new Windmill project with a \`wmill.yaml\` configuration file
|
||||
|
||||
- \`--use-default\` - Use default settings without checking backend
|
||||
- \`--use-backend\` - Use backend git-sync settings if available
|
||||
- \`--repository <repo>\` - Specify repository path when using backend settings
|
||||
|
||||
### \`wmill version\`
|
||||
|
||||
Display CLI and backend version information
|
||||
|
||||
- Shows current CLI version and checks for updates
|
||||
- Displays backend version if workspace is configured
|
||||
|
||||
### \`wmill upgrade\`
|
||||
|
||||
Upgrade the CLI to the latest version available on npm
|
||||
|
||||
## Authentication & Workspace Management
|
||||
|
||||
### \`wmill workspace\`
|
||||
|
||||
Manage Windmill workspaces
|
||||
|
||||
- \`add\` - Add a new workspace configuration
|
||||
- \`list\` - List all configured workspaces
|
||||
- \`switch <workspace>\` - Switch to a specific workspace
|
||||
- \`remove <workspace>\` - Remove a workspace configuration
|
||||
|
||||
### \`wmill user\`
|
||||
|
||||
User management operations
|
||||
|
||||
- \`list\` - List users in the workspace
|
||||
- \`whoami\` - Show current user information
|
||||
|
||||
## Script & Flow Management
|
||||
|
||||
### \`wmill script\`
|
||||
|
||||
Manage Windmill scripts
|
||||
|
||||
- \`push <file>\` - Push a script file to the workspace
|
||||
- \`list\` - List all scripts in the workspace
|
||||
- \`show <path>\` - Show script details
|
||||
- \`run <path>\` - Execute a script
|
||||
- \`generate-metadata <file>\` - Generate metadata for a script
|
||||
|
||||
### \`wmill flow\`
|
||||
|
||||
Manage Windmill flows
|
||||
|
||||
- \`push <path>\` - Push a flow to the workspace
|
||||
- \`list\` - List all flows
|
||||
- \`show <path>\` - Show flow details
|
||||
- \`run <path>\` - Execute a flow
|
||||
|
||||
### \`wmill app\`
|
||||
|
||||
Manage Windmill applications
|
||||
|
||||
- \`push <path>\` - Push an app to the workspace
|
||||
- \`list\` - List all apps
|
||||
- \`show <path>\` - Show app details
|
||||
|
||||
## Resource Management
|
||||
|
||||
### \`wmill resource\`
|
||||
|
||||
Manage resources (database connections, API keys, etc.)
|
||||
|
||||
- \`list\` - List all resources
|
||||
- \`push <file>\` - Push a resource definition
|
||||
- \`show <path>\` - Show resource details
|
||||
|
||||
### \`wmill resource-type\`
|
||||
|
||||
Manage custom resource types
|
||||
|
||||
- Operations for defining and managing custom resource schemas
|
||||
|
||||
### \`wmill variable\`
|
||||
|
||||
Manage workspace variables and secrets
|
||||
|
||||
- \`list\` - List all variables
|
||||
- \`push <file>\` - Push a variable definition
|
||||
- \`show <path>\` - Show variable details
|
||||
|
||||
## Scheduling & Automation
|
||||
|
||||
### \`wmill schedule\`
|
||||
|
||||
Manage scheduled jobs
|
||||
|
||||
- \`list\` - List all schedules
|
||||
- \`push <file>\` - Push a schedule definition
|
||||
- Operations for managing cron-based job scheduling
|
||||
|
||||
### \`wmill trigger\`
|
||||
|
||||
Manage event triggers
|
||||
|
||||
- Operations for managing webhooks and event-based triggers
|
||||
|
||||
## Synchronization
|
||||
|
||||
### \`wmill sync\`
|
||||
|
||||
Synchronize local files with Windmill workspace
|
||||
|
||||
- \`pull\` - Download resources from workspace to local files
|
||||
- \`push\` - Upload local files to workspace
|
||||
- Supports bidirectional sync with conflict resolution
|
||||
- Works with \`wmill.yaml\` configuration
|
||||
|
||||
### \`wmill gitsync-settings\`
|
||||
|
||||
Manage git synchronization settings
|
||||
|
||||
- Configure automatic git sync for the workspace
|
||||
- Pull/push git sync configurations
|
||||
|
||||
## Development Tools
|
||||
|
||||
### \`wmill dev\`
|
||||
|
||||
Start development mode with live reloading
|
||||
|
||||
- Watches local files for changes
|
||||
- Automatically syncs changes to workspace
|
||||
- Provides real-time feedback during development
|
||||
|
||||
### \`wmill hub\`
|
||||
|
||||
Interact with Windmill Hub
|
||||
|
||||
- \`pull\` - Pull resources from the public Windmill Hub
|
||||
- Access community-shared scripts, flows, and resource types
|
||||
|
||||
## Infrastructure Management
|
||||
|
||||
### \`wmill instance\`
|
||||
|
||||
Manage Windmill instance settings (Enterprise)
|
||||
|
||||
- Configure instance-level settings
|
||||
- Manage global configurations
|
||||
|
||||
### \`wmill worker-groups\`
|
||||
|
||||
Manage worker groups for job execution
|
||||
|
||||
- Configure and manage worker pool settings
|
||||
|
||||
### \`wmill workers\`
|
||||
|
||||
Manage individual workers
|
||||
|
||||
- Monitor and configure worker instances
|
||||
|
||||
### \`wmill queues\`
|
||||
|
||||
Manage job queues
|
||||
|
||||
- Monitor and configure job execution queues
|
||||
|
||||
## Utility Commands
|
||||
|
||||
### \`wmill folder\`
|
||||
|
||||
Manage workspace folders and organization
|
||||
|
||||
- Operations for organizing resources into folders
|
||||
|
||||
### \`wmill completions\`
|
||||
|
||||
Generate shell completion scripts
|
||||
|
||||
- Support for bash, zsh, fish, and PowerShell
|
||||
|
||||
## Global Options
|
||||
|
||||
All commands support these global options:
|
||||
|
||||
- \`--workspace <workspace>\` - Specify target workspace
|
||||
- \`--token <token>\` - Specify API token
|
||||
- \`--base-url <url>\` - Specify Windmill instance URL
|
||||
- \`--config-dir <dir>\` - Custom configuration directory
|
||||
- \`--debug/--verbose\` - Enable debug logging
|
||||
- \`--show-diffs\` - Show detailed diff information during sync
|
||||
|
||||
The CLI uses a \`wmill.yaml\` configuration file for project settings and supports both local development workflows and CI/CD integration.
|
||||
`;
|
||||
13
cli/test.nu
13
cli/test.nu
@@ -22,7 +22,7 @@ const languages = [
|
||||
"graphql",
|
||||
"bash",
|
||||
"powershell",
|
||||
# "php",
|
||||
"php",
|
||||
"rust",
|
||||
"csharp",
|
||||
"nu",
|
||||
@@ -37,29 +37,36 @@ def main [] {
|
||||
|
||||
def 'main test deno' [] {
|
||||
main clean;
|
||||
print $"Testing Deno"
|
||||
$languages | each { |l|
|
||||
print $"+ ($l)"
|
||||
deno run -A ../cli/main.ts script bootstrap $"f/tests/(random uuid)" $l
|
||||
}
|
||||
deno run -A ../cli/main.ts script generate-metadata
|
||||
print $"\nDeno has passed!\n"
|
||||
}
|
||||
|
||||
def 'main test node' [] {
|
||||
# TODO: Use node
|
||||
main clean;
|
||||
print $"Testing Node"
|
||||
$languages | each { |l|
|
||||
print $"+ ($l)"
|
||||
node ../cli/npm/esm/main.js script bootstrap $"f/tests/(random uuid)" $l
|
||||
}
|
||||
node ../cli/npm/esm/main.js script generate-metadata
|
||||
print $"\nNode has passed!\n"
|
||||
}
|
||||
|
||||
def 'main clean' [] {
|
||||
print $"Cleaning"
|
||||
rm -rf f/tests/*
|
||||
mkdir f/tests
|
||||
}
|
||||
|
||||
def 'main build' [] {
|
||||
../cli/build.sh
|
||||
print $"Building..."
|
||||
cd ../cli; ./build.sh
|
||||
cd ../local
|
||||
}
|
||||
|
||||
# def main [] {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
4
cli/wasm/py/windmill_parser_wasm.d.ts
vendored
Normal file
4
cli/wasm/py/windmill_parser_wasm.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export function parse_python(code: string, main_override?: string | null): string;
|
||||
export function parse_assets_py(code: string): string;
|
||||
147
cli/wasm/py/windmill_parser_wasm.js
Normal file
147
cli/wasm/py/windmill_parser_wasm.js
Normal file
@@ -0,0 +1,147 @@
|
||||
|
||||
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
let cachedUint8ArrayMemory0 = null;
|
||||
|
||||
function getUint8ArrayMemory0() {
|
||||
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachedUint8ArrayMemory0;
|
||||
}
|
||||
|
||||
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
|
||||
|
||||
const encodeString = function (arg, view) {
|
||||
return cachedTextEncoder.encodeInto(arg, view);
|
||||
};
|
||||
|
||||
function passStringToWasm0(arg, malloc, realloc) {
|
||||
|
||||
if (realloc === undefined) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
const ptr = malloc(buf.length, 1) >>> 0;
|
||||
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||
WASM_VECTOR_LEN = buf.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let len = arg.length;
|
||||
let ptr = malloc(len, 1) >>> 0;
|
||||
|
||||
const mem = getUint8ArrayMemory0();
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (; offset < len; offset++) {
|
||||
const code = arg.charCodeAt(offset);
|
||||
if (code > 0x7F) break;
|
||||
mem[ptr + offset] = code;
|
||||
}
|
||||
|
||||
if (offset !== len) {
|
||||
if (offset !== 0) {
|
||||
arg = arg.slice(offset);
|
||||
}
|
||||
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||
const ret = encodeString(arg, view);
|
||||
|
||||
offset += ret.written;
|
||||
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||
}
|
||||
|
||||
WASM_VECTOR_LEN = offset;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function isLikeNone(x) {
|
||||
return x === undefined || x === null;
|
||||
}
|
||||
|
||||
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
|
||||
|
||||
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
/**
|
||||
* @param {string} code
|
||||
* @param {string | null} [main_override]
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parse_python(code, main_override) {
|
||||
let deferred3_0;
|
||||
let deferred3_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
var ptr1 = isLikeNone(main_override) ? 0 : passStringToWasm0(main_override, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.parse_python(ptr0, len0, ptr1, len1);
|
||||
deferred3_0 = ret[0];
|
||||
deferred3_1 = ret[1];
|
||||
return getStringFromWasm0(ret[0], ret[1]);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parse_assets_py(code) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.parse_assets_py(ptr0, len0);
|
||||
deferred2_0 = ret[0];
|
||||
deferred2_1 = ret[1];
|
||||
return getStringFromWasm0(ret[0], ret[1]);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const imports = {
|
||||
__wbindgen_placeholder__: {
|
||||
__wbindgen_init_externref_table: function() {
|
||||
const table = wasm.__wbindgen_export_0;
|
||||
const offset = table.grow(4);
|
||||
table.set(0, undefined);
|
||||
table.set(offset + 0, undefined);
|
||||
table.set(offset + 1, null);
|
||||
table.set(offset + 2, true);
|
||||
table.set(offset + 3, false);
|
||||
;
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
const wasm_url = new URL('windmill_parser_wasm_bg.wasm', import.meta.url);
|
||||
let wasmCode = '';
|
||||
switch (wasm_url.protocol) {
|
||||
case 'file:':
|
||||
wasmCode = await Deno.readFile(wasm_url);
|
||||
break
|
||||
case 'https:':
|
||||
case 'http:':
|
||||
wasmCode = await (await fetch(wasm_url)).arrayBuffer();
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported protocol: ${wasm_url.protocol}`);
|
||||
}
|
||||
|
||||
const wasmInstance = (await WebAssembly.instantiate(wasmCode, imports)).instance;
|
||||
const wasm = wasmInstance.exports;
|
||||
export const __wasm = wasm;
|
||||
|
||||
wasm.__wbindgen_start();
|
||||
|
||||
BIN
cli/wasm/py/windmill_parser_wasm_bg.wasm
Normal file
BIN
cli/wasm/py/windmill_parser_wasm_bg.wasm
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user