Compare commits

..

173 Commits

Author SHA1 Message Date
wendrul
3cb5660057 Windmill debugger binary 2025-07-30 19:08:25 +02:00
Ruben Fiszel
25aa982318 nit naming 2025-07-30 12:58:33 +00:00
Ruben Fiszel
4b9683f146 feat: prevent too large results (>500Mb) from OOMing database 2025-07-30 12:55:39 +00:00
Ruben Fiszel
14aa6d9681 add more tracing logs 2025-07-30 10:00:48 +00:00
Ruben Fiszel
2619be679e even more indexer tracings 2025-07-30 07:55:19 +00:00
Ruben Fiszel
6971dcdb05 better indexer tracings 2025-07-30 07:25:16 +00:00
Ruben Fiszel
b0616c74cf chore(main): release 1.515.1 (#6295)
* chore(main): release 1.515.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-29 22:21:14 +00:00
Ruben Fiszel
daf3bb0a0a nits logs 2025-07-29 21:01:38 +00:00
Ruben Fiszel
2e7ab919a7 fix: improved logs for script 2025-07-29 20:51:43 +00:00
Ruben Fiszel
b5afff9cc1 chore(main): release 1.515.0 (#6292)
* chore(main): release 1.515.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-29 19:41:05 +00:00
centdix
29c686d62a feat(cli): generate cursor rules on init (#6270)
* create cursor rules on init

* change gen

* add missing resource-type command

* add resource type command in guidance

* add schema option

* revert

* nit

* nit

* add flow guidance

* nit
2025-07-29 19:35:55 +00:00
Diego Imbert
5af47d29e7 Small UI fixes (#6294)
* fix step history not refreshing with staticInputs

* fix array of obj not showing up in json editor in test this step

* datatable scales correctly in DisplayResult and scrolling is much more usable

* avoid next button disapearing and changing layout / hurting ux

* nits

* fix bug when renaming module A to B then module C to A, C takes the schema of A

* fix bug with comments in sql repl

* fix aggrid theme randomly not loading

* bindable script

* better delete button in db manager

* property select doesnt exist

* fix all warnings

* delete $flowStateStore[id] on delete
2025-07-29 19:28:54 +00:00
wendrul
4d6a61403b fix: add size limit to indexer queries on jobs table to avoid oom (#6293)
* Update nix flake

* Update ee repo ref

* update ee-repo-ref

* Move collect stream with limits util to common

* add ee-repo-ref

* update ee-repo-ref

* update eereporef
2025-07-29 18:27:59 +00:00
wendrul
d5c21131df Update nix flake (#6291) 2025-07-29 16:26:38 +00:00
Ruben Fiszel
56671bc75f fix: resource types as arg in typescript handle imported defined types 2025-07-29 16:01:28 +00:00
dieriba
9931311650 fix: resource-type-ts-parser (#6289) 2025-07-29 14:53:24 +00:00
Ruben Fiszel
379880c6c5 nits 2025-07-29 14:51:14 +00:00
Ruben Fiszel
55ba599022 fix: fix DynSelect 2025-07-29 14:33:14 +00:00
Ruben Fiszel
8a2cd1c468 fix import nit 2025-07-29 14:26:55 +00:00
Ruben Fiszel
1084777bd6 support $res: string in form inputs of arrays 2025-07-29 12:40:37 +00:00
Ruben Fiszel
ce4177ebae fix: improve docker logs collection in docker mode 2025-07-28 23:48:28 +00:00
Ruben Fiszel
454b258ab8 chore(main): release 1.514.1 (#6288)
* chore(main): release 1.514.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-28 15:48:28 +00:00
Ruben Fiszel
adf59a36a9 pin rustls to 0.23.29 + pin aws-sdk 2025-07-28 15:18:54 +00:00
Ruben Fiszel
1a85dc7008 fix: pin tokio to 1.46.1 and aws-sdks-ts 2025-07-28 14:49:29 +00:00
Ruben Fiszel
496bbeec36 chore(main): release 1.514.0 (#6283)
* chore(main): release 1.514.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-28 12:30:26 +02:00
centdix
f892f7c9af fix(mcp): add proper check for mcp routes (#6282)
* add proper check for mcp routes

* cleaner

* apply to flow

* fix add checks scopes

---------

Co-authored-by: dieriba <dieriba.pro@gmail.com>
2025-07-28 10:11:36 +00:00
Ruben Fiszel
6e8f5a5b00 feat: migrate audit log ids to bigints (blocking migration for EE) 2025-07-28 08:52:39 +00:00
centdix
d09d90589d remove duplicate caching (#6285) 2025-07-28 08:01:22 +00:00
Ruben Fiszel
a999bc7b28 fix: fix id renaming in apps 2025-07-27 23:15:19 +00:00
Ruben Fiszel
564e92e98d cleanup warnings 2025-07-26 13:48:53 +00:00
Ruben Fiszel
323ec9a994 remove warnings 2025-07-26 13:28:51 +00:00
dieriba
ad441a7ba6 improve: add retry logic for sqs oidc fetch credentials (#6275)
* improve

* update dependency

* push ee ref

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-07-26 13:25:25 +00:00
Ruben Fiszel
c067342369 chore(main): release 1.513.1 (#6272)
* chore(main): release 1.513.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-25 18:11:43 +00:00
centdix
e8f4c2418b fix(mcp): allow mcp scopes to run scripts and flows (#6278)
* allow mcp scopes to run scripts and flows

* fix

* fix
2025-07-25 17:16:03 +00:00
David P. Kleinschmidt
359b34ef08 Use backwards-compatible union type syntax in Python client (#6277) 2025-07-25 15:25:08 +00:00
Ruben Fiszel
8b4dc2ce73 nit flow logs message with error handler 2025-07-25 14:06:53 +00:00
Ruben Fiszel
3f784e3c07 fix: improve error handler behavior wrt to parrallel branchall & forloops (#6273)
* error handler improvement

* fix: improve error handler behavior with parallel for loops

* all

* Error handler
2025-07-25 13:53:43 +00:00
Ruben Fiszel
94e20a97e0 fix: jobLoader for flowstatusviewerinner work with public apps 2025-07-25 09:44:07 +00:00
Ruben Fiszel
06bd89c957 chore(main): release 1.513.0 (#6269)
* chore(main): release 1.513.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-24 23:24:00 +00:00
Ruben Fiszel
241be4897e fix: fix webhook panel reactivity 2025-07-24 23:20:48 +00:00
Ruben Fiszel
9f926bd6bb fix backend tests 2025-07-24 21:21:25 +00:00
hugocasa
79205839b6 fix: add windir env var to pwsh for legacy modules like AD (#6271) 2025-07-24 17:38:26 +00:00
Ruben Fiszel
ed1bee9f66 nit 2025-07-24 17:17:59 +00:00
Ruben Fiszel
f71104e9f6 fix: improve aggrid columnDefs reactivity 2025-07-24 17:11:36 +00:00
Ruben Fiszel
e16c8d0199 fix warning 2025-07-24 12:16:14 +00:00
iqdecay
be3173d048 feat: enable workspace exclusion in custom tags (#6263)
* feat: enable tags that exclude workspaces

* feat: frontend tooltip

* fix: use method to check if tag applies

* refactor: change conversion method

* fix: change operator comparison

* remove redundant tests
2025-07-24 12:11:29 +00:00
Ruben Fiszel
f4073abef0 test fix 2025-07-24 12:08:20 +00:00
Ruben Fiszel
e6f9f4d897 more debug info for completed_runs 2025-07-24 11:40:21 +00:00
claude[bot]
16757f3bb5 nit(aichat): recommend anthropic as provider (#6265)
* feat(aichat): add recommendation alert for Claude latest model in Anthropic provider settings

Add info alert in AI workspace settings recommending Claude latest model for better reliability of AI chat when using Anthropic provider.

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* fix

* nit

* nit

* use badge

* use blue

* add tooltip

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: centdix <farhadg110@gmail.com>
2025-07-23 15:14:51 +00:00
centdix
e869580787 nit(aichat): delete old messages when conversation becomes too long (#6258)
* delete old messages

* nit

* feat(aichat): add recursion bounds and improve token estimation

- Add maxDepth parameter (default 10) to deleteOldestMessage to prevent infinite recursion
- Enhance estimateTokenUsage to handle tool calls, content arrays, and function names
- Improves stability and accuracy of message cleanup when conversations become too long

Co-authored-by: centdix <centdix@users.noreply.github.com>

* nit

* cleaning

* better logic

* fix logic

* fix

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: centdix <centdix@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-07-23 16:36:41 +02:00
Ruben Fiszel
0edb01f774 chore(main): release 1.512.0 (#6260)
* chore(main): release 1.512.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-23 13:49:23 +00:00
Ruben Fiszel
e02f06bf68 decision tree nits 2025-07-23 13:45:41 +00:00
Ruben Fiszel
a57df1f781 fix: fix decision tree graph editor 2025-07-23 13:42:04 +00:00
Diego Imbert
7384aa36f4 fix flickering flow graph in vscode extension (#6266) 2025-07-23 12:33:53 +00:00
Ruben Fiszel
b92fcb90a1 add tagLabel in flow editor 2025-07-23 09:21:17 +00:00
dieriba
10befb995d feat: local type references parsing support for main function args (#5995)
* add base struct

* feat resolve interface and type declarion in entrypoint param's function

* nits

* fix reset dependencies

* update package

* fix handle infinite recursion

* add depth level and handle enum for referenced type

* nits

* nits

* nits

* perf

* fix

* done

* fix schema form cache inconsistency

* fix default type and nits

* remove

* update Object typ for parser

* one level ref from from parent when resolving types and use format for resource

* update cli and use resource type

* nits

* update parsers

* fix: use specific parser versions

---------

Co-authored-by: HugoCasa <hugo@casademont.ch>
2025-07-23 10:05:32 +02:00
Ruben Fiszel
91849baf34 fix: include export from ts relative import tracking 2025-07-22 22:58:49 +00:00
Ruben Fiszel
8591478a6d nit force refresh on reload button 2025-07-22 21:31:24 +00:00
Ruben Fiszel
86e14f8135 fix: run autoscaling scripts with superadmin permissions 2025-07-22 21:08:00 +00:00
Ruben Fiszel
b00fde7f2a chore(main): release 1.511.0 (#6250)
* chore(main): release 1.511.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-22 19:57:31 +00:00
dieriba
37b18f0661 fix: scopes-run-and-webhook-token-display (#6259)
* fix

* update sqlx

* Update backend/windmill-api/src/scopes.rs

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* fix perfect match

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-22 20:57:39 +02:00
Ruben Fiszel
34ccc8e0f7 fix: add error name and message to job_postprocessing span 2025-07-22 18:12:42 +00:00
Diego Imbert
2062a634f6 Fix duckdb array and object args not working properly (#6254) 2025-07-22 15:34:38 +00:00
centdix
71b85dfc42 feat(aichat): give completions to autocomplete for other languages than ts (#6253)
* give completitions for other languages than ts

* nit
2025-07-22 17:24:01 +02:00
centdix
dc242c5a8d feat(aichat): improve autocomplete with ata, editor diagnostic and deletion cues (#6245)
* add diagnostic details

* add types

* remove rest of line if possible

* add visual cue

* test

* adapt to context window

* cleaning

* use suggestions instead of lib files

* better use of suggestions

* cleaning

* add help in context

* better cache

* cleaning

* cleaning

* cleaner code

* better prompts

* fix

* cleaning

* fix

* use comment + no xml

* add cond on deletion cue

* fix

* fix

* cleaner
2025-07-22 14:50:26 +02:00
claude[bot]
8bc2a5733e fixt: Add runScriptByPath and runScriptByHash methods to SDK clients (#6252)
* feat: Add runScriptByPath and runScriptByHash methods to SDK clients

- Add runScriptByPath and runScriptByHash methods to TypeScript client
- Add run_script_by_path and run_script_by_hash methods to Python client
- Split functionality from existing methods that took both path and hash parameters
- Add deprecation warnings to existing run_script methods
- Maintain backward compatibility while encouraging use of focused methods

Closes #6251

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* Refactor SDK methods to eliminate code duplication

- Extract common logic into internal helper methods
- Python client: _run_script_async_internal() and _run_script_internal()
- TypeScript client: _runScriptAsyncInternal() and _runScriptInternal()
- Eliminate duplicated parameter processing and HTTP setup
- Maintain exact same public API surface and functionality
- Reduce lines of code while preserving all existing behaviors

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* fix ts and dev.nu for python

* trade warnings.warn for logging.warning

Signed-off-by: pyranota <pyra@duck.com>

---------

Signed-off-by: pyranota <pyra@duck.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: pyranota <pyra@duck.com>
2025-07-22 12:21:21 +00:00
Ruben Fiszel
1713987317 worker group nits 2025-07-22 07:52:39 +00:00
dieriba
c4178c05be feat: periodic worker group script (#6159)
* feaat periodic script

* feat periodic script

* fix unused

* check minimun time

* fix unused import

* update repo ref

* update ref

* nits and update repo ref

* add missing checks

* fix

* improve UX

* improve UX

* improve UX

* fix

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-07-21 22:57:56 +00:00
Alexander Petric
3d99cbe8f5 additional circular dependancy breaking bundling of cli (#6249) 2025-07-21 22:32:33 +02:00
Alexander Petric
506e17864e oauth client credentials: allow to override token url at resource level (#6233)
* oauth client credentials: allow to override token url at resource level

* ee ref
2025-07-21 20:08:56 +00:00
Ruben Fiszel
4b2c944af2 client nit 2025-07-21 18:31:40 +00:00
Ruben Fiszel
24a6038177 chore(main): release 1.510.1 (#6247)
* chore(main): release 1.510.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-21 16:07:26 +00:00
Ruben Fiszel
2c77012115 clear current id 2 2025-07-21 16:03:38 +00:00
Ruben Fiszel
6f91ee791a improve app loader behavior 2025-07-21 15:51:31 +00:00
dieriba
dd1f520d83 fix: improve ssh agent worker naming (#6211)
* remove ssh suffix

* use hostname tags

* feat

* feat

* update sqlx

* use hostname

* use worker prefix

* nits
2025-07-21 13:43:25 +00:00
pyranota
3efc329fc0 build: combine build-pkgs*.sh into build.nu (#6231)
* build: combine build-pkgs* into build.nu

Signed-off-by: pyranota <pyra@duck.com>

* nit refactor

Signed-off-by: pyranota <pyra@duck.com>

---------

Signed-off-by: pyranota <pyra@duck.com>
2025-07-21 12:11:17 +00:00
Ruben Fiszel
f4dbc97a58 fix: in home, archived and include without main not taken into account 2025-07-21 12:00:45 +00:00
Alexander Petric
6f11a87121 github app: workspace admin (#6225)
* github app: workspace admin

* sqlx upgrade

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-07-21 11:35:43 +00:00
hugocasa
be8c4ed86d fix: quickbooks oauth realmId (#6232)
* fix: quickbooks oauth realmId

* nits

* nit
2025-07-21 11:33:57 +00:00
Ruben Fiszel
8323707b07 fix: empty branches in branch one now return the previous_result 2025-07-21 11:23:22 +00:00
Ruben Fiszel
2bd1600253 display default branch being chosen as green 2025-07-21 10:01:05 +00:00
HugoCasa
e16d5e1e8a fix parser package lock 2025-07-21 11:10:06 +02:00
hugocasa
d2328e3670 fix: update parsers to prevent assets var bug (#6246)
* fix: update parsers to prevent assets var bug

* fix wrong parse function in cli for duckdb

---------

Co-authored-by: Diego Imbert <diego@windmill.dev>
2025-07-21 10:58:09 +02:00
Ruben Fiszel
dddabc8d1b chore(main): release 1.510.0 (#6238)
* chore(main): release 1.510.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-20 18:57:12 +00:00
Ruben Fiszel
1c7b206cd4 optimize script/flow lazy loading 2025-07-20 00:25:50 +00:00
Ruben Fiszel
e5dbbdaf1d nit skeleton 2025-07-20 00:11:01 +00:00
Ruben Fiszel
9201391102 fix: prevent loading script by hash if not permissioned 2025-07-19 23:54:01 +00:00
Ruben Fiszel
4fd5a8861d relax resource created by 2025-07-19 23:16:35 +00:00
Ruben Fiszel
6bdc2c3223 fix sqlx 2025-07-19 21:43:59 +00:00
Ruben Fiszel
8bb59d5a3c update ee-ref 2025-07-19 21:38:00 +00:00
Ruben Fiszel
86fa9f91b6 opt flow loading 2025-07-19 15:00:06 +00:00
Ruben Fiszel
dec72e201b feat: use sse for flow status updates 2025-07-19 14:42:26 +00:00
Ruben Fiszel
33619ab18c flow builder opt 2025-07-19 11:29:51 +00:00
Ruben Fiszel
0aa2ea6954 chore(main): release 1.509.2 (#6237)
* chore(main): release 1.509.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-19 11:08:46 +00:00
Ruben Fiszel
affecdad04 fix: add back asset kind variable 2025-07-19 11:04:28 +00:00
Ruben Fiszel
8b41077116 chore(main): release 1.509.1 (#6236)
* chore(main): release 1.509.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-19 10:01:11 +00:00
Ruben Fiszel
a10509a000 fix: add back asset kind variable 2025-07-19 09:58:03 +00:00
Ruben Fiszel
e183d49684 flow opts 2025-07-19 07:37:56 +00:00
Ruben Fiszel
38d4ebf6d3 nit svelte 5 2025-07-19 06:59:49 +00:00
Ruben Fiszel
d80dd3eeac chore(main): release 1.509.0 (#6213)
* chore(main): release 1.509.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-19 00:13:09 +00:00
Ruben Fiszel
442b644438 nit fast in more cases 2025-07-19 00:07:16 +00:00
Ruben Fiszel
d928a6c285 nit fast in more cases 2025-07-19 00:06:26 +00:00
Ruben Fiszel
aa1d02c0b6 nit 2025-07-18 23:54:46 +00:00
Ruben Fiszel
93accc824a improve ob loader consistency 2025-07-18 23:54:04 +00:00
centdix
8dd4a073b4 fix update sqlx action logic (#6234)
* fix pull

* update postgres
2025-07-18 23:17:28 +00:00
Ruben Fiszel
7315212fc0 implement SSE fallback 2025-07-18 23:10:15 +00:00
claude[bot]
4f993c82b5 feat: implement SSE for job updates polling (#6174)
* feat: implement SSE for job updates polling

- Add /getupdate_sse/:id endpoint for real-time job updates via Server-Sent Events
- SSE streams job status, logs, progress, and flow status updates
- Auto-stops streaming when job completes
- Frontend uses EventSource with graceful fallback to polling on errors
- Reduces server load and improves real-time responsiveness
- Update OpenAPI spec with new SSE endpoint definition

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* all

* all

* v1

* rename test job loader

* iter2

* nit

* all

* all

* improve

* upgrade

* try catch assets json parse

* only display loader of custom component if custom component is rendered

* Fix required properties (#6221)

* whitelabelling nits

* remove fdsfs (#6222)

* add refresh on worker tag select

* whitelabel nits

* merge

* merge

* done

* nit fix

* all

* app db

* all

* all

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: Tammo Ippen <tammo.ippen@posteo.de>
Co-authored-by: Diego Imbert <70353967+diegoimbert@users.noreply.github.com>
2025-07-18 22:48:51 +00:00
pyranota
b018e7c018 nit: remove error (#6230) 2025-07-18 15:43:20 +00:00
Tammo Ippen
47c1b1aae5 flow openapi inconsistency (#6229) 2025-07-18 13:31:55 +00:00
wendrul
2c299fd2a5 fix: tantivy/indexer blocking operations on async code (#6227)
* fix blocking tantivy code by putting it in n_blocking tokio tasks

* update ee-repo-ref to test CI

* Update correct ee-repo-ref
2025-07-18 13:19:09 +00:00
hugocasa
6a012a87ed fix: app s3 multi upload policy (#6228) 2025-07-18 13:14:55 +00:00
hugocasa
7b4d0d6abb remove chat circular dep (#6226) 2025-07-18 11:55:23 +00:00
centdix
b099d96a60 feat(ai chat): search for relevant hub scripts and npm packages in script mode (#6215)
* draft

* move tool

* also use hub search

* add types in context of the chat

* reuse code

* cleaning

* better docs fetching

* cleaning

* adapt limit based on context window

* cleaning

* fix

* fix
2025-07-18 11:36:05 +00:00
centdix
9659b227c3 fix rust version (#6223) 2025-07-18 12:54:36 +02:00
Ruben Fiszel
5acd3c21c0 nit fix 2025-07-18 10:40:37 +00:00
Ruben Fiszel
2119da83f1 nit fix 2025-07-18 10:40:21 +00:00
Ruben Fiszel
ae1793ecd1 whitelabel nits 2025-07-18 10:22:27 +00:00
Ruben Fiszel
1f48a05100 add refresh on worker tag select 2025-07-18 10:10:43 +00:00
Diego Imbert
a7f6d74e05 remove fdsfs (#6222) 2025-07-18 10:07:02 +00:00
Ruben Fiszel
673f10095d whitelabelling nits 2025-07-18 09:05:49 +00:00
Tammo Ippen
8a020bcfd5 Fix required properties (#6221) 2025-07-18 08:44:56 +00:00
Ruben Fiszel
2a739cb04b only display loader of custom component if custom component is rendered 2025-07-18 08:43:31 +00:00
Ruben Fiszel
1e8c181f8e try catch assets json parse 2025-07-17 23:29:22 +00:00
Diego Imbert
2ab5345e61 Assets refactor (#6217)
* Moved logic to FlowAssetsProvider

* Remove assetsMap in flow

* do not parse everything on mount + only check for missing assets fields

* add assets field in backend

* remove fallbackAccessTypes

* better structure and less queries / parsing

* Fix assets not showing when pulling raw_flow from jobs

* flow assets ctx for job run

* Fix transitive assets fetching

* Fix input args asset node

* enablePathScriptAndFlowAssets flag

* edit btn for variable

* untrack refresh

* move parseInputArgsAssets

* Assets tab in runs

* Update FlowStatusViewerInner to svelte 5 + fix asset sync bug

* avoid toast error on bad resource

* fetch res metadata for input arg asset

* Job assets viewer in run page

* r/w selector

* remove indigo badge

* store alt_access_type state in ScriptEditor

* Don't parse assets in flow script editor

* Add alt_access_type in backend

* show Read as selected by default to avoid giving the feeling of having made a decision

* keep alt_access_type when reparsing in flow raw scripts

* Remove variable asset kind, and save assets for scripts

* remove all backend asset parsing

* R/W/RW selector button nits

* fix insert into assets not saving alt access type

* support named arguments in python asset parser

* improve asset usage drawer R/W indicator

* update legacy $res: syntax

* reactivity issue

* remove last variable asset stuff

* sqlx prepare

* tooltip explainer

* deprecated variable asset nit
2025-07-17 22:15:01 +00:00
Alexander Petric
ec1ed0ba6b cli: git sync improvements v2 (#6220)
* log when override is applied vs default taken in git sync

* simplify cli merging options + add explicit override test

* gitsync-settings pull/push ask for confirmation or --yes if tty

* cli legacy backend repo setting detected + interactive migration
2025-07-17 22:14:40 +00:00
hugocasa
a2d9438b92 chore: upgrade bun to 1.2.18 (#6218) 2025-07-17 22:14:09 +00:00
Alexander Petric
8e87d412ac fix: fix circular dependancy breaking bundling of cli (#6219)
* fix: fix circular dependancy breaking bundling of cli

* remove hubpaths
2025-07-17 22:13:55 +00:00
Ruben Fiszel
e7123ced31 fix: prevent idle queries at the sqlx level 2025-07-17 16:08:24 +00:00
dieriba
5f364100f3 feat: granular token scopes (#6093)
* base

* add scopes in the UI

* remove legacy scope, unified create token into a components

* fix layout, convert old scope to new scope

* update scope

* update ui, and clear scope

* remove desc

* almsot there

* fix path

* fix delete and scope resource path matching

* update scope

* update scope and error message

* nits and fix logic

* nits and fix

* added multiple resource and jobs scope for script and flow

* add check_scope for endpoint

* clean front and improve fronetend code

* fix resource validation logic and backward compatibility with old scope

* fix frontend state and scope checks logic

* update scopes

* fix height

* nits: better_naming

* fix route

* fix add missing import

* fix import and move fn

* update repo ref

* fix import

* fix query and nits

* nits

* fix ,missing import

* fix

* revert add admin protection

* handle run action correctly

* fix

* add check scopes to some endpoint

* fix and nits

* nits

* remove unused import

* nits

* add chevron when domain is exapanded

* fix border bottom

* nits adds resource path button

* nits

* fix

* nits

* nits

* nits

* fix merge

* fix

* UI nits

* update repo ref

* add lazy static

* update scopes

---------

Co-authored-by: HugoCasa <hugo@casademont.ch>
2025-07-17 15:18:40 +00:00
hugocasa
936994580e update codeowners to new hugo username (#6216) 2025-07-17 15:18:10 +00:00
Guilhem
9a0b75be84 correctly set flow initial job status on running preview (#6214) 2025-07-17 14:02:06 +00:00
pyranota
71d6bbbdc3 feat(cli): make flow generate-locks respect raw requirements (#6105)
* build: add claude-code to nix shell

Signed-off-by: pyranota <pyra@duck.com>

* stage progress

* stage files

* make it work

* stage work

Signed-off-by: pyranota <pyra@duck.com>

* make it work + refactor logic a bit

Signed-off-by: pyranota <pyra@duck.com>

* small cleanup

Signed-off-by: pyranota <pyra@duck.com>

* rename `local_lockfiles` to `rawReqs`

Signed-off-by: pyranota <pyra@duck.com>

* cleanup

Signed-off-by: pyranota <pyra@duck.com>

* fix transpilation to deno error

Signed-off-by: pyranota <pyra@duck.com>

* update sqlx

Signed-off-by: pyranota <pyra@duck.com>

* cleanup features.default

Signed-off-by: pyranota <pyra@duck.com>

* follow up rename

Signed-off-by: pyranota <pyra@duck.com>

* nits

Signed-off-by: pyranota <pyra@duck.com>

* overwrite flow.yaml on `flow generate-locks`

Signed-off-by: pyranota <pyra@duck.com>

* nits

Signed-off-by: pyranota <pyra@duck.com>

* remove invalid code generated by claude

Signed-off-by: pyranota <pyra@duck.com>

* nits

Signed-off-by: pyranota <pyra@duck.com>

* fix typo

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* remove double quotes

Signed-off-by: pyranota <pyra@duck.com>

* use async to write file

Signed-off-by: pyranota <pyra@duck.com>

---------

Signed-off-by: pyranota <pyra@duck.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-17 14:01:28 +00:00
HugoCasa
1a850cb854 feat: windows memory and vcpu reading (#6212) 2025-07-17 12:39:25 +00:00
Diego Imbert
1842157bd5 remove indigo badge (#6210)
* remove indigo badge

* Revert "remove indigo badge"

This reverts commit 843f446034.

* fix wrong badge

* missing prettifyHeader props in ArgInput / SchemaForm
2025-07-17 11:43:09 +00:00
Ruben Fiszel
01d8a3917b fix windows worker toolchain 2025-07-17 07:25:20 +00:00
Ruben Fiszel
7ca652dede chore(main): release 1.508.0 (#6207)
* chore(main): release 1.508.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-16 23:05:38 +00:00
Ruben Fiszel
36816e08ac fix build 2025-07-16 22:52:29 +00:00
Ruben Fiszel
07b785dcc3 fix: support enum for array list as multiselect 2025-07-16 22:31:15 +00:00
centdix
827e06b4b3 feat(aichat): add logs api endpoint as tool (#6197)
* add logs

* add logs search + better load tools logic

* use json

* nit

* only add for ee

* nit

* filter out search after first fail

* Revert "filter out search after first fail"

This reverts commit 2abf0db6e5a1be84e67d1a153281b74d448cb5cd.

* call endpoint to know if it is available

* cleaning

* Apply suggestion from @graphite-app[bot]

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* fix

* draft

* call enabled endpoint

* not workspaced

* remove from system prompt if not enterprise

* fix eeref command

* update ee ref

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-07-16 17:59:47 +00:00
Guilhem
c076d1332e fix(frontend): add error handler on trigger delete (#6208) 2025-07-16 17:26:48 +00:00
HugoCasa
1d62dd0fc3 feat: better explanation for dev key renewal (#6209)
* fix: hide s3 catalog picker in anonymous apps

* nit reactivity upload text

* no signed s3 object warning

* remove warning

* fix check

* feat: better dev key explanation
2025-07-16 17:26:37 +00:00
HugoCasa
b68193f804 fix: hide s3 catalog picker in anonymous apps (#6204)
* fix: hide s3 catalog picker in anonymous apps

* nit reactivity upload text

* no signed s3 object warning

* remove warning

* fix check
2025-07-16 16:44:19 +00:00
Guilhem
290daec0fa fix(frontend): enable delete triggers on detail page (#6206)
* Allways show trigger update button

* allow delete deployed trigger
2025-07-16 16:44:06 +00:00
Diego Imbert
9e8748b7c8 fix batch rerun reactivity issue (#6205) 2025-07-16 14:30:15 +00:00
Ruben Fiszel
532cdc4b49 chore(main): release 1.507.2 (#6202)
* chore(main): release 1.507.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-16 13:26:45 +00:00
Ruben Fiszel
6c477c109b fix(cli): fix cli --skip-resources --skip-variables 2025-07-16 13:22:45 +00:00
Diego Imbert
2dae9ba0a7 Fix unsatisfying delete button in flow module when clicking fast (#6203) 2025-07-16 10:52:57 +00:00
centdix
7457401877 add small help text (#6200) 2025-07-16 10:47:07 +00:00
HugoCasa
038c179a43 fix: cancel autocomplete on escape + autocomplete qol (#6201)
* fix: cancel autocomplete on escape + autocomplete qol

* missing
2025-07-16 10:46:43 +00:00
Ruben Fiszel
061be92fae chore(main): release 1.507.1 (#6199)
* chore(main): release 1.507.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-16 10:37:50 +00:00
HugoCasa
8247a6c684 fix: nested delete after use in flows (#6194) 2025-07-16 10:28:28 +00:00
Guilhem
475bebdcfc fix flow dev sync (#6198) 2025-07-16 10:28:01 +00:00
Ruben Fiszel
f3151db110 better error message for ill-defined flow 2025-07-16 08:54:41 +00:00
Ruben Fiszel
409a7f4920 nit log 2025-07-16 00:01:28 +00:00
Ruben Fiszel
195bfceff3 chore(main): release 1.507.0 (#6189)
* chore(main): release 1.507.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-15 17:18:16 +00:00
Ruben Fiszel
ef76e65c63 nit compile 2025-07-15 16:45:11 +00:00
Ruben Fiszel
99ffa2d20b nit compile 2025-07-15 16:39:16 +00:00
Ruben Fiszel
7743d7e9dc tracing error exit loop 2025-07-15 16:31:35 +00:00
Alexander Petric
aa37f643e7 feat: git sync improvements (#6182)
* init checkpoint

* ui second pass...

* round 1 backend + saving settings + detecting changes...

* checkpoint

* fix openapi

* saving + correct wmill.yaml diff

* cli refactor

* cli and tests refactor done

* cli multi workspace support

* cli support skip core types to align with ui

* new test framework

* sqlx

* openapi spec

* frontend

* sync + settings changes

* some fixes

* some fixes

* security: Remove hardcoded EE license key, use environment variable only

- Remove hardcoded license key from containerized test backend
- Environment variable EE_LICENSE_KEY now required for EE features
- License key no longer stored in database during tests

* sqlx

* tests

* fixing tests

* fix tests

* checkpoint

* checkpoint

* cli build

* frontend - cli exchange

* settings match

* ee repo ref

* npm check

* openapi

* tests

* checkpoint

* cli + tests

* reset to preview on changes

* merge issue ee

* cleanup

* hubscript

* simplifications

* ee repo ref

* cli fixes

* fix sync and add tests

* extra test

* git sync settings / key change aware

* ee-repo ref

* ee-repo ref

* ee repo ref

* ee ref

* review 1

* ee ref

* Update frontend/src/lib/components/PullGitRepoPopover.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* ee ref

* remove extra includes from ui

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-15 16:25:31 +00:00
HugoCasa
9053a931ce feat: multi s3 arg input (#6187)
* feat: multi s3 arg input

* nits
2025-07-15 15:42:25 +00:00
Ruben Fiszel
d3a05d495d prevent forever loops nit 2025-07-15 15:36:42 +00:00
Ruben Fiszel
2e1b6c1947 fix: prevent worker not exiting if special case of same worker job 2025-07-15 15:32:26 +00:00
Guilhem
885f711e03 fix(frontend): fix dev graph not loading (#6190)
* fix(frontend): fix dev graph not loading

* nit
2025-07-15 14:15:54 +00:00
Ruben Fiszel
27621d9503 prevent tight cpu loop on job pull 2025-07-15 14:14:24 +00:00
Ruben Fiszel
cca8e748aa fix: only close app dropdown when action is done 2025-07-15 13:51:45 +00:00
centdix
decb54fc5a pin rmcp version (#6188) 2025-07-15 12:22:20 +00:00
Ruben Fiszel
dc1c216582 chore(main): release 1.506.0 (#6183)
* chore(main): release 1.506.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-15 09:57:49 +00:00
HugoCasa
b58c46a504 feat(frontend): app static and user resource picker default values (#6179) 2025-07-15 09:52:54 +00:00
centdix
3e2606a14e pull before push (#6186) 2025-07-15 09:39:37 +00:00
Alexander Petric
d562625474 feat: add oauth client_credentials support (#6110)
* feat: add oauth client_credentials support

* refactor: client_id / secret at instance level

* trim

* only visma / custom oauth

* ee ref

* Update SQLx metadata

---------

Co-authored-by: GitHub Action <action@github.com>
2025-07-15 01:50:22 +02:00
Ruben Fiszel
3ae5b3c594 chore(main): release 1.505.4 (#6178)
* chore(main): release 1.505.4

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-14 14:55:12 +00:00
HugoCasa
00ab0e8f38 fix: retry telemetry and renewal (#6175)
* feat: retry telemetry and renewal

* Update ee-repo-ref.txt

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-07-14 14:29:52 +00:00
Ruben Fiszel
0c8bc6077a load secondary storages only on need 2025-07-14 13:00:29 +00:00
413 changed files with 22652 additions and 6624 deletions

6
.github/CODEOWNERS vendored
View File

@@ -1,4 +1,4 @@
* @rubenfiszel @HugoCasa @alpetric
* @rubenfiszel @hugocasa @alpetric
/community/ @rubenfiszel @HugoCasa @alpetric
/frontend/ @rubenfiszel @HugoCasa @alpetric
/community/ @rubenfiszel @hugocasa @alpetric
/frontend/ @rubenfiszel @hugocasa @alpetric

View File

@@ -42,7 +42,7 @@ RUN wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VER
RUN /usr/local/bin/python3 -m pip install pip-tools
# Bun
COPY --from=oven/bun:1.2.4 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.2.18 /usr/local/bin/bun /usr/bin/bun
ARG TARGETPLATFORM

View File

@@ -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

View File

@@ -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:

View File

@@ -33,7 +33,7 @@ jobs:
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: 1.85.0
toolchain: 1.88.0
override: true
- name: Substitute EE code

View File

@@ -82,11 +82,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.85.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
toolchain: 1.88.0
- name: cargo check
working-directory: ./backend
@@ -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

View File

@@ -15,7 +15,7 @@ jobs:
services:
postgres:
image: postgres:14
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
@@ -76,6 +76,10 @@ jobs:
BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq .headRefName)
echo "Checking out PR branch: $BRANCH_NAME"
git checkout $BRANCH_NAME
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git config pull.rebase true
git pull origin $BRANCH_NAME
mkdir frontend/build
cd backend
cargo install sqlx-cli --version 0.8.5
@@ -86,8 +90,6 @@ jobs:
- name: Commit changes if any
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add backend/.sqlx
git commit -m "Update SQLx metadata"
git push origin ${{ env.BRANCH_NAME }}
@@ -149,15 +151,17 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Checking out PR branch: $BRANCH_NAME"
git checkout $BRANCH_NAME
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git config pull.rebase true
git pull origin $BRANCH_NAME
echo "${{ steps.get-commit-hash.outputs.commit_hash }}" > backend/ee-repo-ref.txt
echo "Updated backend/ee-repo-ref.txt with commit hash: ${{ steps.get-commit-hash.outputs.commit_hash }}"
# commit and push the changes
PR_NUMBER=${{ github.event.issue.number }}
BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq .headRefName)
echo "Checking out PR branch: $BRANCH_NAME"
git checkout $BRANCH_NAME
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add backend/ee-repo-ref.txt
git commit -m "Update ee-repo-ref.txt" || echo "No changes to commit"
git push origin $BRANCH_NAME

View File

@@ -35,7 +35,7 @@ jobs:
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: 1.85.0
toolchain: 1.88.0
override: true
- name: Substitute EE code

View File

@@ -1,5 +1,217 @@
# 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)
### Bug Fixes
* add back asset kind variable ([a10509a](https://github.com/windmill-labs/windmill/commit/a10509a000f217c37b45e125fd6e0a70aaaeb260))
## [1.509.0](https://github.com/windmill-labs/windmill/compare/v1.508.0...v1.509.0) (2025-07-19)
### Features
* **ai chat:** search for relevant hub scripts and npm packages in script mode ([#6215](https://github.com/windmill-labs/windmill/issues/6215)) ([b099d96](https://github.com/windmill-labs/windmill/commit/b099d96a60f6abdc037554dbbec2d9ae6271f0bb))
* **cli:** make `flow generate-locks` respect raw requirements ([#6105](https://github.com/windmill-labs/windmill/issues/6105)) ([71d6bbb](https://github.com/windmill-labs/windmill/commit/71d6bbbdc3f2350a12079ea5fe32da1183ce3cf2))
* granular token scopes ([#6093](https://github.com/windmill-labs/windmill/issues/6093)) ([5f36410](https://github.com/windmill-labs/windmill/commit/5f364100f3a80423033e9a9b6213028067222b31))
* implement SSE for job updates polling ([#6174](https://github.com/windmill-labs/windmill/issues/6174)) ([4f993c8](https://github.com/windmill-labs/windmill/commit/4f993c82b5af993b65e466c5abb38276b0857495))
* windows memory and vcpu reading ([#6212](https://github.com/windmill-labs/windmill/issues/6212)) ([1a850cb](https://github.com/windmill-labs/windmill/commit/1a850cb854dd6dcef1d378157d14ad9c624a88a5))
### Bug Fixes
* app s3 multi upload policy ([#6228](https://github.com/windmill-labs/windmill/issues/6228)) ([6a012a8](https://github.com/windmill-labs/windmill/commit/6a012a87ed9693a877a2725800b79e82a1816f1b))
* fix circular dependancy breaking bundling of cli ([#6219](https://github.com/windmill-labs/windmill/issues/6219)) ([8e87d41](https://github.com/windmill-labs/windmill/commit/8e87d412acd2bab4fb6c8ce748065657456e33f5))
* prevent idle queries at the sqlx level ([e7123ce](https://github.com/windmill-labs/windmill/commit/e7123ced31038cf722cda67d16b8ad48589bb53a))
* tantivy/indexer blocking operations on async code ([#6227](https://github.com/windmill-labs/windmill/issues/6227)) ([2c299fd](https://github.com/windmill-labs/windmill/commit/2c299fd2a52e1205892702be7b326fe7656492ab))
## [1.508.0](https://github.com/windmill-labs/windmill/compare/v1.507.2...v1.508.0) (2025-07-16)
### Features
* **aichat:** add logs api endpoint as tool ([#6197](https://github.com/windmill-labs/windmill/issues/6197)) ([827e06b](https://github.com/windmill-labs/windmill/commit/827e06b4b3dd7b2b970b3a0a37e3dfbe98d1fafd))
* better explanation for dev key renewal ([#6209](https://github.com/windmill-labs/windmill/issues/6209)) ([1d62dd0](https://github.com/windmill-labs/windmill/commit/1d62dd0fc323e484ab1d08e740379c8243b52df8))
### Bug Fixes
* **frontend:** add error handler on trigger delete ([#6208](https://github.com/windmill-labs/windmill/issues/6208)) ([c076d13](https://github.com/windmill-labs/windmill/commit/c076d1332e0871b16b5881e2cf3ef165bc9f5dd5))
* **frontend:** enable delete triggers on detail page ([#6206](https://github.com/windmill-labs/windmill/issues/6206)) ([290daec](https://github.com/windmill-labs/windmill/commit/290daec0fa53f77f6ad02e746d82aa30997d8cb0))
* hide s3 catalog picker in anonymous apps ([#6204](https://github.com/windmill-labs/windmill/issues/6204)) ([b68193f](https://github.com/windmill-labs/windmill/commit/b68193f80468aeeadff5af0826cca2f0cc9f05e8))
* support enum for array list as multiselect ([07b785d](https://github.com/windmill-labs/windmill/commit/07b785dcc383ae87eaec65e765177e45fff0af20))
## [1.507.2](https://github.com/windmill-labs/windmill/compare/v1.507.1...v1.507.2) (2025-07-16)
### Bug Fixes
* cancel autocomplete on escape + autocomplete qol ([#6201](https://github.com/windmill-labs/windmill/issues/6201)) ([038c179](https://github.com/windmill-labs/windmill/commit/038c179a432d4f1997a761392e27b16dea829e06))
* **cli:** fix cli --skip-resources --skip-variables ([6c477c1](https://github.com/windmill-labs/windmill/commit/6c477c109bf7e803ec07d4485dec426b770c7f76))
## [1.507.1](https://github.com/windmill-labs/windmill/compare/v1.507.0...v1.507.1) (2025-07-16)
### Bug Fixes
* nested delete after use in flows ([#6194](https://github.com/windmill-labs/windmill/issues/6194)) ([8247a6c](https://github.com/windmill-labs/windmill/commit/8247a6c684ef57ea417771b3e34794c81c4b904a))
## [1.507.0](https://github.com/windmill-labs/windmill/compare/v1.506.0...v1.507.0) (2025-07-15)
### Features
* git sync improvements ([#6182](https://github.com/windmill-labs/windmill/issues/6182)) ([aa37f64](https://github.com/windmill-labs/windmill/commit/aa37f643e78cae09be6246e0a2e84776b60afa49))
* multi s3 arg input ([#6187](https://github.com/windmill-labs/windmill/issues/6187)) ([9053a93](https://github.com/windmill-labs/windmill/commit/9053a931ce4d075eddd8063de73ea95c3a346937))
### Bug Fixes
* **frontend:** fix dev graph not loading ([#6190](https://github.com/windmill-labs/windmill/issues/6190)) ([885f711](https://github.com/windmill-labs/windmill/commit/885f711e03e4430d3fc4ab9756c94f0182b20bcc))
* only close app dropdown when action is done ([cca8e74](https://github.com/windmill-labs/windmill/commit/cca8e748aa68d41da5f1f8196a02d72d8ca84547))
* prevent worker not exiting if special case of same worker job ([2e1b6c1](https://github.com/windmill-labs/windmill/commit/2e1b6c1947e5e7f66061c5b380ce0483342df706))
## [1.506.0](https://github.com/windmill-labs/windmill/compare/v1.505.4...v1.506.0) (2025-07-15)
### Features
* add oauth client_credentials support ([#6110](https://github.com/windmill-labs/windmill/issues/6110)) ([d562625](https://github.com/windmill-labs/windmill/commit/d562625474260d16699329884dd6c3b890f808ad))
* **frontend:** app static and user resource picker default values ([#6179](https://github.com/windmill-labs/windmill/issues/6179)) ([b58c46a](https://github.com/windmill-labs/windmill/commit/b58c46a50452bce098695f52220946f87d62f840))
## [1.505.4](https://github.com/windmill-labs/windmill/compare/v1.505.3...v1.505.4) (2025-07-14)
### Bug Fixes
* retry telemetry and renewal ([#6175](https://github.com/windmill-labs/windmill/issues/6175)) ([00ab0e8](https://github.com/windmill-labs/windmill/commit/00ab0e8f3883d2ef884438a957b8d26a4672836d))
## [1.505.3](https://github.com/windmill-labs/windmill/compare/v1.505.2...v1.505.3) (2025-07-14)

View File

@@ -194,7 +194,7 @@ COPY --from=builder /windmill/target/release/windmill ${APP}/windmill
COPY --from=denoland/deno:2.2.1 --chmod=755 /usr/bin/deno /usr/bin/deno
COPY --from=oven/bun:1.2.4 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.2.18 /usr/local/bin/bun /usr/bin/bun
COPY --from=php:8.3.7-cli /usr/local/bin/php /usr/bin/php
COPY --from=composer:2.7.6 /usr/bin/composer /usr/bin/composer

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_status (id, workflow_as_code_status)\n VALUES ($1, JSONB_SET('{}'::JSONB, array[$2], $3))\n ON CONFLICT (id) DO UPDATE SET\n workflow_as_code_status = JSONB_SET(\n COALESCE(v2_job_status.workflow_as_code_status, '{}'::JSONB), \n array[$2],\n $3\n )",
"query": "INSERT INTO v2_job_status (id, workflow_as_code_status)\n VALUES ($1, JSONB_SET('{}'::JSONB, array[$2], $3))\n ON CONFLICT (id) DO UPDATE SET\n workflow_as_code_status = JSONB_SET(\n COALESCE(v2_job_status.workflow_as_code_status, '{}'::JSONB),\n array[$2],\n $3\n )",
"describe": {
"columns": [],
"parameters": {
@@ -12,5 +12,5 @@
},
"nullable": []
},
"hash": "2e6935811a6d818bc523f076674f794f8be6c6bad3d06e74586e8ab668d91861"
"hash": "140f4ad799fca6c25975a0aca7c9051f0760e22eccdd291c83ed86599ce571cb"
}

View File

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

View File

@@ -0,0 +1,75 @@
{
"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 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": [
{
"ordinal": 0,
"name": "completed",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "running",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "logs",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "mem_peak",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "flow_status: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 5,
"name": "workflow_as_code_status: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "log_offset",
"type_info": "Int4"
},
{
"ordinal": 7,
"name": "created_by!",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "progress",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Int4",
"Text",
"Uuid",
"Bool",
"Bool",
"TextArray"
]
},
"nullable": [
null,
null,
null,
null,
null,
null,
null,
false,
null
]
},
"hash": "278bc6b4f149f824b5db32dacfaa714ee3852dc2ddf2d661dfdd5a986a9bb62b"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT created_by AS \"created_by!\", args as \"args: sqlx::types::Json<Box<RawValue>>\"\n FROM v2_as_completed_job \n WHERE id = $1 AND workspace_id = $2 AND ($3::text[] IS NULL OR tag = ANY($3))",
"query": "SELECT created_by AS \"created_by!\", args as \"args: sqlx::types::Json<Box<RawValue>>\"\n FROM v2_as_completed_job\n WHERE id = $1 AND workspace_id = $2 AND ($3::text[] IS NULL OR tag = ANY($3))",
"describe": {
"columns": [
{
@@ -26,5 +26,5 @@
true
]
},
"hash": "47fb4bceddd36fa60ccbe84f6341436fb133f0b3c97abb1267a9c12326dd1a33"
"hash": "2bfa1ffb3d5869fc3038049ba77890203332e398c865c47aaf019dd5721d59f9"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT created_by AS \"created_by!\", CONCAT(coalesce(v2_as_queue.logs, ''), coalesce(job_logs.logs, '')) as logs, coalesce(job_logs.log_offset, 0) as log_offset, job_logs.log_file_index\n FROM v2_as_queue \n LEFT JOIN job_logs ON job_logs.job_id = v2_as_queue.id \n WHERE v2_as_queue.id = $1 AND v2_as_queue.workspace_id = $2 AND ($3::text[] IS NULL OR v2_as_queue.tag = ANY($3))",
"query": "SELECT created_by AS \"created_by!\", CONCAT(coalesce(v2_as_queue.logs, ''), coalesce(job_logs.logs, '')) as logs, coalesce(job_logs.log_offset, 0) as log_offset, job_logs.log_file_index\n FROM v2_as_queue\n LEFT JOIN job_logs ON job_logs.job_id = v2_as_queue.id\n WHERE v2_as_queue.id = $1 AND v2_as_queue.workspace_id = $2 AND ($3::text[] IS NULL OR v2_as_queue.tag = ANY($3))",
"describe": {
"columns": [
{
@@ -38,5 +38,5 @@
true
]
},
"hash": "95ae90094ec0e2c22660cc2e3788b22231dab9c558723cc54894597ce4cd3d5a"
"hash": "2ea447f9e644554d415367b91042687ee8690d475b8ed31c48e31180689a278f"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT is_admin FROM usr WHERE email = $1 AND workspace_id = $2 AND disabled = false",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "is_admin",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "49ae42f396c55a7c258203de8ffbbd8d5c5d14e176792932064d9f6b5e6f4fa2"
}

View 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"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_status\n SET flow_status = JSONB_SET(flow_status, ARRAY['cleanup_module', 'flow_jobs_to_clean'], COALESCE(flow_status->'cleanup_module'->'flow_jobs_to_clean', '[]'::jsonb) || $1)\n WHERE id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Uuid"
]
},
"nullable": []
},
"hash": "4d66f509815a8af4b43abae7119ed377312a80ec08ca9a65cbb71a96ce5959b8"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id FROM usr WHERE email = $1",
"query": "SELECT workspace_id FROM usr WHERE email = $1 AND is_admin = true AND disabled = false",
"describe": {
"columns": [
{
@@ -18,5 +18,5 @@
false
]
},
"hash": "38c20dba51b1e2b4b28e5eed51f9071e1d3cf78e89ec467927823177c5a439cc"
"hash": "664bb931b02a5d9e0d68ccef12788419c804a8f0d66cc660e4608220655b0f33"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT DISTINCT workspace_id FROM workspace_settings WHERE git_app_installations IS NOT NULL",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "779775a89d225bc1de000d244c63aab3a3be72d8761217d9567400fc3c13c4b8"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO concurrency_counter(concurrency_id, job_uuids) \n VALUES ($1, '{}'::jsonb)",
"query": "INSERT INTO concurrency_counter(concurrency_id, job_uuids)\n VALUES ($1, '{}'::jsonb)",
"describe": {
"columns": [],
"parameters": {
@@ -10,5 +10,5 @@
},
"nullable": []
},
"hash": "05cb171b610bfb45f6228128a385cde8a5b86d7ca377a028004cc382e12faf41"
"hash": "7b5dc50b685afc7de0b99bd68e2217d660529c15b41860a4513f63d465bb9abb"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT coalesce(flow_version_lite.value, flow_version.value) as \"value!: sqlx::types::Json<Box<RawValue>>\" FROM flow \n LEFT JOIN flow_version\n ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n LEFT JOIN flow_version_lite \n ON flow_version_lite.id = flow_version.id\n WHERE flow.path = $1 AND flow.workspace_id = $2 LIMIT 1",
"query": "SELECT coalesce(flow_version_lite.value, flow_version.value) as \"value!: sqlx::types::Json<Box<RawValue>>\" FROM flow\n LEFT JOIN flow_version\n ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n LEFT JOIN flow_version_lite\n ON flow_version_lite.id = flow_version.id\n WHERE flow.path = $1 AND flow.workspace_id = $2 LIMIT 1",
"describe": {
"columns": [
{
@@ -19,5 +19,5 @@
null
]
},
"hash": "625c400bd81369a8142b8c7fa67b2be02f0f945e587c90d7e0f23706bf6e9a31"
"hash": "81ba1e003d3f6e8e7c74a4dd09b980888d71121082390f3fdb91269cd0c5d0b2"
}

View File

@@ -1,68 +0,0 @@
{
"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",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "completed",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "running",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "logs",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "mem_peak",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "flow_status: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 5,
"name": "log_offset",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "created_by!",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "progress",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Int4",
"Text",
"Uuid",
"Bool",
"Bool"
]
},
"nullable": [
null,
null,
null,
null,
null,
null,
false,
null
]
},
"hash": "b075c77caad37cdd75faab1d934b7a63521c1ad788033c564ffd7a2944454378"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n j.id,\n j.kind AS \"kind: _\",\n COALESCE(s.path, f.path) AS \"script_path!\",\n COALESCE(s.hash, f.id) AS \"script_hash!: _\",\n COALESCE(jc.started_at, jq.scheduled_for, make_date(1970, 1, 1)) AS \"scheduled_for!: _\",\n args AS input,\n COALESCE(s.schema, f.schema) AS \"schema: _\"\n FROM v2_job j\n LEFT JOIN script s ON j.runnable_id = s.hash AND j.kind = 'script'\n LEFT JOIN flow_version f ON j.runnable_id = f.id AND j.runnable_path = f.path AND j.kind = 'flow'\n LEFT JOIN v2_job_completed jc ON jc.id = j.id\n LEFT JOIN v2_job_queue jq ON jq.id = j.id\n WHERE j.id = ANY($1)\n AND j.workspace_id = $2\n AND COALESCE(s.hash, f.id) IS NOT NULL\n AND COALESCE(s.path, f.path) IS NOT NULL",
"query": "SELECT\n j.id,\n j.kind AS \"kind: _\",\n COALESCE(s.path, f.path) AS \"script_path!\",\n COALESCE(s.hash, f.id) AS \"script_hash!: _\",\n COALESCE(jc.started_at, jq.scheduled_for, make_date(1970, 1, 1)) AS \"scheduled_for!: _\",\n args AS input,\n COALESCE(s.schema, f.schema) AS \"schema: _\"\n FROM v2_job j\n LEFT JOIN script s ON j.runnable_id = s.hash AND j.kind = 'script'\n LEFT JOIN flow_version f ON j.runnable_id = f.id AND j.runnable_path = f.path AND j.kind = 'flow'\n LEFT JOIN v2_job_completed jc ON jc.id = j.id\n LEFT JOIN v2_job_queue jq ON jq.id = j.id\n WHERE j.id = ANY($1)\n AND j.workspace_id = $2\n AND COALESCE(s.hash, f.id) IS NOT NULL\n AND COALESCE(s.path, f.path) IS NOT NULL",
"describe": {
"columns": [
{
@@ -81,5 +81,5 @@
null
]
},
"hash": "ad88831c9021b79c9037a925c2b53be3a93dbe2a67bdc4d4342c84f439b5eaf9"
"hash": "b3c02fd225a6aa78785d466e7f033b38deb9c7fa17bd3836c9ad8884f27be84a"
}

View File

@@ -0,0 +1,29 @@
{
"db_name": "PostgreSQL",
"query": "SELECT result as \"result: sqlx::types::Json<Box<RawValue>>\", v2_job_queue.running as \"running: Option<bool>\" FROM v2_job_completed FULL OUTER JOIN v2_job_queue USING (id) WHERE (v2_job_queue.id = $1 AND v2_job_queue.workspace_id = $2) OR (v2_job_completed.id = $1 AND v2_job_completed.workspace_id = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "running: Option<bool>",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
true,
false
]
},
"hash": "b9b3c341fe452da916ee29637e14b5c1ad75462eba17083c6f81ff6ef35af77f"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO account (workspace_id, client, expires_at, refresh_token) VALUES ($1, $2, now() + ($3 || ' seconds')::interval, $4) 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": [
{
@@ -14,6 +14,10 @@
"Varchar",
"Varchar",
"Text",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Varchar"
]
},
@@ -21,5 +25,5 @@
false
]
},
"hash": "dc649111c4c862c36c26ad8a9eff8c31c2533a4a73184d0276f9a388ff2fa574"
"hash": "bbc28b92ae8ec3d120a8976be7d3966282fe6543e0eb957fc10864dbf58de58f"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT created_by AS \"created_by!\", CONCAT(coalesce(v2_as_completed_job.logs, ''), coalesce(job_logs.logs, '')) as logs, job_logs.log_offset, job_logs.log_file_index\n FROM v2_as_completed_job \n LEFT JOIN job_logs ON job_logs.job_id = v2_as_completed_job.id \n WHERE v2_as_completed_job.id = $1 AND v2_as_completed_job.workspace_id = $2 AND ($3::text[] IS NULL OR v2_as_completed_job.tag = ANY($3))",
"query": "SELECT created_by AS \"created_by!\", CONCAT(coalesce(v2_as_completed_job.logs, ''), coalesce(job_logs.logs, '')) as logs, job_logs.log_offset, job_logs.log_file_index\n FROM v2_as_completed_job\n LEFT JOIN job_logs ON job_logs.job_id = v2_as_completed_job.id\n WHERE v2_as_completed_job.id = $1 AND v2_as_completed_job.workspace_id = $2 AND ($3::text[] IS NULL OR v2_as_completed_job.tag = ANY($3))",
"describe": {
"columns": [
{
@@ -38,5 +38,5 @@
true
]
},
"hash": "2c5a1b1ebd872ce8a03a1b7e5246c876750c85075cfda816c025c805d4c3cd4c"
"hash": "ca5d9a9d8d18da970c7fd6eab41ecbb3a5c7c29803e4c38b8a0b2ca3790e52f9"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n SELECT unnest($1::uuid[]), $2, $3, $4, $5, $6, $7, $8",
"query": "INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id)\n SELECT unnest($1::uuid[]), $2, $3, $4, $5, $6, $7, $8",
"describe": {
"columns": [],
"parameters": {
@@ -17,5 +17,5 @@
},
"nullable": []
},
"hash": "fe7221651a982861dede4116bc71fe2dce615ff76a53f72cb5386dc17e4e07aa"
"hash": "cad2dfc50d57c2298cb0ec05ed3b42b9fd3edd05c314c9d96ba33cad37fe35c2"
}

View File

@@ -0,0 +1,53 @@
{
"db_name": "PostgreSQL",
"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": [
{
"ordinal": 0,
"name": "client",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "refresh_token",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "grant_type",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "cc_client_id",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "cc_client_secret",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "cc_token_url",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Int4"
]
},
"nullable": [
false,
false,
false,
true,
true,
true
]
},
"hash": "cc269052ffc1e613d7edc31f0f7bb84f6e6301ad1afb028813105a121a69fa7e"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT result as \"result: sqlx::types::Json<Box<RawValue>>\" FROM v2_job_completed WHERE id = $2 AND workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
"Uuid"
]
},
"nullable": [
true
]
},
"hash": "ceb8c2607023883e1eebd4b9539e36ed202a6ecd12e3f90cb070341c38886de4"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE account SET refresh_token = $1, expires_at = $2 WHERE workspace_id = $3 AND id = $4",
"query": "UPDATE account SET refresh_token = $1, expires_at = $2, refresh_error = NULL WHERE workspace_id = $3 AND id = $4",
"describe": {
"columns": [],
"parameters": {
@@ -13,5 +13,5 @@
},
"nullable": []
},
"hash": "cac594031a21b4806de9c4616317d3541522ef9712a83ecff7bd8b5f6e870748"
"hash": "db9831a3218b36c32b305b9f2ba21abceec74c9c277f27231dba12868e68126b"
}

View File

@@ -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"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "WITH uuid_table as (\n select unnest($4::uuid[]) as uuid\n )\n INSERT INTO v2_job_queue\n (id, workspace_id, scheduled_for, tag)\n (SELECT uuid, $1, $2, $3 FROM uuid_table) \n RETURNING id",
"query": "WITH uuid_table as (\n select unnest($4::uuid[]) as uuid\n )\n INSERT INTO v2_job_queue\n (id, workspace_id, scheduled_for, tag)\n (SELECT uuid, $1, $2, $3 FROM uuid_table)\n RETURNING id",
"describe": {
"columns": [
{
@@ -21,5 +21,5 @@
false
]
},
"hash": "70e740465e648d84d32a506916ed437d1e32d4b5bdc7f7fae22e804428660836"
"hash": "f6e63a324664423353b56bccd9f3b322a3512ccfa86485e0f01664942ae04d0f"
}

View File

@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "SELECT result as \"result: sqlx::types::Json<Box<RawValue>>\", v2_job.tag,\n v2_job_queue.running as \"running: Option<bool>\"\n FROM v2_job\n LEFT JOIN v2_job_queue USING (id)\n LEFT JOIN v2_job_completed USING (id)\n WHERE v2_job.id = $2 AND v2_job.workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "running: Option<bool>",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Uuid"
]
},
"nullable": [
true,
false,
false
]
},
"hash": "fab257c4e20aa51b8f785b1882aa0b16fde33b246cbf0749ffa0e4ed63504451"
}

View File

@@ -1,29 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT client, refresh_token FROM account WHERE workspace_id = $1 AND id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "client",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "refresh_token",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Int4"
]
},
"nullable": [
false,
false
]
},
"hash": "fbe14569717b4c4937b74e6dc6f6b4ea29a2b0c2b1cde48ef06d687f6f9e8f15"
}

286
backend/Cargo.lock generated
View File

@@ -746,9 +746,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-config"
version = "1.8.1"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c18d005c70d2b9c0c1ea8876c039db0ec7fb71164d25c73ccea21bf41fd02171"
checksum = "c0baa720ebadea158c5bda642ac444a2af0cdf7bb66b46d1e4533de5d1f449d0"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -776,9 +776,9 @@ dependencies = [
[[package]]
name = "aws-credential-types"
version = "1.2.3"
version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "687bc16bc431a8533fe0097c7f0182874767f920989d7260950172ae8e3c4465"
checksum = "b68c2194a190e1efc999612792e25b1ab3abfefe4306494efaaabc25933c0cbe"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
@@ -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",
@@ -811,9 +811,9 @@ dependencies = [
[[package]]
name = "aws-runtime"
version = "1.5.8"
version = "1.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f6c68419d8ba16d9a7463671593c54f81ba58cab466e9b759418da606dcc2e2"
checksum = "b2090e664216c78e766b6bac10fe74d2f451c02441d43484cd76ac9a295075f7"
dependencies = [
"aws-credential-types",
"aws-sigv4",
@@ -835,9 +835,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sqs"
version = "1.74.0"
version = "1.77.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "256b8f7caffe3240a543f60409be9bba23038ceef5933da63e09d89197fc2333"
checksum = "32a6dcd63dffae1f7872df42ce9f1f0f785f8c9a3d3e880ef6c171d0415439df"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -857,9 +857,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.74.0"
version = "1.77.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a69de9c1b9272da2872af60c7402683e7f45c06267735b4332deacb203239b"
checksum = "18f2f37fea82468fe3f5a059542c05392ef680c4f7f00e0db02df8b6e5c7d0c6"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -879,9 +879,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.75.0"
version = "1.78.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0b161d836fac72bdd5ac1a4cd1cdc38ab888c7af26cfd95f661be4409505e63"
checksum = "ecb4f6eada20e0193450cd48b12ed05e1e66baac86f39160191651b932f2b7d9"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -901,9 +901,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.76.0"
version = "1.79.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb1cd79a3412751a341a28e2cd0d6fa4345241976da427b075a0c0cd5409f886"
checksum = "317377afba3498fca4948c5d32b399ef9a5ad35561a1e8a6f2ac7273dabf802d"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -957,9 +957,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http"
version = "0.62.1"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99335bec6cdc50a346fda1437f9fefe33abf8c99060739a546a16457f2862ca9"
checksum = "43c82ba4cab184ea61f6edaafc1072aad3c2a17dcf4c0fce19ac5694b90d8b5f"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-types",
@@ -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",
@@ -1858,9 +1858,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.29"
version = "1.2.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362"
checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7"
dependencies = [
"jobserver",
"libc",
@@ -2007,9 +2007,9 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
name = "clipboard-win"
version = "5.4.0"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892"
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
dependencies = [
"error-code",
]
@@ -2411,15 +2411,15 @@ dependencies = [
[[package]]
name = "curve25519-dalek"
version = "4.2.0"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373b7c5dbd637569a2cca66e8d66b8c446a1e7bf064ea321d265d7b3dfe7c97e"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest 0.10.7",
"fiat-crypto 0.3.0",
"fiat-crypto 0.2.9",
"rustc_version 0.4.1",
"subtle",
"zeroize",
@@ -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"
@@ -5059,7 +5059,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
dependencies = [
"cfg-if",
"rustix 1.0.7",
"rustix 1.0.8",
"windows-sys 0.59.0",
]
@@ -5090,9 +5090,9 @@ checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77"
[[package]]
name = "fiat-crypto"
version = "0.3.0"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "filetime"
@@ -6377,7 +6377,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"socket2",
"socket2 0.5.10",
"tokio",
"tower-service",
"tracing",
@@ -6451,7 +6451,7 @@ dependencies = [
"tokio",
"tokio-rustls 0.26.2",
"tower-service",
"webpki-roots 1.0.1",
"webpki-roots 1.0.2",
]
[[package]]
@@ -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",
]
@@ -10358,7 +10358,7 @@ dependencies = [
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"webpki-roots 1.0.1",
"webpki-roots 1.0.2",
]
[[package]]
@@ -10493,7 +10493,8 @@ dependencies = [
[[package]]
name = "rmcp"
version = "0.2.1"
source = "git+https://github.com/modelcontextprotocol/rust-sdk?rev=c74b26b3e3c5dfd0be8e85dede8d88f33d46ff06#c74b26b3e3c5dfd0be8e85dede8d88f33d46ff06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37f2048a81a7ff7e8ef6bc5abced70c3d9114c8f03d85d7aaaafd9fd04f12e9e"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -10522,7 +10523,8 @@ dependencies = [
[[package]]
name = "rmcp-macros"
version = "0.2.1"
source = "git+https://github.com/modelcontextprotocol/rust-sdk?rev=c74b26b3e3c5dfd0be8e85dede8d88f33d46ff06#c74b26b3e3c5dfd0be8e85dede8d88f33d46ff06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72398e694b9f6dbb5de960cf158c8699e6a1854cb5bbaac7de0646b2005763c4"
dependencies = [
"darling 0.20.11",
"proc-macro2",
@@ -10661,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"
@@ -10719,15 +10721,15 @@ dependencies = [
[[package]]
name = "rustix"
version = "1.0.7"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
dependencies = [
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys 0.9.4",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -10845,7 +10847,7 @@ checksum = "22557157d7395bc30727745b365d923f1ecc230c4c80b176545f3f4f08c46e33"
dependencies = [
"futures",
"rustls 0.23.29",
"socket2",
"socket2 0.5.10",
"tokio",
]
@@ -11301,9 +11303,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.140"
version = "1.0.141"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
dependencies = [
"indexmap 2.10.0",
"itoa",
@@ -11694,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"
@@ -12127,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]]
@@ -12149,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",
]
@@ -12719,9 +12730,9 @@ dependencies = [
[[package]]
name = "systemstat"
version = "0.2.4"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668a4db78b439df482c238f559e4ea869017f9e62ef0a059c8bfcd841a4df544"
checksum = "5021f5184d44b26fb184acd689671bbe1e4bbd24bbdaa6bc7ec383fad32d2033"
dependencies = [
"bytesize",
"lazy_static",
@@ -12739,9 +12750,9 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "tantivy"
version = "0.22.0"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8d0582f186c0a6d55655d24543f15e43607299425c5ad8352c242b914b31856"
checksum = "96599ea6fccd844fc833fed21d2eecac2e6a7c1afd9e044057391d78b1feb141"
dependencies = [
"aho-corasick",
"arc-swap",
@@ -12904,7 +12915,7 @@ dependencies = [
"fastrand",
"getrandom 0.3.3",
"once_cell",
"rustix 1.0.7",
"rustix 1.0.8",
"windows-sys 0.59.0",
]
@@ -12923,7 +12934,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
dependencies = [
"rustix 1.0.7",
"rustix 1.0.8",
"windows-sys 0.59.0",
]
@@ -13205,7 +13216,7 @@ dependencies = [
"pin-project-lite",
"signal-hook-registry",
"slab",
"socket2",
"socket2 0.5.10",
"tokio-macros",
"tracing",
"windows-sys 0.52.0",
@@ -13273,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",
@@ -13299,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",
@@ -13505,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",
@@ -14487,14 +14498,14 @@ version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
dependencies = [
"webpki-root-certs 1.0.1",
"webpki-root-certs 1.0.2",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86138b15b2b7d561bc4469e77027b8dd005a43dc502e9031d1f5afc8ce1f280e"
checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a"
dependencies = [
"rustls-pki-types",
]
@@ -14505,14 +14516,14 @@ version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
"webpki-roots 1.0.1",
"webpki-roots 1.0.2",
]
[[package]]
name = "webpki-roots"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502"
checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2"
dependencies = [
"rustls-pki-types",
]
@@ -14630,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",
]
@@ -14674,7 +14685,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"axum",
@@ -14703,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",
@@ -14726,7 +14737,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"argon2",
@@ -14737,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",
@@ -14792,6 +14806,7 @@ dependencies = [
"rumqttc",
"rust-embed",
"rust_decimal",
"rustls 0.23.29",
"samael",
"serde",
"serde_json",
@@ -14837,7 +14852,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -14852,7 +14867,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"chrono",
"serde",
@@ -14865,7 +14880,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"serde",
@@ -14879,7 +14894,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"async-stream",
@@ -14930,8 +14945,9 @@ 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",
"tempfile",
@@ -14957,7 +14973,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"regex",
"serde",
@@ -14971,12 +14987,13 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"bytes",
"chrono",
"const_format",
"flume",
"futures",
"lazy_static",
"object_store",
@@ -14994,7 +15011,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -15006,7 +15023,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -15015,7 +15032,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15027,7 +15044,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"serde_json",
@@ -15039,7 +15056,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"gosyn",
@@ -15051,7 +15068,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15063,7 +15080,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"serde_json",
@@ -15075,7 +15092,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"nu-parser",
@@ -15086,7 +15103,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15097,7 +15114,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15109,7 +15126,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -15132,7 +15149,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -15149,7 +15166,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15163,7 +15180,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15181,7 +15198,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"getrandom 0.2.16",
@@ -15205,7 +15222,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"serde_json",
@@ -15215,7 +15232,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -15248,7 +15265,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -15258,7 +15275,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.505.3"
version = "1.515.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -15594,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]]
@@ -15630,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",
@@ -15900,7 +15918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909"
dependencies = [
"libc",
"rustix 1.0.7",
"rustix 1.0.8",
]
[[package]]

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.505.3"
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.505.3"
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"] }
@@ -267,6 +266,7 @@ google-cloud-googleapis = {version = "0.16.1", features = ["pubsub"]}
# TODO: remove once deno fixes the issue on their end
# https://github.com/denoland/deno/issues/28557
winapi = { version = "0.3.9", features = ["sysinfoapi"] }
sysinfo = { version = "0.32.1" }
swc_common = "=0.37.5"
swc_ecma_parser = "=0.149.1"
@@ -353,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"

View File

@@ -1 +1 @@
17b17f57e4c777e7e2ffc0e5def317f017a1683d
cef7486dfa765f1cdfaddba59564c4bb92fe864e

View File

@@ -0,0 +1,4 @@
-- Rollback client_credentials OAuth flow support
ALTER TABLE account DROP COLUMN IF EXISTS grant_type;
ALTER TABLE account DROP COLUMN IF EXISTS cc_client_id;
ALTER TABLE account DROP COLUMN IF EXISTS cc_client_secret;

View File

@@ -0,0 +1,10 @@
-- Add support for client_credentials OAuth flow
-- Add grant_type column to distinguish between OAuth flows for backwards compatibility
ALTER TABLE account ADD COLUMN grant_type VARCHAR(50) NOT NULL DEFAULT 'authorization_code';
-- Add client_id and client_secret columns for resource-level client credentials
-- These are used when grant_type = 'client_credentials' and user wants to use resource-level credentials
-- instead of instance-level credentials configured by admin
ALTER TABLE account
ADD COLUMN cc_client_id VARCHAR(500),
ADD COLUMN cc_client_secret VARCHAR(500);

View File

@@ -0,0 +1,7 @@
-- Add 'variable' kind back
ALTER TABLE asset ALTER column kind TYPE VARCHAR;
DROP TYPE asset_kind;
CREATE TYPE ASSET_KIND AS ENUM ('s3object', 'resource', 'variable');
ALTER TABLE asset ALTER column kind TYPE ASSET_KIND using kind::ASSET_KIND;
ALTER TABLE script DROP COLUMN assets;

View File

@@ -0,0 +1,8 @@
ALTER TABLE script ADD COLUMN assets jsonb;
-- Remove 'variable' kind
DELETE FROM asset WHERE kind = 'variable';
ALTER TABLE asset ALTER column kind TYPE VARCHAR;
DROP TYPE asset_kind;
CREATE TYPE ASSET_KIND AS ENUM ('s3object', 'resource');
ALTER TABLE asset ALTER column kind TYPE ASSET_KIND using kind::ASSET_KIND;

View File

@@ -0,0 +1,2 @@
-- Rollback cc_token_url column addition
ALTER TABLE account DROP COLUMN IF EXISTS cc_token_url;

View File

@@ -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);

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TYPE ASSET_KIND ADD VALUE 'variable';

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE resource ALTER COLUMN created_by TYPE VARCHAR(500);

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,2 @@
ALTER TABLE audit ALTER COLUMN id TYPE bigint;
ALTER SEQUENCE audit_id_seq AS bigint;

View File

@@ -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)),
}

View File

@@ -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

View File

@@ -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![]))),
}
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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,
}

View File

@@ -57,27 +57,27 @@ impl AssetsFinder {
})
.ok_or(())?;
use AssetKind::*;
let (kind, access_type, arg) = match ident.as_str() {
"load_s3_file" => (AssetKind::S3Object, Some(R), Arg::Pos(0)),
"load_s3_file_reader" => (AssetKind::S3Object, Some(R), Arg::Pos(0)),
"write_s3_file" => (AssetKind::S3Object, Some(W), Arg::Pos(0)),
"get_resource" => (AssetKind::Resource, None, Arg::Pos(0)),
"set_resource" => (AssetKind::Resource, Some(W), Arg::Named("path")),
"get_boto3_connection_settings" => (AssetKind::Resource, None, Arg::Pos(0)),
"get_polars_connection_settings" => (AssetKind::Resource, None, Arg::Pos(0)),
"get_duckdb_connection_settings" => (AssetKind::Resource, None, Arg::Pos(0)),
"get_variable" => (AssetKind::Variable, Some(R), Arg::Pos(0)),
"set_variable" => (AssetKind::Variable, Some(W), Arg::Pos(0)),
"load_s3_file" => (S3Object, Some(R), Arg::Pos(0, "s3object")),
"load_s3_file_reader" => (S3Object, Some(R), Arg::Pos(0, "s3object")),
"write_s3_file" => (S3Object, Some(W), Arg::Pos(0, "s3object")),
"get_resource" => (Resource, None, Arg::Pos(0, "path")),
"set_resource" => (Resource, Some(W), Arg::Pos(0, "path")),
"get_boto3_connection_settings" => (Resource, None, Arg::Pos(0, "s3_resource_path")),
"get_polars_connection_settings" => (Resource, None, Arg::Pos(0, "s3_resource_path")),
"get_duckdb_connection_settings" => (Resource, None, Arg::Pos(0, "s3_resource_path")),
_ => return Err(()),
};
let arg_val = match arg {
Arg::Pos(i) => node.args.get(i),
Arg::Named(name) => node
.keywords
.iter()
.find(|kw| kw.arg.as_deref() == Some(name))
.map(|kw| &kw.value),
Arg::Pos(i, name) => node.args.get(i).or_else(|| {
// Get arg by name
node.keywords
.iter()
.find(|kw| kw.arg.as_deref() == Some(name))
.map(|kw| &kw.value)
}),
};
match arg_val {
@@ -93,6 +93,6 @@ impl AssetsFinder {
}
enum Arg {
Pos(usize),
Named(&'static str),
// Positional arguments in python can also be used by their name
Pos(usize, &'static str),
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -84,8 +84,6 @@ impl AssetsFinder {
"denoS3LightClientSettings" => (AssetKind::Resource, None, 0),
"duckdbConnectionSettings" => (AssetKind::Resource, None, 0),
"polarsConnectionSettings" => (AssetKind::Resource, None, 0),
"getVariable" => (AssetKind::Variable, Some(R), 0),
"setVariable" => (AssetKind::Variable, Some(W), 0),
_ => return Err(()),
};

View File

@@ -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()
}

View File

@@ -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),
}
);
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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
}
}
}

View File

@@ -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)
)
}

View File

@@ -30,3 +30,6 @@ popd
pushd "pkg-nu" && npm publish ${args}
popd
pushd "pkg-java" && npm publish ${args}
popd

View File

@@ -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

View File

@@ -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" => {

View File

@@ -15,7 +15,6 @@ use AssetUsageAccessType::*;
pub enum AssetKind {
S3Object,
Resource,
Variable,
}
#[derive(Serialize)]
@@ -57,8 +56,6 @@ pub fn parse_asset_syntax(s: &str) -> Option<(AssetKind, &str)> {
Some((AssetKind::Resource, &s[6..]))
} else if s.starts_with("$res:") {
Some((AssetKind::Resource, &s[5..]))
} else if s.starts_with("var://") {
Some((AssetKind::Variable, &s[6..]))
} else {
None
}

View File

@@ -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,
}

View File

@@ -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),

View File

@@ -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()

View File

@@ -99,4 +99,4 @@ if [ "$MOVE_NEW_FILES" == "YES" ]; then
echo "File moved '${ce_file}' -->> '${ee_file}'"
fi
done
fi
fi

View File

@@ -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
}
@@ -1147,7 +1157,7 @@ async fn test_deno_flow(db: Pool<Postgres>) {
concurrent_limit: None,
concurrency_time_window_s: None,
is_trigger: None,
asset_fallback_access_types: None,
assets: None,
}
.into(),
stop_after_if: Default::default(),
@@ -1191,7 +1201,7 @@ async fn test_deno_flow(db: Pool<Postgres>) {
concurrent_limit: None,
concurrency_time_window_s: None,
is_trigger: None,
asset_fallback_access_types: None,
assets: None,
}
.into(),
stop_after_if: Default::default(),
@@ -1321,7 +1331,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
concurrent_limit: None,
concurrency_time_window_s: None,
is_trigger: None,
asset_fallback_access_types: None,
assets: None,
}.into(),
stop_after_if: Default::default(),
@@ -1376,7 +1386,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
concurrent_limit: None,
concurrency_time_window_s: None,
is_trigger: None,
asset_fallback_access_types: None,
assets: None,
}.into(),
stop_after_if: Default::default(),
stop_after_all_iters_if: Default::default(),
@@ -1416,7 +1426,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
concurrent_limit: None,
concurrency_time_window_s: None,
is_trigger: None,
asset_fallback_access_types: None,
assets: None,
}.into(),
stop_after_if: Default::default(),
@@ -1482,7 +1492,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
concurrent_limit: None,
concurrency_time_window_s: None,
is_trigger: None,
asset_fallback_access_types: None,
assets: None,
}.into(),
stop_after_if: Default::default(),
stop_after_all_iters_if: Default::default(),

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Default directory
EE_DIR="../windmill-ee-private"
@@ -32,4 +32,4 @@ if [[ "$(uname)" == "Darwin" ]]; then
sed -i '' 's/^#samael = { version="0.0.14", features = \["xmlsec"\] }/samael = { version="0.0.14", features = ["xmlsec"] }/' Cargo.toml
# Comment out the git-based samael dependency
sed -i '' 's/^\(samael = { git="https:\/\/github.com\/njaremko\/samael", rev="464d015e3ae393e4b5dd00b4d6baa1b617de0dd6", features = \["xmlsec"\] }\)/# \1/' Cargo.toml
fi
fi

View File

@@ -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"]
@@ -40,7 +40,7 @@ mcp = ["dep:rmcp"]
python = []
[dependencies]
rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", rev = "c74b26b3e3c5dfd0be8e85dede8d88f33d46ff06", features=["transport-streamable-http-server", "transport-streamable-http-server-session", "transport-worker"], optional = true }
rmcp = { version = "0.2.1", features=["transport-streamable-http-server", "transport-streamable-http-server-session", "transport-worker"], optional = true }
windmill-queue.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-audit.workspace = true
@@ -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 }

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.505.3
version: 1.515.1
title: Windmill API
contact:
@@ -581,7 +581,7 @@ paths:
summary: get connected repositories
operationId: getGlobalConnectedRepositories
tags:
- git_sync
- Git Sync
responses:
"200":
description: connected repositories
@@ -2792,6 +2792,22 @@ paths:
schema:
type: string
/tokens/list/scopes:
get:
summary: list of available scopes
operationId: ListAvailableScopes
tags:
- token
responses:
"200":
description: list of available scopes
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ScopeDomain"
/users/tokens/create:
post:
summary: create token
@@ -3099,7 +3115,7 @@ paths:
type: array
items:
$ref: "#/components/schemas/ContextualVariable"
/w/{workspace}/workspaces/get_secondary_storage_names:
get:
summary: get secondary storage names
@@ -3388,6 +3404,18 @@ paths:
type: integer
client:
type: string
grant_type:
type: string
default: "authorization_code"
cc_client_id:
type: string
description: "OAuth client ID for resource-level credentials (client_credentials flow only)"
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
@@ -3399,6 +3427,51 @@ paths:
schema:
type: string
/oauth/connect_client_credentials/{client}:
post:
summary: connect OAuth using client credentials
operationId: connectClientCredentials
tags:
- oauth
parameters:
- name: client
in: path
description: OAuth client name
required: true
schema:
type: string
requestBody:
description: client credentials flow parameters
required: true
content:
application/json:
schema:
type: object
properties:
scopes:
type: array
items:
type: string
cc_client_id:
type: string
description: "OAuth client ID for resource-level authentication"
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
responses:
"200":
description: OAuth token response
content:
application/json:
schema:
$ref: "#/components/schemas/TokenResponse"
/w/{workspace}/oauth/refresh_token/{id}:
post:
summary: refresh token
@@ -3550,6 +3623,10 @@ paths:
type: array
items:
type: string
grant_types:
type: array
items:
type: string
/teams/sync:
post:
@@ -5019,6 +5096,10 @@ paths:
in: query
schema:
type: boolean
- name: authed
in: query
schema:
type: boolean
responses:
"200":
description: script details
@@ -7488,6 +7569,10 @@ paths:
in: query
schema:
type: boolean
- name: no_code
in: query
schema:
type: boolean
responses:
"200":
description: job details
@@ -7589,7 +7674,43 @@ 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:
summary: get job updates via server-sent events
operationId: getJobUpdatesSSE
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/JobId"
- name: running
in: query
schema:
type: boolean
- name: log_offset
in: query
schema:
type: integer
- name: get_progress
in: query
schema:
type: boolean
- name: only_result
in: query
schema:
type: boolean
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
/w/{workspace}/jobs_u/get_log_file/{path}:
get:
@@ -12249,8 +12370,7 @@ paths:
"200":
description: Parquet Preview
content:
application/json:
schema: {}
application/json: {}
/w/{workspace}/job_helpers/load_table_count/{path}:
get:
@@ -12329,8 +12449,7 @@ paths:
"200":
description: Csv Preview
content:
application/json:
schema: {}
application/json: {}
/w/{workspace}/job_helpers/delete_s3_file:
delete:
@@ -13769,20 +13888,27 @@ components:
type: boolean
on_behalf_of_email:
type: string
fallback_access_types:
assets:
type: array
items:
type: object
required: [path, kind, access_type]
required:
- path
- kind
properties:
path:
type: string
kind:
type: string
enum: [s3object, resource]
enum:
- s3object
- resource
access_type:
type: string
enum: [r, w, rw]
alt_access_type:
type: string
enum: [r, w, rw]
required:
- path
@@ -13942,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:
@@ -14049,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:
@@ -14511,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:
@@ -16684,6 +16819,39 @@ components:
- edited_at
- execution_mode
ScopeDefinition:
type: object
properties:
value:
type: string
label:
type: string
description:
type: string
nullable: true
requires_resource_path:
type: boolean
required:
- value
- label
- requires_resource_path
ScopeDomain:
type: object
properties:
name:
type: string
description:
type: string
nullable: true
scopes:
type: array
items:
$ref: "#/components/schemas/ScopeDefinition"
required:
- name
- scopes
ListableRawApp:
type: object
properties:
@@ -16824,6 +16992,8 @@ components:
type: array
items:
type: string
grant_type:
type: string
required:
- access_token
@@ -16949,27 +17119,6 @@ components:
WorkspaceGitSyncSettings:
type: object
properties:
include_path:
type: array
items:
type: string
include_type:
type: array
items:
type: string
enum:
- script
- flow
- app
- folder
- resource
- variable
- secret
- resourcetype
- schedule
- user
- group
- trigger
repositories:
type: array
items:
@@ -17021,6 +17170,42 @@ components:
type: boolean
group_by_folder:
type: boolean
collapsed:
type: boolean
settings:
type: object
properties:
include_path:
type: array
items:
type: string
include_type:
type: array
items:
type: string
enum:
- script
- flow
- app
- folder
- resource
- variable
- secret
- resourcetype
- schedule
- user
- group
- trigger
- settings
- key
exclude_path:
type: array
items:
type: string
extra_include_path:
type: array
items:
type: string
exclude_types_override:
type: array
items:
@@ -17038,6 +17223,8 @@ components:
- user
- group
- trigger
- settings
- key
required:
- script_path
- git_repo_resource_path
@@ -17484,7 +17671,6 @@ components:
enum:
- s3object
- resource
- variable
Asset:
type: object
properties:

View File

@@ -13,7 +13,7 @@ use crate::{
db::{ApiAuthed, DB},
resources::get_resource_value_interpolated_internal,
users::{require_owner_of_path, OptAuthed},
utils::WithStarredInfoQuery,
utils::{check_scopes, WithStarredInfoQuery},
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
@@ -444,6 +444,7 @@ async fn get_app(
Query(query): Query<WithStarredInfoQuery>,
) -> JsonResult<AppWithLastVersionAndStarred> {
let path = path.to_path();
check_scopes(&authed, || format!("apps:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let app_o = if query.with_starred_info.unwrap_or(false) {
@@ -491,6 +492,7 @@ async fn get_app_lite(
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<AppWithLastVersion> {
let path = path.to_path();
check_scopes(&authed, || format!("apps:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let app_o = sqlx::query_as::<_, AppWithLastVersion>(
@@ -518,6 +520,7 @@ async fn get_app_w_draft(
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<AppWithLastVersionAndDraft> {
let path = path.to_path();
check_scopes(&authed, || format!("apps:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let app_o = sqlx::query_as::<_, AppWithLastVersionAndDraft>(
@@ -547,6 +550,8 @@ async fn get_app_history(
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Vec<AppHistory>> {
let path = path.to_path();
check_scopes(&authed, || format!("apps:read:{}", &path))?;
let mut tx = user_db.begin(&authed).await?;
let query_result = sqlx::query!(
"SELECT a.id as app_id, av.id as version_id, dm.deployment_msg as deployment_msg
@@ -554,7 +559,7 @@ async fn get_app_history(
WHERE a.workspace_id = $1 AND a.path = $2
ORDER BY created_at DESC",
w_id,
path.to_path(),
path,
).fetch_all(&mut *tx).await?;
tx.commit().await?;
@@ -574,6 +579,8 @@ async fn get_latest_version(
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Option<AppHistory>> {
let path = path.to_path();
check_scopes(&authed, || format!("apps:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let row = sqlx::query!(
"SELECT a.id as app_id, av.id as version_id, dm.deployment_msg as deployment_msg
@@ -581,7 +588,7 @@ async fn get_latest_version(
WHERE a.workspace_id = $1 AND a.path = $2
ORDER BY created_at DESC",
w_id,
path.to_path(),
path,
).fetch_optional(&mut *tx).await?;
tx.commit().await?;
@@ -609,17 +616,19 @@ async fn update_app_history(
.fetch_optional(&mut *tx)
.await?;
if app_path.is_none() {
let Some(app_path) = app_path else {
tx.commit().await?;
return Err(Error::NotFound(
format!("App with ID {app_id} not found").to_string(),
));
}
};
check_scopes(&authed, || format!("apps:write:{}", &app_path))?;
sqlx::query!(
"INSERT INTO deployment_metadata (workspace_id, path, app_version, deployment_msg) VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, path, app_version) WHERE app_version IS NOT NULL DO UPDATE SET deployment_msg = $4",
w_id,
app_path.unwrap(),
app_path,
app_version,
app_history_update.deployment_msg,
)
@@ -664,6 +673,9 @@ async fn get_app_by_id(
tx.commit().await?;
let app = not_found_if_none(app_o, "App", id.to_string())?;
check_scopes(&authed, || format!("apps:read:{}", &app.path))?;
Ok(Json(app))
}
@@ -756,6 +768,7 @@ async fn get_secret_id(
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("apps:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let id_o = sqlx::query_scalar!(
@@ -861,6 +874,8 @@ async fn create_app_raw<'a>(
)
.await?;
check_scopes(&authed, || format!("apps:write:{}", path))?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateApp { workspace: w_id, path: path.clone() },
@@ -899,6 +914,8 @@ async fn create_app(
Json(app): Json<CreateApp>,
) -> Result<(StatusCode, String)> {
let path = app.path.clone();
check_scopes(&authed, || format!("apps:write:{}", &path))?;
let (new_tx, _path, _id) = create_app_internal(authed, db, user_db, &w_id, false, app).await?;
new_tx.commit().await?;
@@ -1101,6 +1118,7 @@ async fn delete_app(
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("apps:write:{}", path))?;
if path == "g/all/setup_app" && w_id == "admins" {
return Err(Error::BadRequest(
@@ -1184,6 +1202,7 @@ async fn update_app(
) -> Result<String> {
// create_app_internal(authed, user_db, db, &w_id, &mut app).await?;
let path = path.to_path();
check_scopes(&authed, || format!("apps:write:{}", path))?;
let opath = path.to_string();
let (new_tx, npath, _v_id) =
update_app_internal(authed, db, user_db, &w_id, path, false, ns).await?;
@@ -1210,6 +1229,7 @@ async fn update_app_raw<'a>(
multipart: Multipart,
) -> Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("apps:write:{}", path))?;
let opath = path.to_string();
let (npath, _id) = process_app_multipart!(
authed,
@@ -2421,10 +2441,9 @@ async fn exists_app(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<bool> {
let path = path.to_path();
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE path = $1 AND workspace_id = $2)",
path,
path.to_path(),
w_id
)
.fetch_one(&db)

View File

@@ -22,6 +22,7 @@ use tokio::sync::RwLock;
use windmill_common::{
auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims, TOKEN_PREFIX_LEN},
error::Error,
jwt,
users::{COOKIE_NAME, SUPERADMIN_SECRET_EMAIL},
};
@@ -218,7 +219,9 @@ impl AuthCache {
folders,
scopes: None,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
token_prefix: Some(
token[0..TOKEN_PREFIX_LEN].to_string(),
),
})
} else {
let groups = vec![name.to_string()];
@@ -240,7 +243,9 @@ impl AuthCache {
folders,
scopes: None,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
token_prefix: Some(
token[0..TOKEN_PREFIX_LEN].to_string(),
),
})
}
} else {
@@ -303,7 +308,9 @@ impl AuthCache {
folders,
scopes,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
token_prefix: Some(
token[0..TOKEN_PREFIX_LEN].to_string(),
),
})
}
None if super_admin => Some(ApiAuthed {
@@ -315,7 +322,9 @@ impl AuthCache {
folders: vec![],
scopes,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
token_prefix: Some(
token[0..TOKEN_PREFIX_LEN].to_string(),
),
}),
None => None,
}
@@ -491,6 +500,33 @@ where
}
}
pub fn transform_old_scope_to_new_scope(scopes: Option<&mut Vec<String>>) {
if let Some(scopes) = scopes {
for scope in scopes.iter_mut() {
if scope.starts_with("run:") {
let (_, part_scope) = scope.split_once(":").unwrap();
if let Some((kind, path)) = part_scope.split_once("/") {
//appending a 's' as runnable kind is singular while new scope format expect it to be plural
*scope = format!("jobs:run:{}s:{}", kind, path);
}
} else if scope.starts_with("jobs:") {
// Map old jobs scopes to new format
let new_scope = match scope.as_str() {
"jobs:listjobs" => "jobs:read",
"jobs:runscript" => "jobs:run:scripts",
"jobs:runflow" => "jobs:run:flows",
"jobs:resumeflow" => "jobs:run:flows",
"jobs:deletejob" => "jobs:write",
_ => continue,
};
*scope = new_scope.to_string();
}
}
}
}
fn maybe_get_workspace_id_from_path(path_vec: &[&str]) -> Option<String> {
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
Some(path_vec[3].to_owned())
@@ -517,7 +553,7 @@ impl<S> FromRequestParts<S> for ApiAuthed
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
type Rejection = Error;
async fn from_request_parts(
parts: &mut Parts,
@@ -538,7 +574,6 @@ where
} else {
extract_token(parts, state).await
};
if let Some(token) = token_o {
if let Ok(Extension(cache)) =
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
@@ -551,21 +586,22 @@ where
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = maybe_get_workspace_id_from_path(&path_vec);
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
if authed.scopes.as_ref().is_some_and(|scopes| {
scopes
.iter()
.any(|s| s.starts_with("jobs:") || s.starts_with("run:"))
}) && (path_vec.len() < 3
|| (path_vec[4] != "jobs" && path_vec[4] != "jobs_u"))
{
BRUTE_FORCE_COUNTER.increment().await;
return Err((
StatusCode::UNAUTHORIZED,
format!("Unauthorized scoped token: {:?}", authed.scopes),
));
}
if let Some(mut authed) = cache.get_authed(workspace_id.clone(), &token).await {
if authed.scopes.is_some() {
transform_old_scope_to_new_scope(authed.scopes.as_mut());
let path = original_uri.path();
let method = parts.method.as_str();
if let Err(err) = crate::scopes::check_scopes_for_route(
authed.scopes.as_deref(),
path,
method,
) {
BRUTE_FORCE_COUNTER.increment().await;
return Err(err);
}
}
parts.extensions.insert(authed.clone());
Span::current().record("username", &authed.username.as_str());
@@ -579,7 +615,7 @@ where
}
}
BRUTE_FORCE_COUNTER.increment().await;
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
Err(Error::NotAuthorized("Unauthorized".to_string()))
}
}

View File

@@ -1,9 +1,11 @@
use crate::db::{ApiAuthed, DB};
use crate::jobs::{
filter_list_completed_query, filter_list_queue_query, Job, ListCompletedQuery, ListQueueQuery,
UnifiedJob,
use crate::{
db::{ApiAuthed, DB},
jobs::{
filter_list_completed_query, filter_list_queue_query, Job, ListCompletedQuery,
ListQueueQuery, UnifiedJob,
},
utils::check_scopes,
};
use crate::users::check_scopes;
use axum::extract::Path;
use axum::routing::{delete, get};
use axum::{extract::Query, Extension, Json};
@@ -144,7 +146,7 @@ async fn get_concurrent_intervals(
Query(iq): Query<ExtendedJobsParams>,
Query(lq): Query<ListCompletedQuery>,
) -> JsonResult<ExtendedJobs> {
check_scopes(&authed, || format!("jobs:listjobs"))?;
check_scopes(&authed, || format!("jobs:read"))?;
if lq.success.is_some() && lq.running.is_some_and(|x| x) {
return Err(error::Error::BadRequest(

View File

@@ -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",

View File

@@ -8,15 +8,15 @@
use std::collections::HashMap;
use crate::db::ApiAuthed;
use crate::triggers::{
get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail,
};
use crate::utils::WithStarredInfoQuery;
use crate::{
db::DB,
db::{ApiAuthed, DB},
schedule::clear_schedule,
triggers::{
get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail,
},
users::{maybe_refresh_folders, require_owner_of_path},
utils::check_scopes,
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
@@ -333,6 +333,8 @@ async fn list_paths_from_workspace_runnable(
Extension(user_db): Extension<UserDB>,
Path((w_id, runnable_kind, path)): Path<(String, RunnableKind, StripPath)>,
) -> JsonResult<Vec<String>> {
let path = path.to_path();
check_scopes(&authed, || format!("flows:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let runnables = sqlx::query_scalar!(
r#"SELECT f.path
@@ -340,7 +342,7 @@ async fn list_paths_from_workspace_runnable(
JOIN flow f
ON wru.flow_path = f.path AND wru.workspace_id = f.workspace_id
WHERE wru.runnable_path = $1 AND wru.runnable_is_flow = $2 AND wru.workspace_id = $3"#,
path.to_path(),
path,
matches!(runnable_kind, RunnableKind::Flow),
w_id
)
@@ -358,6 +360,7 @@ async fn create_flow(
Path(w_id): Path<String>,
Json(nf): Json<NewFlow>,
) -> Result<(StatusCode, String)> {
check_scopes(&authed, || format!("flows:write:{}", nf.path))?;
if *CLOUD_HOSTED {
let nb_flows =
sqlx::query_scalar!("SELECT COUNT(*) FROM flow WHERE workspace_id = $1", &w_id)
@@ -579,6 +582,7 @@ async fn get_flow_history(
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Vec<FlowVersion>> {
let path = path.to_path();
check_scopes(&authed, || format!("flows:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let flows = sqlx::query_as!(
@@ -603,6 +607,7 @@ async fn get_latest_version(
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Option<FlowVersion>> {
let path = path.to_path();
check_scopes(&authed, || format!("flows:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let version = sqlx::query_as!(
@@ -627,6 +632,7 @@ async fn get_flow_version(
Path((w_id, version, path)): Path<(String, i64, StripPath)>,
) -> JsonResult<Flow> {
let path = path.to_path();
check_scopes(&authed, || format!("flows:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let flow = sqlx::query_as::<_, Flow>(
@@ -660,6 +666,7 @@ async fn update_flow_history(
Json(history_update): Json<FlowHistoryUpdate>,
) -> Result<()> {
let path = path.to_path();
check_scopes(&authed, || format!("flows:write:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let path_o = sqlx::query_scalar!(
"SELECT flow.path FROM flow
@@ -701,6 +708,8 @@ async fn update_flow(
Path((w_id, flow_path)): Path<(String, StripPath)>,
Json(nf): Json<NewFlow>,
) -> Result<String> {
let flow_path = flow_path.to_path();
check_scopes(&authed, || format!("flows:write:{}", flow_path))?;
#[cfg(not(feature = "enterprise"))]
if nf
.value
@@ -714,7 +723,6 @@ async fn update_flow(
));
}
let flow_path = flow_path.to_path();
let authed = maybe_refresh_folders(&flow_path, &w_id, authed, &db).await;
let mut tx = user_db.clone().begin(&authed).await?;
@@ -1037,6 +1045,7 @@ async fn get_flow_by_path(
Query(query): Query<WithStarredInfoQuery>,
) -> JsonResult<FlowWithStarred> {
let path = path.to_path();
check_scopes(&authed, || format!("flows:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let flow_o = if query.with_starred_info.unwrap_or(false) {
@@ -1104,10 +1113,11 @@ async fn get_flow_by_path_w_draft(
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<FlowWDraft> {
let path = path.to_path();
check_scopes(&authed, || format!("flows:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let flow_o = sqlx::query_as::<_, FlowWDraft>(
"SELECT flow.path, flow.summary, flow,description, flow_version.schema, flow_version.value, flow.extra_perms, flow.draft_only, flow.ws_error_handler_muted, flow.dedicated_worker, draft.value as draft, flow.tag, flow.visible_to_runner_only, flow.on_behalf_of_email
"SELECT flow.path, flow.summary, flow.description, flow_version.schema, flow_version.value, flow.extra_perms, flow.draft_only, flow.ws_error_handler_muted, flow.dedicated_worker, draft.value as draft, flow.tag, flow.visible_to_runner_only, flow.on_behalf_of_email
FROM flow
LEFT JOIN draft
ON flow.path = draft.path AND draft.workspace_id = $2 AND draft.typ = 'flow'
@@ -1157,6 +1167,7 @@ async fn archive_flow_by_path(
Json(archived): Json<Archived>,
) -> Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("flows:write:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
@@ -1233,6 +1244,7 @@ async fn delete_flow_by_path(
Query(query): Query<DeleteFlowQuery>,
) -> Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("flows:write:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
@@ -1379,7 +1391,7 @@ mod tests {
concurrent_limit: None,
concurrency_time_window_s: None,
is_trigger: None,
asset_fallback_access_types: None,
assets: None,
}),
stop_after_if: Some(StopAfterIf {
expr: "foo = 'bar'".to_string(),

View File

@@ -8,12 +8,11 @@
use std::sync::Arc;
use crate::db::ApiAuthed;
use crate::{
auth::AuthCache,
db::DB,
db::{ApiAuthed, DB},
users::Tokened,
utils::check_scopes,
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
@@ -419,6 +418,7 @@ async fn get_folder(
Extension(user_db): Extension<UserDB>,
Path((w_id, name)): Path<(String, String)>,
) -> JsonResult<Folder> {
check_scopes(&authed, || format!("folders:read:f/{}", name))?;
let mut tx = user_db.begin(&authed).await?;
let folder = not_found_if_none(get_folderopt(&mut tx, &w_id, &name).await?, "Folder", &name)?;
@@ -457,6 +457,7 @@ async fn get_folder_usage(
Extension(user_db): Extension<UserDB>,
Path((w_id, name)): Path<(String, String)>,
) -> JsonResult<FolderUsage> {
check_scopes(&authed, || format!("folders:read:f/{}", name))?;
let mut tx = user_db.begin(&authed).await?;
let scripts = sqlx::query_scalar!(

View File

@@ -2,16 +2,17 @@
use crate::http_trigger_args::{HttpMethod, RawHttpTriggerArgs};
#[cfg(feature = "parquet")]
use crate::job_helpers_oss::get_workspace_s3_resource;
use crate::resources::try_get_resource_from_db_as;
use crate::trigger_helpers::{
get_runnable_format, trigger_runnable, trigger_runnable_and_wait_for_result, RunnableId,
};
use crate::utils::{non_empty_str, ExpiringCacheEntry};
use crate::{
auth::{AuthCache, OptTokened},
db::{ApiAuthed, DB},
resources::try_get_resource_from_db_as,
trigger_helpers::{
get_runnable_format, trigger_runnable, trigger_runnable_and_wait_for_result, RunnableId,
},
users::fetch_api_authed,
utils::{check_scopes, non_empty_str, ExpiringCacheEntry},
};
use anyhow::anyhow;
use axum::response::Response;
use axum::{
@@ -253,8 +254,9 @@ async fn get_trigger(
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> error::JsonResult<HttpTrigger> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
check_scopes(&authed, || format!("http_triggers:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let trigger = sqlx::query_as!(
HttpTrigger,
r#"
@@ -333,6 +335,10 @@ async fn create_trigger_inner(
new_http_trigger: &NewTrigger,
route_path_key: &str,
) -> WindmillResult<()> {
check_scopes(&authed, || {
format!("http_triggers:write:{}", &new_http_trigger.path)
})?;
sqlx::query!(
r#"
INSERT INTO http_trigger (
@@ -577,6 +583,7 @@ async fn update_trigger(
Json(ct): Json<EditTrigger>,
) -> WindmillResult<String> {
let path = path.to_path();
check_scopes(&authed, || format!("http_triggers:write:{}", path))?;
if *CLOUD_HOSTED && (ct.is_static_website || ct.static_asset_config.is_some()) {
return Err(error::Error::BadRequest(
@@ -761,8 +768,9 @@ async fn delete_trigger(
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> WindmillResult<String> {
require_admin(authed.is_admin, &authed.username)?;
let path = path.to_path();
check_scopes(&authed, || format!("http_triggers:write:{}", path))?;
require_admin(authed.is_admin, &authed.username)?;
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"DELETE FROM http_trigger

File diff suppressed because it is too large Load Diff

View File

@@ -34,11 +34,11 @@ use agent_workers_oss::AgentCache;
use anyhow::Context;
use argon2::Argon2;
use axum::extract::DefaultBodyLimit;
use axum::{middleware::from_extractor, routing::get, routing::post, Extension, Router};
use axum::response::Response;
use axum::http::HeaderValue;
use axum::body::Body;
use axum::extract::DefaultBodyLimit;
use axum::http::HeaderValue;
use axum::response::Response;
use axum::{middleware::from_extractor, routing::get, routing::post, Extension, Router};
use db::DB;
use reqwest::Client;
#[cfg(feature = "oauth2")]
@@ -153,6 +153,7 @@ mod schedule;
#[cfg(feature = "private")]
pub mod scim_ee;
mod scim_oss;
mod scopes;
mod scripts;
mod service_logs;
mod settings;
@@ -178,6 +179,7 @@ mod stripe_oss;
#[cfg(feature = "private")]
pub mod teams_ee;
mod teams_oss;
mod token;
mod tracing_init;
mod triggers;
mod users;
@@ -239,7 +241,6 @@ lazy_static::lazy_static! {
}
// Compliance with cloud events spec.
pub async fn add_webhook_allowed_origin(
req: axum::extract::Request,
@@ -262,7 +263,6 @@ pub async fn add_webhook_allowed_origin(
next.run(req).await
}
#[cfg(not(feature = "tantivy"))]
type IndexReader = ();
@@ -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(
@@ -649,6 +649,7 @@ pub async fn run_server(
scim_oss::global_service()
.route_layer(axum::middleware::from_fn(has_scim_token)),
)
.nest("/tokens", token::global_service())
.nest("/concurrency_groups", concurrency_groups::global_service())
.nest("/scripts_u", scripts::global_unauthed_service())
.nest("/apps_u", {
@@ -668,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")]

View File

@@ -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]

View File

@@ -4,6 +4,7 @@ use crate::{
resources::try_get_resource_from_db_as,
trigger_helpers::{trigger_runnable, TriggerJobArgs},
users::fetch_api_authed,
utils::check_scopes,
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
@@ -519,6 +520,9 @@ pub async fn create_mqtt_trigger(
Path(w_id): Path<String>,
Json(new_mqtt_trigger): Json<NewMqttTrigger>,
) -> error::Result<(StatusCode, String)> {
check_scopes(&authed, || {
format!("mqtt_triggers:write:{}", &new_mqtt_trigger.path)
})?;
if *CLOUD_HOSTED {
return Err(error::Error::BadRequest(
"MQTT triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(),
@@ -701,8 +705,10 @@ pub async fn get_mqtt_trigger(
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<MqttTrigger> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
check_scopes(&authed, || format!("mqtt_triggers:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let trigger = sqlx::query_as!(
MqttTrigger,
r#"
@@ -754,6 +760,8 @@ pub async fn update_mqtt_trigger(
Json(mqtt_trigger): Json<EditMqttTrigger>,
) -> error::Result<String> {
let workspace_path = path.to_path();
check_scopes(&authed, || format!("mqtt_triggers:write:{}", workspace_path))?;
let EditMqttTrigger {
mqtt_resource_path,
subscribe_topics,
@@ -856,6 +864,8 @@ pub async fn delete_mqtt_trigger(
Path((w_id, path)): Path<(String, StripPath)>,
) -> error::Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("mqtt_triggers:write:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
r#"
@@ -931,8 +941,9 @@ pub async fn set_enabled(
Path((w_id, path)): Path<(String, StripPath)>,
Json(payload): Json<SetEnabled>,
) -> error::Result<String> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
check_scopes(&authed, || format!("mqtt_triggers:write:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
// important to set server_id, last_server_ping and error to NULL to stop current mqtt listener
let one_o = sqlx::query_scalar!(

View File

@@ -6,6 +6,7 @@ use std::collections::{
use crate::{
db::{ApiAuthed, DB},
postgres_triggers::mapper::{Mapper, MappingInfo},
utils::check_scopes,
};
use axum::{
extract::{Path, Query},
@@ -373,6 +374,8 @@ pub async fn create_postgres_trigger(
Path(w_id): Path<String>,
Json(new_postgres_trigger): Json<NewPostgresTrigger>,
) -> Result<(StatusCode, String)> {
check_scopes(&authed, || format!("postgres_triggers:write:{}", new_postgres_trigger.path))?;
if *CLOUD_HOSTED {
return Err(error::Error::BadRequest(
"Postgres triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(),
@@ -1150,8 +1153,9 @@ pub async fn get_postgres_trigger(
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<PostgresTrigger> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
check_scopes(&authed, || format!("postgres_triggers:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let trigger = sqlx::query_as!(
PostgresTrigger,
r#"
@@ -1200,6 +1204,7 @@ pub async fn update_postgres_trigger(
Json(postgres_trigger): Json<EditPostgresTrigger>,
) -> Result<String> {
let workspace_path = path.to_path();
check_scopes(&authed, || format!("postgres_triggers:write:{}", workspace_path))?;
let EditPostgresTrigger {
replication_slot_name,
@@ -1332,6 +1337,7 @@ pub async fn delete_postgres_trigger(
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("postgres_triggers:write:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
r#"
@@ -1403,8 +1409,9 @@ pub async fn set_enabled(
Path((w_id, path)): Path<(String, StripPath)>,
Json(payload): Json<SetEnabled>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
check_scopes(&authed, || format!("postgres_triggers:write:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
// important to set server_id, last_server_ping and error to NULL to stop current postgres listener
let one_o = sqlx::query_scalar!(

View File

@@ -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;

View File

@@ -8,6 +8,7 @@
use crate::{
db::{ApiAuthed, DB},
users::require_owner_of_path,
utils::check_scopes,
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
@@ -128,6 +129,7 @@ async fn get_data(
Path((w_id, _version, path)): Path<(String, u16, StripPath)>,
) -> Result<Response> {
let path = path.to_path();
check_scopes(&authed, || format!("raw_apps:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let app_o = sqlx::query_scalar!(
@@ -154,6 +156,7 @@ async fn create_app(
Path(w_id): Path<String>,
Json(app): Json<CreateApp>,
) -> Result<(StatusCode, String)> {
check_scopes(&authed, || format!("raw_apps:write:{}", app.path))?;
if *CLOUD_HOSTED {
let nb_apps = sqlx::query_scalar!(
"SELECT COUNT(*) FROM raw_app WHERE workspace_id = $1",
@@ -233,6 +236,7 @@ async fn delete_app(
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("raw_apps:write:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
@@ -271,6 +275,7 @@ async fn update_app(
use sql_builder::prelude::*;
let path = path.to_path();
check_scopes(&authed, || format!("raw_apps:write:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let mut sqlb = SqlBuilder::update_table("raw_app");
@@ -347,6 +352,7 @@ async fn exists_app(
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<bool> {
let path = path.to_path();
// Note: exists_app doesn't require authentication, so no scope check needed
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM raw_app WHERE path = $1 AND workspace_id = $2)",
path,

View File

@@ -11,6 +11,7 @@ use std::collections::HashMap;
use crate::{
db::{ApiAuthed, DB},
users::{maybe_refresh_folders, require_owner_of_path, Tokened},
utils::check_scopes,
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
@@ -280,6 +281,7 @@ async fn get_resource(
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<ListableResource> {
let path = path.to_path();
check_scopes(&authed, || format!("resources:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let resource_o = sqlx::query_as!(
@@ -331,6 +333,7 @@ async fn get_resource_value(
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Option<serde_json::Value>> {
let path = path.to_path();
check_scopes(&authed, || format!("resources:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let value_o = sqlx::query_scalar!(
@@ -422,12 +425,15 @@ async fn get_resource_value_interpolated(
Path((w_id, path)): Path<(String, StripPath)>,
Query(job_info): Query<JobInfo>,
) -> JsonResult<Option<serde_json::Value>> {
let path = path.to_path();
check_scopes(&authed, || format!("resources:read:{}", path))?;
return get_resource_value_interpolated_internal(
&authed,
Some(user_db),
&db,
w_id.as_str(),
path.to_path(),
path,
job_info.job_id,
token.as_str(),
)
@@ -658,6 +664,7 @@ async fn create_resource(
Query(q): Query<CreateResourceQuery>,
Json(resource): Json<CreateResource>,
) -> Result<(StatusCode, String)> {
check_scopes(&authed, || format!("resources:write:{}", resource.path))?;
if *CLOUD_HOSTED {
let nb_resources = sqlx::query_scalar!(
"SELECT COUNT(*) FROM resource WHERE workspace_id = $1",
@@ -740,6 +747,7 @@ async fn delete_resource(
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("resources:write:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
@@ -798,6 +806,7 @@ async fn update_resource(
use sql_builder::prelude::*;
let path = path.to_path();
check_scopes(&authed, || format!("resources:write:{}", path))?;
let mut sqlb = SqlBuilder::update_table("resource");
sqlb.and_where_eq("path", "?".bind(&path));
@@ -891,6 +900,7 @@ async fn update_resource_value(
Json(nv): Json<UpdateResource>,
) -> Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("resources:write:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(

View File

@@ -7,7 +7,10 @@
*/
use crate::{
db::{ApiAuthed, DB}, settings::{delete_global_setting, set_global_setting_internal}, users::maybe_refresh_folders, utils::require_super_admin
db::{ApiAuthed, DB},
settings::{delete_global_setting, set_global_setting_internal},
users::maybe_refresh_folders,
utils::{check_scopes, require_super_admin},
};
use axum::{
extract::{Extension, Path, Query},
@@ -128,6 +131,8 @@ async fn create_schedule(
Path(w_id): Path<String>,
Json(ns): Json<NewSchedule>,
) -> Result<String> {
check_scopes(&authed, || format!("schedules:write:{}", ns.path))?;
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
#[cfg(not(feature = "enterprise"))]
@@ -294,6 +299,7 @@ async fn edit_schedule(
Json(es): Json<EditSchedule>,
) -> Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("schedules:write:{}", path))?;
let authed = maybe_refresh_folders(&path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?;
@@ -560,6 +566,7 @@ async fn get_schedule(
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Schedule> {
let path = path.to_path();
check_scopes(&authed, || format!("schedules:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let schedule_o = windmill_queue::schedule::get_schedule_opt(&mut *tx, &w_id, path).await?;
@@ -608,6 +615,7 @@ pub async fn set_enabled(
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
check_scopes(&authed, || format!("schedules:write:{}", path))?;
let schedule_o = sqlx::query_as!(
Schedule,
r#"
@@ -745,8 +753,9 @@ async fn delete_schedule(
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
check_scopes(&authed, || format!("schedules:write:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
clear_schedule(&mut tx, path, &w_id).await?;
let exists = sqlx::query_scalar!(

View File

@@ -0,0 +1,919 @@
/*
* Author: Windmill Labs, Inc
* Copyright: Windmill Labs, Inc 2024
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use windmill_common::error::{Error, Result};
/// Comprehensive scope system for JWT token authorization
///
/// Scopes follow the format: {domain}:{action}[:{resource}]
/// Examples:
/// - "jobs:read" - Read access to jobs
/// - "scripts:write:f/folder/*" - Write access to scripts in a folder
/// - "*" - Full access (superuser)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScopeDefinition {
pub domain: String,
pub action: String,
pub kind: Option<String>, // For jobs:run:kind (optional)
pub resource: Option<Vec<String>>,
}
impl ScopeDefinition {
pub fn new(
domain: &str,
action: &str,
kind: Option<&str>,
resource: Option<Vec<String>>,
) -> Self {
Self {
domain: domain.to_string(),
action: action.to_string(),
kind: kind.map(|s| s.to_string()),
resource: resource,
}
}
pub fn from_scope_string(scope: &str) -> Result<Self> {
let parts: Vec<&str> = scope.split(':').collect();
let into_owned_vec = |resources: &str| -> Vec<String> {
let resources = resources
.split(",")
.collect_vec()
.into_iter()
.map(ToOwned::to_owned)
.collect_vec();
resources
};
match parts.len() {
2 => Ok(Self::new(parts[0], parts[1], None, None)), // domain:action
3 => {
if parts[0] == "jobs" && parts[1] == "run" {
Ok(Self::new(parts[0], parts[1], Some(parts[2]), None))
} else {
Ok(Self::new(
parts[0],
parts[1],
None,
Some(into_owned_vec(parts[2])),
))
}
}
4 => {
if parts[0] == "jobs" && parts[1] == "run" {
Ok(Self::new(
parts[0],
parts[1],
Some(parts[2]),
Some(into_owned_vec(parts[3])),
))
} else {
Err(Error::BadRequest(format!(
"Invalid 4-part scope: {}",
scope
)))
}
}
_ => Err(Error::BadRequest(format!(
"Invalid scope format: {}",
scope
))),
}
}
pub fn as_string(&self) -> String {
match (&self.kind, &self.resource) {
(Some(kind), Some(resource)) => {
format!(
"{}:{}:{}:{}",
self.domain,
self.action,
kind,
resource.join(",")
)
}
(Some(kind), None) => {
format!("{}:{}:{}", self.domain, self.action, kind)
}
(None, Some(resource)) => {
format!("{}:{}:{}", self.domain, self.action, resource.join(","))
}
(None, None) => format!("{}:{}", self.domain, self.action),
}
}
pub fn includes(&self, other: &ScopeDefinition) -> bool {
if self.domain != other.domain {
return false;
}
match (self.action.as_str(), other.action.as_str()) {
(a, b) if (a == "write" && b == "read") || (a == b) => {}
_ => return false,
}
if self.domain == "jobs" && self.action == "run" {
match (&self.kind, &other.kind) {
(Some(self_kind), Some(other_kind)) => {
if self_kind != other_kind {
return false;
}
}
(Some(_), None) => {
return false;
}
(None, _) => {
return true;
}
}
}
match (&self.resource, &other.resource) {
(Some(self_resources), Some(other_resources)) => {
resources_match(self_resources, other_resources)
}
(Some(_), None) => false,
(None, _) => true,
}
}
}
fn resources_match(scope_resources: &[String], accepted_resources: &[String]) -> bool {
if scope_resources.contains(&"*".to_string()) || accepted_resources.contains(&"*".to_string()) {
return true;
}
if scope_resources.len() <= 4 && accepted_resources.len() <= 4 {
return resources_match_small(scope_resources, accepted_resources);
}
resources_match_large(scope_resources, accepted_resources)
}
fn resources_match_small(scope_resources: &[String], accepted_resources: &[String]) -> bool {
for required in accepted_resources {
for scope_resource in scope_resources {
if resource_matches_pattern(scope_resource, required) {
return true;
}
}
}
false
}
fn resources_match_large(scope_resources: &[String], accepted_resources: &[String]) -> bool {
let mut exact_matches = HashSet::new();
let mut patterns = Vec::new();
for scope_resource in scope_resources {
if scope_resource.contains('*') {
patterns.push(scope_resource);
} else {
exact_matches.insert(scope_resource);
}
}
for accepted_resource in accepted_resources {
if exact_matches.contains(accepted_resource) {
return true;
}
for pattern in &patterns {
if resource_matches_pattern(pattern, accepted_resource) {
return true;
}
}
}
false
}
fn resource_matches_pattern(scope_resource: &str, accepted_resource: &str) -> bool {
if scope_resource == accepted_resource {
return true;
}
let matches_wildcard = |pattern: &str, resource: &str| -> bool {
if !pattern.ends_with("/*") {
return false;
}
let prefix = &pattern[..pattern.len() - 2];
if !resource.starts_with(prefix) {
return false;
}
// If the resource is exactly the prefix, it matches
if resource.len() == prefix.len() {
return true;
}
// If the resource is longer, the next character must be '/' for a valid match
// This prevents "u/user" from matching "u/use/*"
resource.chars().nth(prefix.len()) == Some('/')
};
// Check if either resource is a wildcard pattern and matches the other
matches_wildcard(scope_resource, accepted_resource)
|| matches_wildcard(accepted_resource, scope_resource)
}
/// Available scope domains (top-level API categories)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScopeDomain {
// Core resource domains
Jobs,
Scripts,
Flows,
Apps,
Variables,
Resources,
Schedules,
Folders,
Users,
Groups,
Workspaces,
// Trigger domains
HttpTriggers,
WebsocketTriggers,
KafkaTriggers,
NatsTriggers,
MqttTriggers,
SqsTriggers,
GcpTriggers,
PostgresTriggers,
// System domains
Audit,
Settings,
Workers,
ServiceLogs,
Configs,
OAuth,
AI,
Indexer,
Teams, // Microsoft Teams integration
GitSync, // Git synchronization
// Special domains
Capture, // Webhook capture
Drafts, // Draft resources
Favorites, // User favorites
Inputs, // Input templates
JobHelpers, // Job helper functions
ConcurrencyGroups, // Concurrency groups
Oidc, // OpenID Connect
Openapi, // OpenAPI generation
// Additional domains
Acls, // Granular access control lists
RawApps, // Raw application data
AgentWorkers, // Agent workers management
Mcp, // MCP
}
impl ScopeDomain {
pub fn as_str(&self) -> &'static str {
match self {
Self::Jobs => "jobs",
Self::Scripts => "scripts",
Self::Flows => "flows",
Self::Apps => "apps",
Self::Variables => "variables",
Self::Resources => "resources",
Self::Schedules => "schedules",
Self::Folders => "folders",
Self::Users => "users",
Self::Groups => "groups",
Self::Workspaces => "workspaces",
Self::HttpTriggers => "http_triggers",
Self::WebsocketTriggers => "websocket_triggers",
Self::KafkaTriggers => "kafka_triggers",
Self::NatsTriggers => "nats_triggers",
Self::MqttTriggers => "mqtt_triggers",
Self::SqsTriggers => "sqs_triggers",
Self::GcpTriggers => "gcp_triggers",
Self::PostgresTriggers => "postgres_triggers",
Self::Audit => "audit",
Self::Settings => "settings",
Self::Workers => "workers",
Self::ServiceLogs => "service_logs",
Self::Configs => "configs",
Self::OAuth => "oauth",
Self::AI => "ai",
Self::Capture => "capture",
Self::Drafts => "drafts",
Self::Favorites => "favorites",
Self::Inputs => "inputs",
Self::JobHelpers => "job_helpers",
Self::ConcurrencyGroups => "concurrency_groups",
Self::Oidc => "oidc",
Self::Openapi => "openapi",
Self::Acls => "acls",
Self::RawApps => "raw_apps",
Self::AgentWorkers => "agent_workers",
Self::Indexer => "indexer",
Self::Teams => "teams",
Self::GitSync => "git_sync",
Self::Mcp => "mcp",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"jobs" | "jobs_u" => Some(Self::Jobs),
"scripts" => Some(Self::Scripts),
"flows" => Some(Self::Flows),
"apps" => Some(Self::Apps),
"variables" => Some(Self::Variables),
"resources" => Some(Self::Resources),
"schedules" => Some(Self::Schedules),
"folders" => Some(Self::Folders),
"users" => Some(Self::Users),
"groups" => Some(Self::Groups),
"workspaces" => Some(Self::Workspaces),
"http_triggers" => Some(Self::HttpTriggers),
"websocket_triggers" => Some(Self::WebsocketTriggers),
"kafka_triggers" => Some(Self::KafkaTriggers),
"nats_triggers" => Some(Self::NatsTriggers),
"mqtt_triggers" => Some(Self::MqttTriggers),
"sqs_triggers" => Some(Self::SqsTriggers),
"gcp_triggers" => Some(Self::GcpTriggers),
"postgres_triggers" => Some(Self::PostgresTriggers),
"audit" => Some(Self::Audit),
"settings" => Some(Self::Settings),
"workers" => Some(Self::Workers),
"service_logs" => Some(Self::ServiceLogs),
"configs" => Some(Self::Configs),
"oauth" => Some(Self::OAuth),
"ai" => Some(Self::AI),
"indexer" | "srch" => Some(Self::Indexer),
"teams" => Some(Self::Teams),
"git_sync" | "github_app" => Some(Self::GitSync),
"capture" => Some(Self::Capture),
"drafts" => Some(Self::Drafts),
"favorites" => Some(Self::Favorites),
"inputs" => Some(Self::Inputs),
"job_helpers" => Some(Self::JobHelpers),
"concurrency_groups" => Some(Self::ConcurrencyGroups),
"oidc" => Some(Self::Oidc),
"openapi" => Some(Self::Openapi),
"acls" => Some(Self::Acls),
"raw_apps" => Some(Self::RawApps),
"agent_workers" => Some(Self::AgentWorkers),
"mcp" => Some(Self::Mcp),
_ => None,
}
}
}
/// Available scope actions
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScopeAction {
Read, // GET operations, list, view
Write, // POST, PUT, PATCH, DELETE operations, create, update, delete
Run, // Special action for running (scripts, flows, etc.)
}
impl ScopeAction {
pub fn as_str(&self) -> &'static str {
match self {
Self::Read => "read",
Self::Write => "write",
Self::Run => "run",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"read" => Some(Self::Read),
"write" => Some(Self::Write),
"delete" => Some(Self::Write),
"run" => Some(Self::Run),
_ => None,
}
}
/// Check if this action includes another action
/// Write includes Read
pub fn includes(&self, other: &ScopeAction) -> bool {
match (self, other) {
(ScopeAction::Write, ScopeAction::Read) => true,
(a, b) => a == b,
}
}
}
pub fn
check_route_access(
token_scopes: &[String],
route_path: &str,
http_method: &str,
) -> Result<()> {
// Map HTTP method to scope action (considering route context)
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, 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 {
if !scope_str.starts_with("if_jobs:filter_tags:") {
if let Ok(scope) = ScopeDefinition::from_scope_string(scope_str) {
if scope_grants_access(
&scope,
required_domain,
required_action,
required_kind.as_deref(),
route_suffix.as_deref(),
)? {
return Ok(());
}
}
if !is_scoped_token {
is_scoped_token = true;
}
}
}
//Edge case for backward compatibility, if only scopes defined was filter tag then don't treat this we don't treat the token
//as a restricted token
if !is_scoped_token {
return Ok(());
}
let scope_display = if let Some(kind) = required_kind {
format!(
"{}:{}:{}",
required_domain.as_str(),
required_action.as_str(),
kind
)
} else {
format!("{}:{}", required_domain.as_str(), required_action.as_str())
};
Err(Error::NotAuthorized(format!(
"Access denied. Required scope: {}",
scope_display
)))
}
const SCRIPT_JOBS: [&'static str; 6] = [
"jobs/run/p",
"jobs/run/h",
"jobs/run_wait_result/p",
"jobs/run_wait_result/h",
"jobs/run/preview_bundle",
"jobs/run/preview",
];
const FLOW_JOBS: [&'static str; 5] = [
"jobs/run/f",
"jobs/run_wait_result/f",
"jobs/run/preview_flow",
"jobs/restart/f",
"jobs/flow/resume",
];
lazy_static::lazy_static! {
static ref RUN_PATH_ACTIONS: Vec<&'static str> = {
let mut v = vec!["jobs/resume/", "jobs/run/batch_rerun_jobs", "jobs/run/workflow_as_code", "jobs/run/dependencies","jobs/run/flow_dependencies",];
v.extend(SCRIPT_JOBS);
v.extend(FLOW_JOBS);
v
};
}
fn map_http_method_to_action(method: &str, route_path: &str) -> ScopeAction {
if RUN_PATH_ACTIONS
.iter()
.any(|run_path| route_path.contains(run_path))
{
return ScopeAction::Run;
}
match method.to_uppercase().as_str() {
"GET" | "HEAD" | "OPTIONS" => ScopeAction::Read,
"POST" | "PUT" | "PATCH" | "DELETE" => ScopeAction::Write,
_ => ScopeAction::Read,
}
}
/// Checks the route path to determine the runnable kind (either "flows" or "scripts").
///
/// The order of checks is important:
/// - Flow-related paths are checked first to avoid false positives, as some flow paths
/// (e.g., `/run_preview_flow`) share prefixes with script paths (e.g., `/run_preview`).
///
/// Returns `"flows"` or `"scripts"` based on the match, or `None` if no match is found.
fn determine_kind_from_route(route_path: &str) -> Option<String> {
if route_path.starts_with("jobs") {
if FLOW_JOBS.iter().any(|path| route_path.starts_with(path)) {
return Some("flows".to_string());
} else if SCRIPT_JOBS.iter().any(|path| route_path.starts_with(path)) {
return Some("scripts".to_string());
}
}
None
}
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, 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);
(domain, kind, Some(route_suffix.to_owned()))
} else if parts.len() >= 3 && parts[1] == "api" {
(
ScopeDomain::from_str(parts[2]),
None,
Some(parts[2..].join("/")),
)
} else {
(None, None, None)
};
if let Some(domain) = domain {
return Ok((domain, kind, route_suffix));
}
Err(Error::BadRequest(format!(
"Could not extract domain from route: {}",
route_path
)))
}
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)
.ok_or_else(|| Error::BadRequest(format!("Invalid scope domain: {}", scope.domain)))?;
if scope_domain != required_domain {
return Ok(false);
}
// Check action match (with hierarchical permissions)
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)
&& !(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 scope_domain == ScopeDomain::Jobs && required_action == ScopeAction::Run {
match (&scope.kind, required_kind) {
(Some(scope_kind), Some(req_kind)) => {
if scope_kind != req_kind {
return Ok(false);
}
}
(None, _) => {}
(Some(_), None) => {
return Ok(false);
}
}
}
// No resource specified means access to entire domain
Ok(true)
}
/// Helper function to check if scopes allow access to a route
pub fn check_scopes_for_route(
token_scopes: Option<&[String]>,
route_path: &str,
http_method: &str,
) -> Result<()> {
// If no scopes defined, allow access (backward compatibility)
let scopes = match token_scopes {
Some(s) if !s.is_empty() => s,
_ => return Ok(()),
};
check_route_access(scopes, route_path, http_method)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scope_definition_parsing() {
let scope = ScopeDefinition::from_scope_string("jobs:read").unwrap();
assert_eq!(scope.domain, "jobs");
assert_eq!(scope.action, "read");
assert_eq!(scope.kind, None);
assert_eq!(scope.resource, None);
let scope = ScopeDefinition::from_scope_string("jobs:run:scripts:f/folder/*").unwrap();
assert_eq!(scope.domain, "jobs");
assert_eq!(scope.action, "run");
assert_eq!(scope.kind, Some("scripts".to_string()));
assert_eq!(scope.resource, Some(vec!["f/folder/*".to_string()]));
// Test jobs:run:kind parsing
let scope = ScopeDefinition::from_scope_string("jobs:run:scripts").unwrap();
assert_eq!(scope.domain, "jobs");
assert_eq!(scope.action, "run");
assert_eq!(scope.kind, Some("scripts".to_string()));
assert_eq!(scope.resource, None);
// Test jobs:run:kind:resource parsing
let scope = ScopeDefinition::from_scope_string("jobs:run:flows:f/folder/*").unwrap();
assert_eq!(scope.domain, "jobs");
assert_eq!(scope.action, "run");
assert_eq!(scope.kind, Some("flows".to_string()));
assert_eq!(scope.resource, Some(vec!["f/folder/*".to_string()]));
// Test comma-separated resources parsing
let scope =
ScopeDefinition::from_scope_string("scripts:read:path1,path2,f/folder/*").unwrap();
assert_eq!(scope.domain, "scripts");
assert_eq!(scope.action, "read");
assert_eq!(scope.kind, None);
assert_eq!(
scope.resource,
Some(vec![
"path1".to_string(),
"path2".to_string(),
"f/folder/*".to_string()
])
);
}
#[test]
fn test_scope_action_hierarchy() {
assert!(ScopeAction::Write.includes(&ScopeAction::Read));
assert!(!ScopeAction::Read.includes(&ScopeAction::Write));
assert!(!ScopeAction::Run.includes(&ScopeAction::Read));
assert!(!ScopeAction::Run.includes(&ScopeAction::Write));
}
#[test]
fn test_route_domain_extraction() {
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, 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]
fn test_specific_scope_access() {
let scopes = vec!["jobs:read".to_string()];
assert!(check_route_access(&scopes, "/api/w/test_workspace/jobs/123", "GET").is_ok());
// DELETE now requires write permission, so it should still fail with read-only scope
assert!(check_route_access(&scopes, "/api/w/test_workspace/jobs/123", "DELETE").is_err());
}
#[test]
fn test_new_domain_parsing() {
// Test that new domains are properly parsed
assert_eq!(ScopeDomain::from_str("acls"), Some(ScopeDomain::Acls));
assert_eq!(
ScopeDomain::from_str("raw_apps"),
Some(ScopeDomain::RawApps)
);
assert_eq!(
ScopeDomain::from_str("agent_workers"),
Some(ScopeDomain::AgentWorkers)
);
// Test that string conversion works both ways
assert_eq!(ScopeDomain::Acls.as_str(), "acls");
assert_eq!(ScopeDomain::RawApps.as_str(), "raw_apps");
assert_eq!(ScopeDomain::AgentWorkers.as_str(), "agent_workers");
}
#[test]
fn test_resource_array_matching() {
// Test wildcard access
let scope_all = ScopeDefinition::new("scripts", "read", None, Some(vec!["*".to_string()]));
let required = ScopeDefinition::new(
"scripts",
"read",
None,
Some(vec!["path1".to_string(), "path2".to_string()]),
);
assert!(scope_all.includes(&required));
// Test exact matches
let scope_exact = ScopeDefinition::new(
"scripts",
"read",
None,
Some(vec!["path1".to_string(), "path2".to_string()]),
);
let required_subset =
ScopeDefinition::new("scripts", "read", None, Some(vec!["path1".to_string()]));
assert!(scope_exact.includes(&required_subset));
// Test partial match - should grant access if ANY required resource matches
let scope_limited =
ScopeDefinition::new("scripts", "read", None, Some(vec!["path1".to_string()]));
let required_partial = ScopeDefinition::new(
"scripts",
"read",
None,
Some(vec!["path1".to_string(), "path2".to_string()]),
);
assert!(scope_limited.includes(&required_partial)); // path1 matches, so access granted
// Test no match - scope doesn't cover any of the required resources
let scope_different =
ScopeDefinition::new("scripts", "read", None, Some(vec!["path3".to_string()]));
let required_no_match = ScopeDefinition::new(
"scripts",
"read",
None,
Some(vec!["path1".to_string(), "path2".to_string()]),
);
assert!(!scope_different.includes(&required_no_match));
// Test pattern matching
let scope_pattern = ScopeDefinition::new(
"scripts",
"read",
None,
Some(vec!["f/folder/*".to_string()]),
);
let required_in_folder = ScopeDefinition::new(
"scripts",
"read",
None,
Some(vec!["f/folder/script1".to_string()]),
);
assert!(scope_pattern.includes(&required_in_folder));
// Test mixed patterns and exact matches
let scope_mixed = ScopeDefinition::new(
"scripts",
"read",
None,
Some(vec!["exact_path".to_string(), "f/folder/*".to_string()]),
);
let required_mixed1 = ScopeDefinition::new(
"scripts",
"read",
None,
Some(vec!["exact_path".to_string()]),
);
let required_mixed2 = ScopeDefinition::new(
"scripts",
"read",
None,
Some(vec!["f/folder/script2".to_string()]),
);
assert!(scope_mixed.includes(&required_mixed1));
assert!(scope_mixed.includes(&required_mixed2));
}
#[test]
fn test_efficiency_small_vs_large_arrays() {
// Test small array optimization path
let scope_small = ScopeDefinition::new(
"scripts",
"read",
None,
Some(vec!["path1".to_string(), "path2".to_string()]),
);
let required_small =
ScopeDefinition::new("scripts", "read", None, Some(vec!["path1".to_string()]));
assert!(scope_small.includes(&required_small));
// Test large array optimization path
let large_scope_vec: Vec<String> = (0..10).map(|i| format!("path{}", i)).collect();
let scope_large = ScopeDefinition::new("scripts", "read", None, Some(large_scope_vec));
let required_large =
ScopeDefinition::new("scripts", "read", None, Some(vec!["path5".to_string()]));
assert!(scope_large.includes(&required_large));
}
#[test]
fn test_user_example_case() {
// User's example: scope has "u/dieri/*", required has ["u/dadad/wqdq", "u/*"]
// Should grant access because scope "u/dieri/*" falls under required pattern "u/*"
let user_scope =
ScopeDefinition::new("scripts", "read", None, Some(vec!["u/dieri/*".to_string()]));
let required_mixed = ScopeDefinition::new(
"scripts",
"read",
None,
Some(vec!["u/dadad/wqdq".to_string(), "u/*".to_string()]),
);
assert!(user_scope.includes(&required_mixed)); // Should match because u/dieri/* falls under u/*
// Another example: scope covers one but not both paths
let scope_specific = ScopeDefinition::new(
"scripts",
"read",
None,
Some(vec!["folder/file1".to_string()]),
);
let required_multi = ScopeDefinition::new(
"scripts",
"read",
None,
Some(vec!["folder/file1".to_string(), "other/file2".to_string()]),
);
assert!(scope_specific.includes(&required_multi)); // Should match because folder/file1 matches exactly
// Test bidirectional pattern matching more explicitly
let scope_broad =
ScopeDefinition::new("scripts", "read", None, Some(vec!["u/*".to_string()]));
let required_specific = ScopeDefinition::new(
"scripts",
"read",
None,
Some(vec!["u/dieri/script.py".to_string()]),
);
assert!(scope_broad.includes(&required_specific)); // u/* covers u/dieri/script.py
let scope_specific_path = ScopeDefinition::new(
"scripts",
"read",
None,
Some(vec!["u/dieri/script.py".to_string()]),
);
let required_broad =
ScopeDefinition::new("scripts", "read", None, Some(vec!["u/*".to_string()]));
assert!(scope_specific_path.includes(&required_broad)); // u/dieri/script.py satisfies u/*
}
}

View File

@@ -14,7 +14,7 @@ use crate::{
get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail,
},
users::{maybe_refresh_folders, require_owner_of_path},
utils::WithStarredInfoQuery,
utils::{check_scopes, WithStarredInfoQuery},
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
@@ -43,7 +43,7 @@ use windmill_audit::ActionKind;
use windmill_worker::process_relative_imports;
use windmill_common::{
assets::{clear_asset_usage, insert_asset_usage, parse_assets, AssetUsageKind},
assets::{clear_asset_usage, insert_asset_usage, AssetUsageKind, AssetWithAltAccessType},
error::to_anyhow,
worker::CLOUD_HOSTED,
};
@@ -116,6 +116,9 @@ pub struct ScriptWDraft {
pub has_preprocessor: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub on_behalf_of_email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[sqlx(json(nullable))]
pub assets: Option<Vec<AssetWithAltAccessType>>,
}
pub fn global_service() -> Router {
@@ -516,6 +519,8 @@ async fn create_script_internal<'c>(
user_db: UserDB,
webhook: WebhookShared,
) -> Result<(ScriptHash, Transaction<'c, Postgres>)> {
check_scopes(&authed, || format!("scripts:write:{}", ns.path))?;
let codebase = ns.codebase.as_ref();
#[cfg(not(feature = "enterprise"))]
if ns.ws_error_handler_muted.is_some_and(|val| val) {
@@ -769,8 +774,8 @@ async fn create_script_internal<'c>(
content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, \
draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, \
dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, \
delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33)",
delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34)",
&w_id,
&hash.0,
ns.path,
@@ -808,6 +813,7 @@ async fn create_script_internal<'c>(
None
},
validate_schema,
ns.assets.as_ref().and_then(|a| serde_json::to_value(a).ok())
)
.execute(&mut *tx)
.await?;
@@ -922,19 +928,18 @@ async fn create_script_internal<'c>(
}
clear_asset_usage(&mut *tx, &w_id, &script_path, AssetUsageKind::Script).await?;
for asset in parse_assets(&ns.content, ns.language)?.iter().flatten() {
insert_asset_usage(
&mut *tx,
&w_id,
asset,
ns.fallback_access_types.as_ref().map(Vec::as_slice),
&ns.path,
AssetUsageKind::Script,
)
.await?;
for asset in ns.assets.as_ref().into_iter().flatten() {
insert_asset_usage(&mut *tx, &w_id, &asset, &ns.path, AssetUsageKind::Script).await?;
}
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,))
@@ -1062,6 +1067,7 @@ async fn get_script_by_path(
Query(query): Query<WithStarredInfoQuery>,
) -> JsonResult<ScriptWithStarred> {
let path = path.to_path();
check_scopes(&authed, || format!("scripts:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let script_o = if query.with_starred_info.unwrap_or(false) {
@@ -1119,10 +1125,11 @@ async fn get_script_by_path_w_draft(
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<ScriptWDraft> {
let path = path.to_path();
check_scopes(&authed, || format!("scripts:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let script_o = sqlx::query_as::<_, ScriptWDraft>(
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, has_preprocessor, on_behalf_of_email FROM script LEFT JOIN draft ON
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, has_preprocessor, on_behalf_of_email, assets FROM script LEFT JOIN draft ON
script.path = draft.path AND script.workspace_id = draft.workspace_id AND draft.typ = 'script'
WHERE script.path = $1 AND script.workspace_id = $2
ORDER BY script.created_at DESC LIMIT 1",
@@ -1142,6 +1149,8 @@ async fn get_script_history(
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Vec<ScriptHistory>> {
let path = path.to_path();
check_scopes(&authed, || format!("scripts:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let query_result = sqlx::query!(
"SELECT s.hash as hash, dm.deployment_msg as deployment_msg
@@ -1149,7 +1158,7 @@ async fn get_script_history(
WHERE s.workspace_id = $1 AND s.path = $2
ORDER by s.created_at DESC",
w_id,
path.to_path(),
path,
)
.fetch_all(&mut *tx)
.await?;
@@ -1170,6 +1179,8 @@ async fn get_latest_version(
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Option<ScriptHistory>> {
let path = path.to_path();
check_scopes(&authed, || format!("scripts:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let row_o = sqlx::query!(
"SELECT s.hash as hash, dm.deployment_msg as deployment_msg
@@ -1177,7 +1188,7 @@ async fn get_latest_version(
WHERE s.workspace_id = $1 AND s.path = $2
ORDER by s.created_at DESC LIMIT 1",
w_id,
path.to_path(),
path,
)
.fetch_optional(&mut *tx)
.await?;
@@ -1200,11 +1211,14 @@ async fn update_script_history(
Path((w_id, script_hash, script_path)): Path<(String, ScriptHash, StripPath)>,
Json(script_history_update): Json<ScriptHistoryUpdate>,
) -> Result<()> {
let script_path = script_path.to_path();
check_scopes(&authed, || format!("scripts:write:{}", script_path))?;
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"INSERT INTO deployment_metadata (workspace_id, path, script_hash, deployment_msg) VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, script_hash) WHERE script_hash IS NOT NULL DO UPDATE SET deployment_msg = $4",
w_id,
script_path.to_path(),
script_path,
script_hash.0,
script_history_update.deployment_msg,
)
@@ -1353,6 +1367,7 @@ async fn raw_script_by_path_internal(
unpin: bool,
) -> Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("scripts:read:{}", path))?;
if !path.ends_with(".py")
&& !path.ends_with(".ts")
&& !path.ends_with(".go")
@@ -1482,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,
@@ -1502,6 +1527,9 @@ async fn get_script_by_hash(
}),
)
.await?;
check_scopes(&authed, || format!("scripts:read:{}", &r.script.path))?;
tx.commit().await?;
Ok(Json(r))
@@ -1566,6 +1594,7 @@ async fn archive_script_by_path(
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<()> {
let path = path.to_path();
check_scopes(&authed, || format!("scripts:write:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
require_owner_of_path(&authed, path)?;
@@ -1638,6 +1667,7 @@ async fn archive_script_by_hash(
.await
.map_err(|e| Error::internal_err(format!("archiving script in {w_id}: {e:#}")))?;
check_scopes(&authed, || format!("scripts:write:{}", &script.path))?;
sqlx::query!(
"DELETE FROM asset WHERE workspace_id = $1 AND usage_kind = 'script' AND usage_path = (SELECT path FROM script WHERE hash = $2 AND workspace_id = $1)",
&w_id,
@@ -1686,6 +1716,7 @@ async fn delete_script_by_hash(
.await
.map_err(|e| Error::internal_err(format!("deleting script by hash {w_id}: {e:#}")))?;
check_scopes(&authed, || format!("scripts:write:{}", &script.path))?;
sqlx::query!(
"DELETE FROM asset WHERE workspace_id = $1 AND usage_kind = 'script' AND usage_path = (SELECT path FROM script WHERE hash = $2 AND workspace_id = $1)",
&w_id,
@@ -1728,6 +1759,7 @@ async fn delete_script_by_path(
Query(query): Query<DeleteScriptQuery>,
) -> JsonResult<String> {
let path = path.to_path();
check_scopes(&authed, || format!("scripts:write:{}", path))?;
if path == "u/admin/hub_sync" && w_id == "admins" {
return Err(Error::BadRequest(

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