Compare commits

..

140 Commits

Author SHA1 Message Date
Alexander Petric
af4e9e6a22 fix: batch job runner empty job kind 2025-10-07 23:06:26 +00:00
Ruben Fiszel
50a6106436 fix: fix runnable inputs not being retriggered on change in some rare cases 2025-10-07 15:15:49 +00:00
Diego Imbert
6806f2193d Fix Ctrl C in app right panel Copying component (#6766) 2025-10-07 12:35:54 +00:00
Diego Imbert
7d5196170c Remove click-to-insert prop feature (#6765)
* Remove click-to-insert prop feature

* CI
2025-10-07 12:03:30 +00:00
centdix
7b9e2c2d68 internal: fix flake and cli dev usage (#6761)
* fix flake

* fix cli build

* fix deno_ffi
2025-10-07 08:07:47 +00:00
Diego Imbert
258b275f9b fix: better ducklake setup (#6763)
* stash

* Much better ducklake setup UX

* nits

* mistake

* sqlx prepare
2025-10-07 07:00:00 +00:00
Ruben Fiszel
f2070e9d9c chore(main): release 1.555.2 (#6758)
* chore(main): release 1.555.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-10-06 15:20:47 +00:00
centdix
820437c897 chore(flow): clean flow conversation code (#6755)
* use own folder for memory

* fixes

* better chat interface

* fix export tab

* move in folder

* dont show flow graph if chat mode

* fix

* fix too long title

* fix user message

* fix

* fix

* remove from server

* cleaner

* cleaning

* cleaning

* cleaning
2025-10-06 15:10:29 +00:00
Diego Imbert
686cf8dd69 Timestamp input in db manager (#6754)
* nullable by default

* unrelated nits

* native timezone input postgres

* Fix timestamptz NaiveDateTime conversion is pg_executor

* remove consolelog
2025-10-06 15:10:17 +00:00
Ruben Fiszel
ac27cdc16e update monaco (#6759) 2025-10-06 13:56:02 +00:00
Diego Imbert
9f7250c9f0 Nit mistake in expression helper text (#6756)
* nit helper mistake

* nit border color

* fill inputs styling nits

* nit bg color
2025-10-06 13:36:48 +00:00
hugocasa
7c757b68f6 fix(backend): use correct ai tool job dir (#6757) 2025-10-06 13:35:59 +00:00
Ruben Fiszel
a12d124871 chore(main): release 1.555.1 (#6752)
* chore(main): release 1.555.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-10-04 17:11:37 +00:00
Ruben Fiszel
abb813c019 nit publish 2025-10-04 17:00:28 +00:00
Ruben Fiszel
75ceba81d9 nits disableAi 2025-10-04 16:54:59 +00:00
Ruben Fiszel
4e171d0aa6 flow inputs nits 2025-10-04 15:31:14 +00:00
Ruben Fiszel
46c49072de improve insert module button menu caching 2025-10-04 15:21:20 +00:00
Ruben Fiszel
9ddde9aa94 split store and aiStore 2025-10-04 09:59:10 +00:00
Ruben Fiszel
1df18a912c use smaller ui builder 2025-10-04 00:04:32 +00:00
Alexander Petric
d9c01e0c03 fix: init git repo preview save (#6753) 2025-10-03 21:48:11 +00:00
Alexander Petric
605c5526f8 fix: app button tooltip also when disabled + audit log filters (#6751)
* also show tooltip on disabled button

* fix: audit log filters
2025-10-03 20:12:05 +00:00
Ruben Fiszel
116c420326 chore(main): release 1.555.0 (#6744)
* chore(main): release 1.555.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-10-03 17:34:09 +00:00
Ruben Fiszel
0bc822dd10 warn for slow queries in push scheduled job 2025-10-03 17:30:40 +00:00
hugocasa
3907c9f951 feat: end user email env var (#6750)
* feat: end user email env var

* nit

* nits
2025-10-03 17:27:58 +00:00
Ruben Fiszel
3c7a12da57 raw app storage on db + s3 (#6749) 2025-10-03 16:30:06 +00:00
Diego Imbert
7c39aa6f6f Fix CI failing due to missing feature flag (#6747) 2025-10-03 16:26:01 +00:00
Alexander Petric
c658f321d6 fix: show that user is disabled in workspacelist (#6748)
* fix: show that user is disabled in workspacelist

* Update SQLx metadata

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2025-10-03 16:25:48 +00:00
centdix
1913979012 fix build (#6746) 2025-10-03 16:13:28 +00:00
Diego Imbert
06b152b295 fix: top level assigment doesn't propagate to setContext (#6745) 2025-10-03 14:53:10 +00:00
centdix
047420e5ad feat(flow): add option to turn flow into chat (#6658)
* add toggle option + chat interface

* backend impl

* draft

* put info in schema

* Revert "backend impl"

This reverts commit c534eeb49986424e2c12e2c5642be4e17ba380d1.

* chat interface in flow input

* cleaning

* add logic for running flow + styling

* handle historic args

* fix frontend changes

* add tables

* add conv list

* add endpoints

* adapt frontend

* list message logic

* save message in db

* save response in db

* cleaning

* better migrations

* refresh on new conv

* better logic for messages

* nit

* genere conversation uuid from frontend

* store chat mode info in flow status

* better ui for chat

* collapse chat

* ui

* infinite scroll on convs

* infinite scroll on messages

* fix ui

* new chat entry on new

* cleaning

* change setting logic

* fix test logic from flow input

* move toggle to input

* add warning modal when enabling chat mode

* add summary and explanation on inline script

* add hint for chat mode on user_message desc

* show chat message instead of input in graph

* add warning for triggers

* one logo when not expanded

* use infinitelist for conversations

* add warning when deployment in progress

* full width button

* better icon for menu

* better input + nits

* put toggle in action

* use waitjob

* cleaning

* cleaning

* scroll on new + cleaning

* use enum

* fix logic

* full screen

* cleaning

* exit on updatesqlx error

* Update SQLx metadata

* fix

* cleaning

* add for wait result endpoint

* add missing drop

* delete cascade

* fix: use macro version of query_as in flow_conversations.rs

Use sqlx::query_as! macro instead of query_as function for compile-time
SQL validation and better type safety

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

* fix: update comment to clarify conversation message update condition

The comment now accurately reflects that the update happens when
it's a flow and it's done (flow_is_done)

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

* fix: only parse chat_input_enabled if conditions are met

Move the parse_chat_input_enabled() call inside the condition check
to avoid unnecessary parsing when the flow is not done or unsuccessful

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

* fix: use the same transaction for conversation creation

Pass transaction to get_or_create_conversation_with_id instead of
creating a new one, ensuring all operations are atomic

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

* fix: remove update trigger and handle updated_at in application code

Remove the database trigger that automatically updates conversation
timestamp and instead update it explicitly when creating messages.
This gives better control and consistency.

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

* Update SQLx metadata

* cleaning

* feat(aiagent): handle memory (#6719)

* implement memory

* s3 logic for memory

* fix typo

* much cleaner

* cleaning

* cleaning

* only if chat

* display nit

* nit

* fix stack overflow

* cleaning

* use len arg from input

* cleaning

* change order

* delete memory when conv deleted

* cleaning

* nit

* show description in expr mode

* opti

* opti

* updatee ref

* store string as simple string

* use markdown

* do not wait for deletion

* add delete loading

* fix logic

* fix markdown

* Update ee-repo-ref.txt

* Update SQLx metadata

* fix in test interface

* nit

* nit

* fix layout

* use memory_id to store memory

* shorter description

* rls + grant

* fix text overflow

* extract output from res

* cleaning

* handle streaming

* cleaning

* fix tool error

* nit

* update ref

* fix

* Update SQLx metadata

* nit

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
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-10-03 14:04:21 +00:00
hugocasa
6d436d7459 fix(backend): prevent s3 file upload infinite loop (#6742)
* fix(backend): prevent s3 file upload infinite loop

* update ref
2025-10-03 14:01:43 +00:00
Diego Imbert
0b5a2425ae improve dark mode readability monaco (#6741)
* improve dark mode readability monaco

* revert color transition
2025-10-03 11:07:22 +00:00
Diego Imbert
f35c9ffe63 S3 Proxy better errors + DuckDB S3 fix (#6740)
* Support for MinIO and other S3 impls in S3 Proxy

* Nice S3 Proxy error messages in DuckDB executor

* nit

* useless code

* super nit

* ee repo ref
2025-10-03 11:06:46 +00:00
Ruben Fiszel
6aaae00d54 chore(main): release 1.554.1 (#6738)
* chore(main): release 1.554.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-10-02 21:15:51 +00:00
hugocasa
cdb7524118 fix(backend): concurrency limits preprocessor (#6727)
* fix(backend): concurrency limits preprocessor

* refactor

* fix concurrency on agent workers

* nit

* nit

* update ref

* fix ci

* chore: update ee ref
2025-10-02 20:31:31 +00:00
Diego Imbert
9f40ce2da9 SelectDropdown reverse animation + ui nits (#6733)
* text-secondary nit

* Select Dropdown reverse animation

* nit misalignment
2025-10-02 20:31:05 +00:00
Diego Imbert
7cd5f26f70 Clarify Ducklake manual setup instructions (#6737) 2025-10-02 20:30:52 +00:00
Diego Imbert
f545b1d572 Fix ducklake instance DB + Add manual instructions (#6736)
* same auth method than worker for tokio_postgres

* Add manual setup instructions for Ducklake

* clarify instruction
2025-10-02 16:15:22 +00:00
Diego Imbert
49f5023fa5 exclude postgres BIGSERIAL / auto generated columns (#6734) 2025-10-02 15:27:13 +00:00
Diego Imbert
a00991a293 Fix save btn disabled on deletion (#6735) 2025-10-02 14:26:12 +00:00
centdix
a0bc0ee318 fix path assigner for nested calls (#6732) 2025-10-02 10:56:37 +00:00
Diego Imbert
36f2ab4715 UI nits (#6731)
* Fix JSON editor resource styling

* fix Edit resource type Object json editor

* oneOfSelected not auto selecting
2025-10-02 10:11:41 +00:00
Diego Imbert
59cdb141c3 NULL Toggle in InsertRow drawer (#6729)
* NULL toggle in InsertRow

* fix long type parsing in postgres

* nits

* graphite catch

* lazy_static

* support for time/timestamp/tz long forms in pg parser

* graphite suggestion
2025-10-02 10:10:53 +00:00
Diego Imbert
ae45a50eb2 Fix app tutorials (#6728)
* Fix tutorial basic

* fix other tutorials

* nit fix bug with button shrinking

* tutorial works backwards

* nit delete field on prev

* remove empty app duplication and magic code

* fix norefreshbar auto binding to false, making app dirty

* fix and improve app tutorial

* fix background runnable tutorial scroll

* fix connection tutorial

* mistake

* isCurrentlyInTutorial global state

* disable component navigation when in tutorial

* ci
2025-10-02 10:09:30 +00:00
Diego Imbert
12a2b58704 misalignment fix (#6726) 2025-10-01 16:16:15 +00:00
Ruben Fiszel
45be618c25 chore(main): release 1.554.0 (#6720)
* chore(main): release 1.554.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-10-01 16:05:35 +00:00
claude[bot]
c7f6ff14af correct typos in user-facing text (#6725)
- Fixed 'occured' -> 'occurred' (10 instances)
- Fixed 'commited' -> 'committed' (2 instances)

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2025-10-01 16:04:15 +00:00
Diego Imbert
8eb6c13c19 Fix flow tutorials (#6722)
* Fix tutorial basic

* fix other tutorials

* nit fix bug with button shrinking

* tutorial works backwards

* nit delete field on prev
2025-10-01 16:00:46 +00:00
Ruben Fiszel
e2e3ae9f02 fix: fix job duration unwrap crash 2025-10-01 15:35:09 +00:00
Alexander Petric
9e6cebac55 feat(cli): allow skipping branch validation (#6721) 2025-10-01 15:04:06 +00:00
Ruben Fiszel
ee0ffa47e2 improve json inputs rendering 2025-10-01 13:46:12 +00:00
Ruben Fiszel
74a75431c0 fix: allow running scripts in json view 2025-10-01 13:37:51 +00:00
Diego Imbert
67325abcbb fix slight offset on hover togglebutton (#6717) 2025-10-01 09:57:36 +00:00
Ruben Fiszel
ee00964a06 nit 2025-10-01 09:57:24 +00:00
Ruben Fiszel
5689411199 nit exit status 2025-10-01 09:18:43 +00:00
Ruben Fiszel
3306ee1718 chore(main): release 1.553.0 (#6711)
* chore(main): release 1.553.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-09-30 21:18:09 +00:00
Ruben Fiszel
be63a69649 nit 2025-09-30 21:11:08 +00:00
hugocasa
526dfd7237 feat(backend): allow multiple workspaces in jwt (#6714)
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-09-30 20:56:52 +00:00
Alexander Petric
e92a99a101 don't include drafts in workspace exports tarball (#6710)
* don't include drafts in workspace exports tarball

* sqlx

* ee repo ref

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-09-30 19:32:34 +00:00
Ruben Fiszel
8be898e5a2 nit 2025-09-30 19:32:00 +00:00
Ruben Fiszel
d92d6c8f84 fix toggle design 2025-09-30 19:10:12 +00:00
Ruben Fiszel
d382ea7c8b feat: support esm mode for codebase bundles (#6709)
* rawAppsS3

* make fn common

* all

* merge

* nit

* fix ingress

* all

* all

* all

* all
2025-09-30 14:45:30 +00:00
Ruben Fiszel
0ba5e3e9c7 nits 2025-09-30 12:25:36 +00:00
hugocasa
898eb6231b feat(backend): array and object params support in pwsh (#6706)
* feat(backend): array and object params support in pwsh

* add bool and improve arg conversion

* fix bash

* update parser

* sqlx
2025-09-30 12:25:12 +00:00
Ruben Fiszel
5aeb3fa0b7 fix: multiselect + jsoneditor nits 2025-09-30 08:48:28 +00:00
Ruben Fiszel
53fd4ccb82 chore(main): release 1.552.1 (#6705)
* chore(main): release 1.552.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-09-29 17:57:52 +00:00
hugocasa
ce653f8a05 fix(frontend): allow dates before 2000 in date picker (#6707)
* fix(frontend): allow dates before 2000 in date picker

* meh
2025-09-29 17:36:24 +00:00
Ruben Fiszel
2055e536a7 fix: fix c# with nsjail 2025-09-29 16:34:29 +00:00
Ruben Fiszel
727314cd44 chore(main): release 1.552.0 (#6700)
* chore(main): release 1.552.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-09-29 13:36:08 +00:00
Ruben Fiszel
7da0548b30 sqlx 2025-09-29 13:31:40 +00:00
Ruben Fiszel
e2608f9aac fix: external links in critical alert 2025-09-29 13:29:43 +00:00
hugocasa
4bbbeb956f feat: powershell private repo support (#6684)
* feat: powershell private repo support

* nits
2025-09-29 13:24:41 +00:00
Ruben Fiszel
d56dea4969 fix: show more autoscaling events (#6704)
* all

* all
2025-09-29 13:24:10 +00:00
Ruben Fiszel
74e9cffe58 min-w-nits 2025-09-29 13:12:30 +00:00
Ruben Fiszel
481c877537 fix: fix app schema form rendering 2025-09-29 13:10:07 +00:00
centdix
2b7ad0e2ef use base internal url (#6703) 2025-09-29 12:42:02 +00:00
Pyra
5f63ce6dd8 fix(uv): log stdout on uv pip install error (#6702)
Signed-off-by: pyranota <pyra@duck.com>
2025-09-29 12:12:06 +00:00
hugocasa
c751a5d6aa fix(frontend): prevent label interference with monaco editor in instance settings (#6701) 2025-09-29 12:04:52 +00:00
centdix
cc2afdb264 fix(mcp): filter out tools with long names (#6692)
* filter out tools with too long names

* do not advertise tool change ability

* add comment

* use id for names

* Revert "use id for names"

This reverts commit 40958cd861.

* use trunc suffix

* cleaning
2025-09-29 10:42:02 +00:00
Ruben Fiszel
9dad8e7e10 chore(main): release 1.551.4 (#6699)
* chore(main): release 1.551.4

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-09-29 09:57:50 +00:00
Ruben Fiszel
cfec8e99fb fix: migrate dotnet from msft images to script install 2025-09-29 09:44:04 +00:00
Ruben Fiszel
8266fb84b0 chore(main): release 1.551.3 (#6698)
* chore(main): release 1.551.3

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-09-29 09:02:42 +00:00
Ruben Fiszel
5ae525a9f1 fix: migrate dotnet from bitnami to microsoft images 2025-09-29 08:55:45 +00:00
Ruben Fiszel
c066ab56fa chore(main): release 1.551.2 (#6697)
* chore(main): release 1.551.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-09-29 07:38:34 +00:00
Osman Mesut OZCAN
629a378320 Fix typo (#6696) 2025-09-29 07:38:15 +00:00
Ruben Fiszel
81616cbe1e fix: fix copy first step input 2025-09-29 07:34:38 +00:00
Ruben Fiszel
4bc0283994 chore(main): release 1.551.1 (#6691)
* chore(main): release 1.551.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-09-28 20:05:31 +00:00
Ruben Fiszel
5d2a79e12b improve tests 2025-09-28 19:47:02 +00:00
Ruben Fiszel
d59b9f5216 improve tests 2025-09-28 19:40:26 +00:00
Ruben Fiszel
2047073c03 improve tests 2025-09-28 19:30:50 +00:00
Ruben Fiszel
477ec7773f improve tests 2025-09-28 19:28:38 +00:00
Ruben Fiszel
14780e49f2 nits 2025-09-28 11:00:15 +00:00
Ruben Fiszel
abf823e85b nits 2025-09-28 10:47:29 +00:00
Ruben Fiszel
a4138e037f nits 2025-09-28 10:46:38 +00:00
Ruben Fiszel
bdd36c0b4d fix: buttons are back to semi-bold 2025-09-28 10:20:46 +00:00
Ruben Fiszel
fe6199a867 nit check 2025-09-28 10:00:36 +00:00
Ruben Fiszel
b71e5c5adc move jobs ee in private 2025-09-28 09:39:06 +00:00
Ruben Fiszel
924891b644 chore(main): release 1.551.0 (#6690)
* chore(main): release 1.551.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-09-27 22:44:41 +00:00
Ruben Fiszel
b76cfa3881 nit UI 2025-09-27 22:37:40 +00:00
Ruben Fiszel
5b2ae8ea43 nit UI 2025-09-27 22:35:01 +00:00
Ruben Fiszel
cd90d0e1ef nit UI 2025-09-27 22:34:33 +00:00
Ruben Fiszel
452fb2b90a nit UI 2025-09-27 22:33:37 +00:00
Ruben Fiszel
86254d89e8 nit UI 2025-09-27 22:31:34 +00:00
Ruben Fiszel
6b907c8823 nit UI 2025-09-27 22:24:12 +00:00
Ruben Fiszel
70d1b99a02 nit UI 2025-09-27 22:21:13 +00:00
Ruben Fiszel
ef7af5a7b8 nit UI 2025-09-27 22:17:49 +00:00
Ruben Fiszel
439b6da720 nit UI 2025-09-27 22:05:46 +00:00
Diego Imbert
72b744c4e1 feat: UX improvements (all inputs)
* Monaco transparent bg

* started improving input transform form

* always show static/f selector

* fix connecting btn changing size

* pretty Fill Inputs button

* ResizeTransitionWrapper

* Prettier TemplateEditor

* prevent double onpointerdown when clicking button to close

* text hint

* force focus border for TemplateEditor

* styling in js mode

* update select style

* fix jittery fake monaco placeholder

* select nits

* aiproviderpicker + nits

* smaller ${...} badge

* no-default-style

* select dropdown slide

* nit

* Refresh button in flow picker quick

* jsonEditor pretty

* ai provider toggle button more

* change resource edit button pos

* ResourcePicker Add and Refresh btn

* fix scrollbar

* Fix FileInput and S3 Arg Input

* fix textarea styling

* nicer refresh button in Test This Step

* fix togglebutton border in darkmode

* rounded nit

* Fix multiselect styling

* Prevent crash when selecting dyn-multiselect

* missing $derived and $state => reactivity issue when switching between DynSelect and DynMultiselect

* forgot $effect.pre

* fix nested objects

* nits

* prettier json toggle and array inputs

* array input nits

* nit

* fix json toggle appearing in fileinputs

* nit

* started updating PropertyEditor

* (stash) fix select dropdown animation teleporting from bottom to top

* nit

* nit

* resize transition in schemaform

* nit

* nit typo

* nit enableFlyTransition

* shadow nit

* small consistency changes

* user setting nit

* resize transition in module preview form

* more space

* nit readability on hover

* DateTimeInput new style

* nit fix

* remove yPadding in template and simple editor

* nits

* Revert "remove yPadding in template and simple editor"

This reverts commit 8f27c8d0b8.

* nit

* Fix proppicker border

* fix inconsistent spacing btw arginput and input transform form field headers

* consistent add item button

* nit

* s3 settings nits

* RunsFilter fix

* gray ${...} badge

* border fix darkmode

* nit

* nit app editor consistency

* fix step input gen style

* nit fix

* nits

* toggle border

* nit toggle button

* nit font-medium

* nit font-medium

* nit font-medium

* nit font-medium

* nit

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-09-27 21:52:13 +00:00
Ruben Fiszel
d6d5fad83b nit customUI AIAgent 2025-09-27 12:01:01 +00:00
Ruben Fiszel
a9c114c4f8 chore(main): release 1.550.0 (#6685)
* chore(main): release 1.550.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-09-27 11:41:15 +00:00
Ruben Fiszel
ad32c940cb nit flow viewer right sidebar layout 2025-09-27 11:36:24 +00:00
Ruben Fiszel
15731e7726 nit 2025-09-27 11:23:54 +00:00
Ruben Fiszel
7df13b3e7b fix: restore set_progress feature with sse 2025-09-27 11:19:58 +00:00
Ruben Fiszel
9b13dd99ba nit perf 2025-09-27 10:15:38 +00:00
Ruben Fiszel
a1db857be4 nit perf 2025-09-27 10:10:27 +00:00
Ruben Fiszel
2a62f8ff8f nit 2025-09-27 09:50:58 +00:00
Ruben Fiszel
2421bdb839 nit 2025-09-27 09:45:31 +00:00
Ruben Fiszel
f28ed9a5f5 fix: limit auto data tables to tables of col < 100 2025-09-27 09:40:33 +00:00
Ruben Fiszel
311b410f2f fix: improve performance of flow viewer 2025-09-27 08:48:01 +00:00
Ruben Fiszel
7add57499c fix: improve graph rendering performances 2025-09-27 08:23:09 +00:00
Ruben Fiszel
dff499484b nit svelte 5 2025-09-27 08:04:17 +00:00
Ruben Fiszel
0ad5618aae nit dynamic enums 2025-09-27 08:02:26 +00:00
Ruben Fiszel
ec9e5a9acb fix: support label + value for dynamic enums of selects 2025-09-27 08:00:48 +00:00
Alexander Petric
ca4f9ee8c1 fix: scim group/users audit logs (#6682)
* scim group/users audit logs

* fixing tailwind class

* ee-repo-ref
2025-09-27 07:06:24 +00:00
dieriba
6ece0ac575 fix: improve dyn select as flow input
* fix lang

* remove print

* fix
2025-09-26 19:19:13 +00:00
Ruben Fiszel
6cc85d78d7 sqlx 2025-09-26 18:12:05 +00:00
Ruben Fiszel
87861301f2 fix: make schedule more resilient in case of pg clock shifts 2025-09-26 18:02:34 +00:00
Ruben Fiszel
341cdcf66e fix: improve behavior for already completed jobs when doing immediate cancels 2025-09-26 17:19:40 +00:00
hugocasa
f990107c45 feat: ai agent streaming (#6644)
* feat: ai agent step streaming

* refactor

* all

* nits

* fix other providers

* nits

* adapt to new streaming process
2025-09-26 17:09:34 +00:00
Ruben Fiszel
bce25bc283 chore(main): release 1.549.1 (#6681)
* chore(main): release 1.549.1

* Apply automatic changes

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <217088191+windmill-internal-app[bot]@users.noreply.github.com>
2025-09-26 15:32:45 +00:00
Ruben Fiszel
db4bc7ee69 fix: fix agent_workers completed job back-compatibility deserialization 2025-09-26 15:27:21 +00:00
Ruben Fiszel
02692111a1 fix: do not request unecessarily get_scheduled_for 2025-09-26 15:06:42 +00:00
Ruben Fiszel
aae0919535 chore(main): release 1.549.0 (#6678)
* chore(main): release 1.549.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-09-26 14:02:17 +00:00
Diego Imbert
4828926be6 Fix SEGFAULT with duckdb and jemalloc (#6680) 2025-09-26 13:57:28 +00:00
hugocasa
8f4fef9804 feat(backend): job result stream optimization (#6673)
* feat(backend): job result stream optimization

* get offset locally instead of from db

* fix: agent worker result stream

* update ref

* nit

* remove foreign key on job

* fix build
2025-09-26 11:26:50 +00:00
Alexander Petric
4205e83cfd fix: scim group handling when deleting instance user + conversion (#6677)
* fix: scim group handling when deleting instance user + conversion

* sqlx + compilation
2025-09-26 11:25:44 +00:00
Ruben Fiszel
78c0bea6b4 chore(main): release 1.548.3 (#6676)
* chore(main): release 1.548.3

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-09-25 10:36:22 +00:00
Ruben Fiszel
b47c21f299 nit 2025-09-25 10:30:44 +00:00
Ruben Fiszel
f5d238edcf fix: fix job loader token initialization 2025-09-25 10:29:08 +00:00
dieriba
a308782bcf fix: websocket runnable #6675 2025-09-24 19:47:52 +00:00
393 changed files with 12602 additions and 6953 deletions

View File

@@ -57,8 +57,12 @@ RUN apt-get update \
RUN rustup component add rustfmt
# C#
COPY --from=bitnami/dotnet-sdk:9.0.101-debian-12-r0 /opt/bitnami/dotnet-sdk /opt/dotnet-sdk
RUN ln -s /opt/dotnet-sdk/bin/dotnet /usr/bin/dotnet
RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \
&& chmod +x dotnet-install.sh \
&& ./dotnet-install.sh --channel 9.0 --install-dir /usr/share/dotnet \
&& ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet \
&& rm dotnet-install.sh
# Nushell
COPY --from=ghcr.io/nushell/nushell:0.101.0-bookworm /usr/bin/nu /usr/bin/nu

View File

@@ -51,7 +51,22 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.85.0
toolchain: 1.90.0
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./ee-repo-ref.txt)" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
repository: windmill-labs/windmill-ee-private
path: ./windmill-ee-private
ref: ${{ env.ee_repo_ref }}
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
- name: Substitute EE code (EE logic is behind feature flag)
run: |
./substitute_ee_code.sh --copy --dir ./windmill-ee-private
- name: cargo test
timeout-minutes: 16
run:
@@ -61,5 +76,5 @@ jobs:
DISABLE_EMBEDDING=true RUST_LOG=info
DENO_PATH=$(which deno) BUN_PATH=$(which bun) GO_PATH=$(which go)
UV_PATH=$(which uv) cargo test --features
enterprise,deno_core,license,python,rust,scoped_cache --all --
enterprise,deno_core,license,python,rust,scoped_cache,private --all --
--nocapture

View File

@@ -1,5 +1,177 @@
# Changelog
## [1.555.2](https://github.com/windmill-labs/windmill/compare/v1.555.1...v1.555.2) (2025-10-06)
### Bug Fixes
* **backend:** use correct ai tool job dir ([#6757](https://github.com/windmill-labs/windmill/issues/6757)) ([7c757b6](https://github.com/windmill-labs/windmill/commit/7c757b68f64bf3cf770cd080c5025f202d40201c))
## [1.555.1](https://github.com/windmill-labs/windmill/compare/v1.555.0...v1.555.1) (2025-10-04)
### Bug Fixes
* app button tooltip also when disabled + audit log filters ([#6751](https://github.com/windmill-labs/windmill/issues/6751)) ([605c552](https://github.com/windmill-labs/windmill/commit/605c5526f83d3b985f04570039bb4671cc5912f4))
* init git repo preview save ([#6753](https://github.com/windmill-labs/windmill/issues/6753)) ([d9c01e0](https://github.com/windmill-labs/windmill/commit/d9c01e0c0392f13e52ad92f87c4216e64e89a3bc))
## [1.555.0](https://github.com/windmill-labs/windmill/compare/v1.554.1...v1.555.0) (2025-10-03)
### Features
* end user email env var ([#6750](https://github.com/windmill-labs/windmill/issues/6750)) ([3907c9f](https://github.com/windmill-labs/windmill/commit/3907c9f9512ebd73daf0a2f3ee2e8db6fb9f4df6))
* **flow:** add option to turn flow into chat ([#6658](https://github.com/windmill-labs/windmill/issues/6658)) ([047420e](https://github.com/windmill-labs/windmill/commit/047420e5ad7b6178291bc7ed75d029794760d18b))
### Bug Fixes
* **backend:** prevent s3 file upload infinite loop ([#6742](https://github.com/windmill-labs/windmill/issues/6742)) ([6d436d7](https://github.com/windmill-labs/windmill/commit/6d436d745994f954a37f33ca2cc2e9f0801b16b9))
* show that user is disabled in workspacelist ([#6748](https://github.com/windmill-labs/windmill/issues/6748)) ([c658f32](https://github.com/windmill-labs/windmill/commit/c658f321d68e2d72622d9d167b20cac67364651c))
* top level assigment doesn't propagate to setContext ([#6745](https://github.com/windmill-labs/windmill/issues/6745)) ([06b152b](https://github.com/windmill-labs/windmill/commit/06b152b295cd4892d7309651d382a05cdcf7d378))
## [1.554.1](https://github.com/windmill-labs/windmill/compare/v1.554.0...v1.554.1) (2025-10-02)
### Bug Fixes
* **backend:** concurrency limits preprocessor ([#6727](https://github.com/windmill-labs/windmill/issues/6727)) ([cdb7524](https://github.com/windmill-labs/windmill/commit/cdb75241188ee0a6d7bc62ca6dd639606eef426a))
## [1.554.0](https://github.com/windmill-labs/windmill/compare/v1.553.0...v1.554.0) (2025-10-01)
### Features
* **cli:** allow skipping branch validation ([#6721](https://github.com/windmill-labs/windmill/issues/6721)) ([9e6ceba](https://github.com/windmill-labs/windmill/commit/9e6cebac557fd9a8530df54c227e915435fd2de5))
### Bug Fixes
* allow running scripts in json view ([74a7543](https://github.com/windmill-labs/windmill/commit/74a75431c072e9f084a43b8d8f195ca7757faf07))
* fix job duration unwrap crash ([e2e3ae9](https://github.com/windmill-labs/windmill/commit/e2e3ae9f0280d4369f77064ed85e5fde7e9d5a0d))
## [1.553.0](https://github.com/windmill-labs/windmill/compare/v1.552.1...v1.553.0) (2025-09-30)
### Features
* **backend:** allow multiple workspaces in jwt ([#6714](https://github.com/windmill-labs/windmill/issues/6714)) ([526dfd7](https://github.com/windmill-labs/windmill/commit/526dfd72377b90fb47b9c2c3924a1377e2715ae1))
* **backend:** array and object params support in pwsh ([#6706](https://github.com/windmill-labs/windmill/issues/6706)) ([898eb62](https://github.com/windmill-labs/windmill/commit/898eb6231beb5ca45d59da069f458f7828f427f0))
* support esm mode for codebase bundles ([#6709](https://github.com/windmill-labs/windmill/issues/6709)) ([d382ea7](https://github.com/windmill-labs/windmill/commit/d382ea7c8b372471dd3393720ff93749fde898f5))
### Bug Fixes
* multiselect + jsoneditor nits ([5aeb3fa](https://github.com/windmill-labs/windmill/commit/5aeb3fa0b74fb0d72f0439ce540baf224654f1ae))
## [1.552.1](https://github.com/windmill-labs/windmill/compare/v1.552.0...v1.552.1) (2025-09-29)
### Bug Fixes
* fix c# with nsjail ([2055e53](https://github.com/windmill-labs/windmill/commit/2055e536a7fcb9cfe155c0fa67de6ae49d925f97))
* **frontend:** allow dates before 2000 in date picker ([#6707](https://github.com/windmill-labs/windmill/issues/6707)) ([ce653f8](https://github.com/windmill-labs/windmill/commit/ce653f8a0538fcc88ef78f6c50960a7340648b0f))
## [1.552.0](https://github.com/windmill-labs/windmill/compare/v1.551.4...v1.552.0) (2025-09-29)
### Features
* powershell private repo support ([#6684](https://github.com/windmill-labs/windmill/issues/6684)) ([4bbbeb9](https://github.com/windmill-labs/windmill/commit/4bbbeb956f8f09ea5a8af241912a1bead1e06520))
### Bug Fixes
* external links in critical alert ([e2608f9](https://github.com/windmill-labs/windmill/commit/e2608f9aacd30e2a7aeb5b850802514f64a41380))
* fix app schema form rendering ([481c877](https://github.com/windmill-labs/windmill/commit/481c8775377f7f01ad01b5db85a98ccffadada91))
* **frontend:** prevent label interference with monaco editor in instance settings ([#6701](https://github.com/windmill-labs/windmill/issues/6701)) ([c751a5d](https://github.com/windmill-labs/windmill/commit/c751a5d6aa49e4bc0970f87b3f1e975e8ee58479))
* **mcp:** filter out tools with long names ([#6692](https://github.com/windmill-labs/windmill/issues/6692)) ([cc2afdb](https://github.com/windmill-labs/windmill/commit/cc2afdb264b0eaa353e5f2736c98e475337b71f7))
* show more autoscaling events ([#6704](https://github.com/windmill-labs/windmill/issues/6704)) ([d56dea4](https://github.com/windmill-labs/windmill/commit/d56dea4969ed5c6eec30c72cf9f1171889444007))
* **uv:** log stdout on `uv pip install` error ([#6702](https://github.com/windmill-labs/windmill/issues/6702)) ([5f63ce6](https://github.com/windmill-labs/windmill/commit/5f63ce6dd8697533de1e0af786e463f3224912c2))
## [1.551.4](https://github.com/windmill-labs/windmill/compare/v1.551.3...v1.551.4) (2025-09-29)
### Bug Fixes
* migrate dotnet from msft images to script install ([cfec8e9](https://github.com/windmill-labs/windmill/commit/cfec8e99fb55928dfed3b7e80fb63cc279553dec))
## [1.551.3](https://github.com/windmill-labs/windmill/compare/v1.551.2...v1.551.3) (2025-09-29)
### Bug Fixes
* migrate dotnet from bitnami to microsoft images ([5ae525a](https://github.com/windmill-labs/windmill/commit/5ae525a9f14de20d45e6075baa979eb4aaac4850))
## [1.551.2](https://github.com/windmill-labs/windmill/compare/v1.551.1...v1.551.2) (2025-09-29)
### Bug Fixes
* fix copy first step input ([81616cb](https://github.com/windmill-labs/windmill/commit/81616cbe1e27bc3f45cfa35cd359ce9a0f493f35))
## [1.551.1](https://github.com/windmill-labs/windmill/compare/v1.551.0...v1.551.1) (2025-09-28)
### Bug Fixes
* buttons are back to semi-bold ([bdd36c0](https://github.com/windmill-labs/windmill/commit/bdd36c0b4d5c590e66bf471c8e4b5f681b9464aa))
## [1.551.0](https://github.com/windmill-labs/windmill/compare/v1.550.0...v1.551.0) (2025-09-27)
### Features
* UX improvements (all inputs) ([72b744c](https://github.com/windmill-labs/windmill/commit/72b744c4e1bc3c1f3098f4d6de6c9474d7a8fb84))
## [1.550.0](https://github.com/windmill-labs/windmill/compare/v1.549.1...v1.550.0) (2025-09-27)
### Features
* ai agent streaming ([#6644](https://github.com/windmill-labs/windmill/issues/6644)) ([f990107](https://github.com/windmill-labs/windmill/commit/f990107c45fbb2e955ef67439e92328976091eb0))
### Bug Fixes
* improve behavior for already completed jobs when doing immediate cancels ([341cdcf](https://github.com/windmill-labs/windmill/commit/341cdcf66efdfd504492be32d9b4f5cb9db2df2a))
* improve dyn select as flow input ([6ece0ac](https://github.com/windmill-labs/windmill/commit/6ece0ac5758d4f5e8c0d55ee78a524e454ad264b))
* improve graph rendering performances ([7add574](https://github.com/windmill-labs/windmill/commit/7add57499c02ac53a7f7adbabbb279d7c41ab275))
* improve performance of flow viewer ([311b410](https://github.com/windmill-labs/windmill/commit/311b410f2f65c3bdfc483c80cc5ef72b6864118a))
* limit auto data tables to tables of col &lt; 100 ([f28ed9a](https://github.com/windmill-labs/windmill/commit/f28ed9a5f5c6032c49734e6770c2f2c9e2e4a001))
* make schedule more resilient in case of pg clock shifts ([8786130](https://github.com/windmill-labs/windmill/commit/87861301f28cab136fe7af094690539e3daa613f))
* restore set_progress feature with sse ([7df13b3](https://github.com/windmill-labs/windmill/commit/7df13b3e7bb095475d0fe54b7f635e3861fb0f73))
* scim group/users audit logs ([#6682](https://github.com/windmill-labs/windmill/issues/6682)) ([ca4f9ee](https://github.com/windmill-labs/windmill/commit/ca4f9ee8c12f01fc7c3bcedf5d41e59dc28eb1f2))
* support label + value for dynamic enums of selects ([ec9e5a9](https://github.com/windmill-labs/windmill/commit/ec9e5a9acbd352b20399d403be55361c73084aff))
## [1.549.1](https://github.com/windmill-labs/windmill/compare/v1.549.0...v1.549.1) (2025-09-26)
### Bug Fixes
* do not request unecessarily get_scheduled_for ([0269211](https://github.com/windmill-labs/windmill/commit/02692111a1a8eefb2675b14d53f109a66c1b9a78))
* fix agent_workers completed job back-compatibility deserialization ([db4bc7e](https://github.com/windmill-labs/windmill/commit/db4bc7ee6963955abc7e290bd67ea913b0f5e2ad))
## [1.549.0](https://github.com/windmill-labs/windmill/compare/v1.548.3...v1.549.0) (2025-09-26)
### Features
* **backend:** job result stream optimization ([#6673](https://github.com/windmill-labs/windmill/issues/6673)) ([8f4fef9](https://github.com/windmill-labs/windmill/commit/8f4fef98042c49346c89bdf5e0b9b1f2d52e371f))
### Bug Fixes
* scim group handling when deleting instance user + conversion ([#6677](https://github.com/windmill-labs/windmill/issues/6677)) ([4205e83](https://github.com/windmill-labs/windmill/commit/4205e83cfde453827eab23c31e76a0f0490d31b7))
## [1.548.3](https://github.com/windmill-labs/windmill/compare/v1.548.2...v1.548.3) (2025-09-25)
### Bug Fixes
* fix job loader token initialization ([f5d238e](https://github.com/windmill-labs/windmill/commit/f5d238edcfed6b0f066d459cdc718679a7b51187))
* websocket runnable [#6675](https://github.com/windmill-labs/windmill/issues/6675) ([a308782](https://github.com/windmill-labs/windmill/commit/a308782bcf7ef9913887521d74796b490619d0c8))
## [1.548.2](https://github.com/windmill-labs/windmill/compare/v1.548.1...v1.548.2) (2025-09-24)

3
backend/.gitignore vendored
View File

@@ -9,4 +9,5 @@ windmill-api/openapi-*.*
.duckdb/*
*ee.rs
generate_mcp_endpoints_tools/venv
bacon.toml
bacon.toml
libwindmill_duckdb_ffi_internal.so

View File

@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "SELECT data FROM app_bundles WHERE app_version_id = $1 AND file_type = $2 AND w_id = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "data",
"type_info": "Bytea"
}
],
"parameters": {
"Left": [
"Int8",
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "01050e7057f3d1971ad9e47ac83bf6a3c3c9f41689c3607f0b264437ae6b3324"
}

View File

@@ -0,0 +1,29 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n eig.igroup as group_name,\n ws.auto_add_instance_groups_roles\n FROM email_to_igroup eig\n INNER JOIN workspace_settings ws ON ws.workspace_id = $1\n WHERE eig.email = $2\n AND eig.igroup = ANY(ws.auto_add_instance_groups)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "group_name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "auto_add_instance_groups_roles",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
true
]
},
"hash": "1ff185d8b8b897a72180cd0002e0f7e9858eef249577ce23969522793c6b5608"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE flow_conversation_message\n SET content = $1\n WHERE job_id = $2\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid"
]
},
"nullable": []
},
"hash": "20888e946a5597989488e2a54e354e4ce2872f2bc3af7b26fcaafe8703ba70bf"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM v2_job_completed WHERE id = $1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "25b7c964336321fa10ea988831526b391cc1f02185ee87dbbda3d8a388cc858a"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT draft_only FROM app WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "draft_only",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true
]
},
"hash": "27b0c827467cc92979f094620957bc0edfa295d6c2292e509a5536765d120bd8"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT draft_only FROM flow WHERE path = $1 AND workspace_id = $2 AND archived = false",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "draft_only",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true
]
},
"hash": "28f1ecca40c8b81cc59dffb75e2913c889b374999ece04173b2e67dc74005f60"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n scheduled_for,\n runnable_path,\n kind as \"kind: JobKind\",\n runnable_id as \"runnable_id: ScriptHash\",\n canceled_reason,\n canceled_by,\n permissioned_as,\n permissioned_as_email,\n flow_status as \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n v2_job.tag,\n script_lang as \"script_lang: ScriptLang\",\n same_worker,\n pre_run_error,\n concurrent_limit,\n concurrency_time_window_s,\n flow_innermost_root_job,\n root_job,\n timeout,\n flow_step_id,\n cache_ttl,\n v2_job_queue.priority,\n preprocessed,\n script_entrypoint_override,\n trigger,\n trigger_kind as \"trigger_kind: JobTriggerKind\",\n visible_to_owner\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1",
"query": "SELECT \n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n scheduled_for,\n runnable_path,\n kind as \"kind: JobKind\",\n runnable_id as \"runnable_id: ScriptHash\",\n canceled_reason,\n canceled_by,\n permissioned_as,\n permissioned_as_email,\n flow_status as \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n v2_job.tag,\n script_lang as \"script_lang: ScriptLang\",\n same_worker,\n pre_run_error,\n concurrent_limit,\n concurrency_time_window_s,\n flow_innermost_root_job,\n root_job,\n timeout,\n flow_step_id,\n cache_ttl,\n v2_job_queue.priority,\n preprocessed,\n script_entrypoint_override,\n trigger,\n trigger_kind as \"trigger_kind: JobTriggerKind\",\n visible_to_owner,\n NULL as permissioned_as_end_user_email\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1",
"describe": {
"columns": [
{
@@ -241,6 +241,11 @@
"ordinal": 31,
"name": "visible_to_owner",
"type_info": "Bool"
},
{
"ordinal": 32,
"name": "permissioned_as_end_user_email",
"type_info": "Text"
}
],
"parameters": {
@@ -280,8 +285,9 @@
true,
true,
true,
false
false,
null
]
},
"hash": "f3f96e066716e61042519a645d487b578bc63792cdb0f7ddaeb82e9771287c22"
"hash": "3162ec92bb32af47a71cc41172cc740b5dea1304ce4dfdb4d3d0efa4266f38c5"
}

View File

@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE usr SET added_via = $1, is_admin = $2, operator = $3 WHERE username = $4 AND workspace_id = $5",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Bool",
"Bool",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "326fd614ebd965b9bb6f3e578f75a54d80812ff144e711100e6ac659785c991d"
}

View File

@@ -1,101 +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 CASE WHEN $7::BOOLEAN THEN NULL ELSE SUBSTR(logs, GREATEST($1 - log_offset, 0)) END AS logs,\n SUBSTR(rs.stream, $8) AS new_result_stream,\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.flow_status, f.flow_status)->>'stream_job')::uuid AS stream_job,\n COALESCE(c.workflow_as_code_status, f.workflow_as_code_status) AS \"workflow_as_code_status: sqlx::types::Json<Box<RawValue>>\",\n CASE WHEN $7::BOOLEAN THEN NULL ELSE job_logs.log_offset + CHAR_LENGTH(job_logs.logs) + 1 END AS log_offset,\n CHAR_LENGTH(rs.stream) + 1 AS stream_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 rs.stream AS \"result_stream: Option<String>\"\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_result_stream rs ON rs.job_id = $3\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": "new_result_stream",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "mem_peak",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "flow_status: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "stream_job",
"type_info": "Uuid"
},
{
"ordinal": 7,
"name": "workflow_as_code_status: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 8,
"name": "log_offset",
"type_info": "Int4"
},
{
"ordinal": 9,
"name": "stream_offset",
"type_info": "Int4"
},
{
"ordinal": 10,
"name": "created_by!",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "progress",
"type_info": "Int4"
},
{
"ordinal": 12,
"name": "result_stream: Option<String>",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Int4",
"Text",
"Uuid",
"Bool",
"Bool",
"TextArray",
"Bool",
"Int4"
]
},
"nullable": [
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
false,
null,
false
]
},
"hash": "40999264f09a781c4393b50c2c41ae5a5e64086198cb67aba72345bb3cdf7773"
}

View File

@@ -0,0 +1,49 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH result_stream AS (\n SELECT \n string_agg(stream, '' order by idx asc) as stream, \n job_id, \n max(idx) + 1 as offset \n FROM job_result_stream_v2\n WHERE job_id = $2 AND idx >= $3\n GROUP BY job_id\n )\n SELECT\n COALESCE(jc.result, NULL) as \"result: sqlx::types::Json<Box<RawValue>>\",\n rs.stream AS \"result_stream: Option<String>\",\n rs.offset AS stream_offset,\n COALESCE(js.flow_status, jc.flow_status) as \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n CASE WHEN $4 THEN NULL ELSE (COALESCE(js.flow_status, jc.flow_status)->>'stream_job')::uuid END as stream_job\n FROM (\n SELECT $2::uuid as job_id, $1::text as workspace_id\n ) base\n LEFT JOIN v2_job_completed jc ON jc.id = base.job_id AND jc.workspace_id = base.workspace_id\n LEFT JOIN v2_job_status js ON js.id = base.job_id\n LEFT JOIN result_stream rs ON rs.job_id = base.job_id\n WHERE base.job_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "result_stream: Option<String>",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "stream_offset",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "flow_status: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 4,
"name": "stream_job",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text",
"Uuid",
"Int4",
"Bool"
]
},
"nullable": [
null,
null,
null,
null,
null
]
},
"hash": "4b056d33215b3a1e9849bb66ce84e96c69d10e3970e38151c97d1fca0cb7388d"
}

View File

@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO job_result_stream (workspace_id, job_id, stream)\n VALUES ($1, $2, $3)\n ON CONFLICT (job_id) DO UPDATE SET stream = job_result_stream.stream || $3\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "4bc533074c720820cebff8d97a203df52520b7606378ecca267e88383a45b49b"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM job_result_stream_v2 WHERE job_id = ANY($1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "5adeb6989648ca42431c96069c4de88d2615e7e6f1267f2fb12ccc325d4e4148"
}

View File

@@ -0,0 +1,64 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, conversation_id, message_type as \"message_type: MessageType\", content, job_id, created_at\n FROM (\n SELECT id, conversation_id, message_type, content, job_id, created_at\n FROM flow_conversation_message\n WHERE conversation_id = $1\n ORDER BY created_at DESC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END\n LIMIT $2 OFFSET $3\n ) AS messages\n ORDER BY created_at ASC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "conversation_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "message_type: MessageType",
"type_info": {
"Custom": {
"name": "message_type",
"kind": {
"Enum": [
"user",
"assistant"
]
}
}
}
},
{
"ordinal": 3,
"name": "content",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "job_id",
"type_info": "Uuid"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid",
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
true,
false
]
},
"hash": "5f57ec3330a6576f4017fd469848a1c214896dc5abbf2b898ac9a68810fd89dd"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value->>'ducklake_user_pg_pwd' FROM global_settings WHERE name = 'ducklake_settings';",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "5fdfc9427f455a4c1bc8f6ca41ddfd426bc0c2ac126792c926f3cf1182ded981"
}

View File

@@ -0,0 +1,27 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO flow_conversation_message (conversation_id, message_type, content, job_id)\n VALUES ($1, $2, $3, $4)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
{
"Custom": {
"name": "message_type",
"kind": {
"Enum": [
"user",
"assistant"
]
}
}
},
"Text",
"Uuid"
]
},
"nullable": []
},
"hash": "69606859fe08d24f0306b866f9f50ad766d56a0d5aa8f0784f79aa5e211b00e4"
}

View File

@@ -0,0 +1,62 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO flow_conversation (id, workspace_id, flow_path, created_by, title)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id, workspace_id, flow_path, title, created_at, updated_at, created_by",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "flow_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "updated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "created_by",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false
]
},
"hash": "6bd23a98838e3eec309e6b696edc776bd56fc9dae1238b3272557d1562400dbe"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT draft_only FROM script WHERE path = $1 AND workspace_id = $2 AND archived = false",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "draft_only",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true
]
},
"hash": "71c945f93c0a1b561a85e8462b1687a54bd098cf6e84f57e5755eb84e1552345"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value->'instance_catalog_db_status' FROM global_settings WHERE name = 'ducklake_settings'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "73c1c88bdf26ea0559b83314fed7a67d850e4e4dd60f4424ffb0b6f472acc8d5"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT (flow_status->>'memory_id')::uuid as memory_id \n FROM v2_job_status \n WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "memory_id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "7628e7690aaf6da3dba3cf78dc9e78fe32c3a001c08920e02fb850695afdb7ec"
}

View File

@@ -0,0 +1,101 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH result_stream AS (\n SELECT \n string_agg(stream, '' order by idx asc) as stream, \n job_id, \n max(idx) + 1 as offset \n FROM job_result_stream_v2\n WHERE job_id = $3 AND idx >= $8\n GROUP BY job_id\n )\n 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 CASE WHEN $7::BOOLEAN THEN NULL ELSE SUBSTR(logs, GREATEST($1 - log_offset, 0)) END AS logs,\n rs.stream AS new_result_stream,\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.flow_status, f.flow_status)->>'stream_job')::uuid AS stream_job,\n COALESCE(c.workflow_as_code_status, f.workflow_as_code_status) AS \"workflow_as_code_status: sqlx::types::Json<Box<RawValue>>\",\n CASE WHEN $7::BOOLEAN THEN NULL ELSE job_logs.log_offset + CHAR_LENGTH(job_logs.logs) + 1 END AS log_offset,\n rs.offset AS stream_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 rs.stream AS \"result_stream: Option<String>\"\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 result_stream rs ON rs.job_id = $3\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": "new_result_stream",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "mem_peak",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "flow_status: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "stream_job",
"type_info": "Uuid"
},
{
"ordinal": 7,
"name": "workflow_as_code_status: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 8,
"name": "log_offset",
"type_info": "Int4"
},
{
"ordinal": 9,
"name": "stream_offset",
"type_info": "Int4"
},
{
"ordinal": 10,
"name": "created_by!",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "progress",
"type_info": "Int4"
},
{
"ordinal": 12,
"name": "result_stream: Option<String>",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Int4",
"Text",
"Uuid",
"Bool",
"Bool",
"TextArray",
"Bool",
"Int4"
]
},
"nullable": [
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
false,
null,
null
]
},
"hash": "7b524ee24bb78e494a93c5ea205259bd989a9d769a6a300d37ab116960438882"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "VACUUM v2_job, v2_job_completed, job_result_stream, job_stats, job_logs, concurrency_key, log_file, metrics",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "807c920bff25f56b10e88900d879cf5e8484c147e457044d6b075323b163ebaa"
}

View File

@@ -1,49 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n COALESCE(jc.result, NULL) as \"result: sqlx::types::Json<Box<RawValue>>\",\n SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\",\n CHAR_LENGTH(rs.stream) + 1 AS stream_offset,\n COALESCE(js.flow_status, jc.flow_status) as \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n CASE WHEN $4 THEN NULL ELSE (COALESCE(js.flow_status, jc.flow_status)->>'stream_job')::uuid END as stream_job\n FROM (\n SELECT $2::uuid as job_id, $1::text as workspace_id\n ) base\n LEFT JOIN v2_job_completed jc ON jc.id = base.job_id AND jc.workspace_id = base.workspace_id\n LEFT JOIN v2_job_status js ON js.id = base.job_id\n LEFT JOIN job_result_stream rs ON rs.job_id = base.job_id\n WHERE base.job_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "result_stream: Option<String>",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "stream_offset",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "flow_status: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 4,
"name": "stream_job",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text",
"Uuid",
"Int4",
"Bool"
]
},
"nullable": [
null,
null,
null,
null,
null
]
},
"hash": "80809d397cf84f7278ebb276078871b371663257a127eb35512695c487066fd7"
}

View File

@@ -1,55 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n jc.result as \"result: sqlx::types::Json<Box<RawValue>>\",\n v2_job.tag,\n v2_job_queue.running as \"running: Option<bool>\",\n SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\",\n CHAR_LENGTH(rs.stream) AS stream_offset,\n CASE WHEN $4 THEN NULL ELSE (COALESCE(js.flow_status, jc.flow_status)->>'stream_job')::uuid END as stream_job\n FROM v2_job\n LEFT JOIN v2_job_queue USING (id)\n LEFT JOIN v2_job_completed jc USING (id)\n LEFT JOIN v2_job_status js USING (id)\n LEFT JOIN job_result_stream rs ON rs.job_id = $2\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"
},
{
"ordinal": 3,
"name": "result_stream: Option<String>",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "stream_offset",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "stream_job",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text",
"Uuid",
"Int4",
"Bool"
]
},
"nullable": [
true,
false,
false,
null,
null,
null
]
},
"hash": "8126b118704341846e88bd289f1afe83c07b7a8b422f48022994370b3e433f34"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM flow_conversation WHERE id = $1 AND workspace_id = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
null
]
},
"hash": "81e997576319ae1d6d9a91f76465f6fa53892ff223f3b9d9ad3f4a1d2e720cc8"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31)",
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email) \n values ($1, $32, $33, $34, $35, $36, $37, $2, $41) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31)",
"describe": {
"columns": [],
"parameters": {
@@ -123,10 +123,11 @@
}
}
},
"Bool"
"Bool",
"Varchar"
]
},
"nullable": []
},
"hash": "193d292c5ed44bf5266ad52c83704c3a36aa284fab3b7e638dbca12ac846b82b"
"hash": "8d67ed8e1271a27b072a8e8ae9973e372949f5ec7d71b80d1ba3eaddf851adce"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (SELECT 1 FROM pg_catalog.pg_database WHERE datname = $1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Name"
]
},
"nullable": [
null
]
},
"hash": "97e3a1439202e13e739ad2e3f22b3a21d0c9b0e57d7d35326753e8f6a804d4f8"
}

View File

@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO job_result_stream_v2 (workspace_id, job_id, stream, idx)\n VALUES (\n $1, \n $2,\n $3, \n $4\n )\n ON CONFLICT (job_id, idx) DO UPDATE SET stream = job_result_stream_v2.stream || $3\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text",
"Int4"
]
},
"nullable": []
},
"hash": "9bfc2a821b25641af48b0e3954163078922340294f1c6515400fb2c896666fde"
}

View File

@@ -0,0 +1,55 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH result_stream AS (\n SELECT \n string_agg(stream, '' order by idx asc) as stream, \n job_id, \n max(idx) + 1 as offset \n FROM job_result_stream_v2\n WHERE job_id = $2 AND idx >= $3\n GROUP BY job_id\n )\n SELECT \n jc.result as \"result: sqlx::types::Json<Box<RawValue>>\",\n v2_job.tag,\n v2_job_queue.running as \"running: Option<bool>\",\n rs.stream AS \"result_stream: Option<String>\",\n rs.offset AS stream_offset,\n CASE WHEN $4 THEN NULL ELSE (COALESCE(js.flow_status, jc.flow_status)->>'stream_job')::uuid END as stream_job\n FROM v2_job\n LEFT JOIN v2_job_queue USING (id)\n LEFT JOIN v2_job_completed jc USING (id)\n LEFT JOIN v2_job_status js USING (id)\n LEFT JOIN result_stream rs ON rs.job_id = $2\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"
},
{
"ordinal": 3,
"name": "result_stream: Option<String>",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "stream_offset",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "stream_job",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text",
"Uuid",
"Int4",
"Bool"
]
},
"nullable": [
true,
false,
false,
null,
null,
null
]
},
"hash": "9c0bbd44902d8eee393236f7c2372b273d14a093bb29ec12dda8bbfaecd49a35"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id as workspace, path, summary, description, schema FROM script as o \n WHERE created_at = (select max(created_at) from script where o.path = path and workspace_id = $1 AND archived = false) \n AND workspace_id = $1 and archived = false",
"query": "SELECT workspace_id as workspace, path, summary, description, schema FROM script as o\n WHERE created_at = (select max(created_at) from script where o.path = path and workspace_id = $1 AND archived = false)\n AND workspace_id = $1 and archived = false",
"describe": {
"columns": [
{
@@ -42,5 +42,5 @@
true
]
},
"hash": "2d5f58dd2aff3bd49f3891ae76df23e2aa39891931516426f65b229314a0cee1"
"hash": "9c3ddb90295db7d6afcbdb077f017950620e753dde97c8d2d88cd60ff8c3f339"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, worker_group, event_type::text, desired_workers, reason, applied_at FROM autoscaling_event WHERE worker_group = $1 ORDER BY applied_at DESC LIMIT 5",
"query": "SELECT id, worker_group, event_type::text, desired_workers, reason, applied_at FROM autoscaling_event WHERE worker_group = $1 ORDER BY applied_at DESC LIMIT $2 OFFSET $3",
"describe": {
"columns": [
{
@@ -36,7 +36,9 @@
],
"parameters": {
"Left": [
"Text"
"Text",
"Int8",
"Int8"
]
},
"nullable": [
@@ -48,5 +50,5 @@
false
]
},
"hash": "6d134b137ae81534e145fc5b6474cf963ee26a3ad3a0a3d8dc064cb14c8fd9a6"
"hash": "a38df5d7dc4577c715d9acdaf87c38535ad388b1948a95efafd71135cfe5e3a6"
}

View File

@@ -1,49 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n COALESCE(jc.result, jc.result) as \"result: sqlx::types::Json<Box<RawValue>>\",\n jq.running as \"running: Option<bool>\",\n SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\",\n CHAR_LENGTH(rs.stream) + 1 AS stream_offset,\n CASE WHEN $4 THEN NULL ELSE (COALESCE(js.flow_status, jc.flow_status)->>'stream_job')::uuid END as stream_job\n FROM (\n SELECT $1::uuid as job_id, $2::text as workspace_id\n ) base\n LEFT JOIN v2_job_completed jc ON jc.id = base.job_id AND jc.workspace_id = base.workspace_id\n LEFT JOIN v2_job_queue jq ON jq.id = base.job_id AND jq.workspace_id = base.workspace_id\n LEFT JOIN v2_job_status js ON js.id = base.job_id\n LEFT JOIN job_result_stream rs ON rs.job_id = base.job_id\n WHERE base.job_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "running: Option<bool>",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "result_stream: Option<String>",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "stream_offset",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "stream_job",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"Int4",
"Bool"
]
},
"nullable": [
null,
false,
null,
null,
null
]
},
"hash": "a58a345f7082181f89e7f88929b7149791de48bc2e489edb55d63f67702cce05"
}

View File

@@ -0,0 +1,49 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH result_stream AS (\n SELECT \n string_agg(stream, '' order by idx asc) as stream, \n job_id, \n max(idx) + 1 as offset \n FROM job_result_stream_v2\n WHERE job_id = $1 AND idx >= $3\n GROUP BY job_id\n )\n SELECT\n COALESCE(jc.result, jc.result) as \"result: sqlx::types::Json<Box<RawValue>>\",\n jq.running as \"running: Option<bool>\",\n rs.stream AS \"result_stream: Option<String>\",\n rs.offset AS stream_offset,\n CASE WHEN $4 THEN NULL ELSE (COALESCE(js.flow_status, jc.flow_status)->>'stream_job')::uuid END as stream_job\n FROM (\n SELECT $1::uuid as job_id, $2::text as workspace_id\n ) base\n LEFT JOIN v2_job_completed jc ON jc.id = base.job_id AND jc.workspace_id = base.workspace_id\n LEFT JOIN v2_job_queue jq ON jq.id = base.job_id AND jq.workspace_id = base.workspace_id\n LEFT JOIN v2_job_status js ON js.id = base.job_id\n LEFT JOIN result_stream rs ON rs.job_id = base.job_id\n WHERE base.job_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "running: Option<bool>",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "result_stream: Option<String>",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "stream_offset",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "stream_job",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"Int4",
"Bool"
]
},
"nullable": [
null,
false,
null,
null,
null
]
},
"hash": "a76eec5797ca8f97e63ed5542bf03873e7dfd1cf9fe984c769afb5a8bdb48d49"
}

View File

@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO app_bundles (app_version_id, w_id, file_type, data) VALUES ($1, $2, $3, $4)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Varchar",
"Varchar",
"Bytea"
]
},
"nullable": []
},
"hash": "abaae3dde751a41b2dbb7856ece1c840d0ea8d59346ed9e88f6f609edb543d7e"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT ws.ducklake->'ducklakes' AS ducklake_name\n FROM workspace_settings ws\n WHERE ws.workspace_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "ducklake_name",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "b344ba5a32ec873181390e205e16356f1b79bd994a4bd1a8655dbe17bd1e4a30"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM job_result_stream_v2 WHERE job_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "bb46e5dcf5490ef3511faa131ad5693dedf34366e51044ddf30695995d194090"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM autoscaling_event WHERE applied_at <= now() - ($1::bigint::text || ' s')::interval",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "bd1b62a6435cfa7d8235bcb3cb104ef01831f400ba9bd1c3433412770d1d2a82"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE flow_conversation SET updated_at = NOW() WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "bdb1ece5c233f242cf341c089a2f2b785dfa5cc14d9be224c0707e10247ed8b7"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color, workspace.parent_workspace_id,\n CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings\n FROM workspace\n JOIN usr ON usr.workspace_id = workspace.id\n JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id\n WHERE usr.email = $1 AND workspace.deleted = false",
"query": "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color, workspace.parent_workspace_id,\n CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings,\n usr.disabled\n FROM workspace\n JOIN usr ON usr.workspace_id = workspace.id\n JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id\n WHERE usr.email = $1 AND workspace.deleted = false",
"describe": {
"columns": [
{
@@ -32,6 +32,11 @@
"ordinal": 5,
"name": "operator_settings",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "disabled",
"type_info": "Bool"
}
],
"parameters": {
@@ -45,8 +50,9 @@
false,
true,
true,
null
null,
false
]
},
"hash": "d0037961e8e787c4277afc3eb79f3b72e3323f878387de8b6fa31493f1215a77"
"hash": "c095a9658c542efc9f0255a1b536d2fd8a25fe4cd57c223db7d744493f8470c6"
}

View File

@@ -0,0 +1,59 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, workspace_id, flow_path, title, created_at, updated_at, created_by\n FROM flow_conversation\n WHERE id = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "flow_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "updated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "created_by",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false
]
},
"hash": "c383cc023714b361d10c10e8fef1fc148ab1da942951ee9ffdddaecee76a6be9"
}

View File

@@ -0,0 +1,29 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n string_agg(stream, '' order by idx asc) as stream, \n max(idx) + 1 as offset \n FROM job_result_stream_v2\n WHERE job_id = $2 AND idx >= $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "stream",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "offset",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Int4",
"Uuid"
]
},
"nullable": [
null,
null
]
},
"hash": "dd92bce0ddbfbf06834807aab4d589fc104647cae7abf37ddce3ef7109726261"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, on_behalf_of_email, edited_by, flow_version.id AS version\n FROM flow\n INNER JOIN flow_version\n ON flow_version.id = $3\n WHERE flow.path = $1 and flow.workspace_id = $2",
"query": "SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, on_behalf_of_email, edited_by, flow_version.id AS version\n FROM flow\n INNER JOIN flow_version\n ON flow_version.id = $3\n WHERE flow.path = $1 and flow.workspace_id = $2",
"describe": {
"columns": [
{
@@ -25,16 +25,21 @@
},
{
"ordinal": 4,
"name": "chat_input_enabled",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "on_behalf_of_email",
"type_info": "Text"
},
{
"ordinal": 5,
"ordinal": 6,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "version",
"type_info": "Int8"
}
@@ -51,10 +56,11 @@
true,
null,
null,
null,
true,
false,
false
]
},
"hash": "9b60fa8a1003015bc5a7cdbee9a4486b313d45347dfd9d4793d60e2760763ca3"
"hash": "e1409c67b93881cf68f6ac9c8bae0856cf426c7e7860c8b5b799972baa8e6945"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM flow_conversation_message WHERE job_id = $1 AND message_type = 'assistant')",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "e2f4eeb896c22e3215370ed30ed5cc890503e4197b0fdb3a906504eba166aa97"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM flow_conversation WHERE id = $1 AND workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "e6c4454e552dc82db1af2d6b887ebb4b78eb58600cc21e3ffb12dd9e5a0a6f08"
}

View File

@@ -0,0 +1,41 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, is_admin, operator, added_via FROM usr WHERE username = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "is_admin",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "operator",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "added_via",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
true
]
},
"hash": "eb110e722ba8ac32d9d69010dc7e5f5763c55a5ac255f939f1fda8d3f9200f8d"
}

View File

@@ -1,29 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT SUBSTR(rs.stream, $1) AS new_result_stream, CHAR_LENGTH(rs.stream) + 1 AS stream_offset FROM job_result_stream rs WHERE rs.job_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "new_result_stream",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "stream_offset",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Int4",
"Uuid"
]
},
"nullable": [
null,
null
]
},
"hash": "f17f914d2522bf7cb5de9d7ba5557ee0dce940039ab42fd39bf079d87b6cad8a"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_status \n SET flow_status = jsonb_set(\n flow_status,\n '{memory_id}',\n to_jsonb($2::uuid)\n )\n WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": []
},
"hash": "f5568a691ec5931634cf986f806f5eae7bb8ed0f5c6e54ca3f49a991c53ed50d"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "VACUUM v2_job, v2_job_completed, job_result_stream_v2, job_stats, job_logs, concurrency_key, log_file, metrics",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "f8ac5379ecfbff7b8ae75c821680737b249a64c8d9e8f7dbcc46fce98e874571"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT app.versions[array_upper(app.versions, 1)] FROM app\n WHERE app.path = $1 AND app.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "versions",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "fad966db585b91c9ce143c9aa26a826aec1ddb193a7f4988c5f12b1a2d8ce071"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE global_settings SET value = jsonb_set(value, '{instance_catalog_db_status}', (COALESCE(value->'instance_catalog_db_status', '{}'::jsonb) || to_jsonb($1::json))) WHERE name = 'ducklake_settings'",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Json"
]
},
"nullable": []
},
"hash": "fd55112d55995ab08d2c275aa6430cdec1cacebdf2f2b3dd6f678b434643eb50"
}

551
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.548.2"
version = "1.555.2"
authors.workspace = true
edition.workspace = true
@@ -34,7 +34,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.548.2"
version = "1.555.2"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"

View File

@@ -1,8 +1,8 @@
#!/bin/bash
# This script outputs all features except private. Usage :
# > cargo build --features $(./all_features_oss.sh)
#!/bin/bash
# Path to the Cargo.toml file
CARGO_TOML_PATH="./Cargo.toml"

View File

@@ -1 +1 @@
aa4c7936b99d0de2f7af31cde79dfc7b1b482c29
33bdef405c678616b12084cf779a68a62d1f477e

View File

@@ -0,0 +1,4 @@
-- Add down migration script here
DROP TABLE job_result_stream_v2;
ALTER TABLE job_result_stream ADD CONSTRAINT fk_job_result_stream_job_id FOREIGN KEY (job_id) REFERENCES v2_job_queue(id) ON DELETE CASCADE;

View File

@@ -0,0 +1,13 @@
-- Add up migration script here
CREATE TABLE job_result_stream_v2 (
job_id UUID NOT NULL,
workspace_id TEXT NOT NULL,
stream TEXT NOT NULL,
idx INT NOT NULL,
PRIMARY KEY (job_id, idx)
);
GRANT ALL ON TABLE job_result_stream_v2 TO windmill_admin;
GRANT ALL ON TABLE job_result_stream_v2 TO windmill_user;
ALTER TABLE job_result_stream DROP CONSTRAINT fk_job_result_stream_job_id;

View File

@@ -0,0 +1,12 @@
-- Add down migration script here
-- Drop indexes
DROP INDEX IF EXISTS idx_conversation_message_conversation_time;
DROP INDEX IF EXISTS idx_flow_conversation_workspace_path;
-- Drop tables (order matters due to foreign keys)
DROP TABLE IF EXISTS flow_conversation_message;
DROP TABLE IF EXISTS flow_conversation;
-- Drop enum
DROP TYPE IF EXISTS MESSAGE_TYPE;

View File

@@ -0,0 +1,57 @@
-- Add up migration script here
-- Create message_type enum
CREATE TYPE MESSAGE_TYPE AS ENUM ('user', 'assistant');
-- Create flow_conversation table
CREATE TABLE flow_conversation (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id),
flow_path VARCHAR(255) NOT NULL,
title VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_by VARCHAR(50) NOT NULL
);
-- Create flow_conversation_message table
CREATE TABLE flow_conversation_message (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID NOT NULL REFERENCES flow_conversation(id) ON DELETE CASCADE,
message_type MESSAGE_TYPE NOT NULL,
content TEXT NOT NULL,
job_id UUID REFERENCES v2_job(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Basic indexes for performance
CREATE INDEX idx_flow_conversation_workspace_path ON flow_conversation(workspace_id, flow_path, updated_at DESC);
CREATE INDEX idx_conversation_message_conversation_time ON flow_conversation_message(conversation_id, created_at DESC);
-- Grant permissions
GRANT ALL ON flow_conversation TO windmill_admin;
GRANT ALL ON flow_conversation TO windmill_user;
GRANT ALL ON flow_conversation_message TO windmill_admin;
GRANT ALL ON flow_conversation_message TO windmill_user;
-- RLS policies
ALTER TABLE flow_conversation ENABLE ROW LEVEL SECURITY;
ALTER TABLE flow_conversation_message ENABLE ROW LEVEL SECURITY;
-- Admin policies - admins can access all conversations
CREATE POLICY admin_policy ON flow_conversation FOR ALL TO windmill_admin USING (true);
CREATE POLICY admin_policy ON flow_conversation_message FOR ALL TO windmill_admin USING (true);
-- User policies - users can only access their own conversations
CREATE POLICY see_own ON flow_conversation FOR ALL TO windmill_user
USING (flow_conversation.created_by = current_setting('session.user'));
-- Users can see messages of conversations they own
CREATE POLICY see_own ON flow_conversation_message FOR ALL TO windmill_user
USING (
EXISTS (
SELECT 1 FROM flow_conversation
WHERE flow_conversation.id = flow_conversation_message.conversation_id
AND flow_conversation.created_by = current_setting('session.user')
)
);

View File

@@ -0,0 +1,2 @@
-- Add down migration script here
DROP TABLE app_bundles;

View File

@@ -0,0 +1,8 @@
-- Add up migration script here
CREATE TABLE app_bundles (
app_version_id BIGINT NOT NULL,
w_id VARCHAR(255) NOT NULL,
file_type VARCHAR(10) NOT NULL,
data BYTEA NOT NULL,
PRIMARY KEY (app_version_id, file_type)
);

View File

@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE job_perms DROP COLUMN end_user_email;

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE job_perms ADD COLUMN end_user_email VARCHAR(255);

View File

@@ -0,0 +1,6 @@
INSERT INTO global_settings (name, value) VALUES (
'ducklake_user_pg_pwd',
(SELECT g2.value->'ducklake_user_pg_pwd' FROM global_settings g2 WHERE g2.name = 'ducklake_settings')
);
DELETE FROM global_settings WHERE name = 'ducklake_settings';

View File

@@ -0,0 +1,9 @@
INSERT INTO global_settings (name, value) VALUES (
'ducklake_settings',
(SELECT json_build_object(
'ducklake_user_pg_pwd', g2.value,
'instance_catalog_db_status', '{}'::json
) FROM global_settings g2 WHERE g2.name = 'ducklake_user_pg_pwd')
);
DELETE FROM global_settings WHERE name = 'ducklake_user_pg_pwd';

View File

@@ -10,7 +10,7 @@ use regex_lite::Regex;
use serde_json::json;
use std::{collections::HashMap, str::FromStr};
use windmill_parser::{Arg, MainArgSignature, Typ};
use windmill_parser::{Arg, MainArgSignature, ObjectType, Typ};
pub fn parse_bash_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let parsed = parse_bash_file(&code)?;
@@ -48,7 +48,7 @@ lazy_static::lazy_static! {
static ref RE_BASH: Regex = Regex::new(r#"(?m)^(\w+)="\$(?:(\d+)|\{(\d+)\}|\{(\d+):-(.*)\})"(?:[\t ]*)?(?:#.*)?$"#).unwrap();
pub static ref RE_POWERSHELL_PARAM: Regex = Regex::new(r#"(?m)param[\t ]*\(([^)]*)\)"#).unwrap();
static ref RE_POWERSHELL_ARGS: Regex = Regex::new(r#"(?:\[(\w+)\])?\$(\w+)[\t ]*(?:=[\t ]*(?:(?:(?:"|')([^"\n\r\$]*)(?:"|'))|([\d.]+)))?"#).unwrap();
static ref RE_POWERSHELL_ARGS: Regex = Regex::new(r#"(?:\[([\w\[\]]+)\])?\$(\w+)[\t ]*(?:=[\t ]*(?:(?:(?:"|')([^"\n\r\$]*)(?:"|'))|([\d.]+)))?"#).unwrap();
}
fn parse_bash_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
@@ -124,6 +124,18 @@ fn split_pwsh_args(code: &str) -> Vec<&str> {
splits
}
fn parse_powershell_single_typ(typ: &str) -> Typ {
match typ.to_lowercase().as_str() {
"string" => Typ::Str(None),
"int" | "long" => Typ::Int,
"decimal" | "double" | "single" => Typ::Float,
"datetime" => Typ::Datetime,
"bool" => Typ::Bool,
"pscustomobject" => Typ::Object(ObjectType::new(None, None)),
_ => Typ::Str(None),
}
}
fn parse_powershell_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let param_wrapper = RE_POWERSHELL_PARAM.captures(code);
let mut args = vec![];
@@ -136,12 +148,12 @@ fn parse_powershell_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let name = cap.get(2).unwrap().as_str().to_string();
let mut parsed_typ = if let Some(typ) = typ {
match typ.as_str() {
"string" => Some(Typ::Str(None)),
"int" | "long" => Some(Typ::Int),
"decimal" | "double" | "single" => Some(Typ::Float),
"datetime" | "DateTime" => Some(Typ::Datetime),
_ => None,
if typ.as_str().ends_with("[]") {
Some(Typ::List(Box::new(parse_powershell_single_typ(
typ.as_str().strip_suffix("[]").unwrap(),
))))
} else {
Some(parse_powershell_single_typ(typ.as_str()))
}
} else {
None
@@ -254,7 +266,7 @@ non_required="${5:-}"
#[test]
fn test_parse_powershell_sig() -> anyhow::Result<()> {
let code = r#"param($Msg, [string]$Msg2, $Dflt = "default value, with comma", [int]$Nb = 3 , $Nb2 = 5.0, $Nb3 = 5, $Wahoo = $env:WAHOO)"#;
let code = r#"param($Msg, [string]$Msg2, $Dflt = "default value, with comma", [int]$Nb = 3 , $Nb2 = 5.0, $Nb3 = 5, $Wahoo = $env:WAHOO, [PSCustomObject]$Obj, [string[]]$Arr)"#;
assert_eq!(
parse_powershell_sig(code)?,
MainArgSignature {
@@ -316,6 +328,22 @@ non_required="${5:-}"
default: None,
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "Obj".to_string(),
typ: Typ::Object(ObjectType::new(None, None)),
default: None,
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "Arr".to_string(),
typ: Typ::List(Box::new(Typ::Str(None))),
default: None,
has_default: false,
oidx: None
}
],
no_main_func: None,

View File

@@ -2,6 +2,7 @@
use anyhow::anyhow;
use lazy_static::lazy_static;
#[cfg(not(target_arch = "wasm32"))]
use regex::Regex;
#[cfg(target_arch = "wasm32")]
@@ -491,13 +492,15 @@ fn parse_pg_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let mut args = vec![];
let mut hm: HashMap<i32, String> = HashMap::new();
for cap in RE_CODE_PGSQL.captures_iter(code) {
let typ = cap
.get(2)
.map(|cap| transform_types_with_spaces(&cap, &code))
.unwrap_or("text");
hm.insert(
cap.get(1)
.and_then(|x| x.as_str().parse::<i32>().ok())
.ok_or_else(|| anyhow!("Impossible to parse arg digit"))?,
cap.get(2)
.map(|x| x.as_str().to_string())
.unwrap_or_else(|| "text".to_string()),
typ.to_string(),
);
}
for (i, v) in hm.iter() {
@@ -543,6 +546,37 @@ fn parse_pg_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
Ok(Some(args))
}
// The regex doesn't parse types with space such as "character varying"
// So we look for them manually and replace them with their shorter counterpart
fn transform_types_with_spaces<'a>(cap: &regex::Match<'a>, code: &str) -> &'a str {
lazy_static! {
static ref TYPES: [(&'static str, &'static str); 6] = [
("character varying", "varchar"),
("double precision", "double"),
("time with time zone", "timetz"),
("time without time zone", "time"),
("timestamp with time zone", "timestamptz"),
("timestamp without time zone", "timestamp"),
];
}
let typ = &code[cap.start()..];
for (long_type, alias) in TYPES.iter() {
let mut typ = typ;
let mut found_mismatch = false;
for token in long_type.split(' ') {
if typ.len() < token.len() || !typ[..token.len()].eq_ignore_ascii_case(token) {
found_mismatch = true;
break;
}
typ = typ[token.len()..].trim_start();
}
if !found_mismatch {
return alias;
}
}
cap.as_str()
}
pub fn parse_sql_statement_named_params(code: &str, prefix: char) -> HashSet<String> {
let mut arg_names = HashSet::new();
run_on_sql_statement_matches(

View File

@@ -10,9 +10,10 @@ use monitor::{
load_base_url, load_otel, reload_critical_alerts_on_db_oversize,
reload_delete_logs_periodically_setting, reload_indexer_config,
reload_instance_python_version_setting, reload_maven_repos_setting,
reload_no_default_maven_setting, reload_nuget_config_setting, reload_ruby_repos_setting,
reload_timeout_wait_result_setting, send_current_log_file_to_object_store,
send_logs_to_object_store, WORKERS_NAMES,
reload_no_default_maven_setting, reload_nuget_config_setting,
reload_powershell_repo_pat_setting, reload_powershell_repo_url_setting,
reload_ruby_repos_setting, reload_timeout_wait_result_setting,
send_current_log_file_to_object_store, send_logs_to_object_store, WORKERS_NAMES,
};
use rand::Rng;
use sqlx::postgres::PgListener;
@@ -45,7 +46,8 @@ use windmill_common::{
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING,
LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING,
NO_DEFAULT_MAVEN_SETTING, NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OAUTH_SETTING,
OTEL_SETTING, PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
OTEL_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING,
POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
RUBY_REPOS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TEAMS_SETTING,
TIMEOUT_WAIT_RESULT_SETTING,
@@ -1054,6 +1056,12 @@ Windmill Community Edition {GIT_VERSION}
NUGET_CONFIG_SETTING => {
reload_nuget_config_setting(&conn).await
},
POWERSHELL_REPO_URL_SETTING => {
reload_powershell_repo_url_setting(&conn).await
},
POWERSHELL_REPO_PAT_SETTING => {
reload_powershell_repo_pat_setting(&conn).await
},
MAVEN_REPOS_SETTING => {
reload_maven_repos_setting(&conn).await
},

View File

@@ -53,7 +53,8 @@ use windmill_common::{
HUB_BASE_URL_SETTING, INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING,
JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING,
MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING,
OTEL_SETTING, PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
OTEL_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING,
POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
},
@@ -83,7 +84,8 @@ use windmill_queue::{cancel_job, MiniPulledJob, SameWorkerPayload};
use windmill_worker::{
handle_job_error, JobCompletedSender, SameWorkerSender, BUNFIG_INSTALL_SCOPES,
INSTANCE_PYTHON_VERSION, JOB_DEFAULT_TIMEOUT, KEEP_JOB_DIR, MAVEN_REPOS, NO_DEFAULT_MAVEN,
NPM_CONFIG_REGISTRY, NUGET_CONFIG, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL,
NPM_CONFIG_REGISTRY, NUGET_CONFIG, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, POWERSHELL_REPO_PAT,
POWERSHELL_REPO_URL,
};
#[cfg(feature = "parquet")]
@@ -298,6 +300,8 @@ pub async fn initial_load(
reload_bunfig_install_scopes_setting(&conn).await;
reload_instance_python_version_setting(&conn).await;
reload_nuget_config_setting(&conn).await;
reload_powershell_repo_url_setting(&conn).await;
reload_powershell_repo_pat_setting(&conn).await;
reload_maven_repos_setting(&conn).await;
reload_no_default_maven_setting(&conn).await;
reload_ruby_repos_setting(&conn).await;
@@ -867,6 +871,16 @@ pub async fn delete_expired_items(db: &DB) -> () {
tracing::error!("Error deleting audit log on CE: {:?}", e);
}
if let Err(e) = sqlx::query_scalar!(
"DELETE FROM autoscaling_event WHERE applied_at <= now() - ($1::bigint::text || ' s')::interval",
30 * 24 * 60 * 60, // 30 days
)
.fetch_all(db)
.await
{
tracing::error!("Error deleting autoscaling event on CE: {:?}", e);
}
match sqlx::query_scalar!(
"DELETE FROM agent_token_blacklist WHERE expires_at <= now() RETURNING token",
)
@@ -950,6 +964,17 @@ pub async fn delete_expired_items(db: &DB) -> () {
{
tracing::error!("Error deleting job: {:?}", e);
}
// should already be deleted but just in case
if let Err(e) = sqlx::query!(
"DELETE FROM job_result_stream_v2 WHERE job_id = ANY($1)",
&deleted_jobs
)
.execute(&mut *tx)
.await
{
tracing::error!("Error deleting job result stream: {:?}", e);
}
}
}
Err(e) => {
@@ -1104,6 +1129,26 @@ pub async fn reload_nuget_config_setting(conn: &Connection) {
.await;
}
pub async fn reload_powershell_repo_url_setting(conn: &Connection) {
reload_option_setting_with_tracing(
conn,
POWERSHELL_REPO_URL_SETTING,
"POWERSHELL_REPO_URL",
POWERSHELL_REPO_URL.clone(),
)
.await;
}
pub async fn reload_powershell_repo_pat_setting(conn: &Connection) {
reload_option_setting_with_tracing(
conn,
POWERSHELL_REPO_PAT_SETTING,
"POWERSHELL_REPO_PAT",
POWERSHELL_REPO_PAT.clone(),
)
.await;
}
pub async fn reload_maven_repos_setting(conn: &Connection) {
reload_option_setting_with_tracing(
conn,
@@ -1654,7 +1699,7 @@ pub async fn monitor_db(
}
async fn vacuuming_tables(db: &Pool<Postgres>) -> error::Result<()> {
sqlx::query!("VACUUM v2_job, v2_job_completed, job_result_stream, job_stats, job_logs, concurrency_key, log_file, metrics")
sqlx::query!("VACUUM v2_job, v2_job_completed, job_result_stream_v2, job_stats, job_logs, concurrency_key, log_file, metrics")
.execute(db)
.await?;
Ok(())

View File

@@ -156,6 +156,7 @@ impl RunJob {
None,
None,
false,
None,
)
.await
.expect("push has to succeed");

View File

@@ -2302,7 +2302,7 @@ async fn test_rust_client(db: Pool<Postgres>) -> anyhow::Result<()> {
Ok(())
}
#[cfg(feature = "enterprise")]
#[cfg(all(feature = "enterprise", feature = "private"))]
#[sqlx::test(fixtures("base", "schedule"))]
async fn test_script_schedule_handlers(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
@@ -2459,7 +2459,7 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) -> anyhow::Result<()>
Ok(())
}
#[cfg(feature = "enterprise")]
#[cfg(all(feature = "enterprise", feature = "private"))]
#[sqlx::test(fixtures("base", "schedule"))]
async fn test_flow_schedule_handlers(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;

View File

@@ -1,5 +1,7 @@
#!/usr/bin/env bash
set -e
# Default directory
EE_DIR="../windmill-ee-private"
@@ -21,9 +23,17 @@ if [[ "$(uname)" == "Darwin" ]]; then
sed -i '' 's/^samael = { version="0.0.14", features = \["xmlsec"\] }/#samael = { version="0.0.14", features = ["xmlsec"] }/' Cargo.toml
# Uncomment the git-based samael dependency
sed -i '' 's/^# \(samael = { git="https:\/\/github.com\/njaremko\/samael", rev="464d015e3ae393e4b5dd00b4d6baa1b617de0dd6", features = \["xmlsec"\] }\)/\1/' Cargo.toml
# Run cargo sqlx prepare with deno_core_mac
echo "Running cargo sqlx prepare with deno_core_mac..."
cargo sqlx prepare --workspace -- --all-targets --features all_sqlx_features,private,deno_core_mac
else
# Run cargo sqlx prepare
echo "Running cargo sqlx prepare..."
cargo sqlx prepare --workspace -- --all-targets --features all_sqlx_features,private
fi
cargo sqlx prepare --workspace -- --all-targets --features all_sqlx_features,private
# Undo the samael changes on macOS
if [[ "$(uname)" == "Darwin" ]]; then

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.548.2
version: 1.555.2
title: Windmill API
contact:
@@ -575,6 +575,27 @@ paths:
text/plain:
schema:
type: string
/w/{workspace}/users/convert_to_group/{username}:
post:
summary: convert manual user to group user (require admin privilege)
operationId: convertUserToGroup
tags:
- user
- admin
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: username
in: path
required: true
schema:
type: string
responses:
"200":
description: convert user to group user
content:
text/plain:
schema:
type: string
/github_app/connected_repositories:
get:
@@ -749,34 +770,26 @@ paths:
schema:
type: boolean
/settings/databases_exist:
/settings/get_ducklake_instance_catalog_db_status:
post:
summary: checks that all given databases exist or else return the ones that don't
operationId: databasesExist
summary: Returns the set-up statuses of ducklake instance catalog dbs
operationId: getDucklakeInstanceCatalogDbStatus
tags:
- setting
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
type: string
responses:
"200":
description: databases that do not exist
description: Statuses of all ducklake instance catalog dbs
content:
application/json:
schema:
type: array
items:
type: string
type: object
additionalProperties:
$ref: "#/components/schemas/DucklakeInstanceCatalogDbStatus"
/settings/create_ducklake_database/{name}:
/settings/setup_ducklake_catalog_db/{name}:
post:
summary: Runs CREATE DATABASE on the Windmill Postgres and grants access to the ducklake_user
operationId: createDucklakeDatabase
operationId: setupDucklakeCatalogDb
tags:
- setting
parameters:
@@ -791,7 +804,8 @@ paths:
description: status
content:
application/json:
schema: {}
schema:
$ref: "#/components/schemas/DucklakeInstanceCatalogDbStatus"
/settings/global/{key}:
get:
@@ -2285,7 +2299,6 @@ paths:
channel_id:
type: string
/w/{workspace}/workspaces/connect_teams:
post:
summary: connect teams
@@ -3948,7 +3961,6 @@ paths:
items:
type: string
/teams/activities:
post:
summary: send update to Microsoft Teams activity
@@ -5708,6 +5720,12 @@ paths:
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
requestBody:
description: script args
@@ -5724,6 +5742,230 @@ paths:
application/json:
schema: {}
/w/{workspace}/jobs/run_and_stream/f/{path}:
post:
summary: run flow by path and stream updates via SSE
operationId: runAndStreamFlowByPath
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
- name: poll_delay_ms
description: delay between polling for job updates in milliseconds
in: query
schema:
type: integer
format: int64
requestBody:
description: flow args
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ScriptArgs"
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
get:
summary: run flow by path with GET and stream updates via SSE
operationId: runAndStreamFlowByPathGet
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/Payload"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
- name: poll_delay_ms
description: delay between polling for job updates in milliseconds
in: query
schema:
type: integer
format: int64
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
/w/{workspace}/jobs/run_and_stream/p/{path}:
post:
summary: run script by path and stream updates via SSE
operationId: runAndStreamScriptByPath
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/CacheTtl"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: poll_delay_ms
description: delay between polling for job updates in milliseconds
in: query
schema:
type: integer
format: int64
requestBody:
description: script args
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ScriptArgs"
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
get:
summary: run script by path with GET and stream updates via SSE
operationId: runAndStreamScriptByPathGet
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/CacheTtl"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/Payload"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: poll_delay_ms
description: delay between polling for job updates in milliseconds
in: query
schema:
type: integer
format: int64
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
/w/{workspace}/jobs/run_and_stream/h/{hash}:
post:
summary: run script by hash and stream updates via SSE
operationId: runAndStreamScriptByHash
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: hash
in: path
required: true
schema:
type: string
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/CacheTtl"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: poll_delay_ms
description: delay between polling for job updates in milliseconds
in: query
schema:
type: integer
format: int64
requestBody:
description: script args
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ScriptArgs"
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
get:
summary: run script by hash with GET and stream updates via SSE
operationId: runAndStreamScriptByHashGet
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: hash
in: path
required: true
schema:
type: string
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/CacheTtl"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/Payload"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: poll_delay_ms
description: delay between polling for job updates in milliseconds
in: query
schema:
type: integer
format: int64
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
/w/{workspace}/jobs/result_by_id/{flow_job_id}/{node_id}:
get:
summary: get job result by id
@@ -6231,6 +6473,81 @@ paths:
schema:
type: string
/w/{workspace}/flow_conversations/list:
get:
summary: list flow conversations
operationId: listFlowConversations
tags:
- flow_conversation
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
- name: flow_path
description: filter conversations by flow path
in: query
schema:
type: string
responses:
"200":
description: flow conversations list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/FlowConversation"
/w/{workspace}/flow_conversations/delete/{conversation_id}:
delete:
summary: delete flow conversation
operationId: deleteFlowConversation
tags:
- flow_conversation
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: conversation_id
description: conversation id
in: path
required: true
schema:
type: string
format: uuid
responses:
"200":
description: flow conversation deleted
content:
text/plain:
schema:
type: string
/w/{workspace}/flow_conversations/{conversation_id}/messages:
get:
summary: list conversation messages
operationId: listConversationMessages
tags:
- flow_conversation
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: conversation_id
description: conversation id
in: path
required: true
schema:
type: string
format: uuid
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: conversation messages
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/FlowConversationMessage"
/w/{workspace}/raw_apps/list:
get:
summary: list all raw apps
@@ -6679,6 +6996,23 @@ paths:
schema:
type: string
/w/{workspace}/apps/secret_of_latest_version/{path}:
get:
summary: get public secret of latest version of an app bundle
operationId: getPublicSecretOfLatestVersionOfApp
tags:
- app
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: app secret
content:
text/plain:
schema:
type: string
/w/{workspace}/apps/get/v/{id}:
get:
summary: get app by version
@@ -7103,6 +7437,12 @@ paths:
in: query
schema:
type: boolean
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
requestBody:
description: flow args
required: true
@@ -12058,6 +12398,8 @@ paths:
required: true
schema:
type: string
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: List of autoscaling events
@@ -14418,6 +14760,66 @@ components:
# -- INLINE END --
# Do not change line above
FlowConversation:
type: object
required:
[id, workspace_id, flow_path, created_at, updated_at, created_by]
properties:
id:
type: string
format: uuid
description: Unique identifier for the conversation
workspace_id:
type: string
description: The workspace ID where the conversation belongs
flow_path:
type: string
description: Path of the flow this conversation is for
title:
type: string
description: Optional title for the conversation
nullable: true
created_at:
type: string
format: date-time
description: When the conversation was created
updated_at:
type: string
format: date-time
description: When the conversation was last updated
created_by:
type: string
description: Username who created the conversation
FlowConversationMessage:
type: object
required: [id, conversation_id, message_type, content, created_at]
properties:
id:
type: string
format: uuid
description: Unique identifier for the message
conversation_id:
type: string
format: uuid
description: The conversation this message belongs to
message_type:
type: string
enum: [user, assistant, system]
description: Type of the message
content:
type: string
description: The message content
job_id:
type: string
format: uuid
nullable: true
description: Associated job ID if this message came from a flow run
created_at:
type: string
format: date-time
description: When the message was created
EndpointTool:
type: object
required: [name, description, instructions, path, method]
@@ -15353,6 +15755,8 @@ components:
AuditLog:
type: object
properties:
workspace_id:
type: string
id:
type: integer
timestamp:
@@ -15458,6 +15862,7 @@ components:
span:
type: string
required:
- workspace_id
- id
- timestamp
- username
@@ -16921,6 +17326,49 @@ components:
- enabled
- aws_auth_resource_type
LoggedWizardStatus:
type: string
enum:
- OK
- SKIP
- FAIL
DucklakeInstanceCatalogDbStatusLogs:
type: object
properties:
super_admin:
$ref: "#/components/schemas/LoggedWizardStatus"
database_credentials:
$ref: "#/components/schemas/LoggedWizardStatus"
valid_dbname:
$ref: "#/components/schemas/LoggedWizardStatus"
created_database:
$ref: "#/components/schemas/LoggedWizardStatus"
description: Created database status log
db_connect:
$ref: "#/components/schemas/LoggedWizardStatus"
grant_permissions:
$ref: "#/components/schemas/LoggedWizardStatus"
DucklakeInstanceCatalogDbStatus:
type: object
required:
- logs
- success
properties:
logs:
$ref: "#/components/schemas/DucklakeInstanceCatalogDbStatusLogs"
success:
type: boolean
description: Whether the operation completed successfully
example: true
error:
type: string
nullable: true
description: Error message if the operation failed
example: "Connection timeout"
NewSqsTrigger:
type: object
properties:
@@ -17615,11 +18063,14 @@ components:
created_by:
type: string
nullable: true
disabled:
type: boolean
required:
- id
- name
- username
- color
- disabled
required:
- email
- workspaces

View File

@@ -17,7 +17,7 @@ use crate::db::DB;
use axum::Router;
#[cfg(not(feature = "private"))]
pub fn global_service() -> Router {
pub fn global_service(_job_completed_tx: windmill_worker::JobCompletedSender) -> Router {
Router::new()
}

View File

@@ -88,6 +88,10 @@ pub fn workspaced_service() -> Router {
.route("/get/lite/*path", get(get_app_lite))
.route("/get/draft/*path", get(get_app_w_draft))
.route("/secret_of/*path", get(get_secret_id))
.route(
"/secret_of_latest_version/*path",
get(get_latest_version_secret_id),
)
.route("/get/v/*id", get(get_app_by_id))
.route("/get_data/v/*id", get(get_raw_app_data))
.route("/exists/*path", get(exists_app))
@@ -389,19 +393,83 @@ async fn list_apps(
Ok(Json(rows))
}
async fn get_raw_app_data(Path((w_id, version_id)): Path<(String, String)>) -> Result<Response> {
let file_path = format!("/tmp/wmill/{}/{}", w_id, version_id);
let file = tokio::fs::File::open(file_path).await?;
let stream = tokio_util::io::ReaderStream::new(file);
let res = Response::builder().header(
http::header::CONTENT_TYPE,
if version_id.ends_with(".css") {
"text/css"
} else {
"text/javascript"
},
);
Ok(res.body(Body::from_stream(stream)).unwrap())
async fn get_raw_app_data(
Path((w_id, secret_with_ext)): Path<(String, String)>,
Extension(db): Extension<DB>,
) -> Result<Response> {
#[cfg(all(feature = "enterprise", feature = "parquet"))]
let object_store = windmill_common::s3_helpers::get_object_store().await;
// tracing::info!("secret_with_ext: {}", secret_with_ext);
let mut splitted = secret_with_ext.split('.');
let secret_id = splitted.next().unwrap_or("");
if secret_id.is_empty() {
return Err(Error::BadRequest("Invalid secret".to_string()));
}
let id = get_id_from_secret(
&db,
&w_id,
secret_id.to_string(),
Some(BUNDLE_SECRET_PREFIX),
)
.await?;
let file_type = splitted.next().unwrap_or("");
let file_type = if file_type == "css" {
"css"
} else if file_type == "js" {
"js"
} else {
return Err(Error::BadRequest(
"Invalid file type, only .css and .js are supported".to_string(),
));
};
// tracing::info!("file_type: {}", file_type);
#[allow(unused_assignments)]
let mut body: Option<Body> = None;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = object_store {
let path = format!("/app_bundles/{}/{}.{}", w_id, id, file_type);
let stream = os
.get(&object_store::path::Path::from(path))
.await?
.bytes()
.await?;
tracing::info!("stream: {}", stream.len());
body = Some(Body::from(stream));
}
if body.is_none() {
let get_raw_app_file = sqlx::query_scalar!(
"SELECT data FROM app_bundles WHERE app_version_id = $1 AND file_type = $2 AND w_id = $3",
id,
file_type,
&w_id,
)
.fetch_optional(&db)
.await?;
if let Some(file) = get_raw_app_file {
body = Some(Body::from(file));
}
}
if let Some(body) = body {
// let stream = tokio_util::io::ReaderStream::new(file);
let res = Response::builder().header(
http::header::CONTENT_TYPE,
if file_type == "css" {
"text/css"
} else {
"text/javascript"
},
);
Ok(res.body(body).unwrap())
} else {
return Err(Error::NotFound("File not found".to_string()));
}
}
// async fn get_app_version(
@@ -692,14 +760,7 @@ async fn get_public_app_by_secret(
Extension(db): Extension<DB>,
Path((w_id, secret)): Path<(String, String)>,
) -> JsonResult<AppWithLastVersion> {
let mc = build_crypt(&db, &w_id).await?;
let decrypted = mc
.decrypt_bytes_to_bytes(&(hex::decode(secret)?))
.map_err(|e| Error::internal_err(e.to_string()))?;
let bytes = str::from_utf8(&decrypted).map_err(to_anyhow)?;
let id: i64 = bytes.parse().map_err(to_anyhow)?;
let id = get_id_from_secret(&db, &w_id, secret, None).await?;
let app_o = sqlx::query_as::<_, AppWithLastVersion>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
@@ -747,6 +808,27 @@ async fn get_public_app_by_secret(
Ok(Json(app))
}
async fn get_id_from_secret(
db: &DB,
w_id: &str,
secret: String,
prefix: Option<&str>,
) -> Result<i64> {
let mc = build_crypt(db, w_id).await?;
let decrypted = mc
.decrypt_bytes_to_bytes(&(hex::decode(secret)?))
.map_err(|e| Error::internal_err(e.to_string()))?;
let mut bytes = str::from_utf8(&decrypted).map_err(to_anyhow)?;
if let Some(prefix) = prefix {
if !bytes.starts_with(prefix) {
return Err(Error::BadRequest("Invalid secret".to_string()));
}
bytes = bytes.strip_prefix(prefix).unwrap_or("");
}
let id: i64 = bytes.parse().map_err(to_anyhow)?;
Ok(id)
}
async fn get_public_resource(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
@@ -803,16 +885,85 @@ async fn get_secret_id(
Ok(hx)
}
const BUNDLE_SECRET_PREFIX: &str = "bundle_";
async fn get_latest_version_secret_id(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
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!(
"SELECT app.versions[array_upper(app.versions, 1)] FROM app
WHERE app.path = $1 AND app.workspace_id = $2",
path,
&w_id
)
.fetch_optional(&mut *tx)
.await?
.flatten();
tx.commit().await?;
let id = not_found_if_none(id_o, "App", path.to_string())?;
let mc = build_crypt(&db, &w_id).await?;
let hx = hex::encode(mc.encrypt_str_to_bytes(format!("{}{}", BUNDLE_SECRET_PREFIX, id)));
Ok(hx)
}
async fn store_raw_app_file<'a>(
w_id: &str,
id: &i64,
file_type: &str,
data: bytes::Bytes,
tx: &mut sqlx::Transaction<'a, sqlx::Postgres>,
) -> Result<()> {
#[cfg(all(feature = "enterprise", feature = "parquet"))]
{
let object_store = windmill_common::s3_helpers::get_object_store().await;
let path: String = format!("/app_bundles/{}/{}.{}", w_id, id, file_type);
if let Some(os) = object_store {
if let Err(e) = os
.put(&object_store::path::Path::from(path.clone()), data.into())
.await
{
tracing::error!("Failed to put snapshot to s3 at {path}: {:?}", e);
return Err(windmill_common::error::Error::ExecutionErr(format!(
"Failed to put {path} to s3"
)));
}
tracing::info!("Successfully put snapshot to s3 at {path}");
return Ok(());
}
}
sqlx::query!(
"INSERT INTO app_bundles (app_version_id, w_id, file_type, data) VALUES ($1, $2, $3, $4)",
id,
w_id,
file_type,
data.to_vec()
)
.execute(&mut **tx)
.await?;
Ok(())
}
macro_rules! process_app_multipart {
($authed:expr, $user_db:expr, $db:expr, $w_id:expr, $path:expr, $multipart:expr, $internal_fn:expr) => {
async {
let mut saved_app = None;
let mut uploaded_js = false;
//todo: use s3 instead
let file_path = format!("/tmp/wmill/{}", $w_id);
std::fs::create_dir_all(&file_path).unwrap();
let mut multipart = $multipart;
while let Some(field) = multipart.next_field().await.unwrap() {
let name = field.name().unwrap().to_string();
@@ -831,9 +982,8 @@ macro_rules! process_app_multipart {
.await?;
saved_app = Some((npath, nid, ntx));
} else if name == "js" {
if let Some((_npath, id, _tx)) = saved_app.as_ref() {
let file_path = format!("{}/{}.js", file_path, id);
std::fs::write(file_path, data).unwrap();
if let Some((_npath, id, tx)) = saved_app.as_mut() {
store_raw_app_file($w_id, &id, "js", data, tx).await?;
uploaded_js = true;
} else {
return Err(Error::BadRequest(
@@ -841,9 +991,8 @@ macro_rules! process_app_multipart {
));
}
} else if name == "css" {
if let Some((_npath, id, _tx)) = saved_app.as_ref() {
let file_path = format!("{}/{}.css", file_path, id);
std::fs::write(file_path, data).unwrap();
if let Some((_npath, id, tx)) = saved_app.as_mut() {
store_raw_app_file($w_id, &id, "css", data, tx).await?;
} else {
return Err(Error::BadRequest(
"App payload need to be created first".to_string(),
@@ -1090,6 +1239,7 @@ async fn create_app_internal<'a>(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
@@ -1469,6 +1619,7 @@ async fn update_app_internal<'a>(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
@@ -1755,6 +1906,8 @@ async fn execute_component(
(email.as_str(), permissioned_as)
};
let end_user_email = opt_authed.as_ref().map(|a| a.email.clone());
let (uuid, tx) = push(
&db,
tx,
@@ -1784,6 +1937,7 @@ async fn execute_component(
None,
None,
false,
end_user_email,
)
.await?;
tx.commit().await?;

View File

@@ -121,11 +121,10 @@ impl AuthCache {
match jwt_result {
Ok(claims) => {
if w_id.is_some_and(|w_id| w_id != claims.workspace_id) {
if w_id.is_some_and(|w_id| !claims.allowed_in_workspace(&w_id)) {
tracing::error!("JWT auth error: workspace_id mismatch");
return None;
}
let username_override = username_override_from_label(claims.label);
let authed = crate::db::ApiAuthed {
email: claims.email,

View File

@@ -7,7 +7,7 @@
*/
use axum::{
extract::{Extension, Path},
extract::{Extension, Path, Query},
routing::{get, post},
Json, Router,
};
@@ -18,6 +18,7 @@ use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_common::{
error::{self},
utils::Pagination,
worker::MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS,
DB,
};
@@ -239,11 +240,19 @@ struct AutoscalingEvent {
async fn list_autoscaling_events(
Extension(db): Extension<DB>,
Path(worker_group): Path<String>,
Query(mut pagination): Query<Pagination>,
) -> error::JsonResult<Vec<AutoscalingEvent>> {
if pagination.per_page.is_none() {
pagination.per_page = Some(5);
}
let (per_page, offset) = windmill_common::utils::paginate(pagination);
let events = sqlx::query_as!(
AutoscalingEvent,
"SELECT id, worker_group, event_type::text, desired_workers, reason, applied_at FROM autoscaling_event WHERE worker_group = $1 ORDER BY applied_at DESC LIMIT 5",
worker_group
"SELECT id, worker_group, event_type::text, desired_workers, reason, applied_at FROM autoscaling_event WHERE worker_group = $1 ORDER BY applied_at DESC LIMIT $2 OFFSET $3",
worker_group,
per_page as i64,
offset as i64
)
.fetch_all(&db)
.await?;
@@ -263,8 +272,7 @@ async fn native_kubernetes_autoscaling_healthcheck(
}
#[cfg(not(all(feature = "enterprise", feature = "private")))]
async fn native_kubernetes_autoscaling_healthcheck(
) -> Result<(), error::Error> {
async fn native_kubernetes_autoscaling_healthcheck() -> Result<(), error::Error> {
Err(error::Error::BadRequest(
"Native Kubernetes autoscaling available only in the enterprise version".to_string(),
))

View File

@@ -0,0 +1,297 @@
use axum::{
extract::{Path, Query},
routing::{delete, get},
Extension, Json, Router,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sql_builder::prelude::*;
use sqlx::{FromRow, Postgres};
use uuid::Uuid;
use crate::db::ApiAuthed;
use windmill_common::{
db::UserDB,
error::{JsonResult, Result},
utils::{not_found_if_none, paginate, Pagination},
};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
#[sqlx(type_name = "MESSAGE_TYPE", rename_all = "lowercase")]
#[serde(rename_all = "lowercase")]
pub enum MessageType {
User,
Assistant,
}
pub fn workspaced_service() -> Router {
Router::new()
.route("/list", get(list_conversations))
.route("/delete/:conversation_id", delete(delete_conversation))
.route("/:conversation_id/messages", get(list_messages))
}
#[derive(Serialize, FromRow, Debug)]
pub struct FlowConversation {
pub id: Uuid,
pub workspace_id: String,
pub flow_path: String,
pub title: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub created_by: String,
}
#[derive(Serialize, FromRow, Debug)]
pub struct FlowConversationMessage {
pub id: Uuid,
pub conversation_id: Uuid,
pub message_type: MessageType,
pub content: String,
pub job_id: Option<Uuid>,
pub created_at: DateTime<Utc>,
}
#[derive(Deserialize)]
pub struct ListConversationsQuery {
pub flow_path: Option<String>,
}
async fn list_conversations(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(query): Query<ListConversationsQuery>,
) -> JsonResult<Vec<FlowConversation>> {
let (per_page, offset) = paginate(pagination);
let mut tx = user_db.clone().begin(&authed).await?;
let mut sqlb = SqlBuilder::select_from("flow_conversation");
sqlb.fields(&[
"id",
"workspace_id",
"flow_path",
"title",
"created_at",
"updated_at",
"created_by",
])
.and_where_eq("workspace_id", "?".bind(&w_id));
if let Some(flow_path) = &query.flow_path {
sqlb.and_where_eq("flow_path", "?".bind(flow_path));
}
sqlb.order_by("updated_at", true)
.limit(per_page as i64)
.offset(offset as i64);
let sql = sqlb.sql().map_err(|e| {
windmill_common::error::Error::internal_err(format!("Failed to build SQL: {}", e))
})?;
let conversations = sqlx::query_as::<Postgres, FlowConversation>(&sql)
.fetch_all(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(conversations))
}
pub async fn get_or_create_conversation_with_id(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
w_id: &str,
flow_path: &str,
username: &str,
title: &str,
conversation_id: Uuid,
) -> Result<FlowConversation> {
// Check if conversation already exists
let existing_conversation = sqlx::query_as!(
FlowConversation,
"SELECT id, workspace_id, flow_path, title, created_at, updated_at, created_by
FROM flow_conversation
WHERE id = $1 AND workspace_id = $2",
conversation_id,
w_id
)
.fetch_optional(&mut **tx)
.await?;
if let Some(existing) = existing_conversation {
return Ok(existing);
}
// Truncate title to 25 char characters max
let title = if title.len() > 25 {
format!("{}...", &title[..25])
} else {
title.to_string()
};
// Create new conversation with provided ID
let conversation = sqlx::query_as!(
FlowConversation,
"INSERT INTO flow_conversation (id, workspace_id, flow_path, created_by, title)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, workspace_id, flow_path, title, created_at, updated_at, created_by",
conversation_id,
w_id,
flow_path,
username,
title
)
.fetch_one(&mut **tx)
.await?;
Ok(conversation)
}
async fn delete_conversation(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, conversation_id)): Path<(String, Uuid)>,
) -> Result<String> {
let mut tx = user_db.clone().begin(&authed).await?;
// Verify the conversation exists and belongs to the user
let conversation = sqlx::query_as!(
FlowConversation,
"SELECT id, workspace_id, flow_path, title, created_at, updated_at, created_by
FROM flow_conversation
WHERE id = $1 AND workspace_id = $2",
conversation_id,
&w_id
)
.fetch_optional(&mut *tx)
.await?;
not_found_if_none(conversation, "Conversation", conversation_id.to_string())?;
// Delete the conversation (messages will be cascade deleted)
sqlx::query!(
"DELETE FROM flow_conversation WHERE id = $1 AND workspace_id = $2",
conversation_id,
&w_id
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
// Delete associated memory in background (non-blocking cleanup)
let w_id_clone = w_id.clone();
tokio::spawn(async move {
if let Err(e) =
windmill_worker::memory_oss::delete_conversation_memory(&w_id_clone, conversation_id)
.await
{
tracing::error!(
"Failed to delete memory for conversation {} in workspace {}: {:?}",
conversation_id,
w_id_clone,
e
);
}
});
Ok(format!("Conversation {} deleted", conversation_id))
}
async fn list_messages(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, conversation_id)): Path<(String, Uuid)>,
Query(pagination): Query<Pagination>,
) -> JsonResult<Vec<FlowConversationMessage>> {
let (per_page, offset) = paginate(pagination);
let mut tx = user_db.clone().begin(&authed).await?;
// Verify the conversation exists and belongs to the user
let conversation_exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM flow_conversation WHERE id = $1 AND workspace_id = $2)",
conversation_id,
&w_id
)
.fetch_one(&mut *tx)
.await?
.unwrap_or(false);
if !conversation_exists {
return Err(windmill_common::error::Error::NotFound(format!(
"Conversation not found or access denied: {}",
conversation_id
)));
}
// Fetch messages for this conversation, oldest first, but reverse the order of the messages for easy rendering on the frontend
let messages = sqlx::query_as!(
FlowConversationMessage,
r#"SELECT id, conversation_id, message_type as "message_type: MessageType", content, job_id, created_at
FROM (
SELECT id, conversation_id, message_type, content, job_id, created_at
FROM flow_conversation_message
WHERE conversation_id = $1
ORDER BY created_at DESC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END
LIMIT $2 OFFSET $3
) AS messages
ORDER BY created_at ASC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END
"#,
conversation_id,
per_page as i64,
offset as i64
)
.fetch_all(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(messages))
}
// Helper function to create a message using an existing transaction
pub async fn create_message(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
conversation_id: Uuid,
message_type: MessageType,
content: &str,
job_id: Option<Uuid>,
workspace_id: &str,
) -> windmill_common::error::Result<()> {
// Verify the conversation exists and belongs to the user
let conversation_exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM flow_conversation WHERE id = $1 AND workspace_id = $2)",
conversation_id,
workspace_id
)
.fetch_one(&mut **tx)
.await?
.unwrap_or(false);
if !conversation_exists {
return Err(windmill_common::error::Error::NotFound(format!(
"Conversation not found or access denied: {}",
conversation_id
)));
}
// Insert the message
sqlx::query!(
"INSERT INTO flow_conversation_message (conversation_id, message_type, content, job_id)
VALUES ($1, $2, $3, $4)",
conversation_id,
message_type as MessageType,
content,
job_id
)
.execute(&mut **tx)
.await?;
// Update conversation updated_at timestamp
sqlx::query!(
"UPDATE flow_conversation SET updated_at = NOW() WHERE id = $1",
conversation_id
)
.execute(&mut **tx)
.await?;
Ok(())
}

View File

@@ -545,6 +545,7 @@ async fn create_flow(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
@@ -937,7 +938,7 @@ async fn update_flow(
clear_schedule(&mut tx, &schedule.path, &w_id).await?;
if schedule.enabled {
tx = push_scheduled_job(&db, tx, &schedule, None).await?;
tx = push_scheduled_job(&db, tx, &schedule, None, None).await?;
}
}
@@ -1012,6 +1013,7 @@ async fn update_flow(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
sqlx::query!(
@@ -1593,6 +1595,7 @@ mod tests {
priority: None,
early_return: None,
concurrency_key: None,
chat_input_enabled: None,
};
let expect = serde_json::json!({
"modules": [

View File

@@ -35,13 +35,13 @@ use windmill_common::jobs::{
check_tag_available_for_workspace_internal, format_completed_job_result, format_result,
DynamicInput, ENTRYPOINT_OVERRIDE,
};
use windmill_common::s3_helpers::{upload_artifact_to_store, BundleFormat};
use windmill_common::utils::{RunnableKind, WarnAfterExt};
use windmill_common::worker::{Connection, CLOUD_HOSTED, TMP_DIR};
use windmill_common::DYNAMIC_INPUT_CACHE;
#[cfg(all(feature = "enterprise", feature = "smtp"))]
use windmill_common::{email_oss::send_email_html, server::load_smtp_config};
use windmill_common::scripts::PREVIEW_IS_CODEBASE_HASH;
use windmill_common::variables::get_workspace_key;
use crate::triggers::trigger_helpers::ScriptId;
@@ -102,6 +102,8 @@ use windmill_queue::{
PushArgsOwned, PushIsolationLevel,
};
use crate::flow_conversations::{self, MessageType};
pub fn workspaced_service() -> Router {
let cors = CorsLayer::new()
.allow_methods([http::Method::GET, http::Method::POST])
@@ -175,6 +177,7 @@ pub fn workspaced_service() -> Router {
.route(
"/run_and_stream/f/*script_path",
get(stream_flow_by_path)
.post(stream_flow_by_path)
.head(|| async { "" })
.layer(cors.clone())
.layer(ce_headers.clone()),
@@ -182,6 +185,7 @@ pub fn workspaced_service() -> Router {
.route(
"/run_and_stream/p/*script_path",
get(stream_script_by_path)
.post(stream_script_by_path)
.head(|| async { "" })
.layer(cors.clone())
.layer(ce_headers.clone()),
@@ -189,6 +193,7 @@ pub fn workspaced_service() -> Router {
.route(
"/run_and_stream/h/:hash",
get(stream_script_by_hash)
.post(stream_script_by_hash)
.head(|| async { "" })
.layer(cors.clone())
.layer(ce_headers.clone()),
@@ -1752,6 +1757,7 @@ pub struct RunJobQuery {
pub cache_ttl: Option<i32>,
pub skip_preprocessor: Option<bool>,
pub poll_delay_ms: Option<u64>,
pub memory_id: Option<Uuid>,
}
impl RunJobQuery {
@@ -3523,6 +3529,7 @@ struct Preview {
tag: Option<String>,
dedicated_worker: Option<bool>,
lock: Option<String>,
format: Option<String>,
}
#[derive(Deserialize)]
@@ -3552,7 +3559,7 @@ pub enum DynamicSelectRunnableRef {
#[serde(rename = "deployed")]
Deployed { path: String, runnable_kind: RunnableKind },
#[serde(rename = "inline")]
Inline { code: String, language: Option<ScriptLang> },
Inline { code: String, lang: Option<ScriptLang> },
}
pub struct QueryOrBody<D>(pub Option<D>);
@@ -3907,6 +3914,91 @@ async fn batch_rerun_handle_job(
))
}
/// Set the memory_id in flow_status for agent memory persistence
async fn set_flow_memory_id(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
job_id: Uuid,
memory_id: Uuid,
) -> error::Result<()> {
sqlx::query!(
"UPDATE v2_job_status
SET flow_status = jsonb_set(
flow_status,
'{memory_id}',
to_jsonb($2::uuid)
)
WHERE id = $1",
job_id,
memory_id
)
.execute(&mut **tx)
.await?;
Ok(())
}
async fn handle_chat_conversation_messages(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
authed: &ApiAuthed,
w_id: &str,
flow_path: &str,
run_query: &RunJobQuery,
user_message_raw: Option<&Box<serde_json::value::RawValue>>,
uuid: Uuid,
) -> error::Result<()> {
let memory_id = run_query.memory_id.ok_or_else(|| {
windmill_common::error::Error::BadRequest(
"memory_id is required for chat-enabled flows".to_string(),
)
})?;
let user_message_raw = user_message_raw.ok_or_else(|| {
windmill_common::error::Error::BadRequest(
"user_message argument is required for chat-enabled flows".to_string(),
)
})?;
// Deserialize the RawValue to get the actual string without quotes
let user_message: String = serde_json::from_str(user_message_raw.get())
.map_err(|e| windmill_common::error::Error::BadRequest(
format!("Failed to deserialize user_message: {}", e)
))?;
// Create conversation with provided ID (or get existing one)
flow_conversations::get_or_create_conversation_with_id(
tx,
w_id,
flow_path,
&authed.username,
&user_message,
memory_id,
)
.await?;
// Create user message
flow_conversations::create_message(
tx,
memory_id,
MessageType::User,
&user_message,
None, // No job_id for user message
w_id,
)
.await?;
// Create placeholder assistant message in the same transaction as the job
flow_conversations::create_message(
tx,
memory_id,
MessageType::Assistant,
"", // Empty content, will be updated when job completes
Some(uuid), // Associate with the job
w_id,
)
.await?;
Ok(())
}
pub async fn run_flow_by_path(
authed: ApiAuthed,
Extension(db): Extension<DB>,
@@ -3953,6 +4045,7 @@ pub async fn run_flow_by_path_inner(
tag,
dedicated_worker,
has_preprocessor,
chat_input_enabled,
on_behalf_of_email,
edited_by,
early_return,
@@ -3978,11 +4071,11 @@ pub async fn run_flow_by_path_inner(
&authed.email,
username_to_permissioned_as(&authed.username),
Some(authed.clone().into()),
PushIsolationLevel::Isolated(user_db, authed.clone().into()),
PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into()),
)
};
let (uuid, tx) = push(
let (uuid, mut tx) = push(
&db,
tx,
&w_id,
@@ -4014,8 +4107,29 @@ pub async fn run_flow_by_path_inner(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
// Set memory_id if provided (for agent memory)
if let Some(memory_id) = run_query.memory_id {
set_flow_memory_id(&mut tx, uuid, memory_id).await?;
}
// Handle conversation messages for chat-enabled flows
if chat_input_enabled.unwrap_or(false) {
handle_chat_conversation_messages(
&mut tx,
&authed,
&w_id,
&flow_path.to_string(),
&run_query,
args.args.get("user_message"),
uuid,
)
.await?;
}
tx.commit().await?;
Ok((uuid, early_return))
}
@@ -4110,6 +4224,7 @@ pub async fn restart_flow(
completed_job.priority,
Some(&authed.clone().into()),
false,
None,
)
.await?;
tx.commit().await?;
@@ -4212,6 +4327,7 @@ pub async fn run_script_by_path_inner(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
tx.commit().await?;
@@ -4364,6 +4480,7 @@ pub async fn run_workflow_as_code(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
@@ -4907,6 +5024,7 @@ pub async fn run_wait_result_job_by_path_get(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
tx.commit().await?;
@@ -5059,6 +5177,7 @@ pub async fn run_wait_result_script_by_path_internal(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
tx.commit().await?;
@@ -5175,6 +5294,7 @@ pub async fn run_wait_result_script_by_hash(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
tx.commit().await?;
@@ -5220,6 +5340,7 @@ pub async fn stream_flow_by_path(
Extension(user_db): Extension<UserDB>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
method: hyper::http::Method,
args: RawWebhookArgs,
) -> error::Result<Response> {
stream_job(
@@ -5230,6 +5351,7 @@ pub async fn stream_flow_by_path(
RunnableId::from_flow_path(flow_path.to_path()),
args,
run_query,
method == http::Method::GET,
)
.await
}
@@ -5240,6 +5362,7 @@ pub async fn stream_script_by_path(
Extension(user_db): Extension<UserDB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
method: hyper::http::Method,
args: RawWebhookArgs,
) -> error::Result<Response> {
stream_job(
@@ -5250,6 +5373,7 @@ pub async fn stream_script_by_path(
RunnableId::from_script_path(script_path.to_path()),
args,
run_query,
method == http::Method::GET,
)
.await
}
@@ -5260,6 +5384,7 @@ pub async fn stream_script_by_hash(
Extension(user_db): Extension<UserDB>,
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
Query(run_query): Query<RunJobQuery>,
method: hyper::http::Method,
args: RawWebhookArgs,
) -> error::Result<Response> {
stream_job(
@@ -5270,6 +5395,7 @@ pub async fn stream_script_by_hash(
RunnableId::from_script_hash(script_hash),
args,
run_query,
method == http::Method::GET,
)
.await
}
@@ -5282,24 +5408,39 @@ pub async fn stream_job(
runnable_id: RunnableId,
args: RawWebhookArgs,
run_query: RunJobQuery,
is_get: bool,
) -> error::Result<Response> {
let payload_r = run_query.payload.clone().map(decode_payload).map(|x| {
x.map_err(|e| Error::internal_err(format!("Impossible to decode query payload: {e:#?}")))
});
let args = if is_get {
let payload_r = run_query.payload.clone().map(decode_payload).map(|x| {
x.map_err(|e| {
Error::internal_err(format!("Impossible to decode query payload: {e:#?}"))
})
});
let payload_args = if let Some(payload) = payload_r {
payload?
let payload_args = if let Some(payload) = payload_r {
payload?
} else {
HashMap::new()
};
let mut args = args.process_args(&authed, &db, &w_id, None).await?;
args.body = args::Body::HashMap(payload_args);
let args = args
.to_args_from_runnable(&db, &w_id, runnable_id.clone(), run_query.skip_preprocessor)
.await?;
args
} else {
HashMap::new()
args.to_args_from_runnable(
&authed,
&db,
&w_id,
runnable_id.clone(),
run_query.skip_preprocessor,
)
.await?
};
let mut args = args.process_args(&authed, &db, &w_id, None).await?;
args.body = args::Body::HashMap(payload_args);
let args = args
.to_args_from_runnable(&db, &w_id, runnable_id.clone(), run_query.skip_preprocessor)
.await?;
let poll_delay_ms = run_query.poll_delay_ms;
let uuid = match runnable_id {
RunnableId::ScriptId(ScriptId::ScriptPath(script_path))
@@ -5406,6 +5547,7 @@ pub async fn run_wait_result_flow_by_path_internal(
dedicated_worker,
early_return,
has_preprocessor,
chat_input_enabled,
on_behalf_of_email,
edited_by,
version,
@@ -5428,11 +5570,11 @@ pub async fn run_wait_result_flow_by_path_internal(
&authed.email,
username_to_permissioned_as(&authed.username),
Some(authed.clone().into()),
PushIsolationLevel::Isolated(user_db, authed.clone().into()),
PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into()),
)
};
let (uuid, tx) = push(
let (uuid, mut tx) = push(
&db,
tx,
&w_id,
@@ -5464,8 +5606,29 @@ pub async fn run_wait_result_flow_by_path_internal(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
// Set conversation_id if provided (for agent memory)
if let Some(memory_id) = run_query.memory_id {
set_flow_memory_id(&mut tx, uuid, memory_id).await?;
}
// Handle conversation messages for chat-enabled flows
if chat_input_enabled.unwrap_or(false) {
handle_chat_conversation_messages(
&mut tx,
&authed,
&w_id,
&flow_path.to_string(),
&run_query,
args.args.get("user_message"),
uuid,
)
.await?;
}
tx.commit().await?;
run_wait_result(&db, uuid, w_id, early_return, &authed.username).await
@@ -5535,6 +5698,7 @@ async fn run_preview_script(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
tx.commit().await?;
@@ -5574,8 +5738,6 @@ async fn run_bundle_preview_script(
Query(run_query): Query<RunJobQuery>,
mut multipart: axum::extract::Multipart,
) -> error::Result<(StatusCode, String)> {
use windmill_common::scripts::PREVIEW_IS_TAR_CODEBASE_HASH;
if authed.is_operator {
return Err(error::Error::NotAuthorized(
"Operators cannot run preview jobs for security reasons".to_string(),
@@ -5586,6 +5748,7 @@ async fn run_bundle_preview_script(
let mut tx = None;
let mut uploaded = false;
let mut is_tar = false;
let mut format = BundleFormat::Cjs;
while let Some(field) = multipart.next_field().await.unwrap() {
let name = field.name().unwrap().to_string();
@@ -5593,6 +5756,10 @@ async fn run_bundle_preview_script(
let data = data.map_err(to_anyhow)?;
if name == "preview" {
let preview: Preview = serde_json::from_slice(&data).map_err(to_anyhow)?;
format = preview
.format
.and_then(|s| BundleFormat::from_string(&s))
.unwrap_or(BundleFormat::Cjs);
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tag = run_query.tag.clone().or(preview.tag.clone());
@@ -5613,11 +5780,10 @@ async fn run_bundle_preview_script(
ltx,
&w_id,
JobPayload::Code(RawCode {
hash: if is_tar {
Some(PREVIEW_IS_TAR_CODEBASE_HASH)
} else {
Some(PREVIEW_IS_CODEBASE_HASH)
},
hash: Some(windmill_common::scripts::codebase_to_hash(
is_tar,
format == BundleFormat::Esm,
)),
content: preview.content.unwrap_or_default(),
path: preview.path,
language: preview.language.unwrap_or(ScriptLang::Deno),
@@ -5649,6 +5815,7 @@ async fn run_bundle_preview_script(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
job_id = Some(uuid);
@@ -5666,52 +5833,22 @@ async fn run_bundle_preview_script(
// tracing::info!("is_tar 2: {is_tar}");
if format == BundleFormat::Esm {
id = format!("{}.esm", id);
}
if is_tar {
id = format!("{}.tar", id);
}
uploaded = true;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
let object_store = windmill_common::s3_helpers::get_object_store().await;
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
let object_store: Option<()> = None;
if &windmill_common::utils::MODE_AND_ADDONS.mode
== &windmill_common::utils::Mode::Standalone
&& object_store.is_none()
{
std::fs::create_dir_all(
windmill_common::worker::ROOT_STANDALONE_BUNDLE_DIR.clone(),
)?;
windmill_common::worker::write_file_bytes(
&windmill_common::worker::ROOT_STANDALONE_BUNDLE_DIR,
&id,
&data,
)?;
} else {
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
{
return Err(Error::ExecutionErr("codebase is an EE feature".to_string()));
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = object_store {
check_license_key_valid().await?;
let path = windmill_common::s3_helpers::bundle(&w_id, &id);
if let Err(e) = os
.put(&object_store::path::Path::from(path.clone()), data.into())
.await
{
tracing::info!("Failed to put snapshot to s3 at {path}: {:?}", e);
return Err(Error::ExecutionErr(format!("Failed to put {path} to s3")));
}
} else {
return Err(Error::BadConfig("Object store is required for snapshot script and is not configured for servers".to_string()));
}
}
let path = windmill_common::s3_helpers::bundle(&w_id, &id);
upload_artifact_to_store(
&path,
data,
&windmill_common::worker::ROOT_STANDALONE_BUNDLE_DIR,
)
.await?;
}
// println!("Length of `{}` is {} bytes", name, data.len());
}
@@ -5816,6 +5953,7 @@ async fn run_dependencies_job(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
tx.commit().await?;
@@ -5883,6 +6021,7 @@ async fn run_flow_dependencies_job(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
tx.commit().await?;
@@ -6226,6 +6365,7 @@ async fn run_preview_flow_job(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
tx.commit().await?;
@@ -6351,7 +6491,7 @@ async fn run_dynamic_select(
dynamic_input = dynamic_input_res;
}
},
DynamicSelectRunnableRef::Inline { code, language } => {
DynamicSelectRunnableRef::Inline { code, lang: language } => {
dynamic_input = DynamicInput {
x_windmill_dyn_select_code: code,
x_windmill_dyn_select_lang: language.unwrap_or_default(),
@@ -6399,6 +6539,7 @@ async fn run_dynamic_select(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
tx.commit().await?;
@@ -6526,6 +6667,7 @@ pub async fn run_job_by_hash_inner(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
tx.commit().await?;
@@ -6678,7 +6820,7 @@ async fn get_job_update(
&job_id,
log_offset,
stream_offset,
get_progress,
get_progress.unwrap_or(false),
running,
true,
false,
@@ -6782,6 +6924,7 @@ fn start_job_update_sse_stream(
// Send initial update immediately
let mut running = running;
let mut mem_peak = 0;
match get_job_update_data(
&opt_authed,
&opt_tokened,
@@ -6790,7 +6933,7 @@ fn start_job_update_sse_stream(
&job_id,
log_offset,
stream_offset,
get_progress,
false,
running,
true,
true,
@@ -6847,11 +6990,12 @@ fn start_job_update_sse_stream(
}
}
let mut get_progress_m: bool = false;
// Poll for updates every 1 second
let mut i = 0;
let start = Instant::now();
let mut last_ping = Instant::now();
let mut last_progress_check = Instant::now();
loop {
i += 1;
@@ -6894,6 +7038,10 @@ fn start_job_update_sse_stream(
}
tokio::time::sleep(std::time::Duration::from_millis(ms_duration)).await;
// Check progress if the user requested it, and check periodically if the job has progress
// Once it has progress, we always check progress
let check_progress = get_progress.unwrap_or(false)
&& (get_progress_m || last_progress_check.elapsed().as_secs() > 5);
match get_job_update_data(
&opt_authed,
&opt_tokened,
@@ -6902,7 +7050,7 @@ fn start_job_update_sse_stream(
&job_id,
log_offset,
stream_offset,
get_progress,
check_progress,
running,
false,
true,
@@ -6923,6 +7071,13 @@ fn start_job_update_sse_stream(
if update.new_logs.as_ref().is_some_and(|x| x.is_empty()) {
update.new_logs = None;
}
if check_progress {
if update.progress.is_some() {
get_progress_m = true;
} else {
last_progress_check = Instant::now();
}
}
// if !only_result.unwrap_or(false) {
// tracing::error!("update {:?}", update);
@@ -6990,14 +7145,20 @@ async fn get_flow_stream_delta(
) -> error::Result<Option<(Option<String>, Option<i32>)>> {
if let Some(job_id) = flow_stream_job_id {
let record = sqlx::query!(
"SELECT SUBSTR(rs.stream, $1) AS new_result_stream, CHAR_LENGTH(rs.stream) + 1 AS stream_offset FROM job_result_stream rs WHERE rs.job_id = $2",
"
SELECT
string_agg(stream, '' order by idx asc) as stream,
max(idx) + 1 as offset
FROM job_result_stream_v2
WHERE job_id = $2 AND idx >= $1
",
stream_offset.unwrap_or(0),
job_id,
)
.fetch_optional(db)
.await?;
if let Some(record) = record {
Ok(Some((record.new_result_stream, record.stream_offset)))
Ok(Some((record.stream, record.offset)))
} else {
Ok(None)
}
@@ -7014,7 +7175,7 @@ async fn get_job_update_data(
job_id: &Uuid,
log_offset: Option<i32>,
stream_offset: Option<i32>,
get_progress: Option<bool>,
get_progress: bool,
running: Option<bool>,
log_view: bool,
get_full_job_on_completion: bool,
@@ -7046,18 +7207,28 @@ async fn get_job_update_data(
let (result, running, mut result_stream, mut new_stream_offset, new_flow_stream_job_id) =
if let Some(tags) = tags {
let r = sqlx::query!(
"SELECT
"
WITH result_stream AS (
SELECT
string_agg(stream, '' order by idx asc) as stream,
job_id,
max(idx) + 1 as offset
FROM job_result_stream_v2
WHERE job_id = $2 AND idx >= $3
GROUP BY job_id
)
SELECT
jc.result as \"result: sqlx::types::Json<Box<RawValue>>\",
v2_job.tag,
v2_job_queue.running as \"running: Option<bool>\",
SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\",
CHAR_LENGTH(rs.stream) AS stream_offset,
rs.stream AS \"result_stream: Option<String>\",
rs.offset AS stream_offset,
CASE WHEN $4 THEN NULL ELSE (COALESCE(js.flow_status, jc.flow_status)->>'stream_job')::uuid END as stream_job
FROM v2_job
LEFT JOIN v2_job_queue USING (id)
LEFT JOIN v2_job_completed jc USING (id)
LEFT JOIN v2_job_status js USING (id)
LEFT JOIN job_result_stream rs ON rs.job_id = $2
LEFT JOIN result_stream rs ON rs.job_id = $2
WHERE v2_job.id = $2 AND v2_job.workspace_id = $1",
w_id,
job_id,
@@ -7086,11 +7257,21 @@ async fn get_job_update_data(
} else {
if running.is_some_and(|x| !x) {
let r = sqlx::query!(
"SELECT
"
WITH result_stream AS (
SELECT
string_agg(stream, '' order by idx asc) as stream,
job_id,
max(idx) + 1 as offset
FROM job_result_stream_v2
WHERE job_id = $1 AND idx >= $3
GROUP BY job_id
)
SELECT
COALESCE(jc.result, jc.result) as \"result: sqlx::types::Json<Box<RawValue>>\",
jq.running as \"running: Option<bool>\",
SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\",
CHAR_LENGTH(rs.stream) + 1 AS stream_offset,
rs.stream AS \"result_stream: Option<String>\",
rs.offset AS stream_offset,
CASE WHEN $4 THEN NULL ELSE (COALESCE(js.flow_status, jc.flow_status)->>'stream_job')::uuid END as stream_job
FROM (
SELECT $1::uuid as job_id, $2::text as workspace_id
@@ -7098,7 +7279,7 @@ async fn get_job_update_data(
LEFT JOIN v2_job_completed jc ON jc.id = base.job_id AND jc.workspace_id = base.workspace_id
LEFT JOIN v2_job_queue jq ON jq.id = base.job_id AND jq.workspace_id = base.workspace_id
LEFT JOIN v2_job_status js ON js.id = base.job_id
LEFT JOIN job_result_stream rs ON rs.job_id = base.job_id
LEFT JOIN result_stream rs ON rs.job_id = base.job_id
WHERE base.job_id = $1",
job_id,
w_id,
@@ -7119,10 +7300,20 @@ async fn get_job_update_data(
}
} else {
let q = sqlx::query!(
"SELECT
"
WITH result_stream AS (
SELECT
string_agg(stream, '' order by idx asc) as stream,
job_id,
max(idx) + 1 as offset
FROM job_result_stream_v2
WHERE job_id = $2 AND idx >= $3
GROUP BY job_id
)
SELECT
COALESCE(jc.result, NULL) as \"result: sqlx::types::Json<Box<RawValue>>\",
SUBSTR(rs.stream, $3) AS \"result_stream: Option<String>\",
CHAR_LENGTH(rs.stream) + 1 AS stream_offset,
rs.stream AS \"result_stream: Option<String>\",
rs.offset AS stream_offset,
COALESCE(js.flow_status, jc.flow_status) as \"flow_status: sqlx::types::Json<Box<RawValue>>\",
CASE WHEN $4 THEN NULL ELSE (COALESCE(js.flow_status, jc.flow_status)->>'stream_job')::uuid END as stream_job
FROM (
@@ -7130,7 +7321,7 @@ async fn get_job_update_data(
) base
LEFT JOIN v2_job_completed jc ON jc.id = base.job_id AND jc.workspace_id = base.workspace_id
LEFT JOIN v2_job_status js ON js.id = base.job_id
LEFT JOIN job_result_stream rs ON rs.job_id = base.job_id
LEFT JOIN result_stream rs ON rs.job_id = base.job_id
WHERE base.job_id = $2",
w_id,
job_id,
@@ -7180,20 +7371,30 @@ async fn get_job_update_data(
})
} else {
let mut record = sqlx::query!(
"SELECT
"
WITH result_stream AS (
SELECT
string_agg(stream, '' order by idx asc) as stream,
job_id,
max(idx) + 1 as offset
FROM job_result_stream_v2
WHERE job_id = $3 AND idx >= $8
GROUP BY job_id
)
SELECT
c.id IS NOT NULL AS completed,
CASE
WHEN q.id IS NOT NULL THEN (CASE WHEN NOT $5 AND q.running THEN true ELSE null END)
ELSE false
END AS running,
CASE WHEN $7::BOOLEAN THEN NULL ELSE SUBSTR(logs, GREATEST($1 - log_offset, 0)) END AS logs,
SUBSTR(rs.stream, $8) AS new_result_stream,
rs.stream AS new_result_stream,
COALESCE(r.memory_peak, c.memory_peak) AS mem_peak,
COALESCE(c.flow_status, f.flow_status) AS \"flow_status: sqlx::types::Json<Box<RawValue>>\",
(COALESCE(c.flow_status, f.flow_status)->>'stream_job')::uuid AS stream_job,
COALESCE(c.workflow_as_code_status, f.workflow_as_code_status) AS \"workflow_as_code_status: sqlx::types::Json<Box<RawValue>>\",
CASE WHEN $7::BOOLEAN THEN NULL ELSE job_logs.log_offset + CHAR_LENGTH(job_logs.logs) + 1 END AS log_offset,
CHAR_LENGTH(rs.stream) + 1 AS stream_offset,
rs.offset AS stream_offset,
created_by AS \"created_by!\",
CASE WHEN $4::BOOLEAN THEN (
SELECT scalar_int FROM job_stats WHERE job_id = $3 AND metric_id = 'progress_perc'
@@ -7204,14 +7405,14 @@ async fn get_job_update_data(
LEFT JOIN v2_job_runtime r USING (id)
LEFT JOIN v2_job_status f USING (id)
LEFT JOIN v2_job_completed c USING (id)
LEFT JOIN job_result_stream rs ON rs.job_id = $3
LEFT JOIN result_stream rs ON rs.job_id = $3
LEFT JOIN job_logs ON job_logs.job_id = $3
WHERE j.workspace_id = $2 AND j.id = $3
AND ($6::text[] IS NULL OR j.tag = ANY($6))",
log_offset,
w_id,
job_id,
get_progress.unwrap_or(false),
get_progress,
running,
tags.as_ref().map(|v| v.as_slice()) as Option<&[&str]>,
no_logs.unwrap_or(false),

View File

@@ -87,6 +87,7 @@ pub mod ee;
pub mod ee_oss;
pub mod embeddings;
mod favorite;
mod flow_conversations;
pub mod flows;
mod folders;
mod granular_acls;
@@ -411,7 +412,7 @@ pub async fn run_server(
};
#[cfg(feature = "agent_worker_server")]
let (agent_workers_router, agent_workers_bg_processor, agent_workers_killpill_tx) =
let (agent_workers_router, agent_workers_bg_processor, agent_workers_job_completed_tx) =
if server_mode {
agent_workers_oss::workspaced_service(db.clone(), _base_internal_url.clone())
} else {
@@ -443,6 +444,10 @@ pub async fn run_server(
.nest("/drafts", drafts::workspaced_service())
.nest("/favorites", favorite::workspaced_service())
.nest("/flows", flows::workspaced_service())
.nest(
"/flow_conversations",
flow_conversations::workspaced_service(),
)
.nest("/folders", folders::workspaced_service())
.nest("/groups", groups::workspaced_service())
.nest("/inputs", inputs::workspaced_service())
@@ -535,7 +540,14 @@ pub async fn run_server(
.nest("/agent_workers", {
#[cfg(feature = "agent_worker_server")]
{
agent_workers_oss::global_service().layer(Extension(agent_cache.clone()))
if let Some(agent_workers_job_completed_tx) =
agent_workers_job_completed_tx.clone()
{
agent_workers_oss::global_service(agent_workers_job_completed_tx)
.layer(Extension(agent_cache.clone()))
} else {
Router::new()
}
}
#[cfg(not(feature = "agent_worker_server"))]
{
@@ -705,8 +717,8 @@ pub async fn run_server(
let server = server.with_graceful_shutdown(async move {
killpill_rx.recv().await.ok();
#[cfg(feature = "agent_worker_server")]
if let Some(agent_workers_killpill_tx) = agent_workers_killpill_tx {
if let Err(e) = agent_workers_killpill_tx.kill().await {
if let Some(agent_workers_job_completed_tx) = agent_workers_job_completed_tx {
if let Err(e) = agent_workers_job_completed_tx.kill().await {
tracing::error!("Error killing agent workers: {e:#}");
}
}

View File

@@ -151,6 +151,22 @@ impl ServerHandler for Runner {
check_scopes(authed)?;
if request.name.ends_with("_TRUNC") {
return Ok(CallToolResult::error(
vec![
Annotated::new(
RawContent::Text(RawTextContent {
text:
"Tool path is too long. Consider shortening it to make it compatible with MCP."
.to_string(),
meta: None,
}),
None
),
]
));
}
let db = http_parts.extensions.get::<DB>().ok_or_else(|| {
tracing::error!("DB Axum extension not found");
ErrorData::internal_error("DB Axum extension not found", None)
@@ -435,7 +451,6 @@ impl ServerHandler for Runner {
protocol_version: ProtocolVersion::default(),
capabilities: ServerCapabilities::builder()
.enable_tools()
.enable_tool_list_changed()
.build(),
server_info: Implementation::from_build_env(),
instructions: Some("This server provides a list of scripts and flows the user can run on Windmill. Each flow and script is a tool callable with their respective arguments.".to_string()),

View File

@@ -6,9 +6,8 @@
use crate::db::ApiAuthed;
use rmcp::{model::Tool, ErrorData};
use std::sync::Arc;
use windmill_common::auth::create_jwt_token;
use windmill_common::db::Authed;
use windmill_common::BASE_URL;
use windmill_common::{auth::create_jwt_token, BASE_INTERNAL_URL};
// Import the auto-generated tools
use super::auto_generated_endpoints;
@@ -130,7 +129,7 @@ pub async fn call_endpoint_tool(
let query_string = build_query_string(args_map, &tool.query_params_schema);
let full_url = format!(
"{}/api{}{}",
BASE_URL.read().await,
BASE_INTERNAL_URL.as_str(),
path_template,
query_string
);

View File

@@ -5,22 +5,34 @@
use super::models::SchemaType;
// MCP clients do not allow names longer than 60 characters
const MAX_PATH_LENGTH: usize = 60;
/// Transform the path for workspace scripts/flows
///
/// This function takes a path and a type string and formats the transformed
/// path with the type prefix. This is used when listing, because we can't
/// have names with slashes. Because we replace slashes with underscores,
///
/// This function takes a path and a type string and formats the transformed
/// path with the type prefix. This is used when listing, because we can't
/// have names with slashes. Because we replace slashes with underscores,
/// we also need to escape underscores.
pub fn transform_path(path: &str, type_str: &str) -> String {
let escaped_path = path.replace('_', "__").replace('/', "_");
// first letter of type_str is used as prefix, only one letter to avoid reaching 60 char name limit
format!("{}-{}", &type_str[..1], escaped_path)
let transformed_path = format!("{}-{}", &type_str[..1], escaped_path);
if transformed_path.len() > MAX_PATH_LENGTH {
let suffix = "_TRUNC";
return format!(
"{}{}",
&transformed_path[..MAX_PATH_LENGTH - suffix.len()],
suffix
);
}
transformed_path
}
/// Reverse the transformation of a path
///
/// This function takes a transformed path and reverses the transformation
/// applied by `transform_path`. It checks if the path starts with "h"
/// This function takes a transformed path and reverses the transformation
/// applied by `transform_path`. It checks if the path starts with "h"
/// (indicating a Hub script) and removes the prefix if present.
/// It then determines the type of the item (script or flow) based on the prefix.
/// This is used in call_tool to get the original path, and the type of the item.
@@ -54,7 +66,10 @@ pub fn reverse_transform(transformed_path: &str) -> Result<(&str, String, bool),
parts[0].to_string()
} else {
const TEMP_PLACEHOLDER: &str = "@@UNDERSCORE@@";
mangled_path.replace("__", TEMP_PLACEHOLDER).replace('_', "/").replace(TEMP_PLACEHOLDER, "_")
mangled_path
.replace("__", TEMP_PLACEHOLDER)
.replace('_', "/")
.replace(TEMP_PLACEHOLDER, "_")
};
Ok((type_str, original_path, is_hub))
@@ -64,7 +79,7 @@ pub fn reverse_transform(transformed_path: &str) -> Result<(&str, String, bool),
///
/// This function takes a key and replaces spaces with underscores.
/// It also removes any characters that are not alphanumeric or underscores.
/// This is used when listing, because we can't have names with spaces
/// This is used when listing, because we can't have names with spaces
/// or special characters in the schema properties.
pub fn apply_key_transformation(key: &str) -> String {
key.replace(' ', "_")
@@ -75,8 +90,8 @@ pub fn apply_key_transformation(key: &str) -> String {
/// Reverse the transformation of a key
///
/// This function takes a transformed key and a schema object and reverses
/// the transformation applied by `apply_key_transformation`. This can be
/// This function takes a transformed key and a schema object and reverses
/// the transformation applied by `apply_key_transformation`. This can be
/// subject to collisions, but it's unlikely and is ok for our use case.
pub fn reverse_transform_key(transformed_key: &str, schema_obj: &Option<SchemaType>) -> String {
let schema_obj = match schema_obj {
@@ -98,4 +113,4 @@ pub fn reverse_transform_key(transformed_key: &str, schema_obj: &Option<SchemaTy
}
transformed_key.to_string()
}
}

View File

@@ -515,7 +515,8 @@ pub async fn transform_json_value<'c>(
match v {
Value::String(y) if y.starts_with("$var:") => {
let path = y.strip_prefix("$var:").unwrap();
let userdb_authed = UserDbWithOptAuthed { authed: authed, user_db: user_db.clone(), db: db.clone() };
let userdb_authed =
UserDbWithOptAuthed { authed: authed, user_db: user_db.clone(), db: db.clone() };
let v = crate::variables::get_value_internal(
&userdb_authed,
@@ -616,6 +617,7 @@ pub async fn transform_json_value<'c>(
job.root_job.map(|x| x.to_string()),
Some(job.scheduled_for.clone()),
None,
None,
)
.await;

View File

@@ -277,7 +277,7 @@ async fn create_schedule(
.await?;
if ns.enabled.unwrap_or(true) {
tx = push_scheduled_job(&db, tx, &schedule, Some(&authed.clone().into())).await?
tx = push_scheduled_job(&db, tx, &schedule, Some(&authed.clone().into()), None).await?
}
tx.commit().await?;
@@ -419,7 +419,7 @@ async fn edit_schedule(
.await?;
if schedule.enabled {
tx = push_scheduled_job(&db, tx, &schedule, None).await?;
tx = push_scheduled_job(&db, tx, &schedule, None, None).await?;
}
tx.commit().await?;
@@ -683,7 +683,7 @@ pub async fn set_enabled(
.await?;
if payload.enabled {
tx = push_scheduled_job(&db, tx, &schedule, None).await?;
tx = push_scheduled_job(&db, tx, &schedule, None, None).await?;
}
tx.commit().await?;

View File

@@ -43,6 +43,7 @@ use windmill_worker::process_relative_imports;
use windmill_common::{
assets::{clear_asset_usage, insert_asset_usage, AssetUsageKind, AssetWithAltAccessType},
error::to_anyhow,
s3_helpers::upload_artifact_to_store,
scripts::hash_script,
utils::WarnAfterExt,
worker::CLOUD_HOSTED,
@@ -421,45 +422,13 @@ async fn create_snapshot_script(
uploaded = true;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
let object_store = windmill_common::s3_helpers::get_object_store().await;
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
let object_store: Option<()> = None;
if &windmill_common::utils::MODE_AND_ADDONS.mode
== &windmill_common::utils::Mode::Standalone
&& object_store.is_none()
{
std::fs::create_dir_all(
windmill_common::worker::ROOT_STANDALONE_BUNDLE_DIR.clone(),
)?;
windmill_common::worker::write_file_bytes(
&windmill_common::worker::ROOT_STANDALONE_BUNDLE_DIR,
&hash,
&data,
)?;
} else {
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
{
return Err(Error::ExecutionErr("codebase is an EE feature".to_string()));
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = object_store {
let path = windmill_common::s3_helpers::bundle(&w_id, &hash);
if let Err(e) = os
.put(&object_store::path::Path::from(path.clone()), data.into())
.await
{
tracing::info!("Failed to put snapshot to s3 at {path}: {:?}", e);
return Err(Error::ExecutionErr(format!("Failed to put {path} to s3")));
}
} else {
return Err(Error::BadConfig("Object store is required for snapshot script and is not configured for servers".to_string()));
}
}
let path = windmill_common::s3_helpers::bundle(&w_id, &hash);
upload_artifact_to_store(
&path,
data,
&windmill_common::worker::ROOT_STANDALONE_BUNDLE_DIR,
)
.await?;
}
// println!("Length of `{}` is {} bytes", name, data.len());
}
@@ -900,7 +869,7 @@ async fn create_script_internal<'c>(
clear_schedule(&mut tx, &schedule.path, &w_id).await?;
if schedule.enabled {
tx = push_scheduled_job(&db, tx, &schedule, None).await?;
tx = push_scheduled_job(&db, tx, &schedule, None, None).await?;
}
}
} else {
@@ -1020,6 +989,7 @@ async fn create_script_internal<'c>(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
Ok((hash, new_tx, None))

View File

@@ -6,7 +6,7 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::time::Duration;
use std::{collections::HashMap, time::Duration};
use crate::{
db::{ApiAuthed, DB},
@@ -23,6 +23,7 @@ use axum::{
#[cfg(feature = "enterprise")]
use axum::extract::Query;
use serde_json::json;
#[cfg(feature = "enterprise")]
use crate::utils::require_devops_role;
@@ -42,7 +43,6 @@ use windmill_common::{
},
parse_postgres_url,
server::Smtp,
utils::build_arg_str,
};
pub fn global_service() -> Router {
@@ -69,10 +69,13 @@ pub fn global_service() -> Router {
"/critical_alerts/:id/acknowledge",
post(acknowledge_critical_alert),
)
.route("/databases_exist", post(databases_exist))
.route(
"/create_ducklake_database/:name",
post(create_ducklake_database),
"/get_ducklake_instance_catalog_db_status",
post(get_ducklake_instance_catalog_db_status),
)
.route(
"/setup_ducklake_catalog_db/:name",
post(setup_ducklake_catalog_db),
)
.route(
"/critical_alerts/acknowledge_all",
@@ -569,91 +572,164 @@ pub async fn acknowledge_all_critical_alerts() -> error::Error {
error::Error::NotFound("Critical Alerts require EE".to_string())
}
async fn databases_exist(
_authed: ApiAuthed,
Extension(db): Extension<DB>,
Json(database_names): Json<Vec<String>>,
) -> JsonResult<Vec<String>> {
let result = sqlx::query_scalar!(
r#"SELECT elem FROM (SELECT unnest($1::TEXT[]) AS elem) AS e
WHERE elem NOT IN (SELECT datname FROM pg_catalog.pg_database);"#,
database_names.as_slice()
)
.fetch_all(&db)
.await?
.into_iter()
.filter_map(|x| x)
.collect();
Ok(Json(result))
#[derive(Deserialize, Debug, Serialize)]
struct DucklakeInstanceCatalogDbStatus {
logs: DucklakeInstanceCatalogDbStatusLogs, // (Step, Message)[]
success: bool,
error: Option<String>,
}
async fn create_ducklake_database(
#[derive(Deserialize, Debug, Serialize, Default)]
#[serde(default)]
struct DucklakeInstanceCatalogDbStatusLogs {
super_admin: String,
#[serde(skip_serializing_if = "String::is_empty")]
database_credentials: String,
#[serde(skip_serializing_if = "String::is_empty")]
valid_dbname: String,
#[serde(skip_serializing_if = "String::is_empty")]
created_database: String,
#[serde(skip_serializing_if = "String::is_empty")]
db_connect: String,
#[serde(skip_serializing_if = "String::is_empty")]
grant_permissions: String,
}
async fn get_ducklake_instance_catalog_db_status(
_authed: ApiAuthed,
Extension(db): Extension<DB>,
) -> JsonResult<HashMap<String, DucklakeInstanceCatalogDbStatus>> {
let result = sqlx::query_scalar!(
r#"SELECT value->'instance_catalog_db_status' FROM global_settings WHERE name = 'ducklake_settings'"#,
)
.fetch_one(&db)
.await?
.ok_or_else(|| error::Error::ExecutionErr("Couldn't find ducklake_settings".to_string()))?;
let result = serde_json::from_value(result).map_err(|e| {
error::Error::ExecutionErr(format!(
"couldn't parse instance_catalog_db_status : {}",
e.to_string()
))
})?;
return Ok(Json(result));
}
async fn setup_ducklake_catalog_db(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(dbname): Path<String>,
) -> JsonResult<DucklakeInstanceCatalogDbStatus> {
let mut logs = DucklakeInstanceCatalogDbStatusLogs::default();
let result = setup_ducklake_catalog_db_inner(authed, &db, &dbname, &mut logs).await;
let success = result.is_ok();
let error = result.err().map(|e| e.to_string());
let status = DucklakeInstanceCatalogDbStatus { logs, success, error };
let status_json = serde_json::to_value(&status).map_err(to_anyhow)?;
// Save that the database was setup successfully
sqlx::query!(
r#"UPDATE global_settings SET value = jsonb_set(value, '{instance_catalog_db_status}', (COALESCE(value->'instance_catalog_db_status', '{}'::jsonb) || to_jsonb($1::json))) WHERE name = 'ducklake_settings'"#,
json!({ dbname: status_json })
).execute(&db).await?;
Ok(Json(status))
}
async fn setup_ducklake_catalog_db_inner(
authed: ApiAuthed,
db: &DB,
dbname: &str,
logs: &mut DucklakeInstanceCatalogDbStatusLogs,
) -> Result<()> {
require_super_admin(&db, &authed.email).await?;
require_super_admin(db, &authed.email).await?;
logs.super_admin = "OK".to_string();
let pg_creds = &get_database_url().await?;
let pg_creds = parse_postgres_url(pg_creds)?;
logs.database_credentials = "OK".to_string();
// Validate name to ensure it only contains alphanumeric characters
// Prevents SQL injection on the instance database
let valid_name = regex::Regex::new(r"^[a-zA-Z0-9_]+$")
.map_err(|_| error::Error::internal_err("Failed to compile regex".to_string()))?;
if !valid_name.is_match(&dbname) {
lazy_static::lazy_static! {
static ref VALID_NAME: regex::Regex = regex::Regex::new(r"^[a-zA-Z0-9_]+$").unwrap();
}
if !VALID_NAME.is_match(dbname) {
return Err(error::Error::BadRequest(
"Invalid database name".to_string(),
"Catalog name must be alphanumeric, underscores allowed".to_string(),
));
}
if pg_creds.database.trim().eq_ignore_ascii_case(dbname.trim()) {
return Err(error::Error::BadRequest(
"Database name cannot be the same as the main database".to_string(),
));
}
logs.valid_dbname = "OK".to_string();
sqlx::query(&format!("CREATE DATABASE \"{dbname}\""))
.execute(&db)
.await?;
let db_exists = sqlx::query_scalar!(
"SELECT EXISTS (SELECT 1 FROM pg_catalog.pg_database WHERE datname = $1)",
dbname
)
.fetch_one(db)
.await?
.unwrap_or(false);
sqlx::query(&format!(
"GRANT CONNECT ON DATABASE \"{dbname}\" TO ducklake_user"
))
.execute(&db)
.await?;
logs.created_database = "SKIP".to_string();
if !db_exists {
sqlx::query(&format!("CREATE DATABASE \"{dbname}\""))
.execute(db)
.await?;
logs.created_database = "OK".to_string();
}
// We have to connect to the newly created database as admin to grant permissions
let pg_creds = parse_postgres_url(&get_database_url().await?)?;
let Some(wm_pg_pwd) = pg_creds.password else {
return Err(error::Error::BadRequest("Password not found".to_string()));
let ssl_mode = match pg_creds.ssl_mode.as_deref() {
Some("allow") => "prefer".to_string(),
Some("verify-ca") | Some("verify-full") => "require".to_string(),
Some(s) => s.to_string(),
None => "prefer".to_string(),
};
let conn_str: String = build_arg_str(
&[
("host", Some(&pg_creds.host)),
("port", pg_creds.port.map(|p| p.to_string()).as_deref()),
("password", Some(&wm_pg_pwd)),
("user", pg_creds.username.as_deref()),
("dbname", Some(&dbname)),
],
" ",
"=",
// We have to connect to the newly created database as admin to grant permissions
let conn_str = format!(
"postgres://{user}:{password}@{host}:{port}/{dbname}?sslmode={sslmode}",
user = urlencoding::encode(&pg_creds.username.unwrap_or_else(|| "postgres".to_string())),
password = urlencoding::encode(&pg_creds.password.as_deref().unwrap_or("")),
host = urlencoding::encode(&pg_creds.host),
port = pg_creds.port.unwrap_or(5432),
dbname = dbname,
sslmode = ssl_mode
);
let (client, connection) = tokio::time::timeout(
std::time::Duration::from_secs(20),
tokio_postgres::connect(&conn_str, tokio_postgres::NoTls),
)
.await
.map_err(to_anyhow)?
.map_err(to_anyhow)?;
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("connection error: {}", e);
}
});
.map_err(|e| error::Error::ExecutionErr(format!("timeout: {}", e.to_string())))?
.map_err(|e| error::Error::ExecutionErr(format!("error: {}", e.to_string())))?;
let join_handle = tokio::spawn(async move { connection.await });
logs.db_connect = "OK".to_string();
client
.batch_execute(&format!(
"GRANT USAGE ON SCHEMA public TO ducklake_user;
"GRANT CONNECT ON DATABASE \"{dbname}\" TO ducklake_user;
GRANT USAGE ON SCHEMA public TO ducklake_user;
GRANT CREATE ON SCHEMA public TO ducklake_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ducklake_user;"
))
.await
.map_err(to_anyhow)?;
.map_err(|e| {
error::Error::ExecutionErr(format!(
"Failed to grant permissions to ducklake_user: {}",
e.to_string(),
))
})?;
logs.grant_permissions = "OK".to_string();
drop(client); // /!\ Drop before joining to avoid deadlock
join_handle
.await
.map_err(|e| error::Error::ExecutionErr(format!("join error: {}", e.to_string())))?
.map_err(|e| {
error::Error::ExecutionErr(format!("tokio_postgres error: {}", e.to_string()))
})?;
Ok(())
}

View File

@@ -684,7 +684,8 @@ pub async fn trigger_runnable_and_wait_for_raw_result_with_error_ctx(
error_handler_path,
error_handler_args,
trigger_path,
).await?;
)
.await?;
if !success {
Err(windmill_common::error::Error::internal_err(format!(
@@ -857,6 +858,7 @@ async fn trigger_script_with_retry_and_error_handler(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
tx.commit().await?;

View File

@@ -12,6 +12,7 @@ use tokio_tungstenite::connect_async;
use windmill_common::{
db::UserDB,
error::{Error, Result},
worker::to_raw_value,
};
use windmill_git_sync::DeployedObject;
@@ -236,7 +237,7 @@ impl TriggerCrud for WebsocketTrigger {
url.starts_with("$flow:"),
&db,
authed.clone(),
config.url_runnable_args.as_ref(),
config.url_runnable_args.as_ref().map(to_raw_value).as_ref(),
&workspace_id,
)
.await?,

View File

@@ -5,7 +5,7 @@ use crate::triggers::{
trigger_runnable, trigger_runnable_and_wait_for_raw_result,
trigger_runnable_and_wait_for_raw_result_with_error_ctx, TriggerJobArgs,
},
websocket::WebsocketConfig,
websocket::{get_url_from_runnable_value, WebsocketConfig},
Listener,
};
use anyhow::Context;
@@ -31,43 +31,6 @@ use windmill_common::{
use windmill_queue::PushArgsOwned;
impl ListeningTrigger<WebsocketConfig> {
async fn get_url_from_runnable(&self, db: &DB) -> Result<String> {
let runnable_kind = if self.is_flow { "Flow" } else { "Script" };
tracing::info!(
"Running {} {} to get WebSocket URL",
runnable_kind.to_lowercase(),
self.path
);
let authed = self.authed(db, "ws").await?;
let args = raw_value_to_args_hashmap(
self.trigger_config.url_runnable_args.as_ref().map(|r| &r.0),
)?;
let result = trigger_runnable_and_wait_for_raw_result_with_error_ctx(
db,
None,
authed,
&self.workspace_id,
&self.script_path,
self.is_flow,
PushArgsOwned { args, extra: None },
None,
None,
None,
"".to_string(), // doesn't matter as no retry/error handler
)
.await?;
serde_json::from_str::<String>(result.get()).map_err(|_| {
Error::BadConfig(format!(
"{} {} did not return a string",
runnable_kind, self.path,
))
})
}
async fn send_initial_messages(
&self,
writer: &mut SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,
@@ -189,8 +152,12 @@ impl Listener for WebsocketTrigger {
)) => {
return Ok(None);
},
url_result = listening_trigger.get_url_from_runnable(&db) => match url_result {
url_result = {
let authed = listening_trigger.authed(db, "ws").await?;
let args = listening_trigger.trigger_config.url_runnable_args.as_ref().map(|r| &r.0);
let path = url.splitn(2, ':').nth(1).unwrap();
get_url_from_runnable_value(path, url.starts_with("$flow:"), db, authed, args, &listening_trigger.workspace_id)
} => match url_result {
Ok(url) => Cow::Owned(url),
Err(err) => {
return Err(anyhow::anyhow!("Error getting WebSocket URL from runnable after 5 tries: {:?}", err).into());

View File

@@ -61,11 +61,11 @@ pub struct TestWebsocketConfig {
}
pub fn value_to_args_hashmap(
args: Option<&serde_json::Value>,
args: Option<&Box<RawValue>>,
) -> Result<HashMap<String, Box<RawValue>>> {
let args = if let Some(args) = args {
let args_map: Option<HashMap<String, serde_json::Value>> =
serde_json::from_value(args.clone())
serde_json::from_str(args.get())
.map_err(|e| Error::BadRequest(format!("invalid json: {}", e)))?;
args_map
@@ -89,7 +89,7 @@ pub async fn get_url_from_runnable_value(
is_flow: bool,
db: &DB,
authed: ApiAuthed,
args: Option<&serde_json::Value>,
args: Option<&Box<RawValue>>,
workspace_id: &str,
) -> Result<String> {
tracing::info!(

View File

@@ -71,6 +71,7 @@ pub fn workspaced_service() -> Router {
.route("/get/:user", get(get_workspace_user))
.route("/update/:user", post(update_workspace_user))
.route("/delete/:user", delete(delete_workspace_user))
.route("/convert_to_group/:user", post(convert_user_to_group))
.route("/is_owner/*path", get(is_owner_of_path))
.route("/whois/:username", get(whois))
.route("/whoami", get(whoami))
@@ -1362,6 +1363,138 @@ async fn update_workspace_user(
Ok(format!("user {} updated", user_email))
}
async fn convert_user_to_group(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path((w_id, username_to_convert)): Path<(String, String)>,
) -> Result<String> {
require_admin(authed.is_admin, &authed.username)?;
let mut tx = db.begin().await?;
// Get user email and current status
let user_info = sqlx::query!(
"SELECT email, is_admin, operator, added_via FROM usr WHERE username = $1 AND workspace_id = $2",
username_to_convert,
&w_id
)
.fetch_optional(&mut *tx)
.await?;
let user_info = not_found_if_none(user_info, "User", &username_to_convert)?;
// Check if user is already a group user
if let Some(added_via) = &user_info.added_via {
if added_via.get("source").and_then(|v| v.as_str()) == Some("instance_group") {
return Err(Error::BadRequest("User is already a group user".to_string()));
}
}
// Find which instance groups this user belongs to that are configured for auto-add in this workspace
let eligible_groups = sqlx::query!(
r#"
SELECT
eig.igroup as group_name,
ws.auto_add_instance_groups_roles
FROM email_to_igroup eig
INNER JOIN workspace_settings ws ON ws.workspace_id = $1
WHERE eig.email = $2
AND eig.igroup = ANY(ws.auto_add_instance_groups)
"#,
&w_id,
&user_info.email
)
.fetch_all(&mut *tx)
.await?;
if eligible_groups.is_empty() {
return Err(Error::BadRequest(
"User is not a member of any instance groups configured for auto-add in this workspace".to_string()
));
}
// Determine the group with highest precedence (same logic as process_instance_group_auto_adds)
let roles: std::collections::HashMap<String, String> = if let Some(roles_json) = &eligible_groups[0].auto_add_instance_groups_roles {
serde_json::from_value(roles_json.clone()).unwrap_or_default()
} else {
std::collections::HashMap::new()
};
let mut best_group = &eligible_groups[0].group_name;
let mut best_precedence = 0u8;
for group in &eligible_groups {
let default_role = "developer".to_string();
let role = roles.get(&group.group_name).unwrap_or(&default_role);
let precedence = match role.as_str() {
"admin" => 3,
"developer" => 2,
"operator" => 1,
_ => 2,
};
if precedence > best_precedence {
best_precedence = precedence;
best_group = &group.group_name;
}
}
let primary_group_name = best_group;
// Determine role from group configuration using the selected primary group
let default_role = "developer".to_string();
let role = roles.get(primary_group_name).unwrap_or(&default_role).as_str();
let (is_admin, is_operator) = match role {
"admin" => (true, false),
"operator" => (false, true),
_ => (false, false),
};
// Update user with instance group information
let instance_group_source = serde_json::json!({
"source": "instance_group",
"group": primary_group_name
});
sqlx::query!(
"UPDATE usr SET added_via = $1, is_admin = $2, operator = $3 WHERE username = $4 AND workspace_id = $5",
instance_group_source,
is_admin,
is_operator,
username_to_convert,
&w_id
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed,
"users.convert_to_group",
ActionKind::Update,
&w_id,
Some(&username_to_convert),
Some([("group", primary_group_name.as_str()), ("role", role)].into()),
)
.await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
windmill_git_sync::DeployedObject::User { email: user_info.email.clone() },
Some(format!("Converted user '{}' to group user (group: {}, role: {})", &user_info.email, primary_group_name, role)),
true,
)
.await?;
Ok(format!("User {} converted to group user (group: {}, role: {})", username_to_convert, primary_group_name, role))
}
async fn update_user(
authed: ApiAuthed,
Path(email_to_update): Path<String>,
@@ -1459,6 +1592,12 @@ async fn delete_user(
.execute(&mut *tx)
.await?;
}
// Remove user from all instance groups email_to_igroup
sqlx::query!("DELETE FROM email_to_igroup WHERE email = $1", &email_to_delete)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed,
@@ -2191,8 +2330,8 @@ async fn get_all_runnables(
.collect::<Vec<_>>(),
);
let scripts = sqlx::query!(
"SELECT workspace_id as workspace, path, summary, description, schema FROM script as o
WHERE created_at = (select max(created_at) from script where o.path = path and workspace_id = $1 AND archived = false)
"SELECT workspace_id as workspace, path, summary, description, schema FROM script as o
WHERE created_at = (select max(created_at) from script where o.path = path and workspace_id = $1 AND archived = false)
AND workspace_id = $1 and archived = false", workspace
)
.fetch_all(&mut *tx)

View File

@@ -83,6 +83,7 @@ async fn list_contextual_variables(
Some("017e0ad5-f499-73b6-5488-92a61c5196dd".to_string()),
Some(chrono::offset::Utc::now()),
Some(ScriptHash(1234567890)),
None,
)
.await
.to_vec(),
@@ -843,11 +844,11 @@ pub async fn get_value_internal<'a, 'e, A: sqlx::Acquire<'e, Database = Postgres
} else if !value.is_empty() {
let mc = build_crypt(&db, &w_id).await?;
decrypt(&mc, value).map_err(|e| {
Error::internal_err(format!(
"Error decrypting variable {}: {}",
variable.path, e
))
})?
Error::internal_err(format!(
"Error decrypting variable {}: {}",
variable.path, e
))
})?
} else {
"".to_string()
}
@@ -884,11 +885,8 @@ pub async fn get_variable_or_self(path: String, db: &DB, w_id: &str) -> Result<S
if record.is_secret {
let mc = build_crypt(db, w_id).await?;
value = decrypt(&mc, value).map_err(|e| {
Error::internal_err(format!(
"Error decrypting variable {}: {}",
path, e
))
})?;
Error::internal_err(format!("Error decrypting variable {}: {}", path, e))
})?;
}
Ok(value)

View File

@@ -366,6 +366,7 @@ struct UserWorkspace {
pub color: Option<String>,
pub operator_settings: Option<Option<serde_json::Value>>,
pub parent_workspace_id: Option<String>,
pub disabled: bool,
}
#[derive(Deserialize)]
@@ -1022,10 +1023,11 @@ async fn edit_ducklake_config(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
ApiAuthed { is_admin, username, .. }: ApiAuthed,
ApiAuthed { is_admin, username, email, .. }: ApiAuthed,
Json(new_config): Json<EditDucklakeConfig>,
) -> Result<String> {
require_admin(is_admin, &username)?;
let is_superadmin = require_super_admin(&db, &email).await.is_ok();
let mut tx = db.begin().await?;
@@ -1064,6 +1066,38 @@ async fn edit_ducklake_config(
}
}
// Check that non-superadmins are not abusing Instance catalogs
if !is_superadmin {
let old_ducklakes = sqlx::query_scalar!(
r#"
SELECT ws.ducklake->'ducklakes' AS ducklake_name
FROM workspace_settings ws
WHERE ws.workspace_id = $1
"#,
&w_id
)
.fetch_one(&db)
.await?
.unwrap_or(serde_json::Value::Null);
let old_ducklakes: HashMap<String, Ducklake> =
serde_json::from_value(old_ducklakes).unwrap_or_default();
for (name, dl) in new_config.settings.ducklakes.iter() {
if dl.catalog.resource_type == DucklakeCatalogResourceType::Instance {
let old_dl = old_ducklakes.get(name);
if old_dl.is_none()
|| old_dl.unwrap().catalog.resource_type
!= DucklakeCatalogResourceType::Instance
|| old_dl.unwrap().catalog.resource_path != dl.catalog.resource_path
{
return Err(Error::BadRequest(
"Only superadmins can create or modify ducklakes with Instance catalogs"
.to_string(),
));
}
}
}
}
let config: serde_json::Value = serde_json::to_value(new_config.settings)
.map_err(|err| Error::internal_err(err.to_string()))?;
@@ -2109,7 +2143,8 @@ async fn user_workspaces(
let workspaces = sqlx::query_as!(
UserWorkspace,
"SELECT workspace.id, workspace.name, usr.username, workspace_settings.color, workspace.parent_workspace_id,
CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings
CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings,
usr.disabled
FROM workspace
JOIN usr ON usr.workspace_id = workspace.id
JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id

View File

@@ -204,7 +204,6 @@ where
"edited_by",
"archived",
"has_draft",
"draft_only",
"error",
"last_server_ping",
"server_id",
@@ -354,6 +353,7 @@ pub(crate) async fn tarball_workspace(
{
let scripts = sqlx::query_as::<_, Script>(
"SELECT * FROM script as o WHERE workspace_id = $1 AND archived = false
AND (draft_only IS NULL OR draft_only = false)
AND created_at = (select max(created_at) from script where path = o.path AND \
workspace_id = $1)",
)
@@ -476,7 +476,7 @@ pub(crate) async fn tarball_workspace(
"SELECT flow.workspace_id, flow.path, flow.summary, flow.description, flow.archived, flow.extra_perms, flow.draft_only, flow.dedicated_worker, flow.tag, flow.ws_error_handler_muted, flow.timeout, flow.visible_to_runner_only, flow.on_behalf_of_email, flow_version.schema, flow_version.value, flow_version.created_at as edited_at, flow_version.created_by as edited_by
FROM flow
LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
WHERE flow.workspace_id = $1 AND flow.archived = false",
WHERE flow.workspace_id = $1 AND flow.archived = false AND (flow.draft_only IS NULL OR flow.draft_only = false)",
)
.bind(&w_id)
.fetch_all(&mut *tx)
@@ -527,7 +527,8 @@ pub(crate) async fn tarball_workspace(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by from app, app_version
WHERE app.workspace_id = $1 AND app_version.id = app.versions[array_upper(app.versions, 1)] AND app_version.raw_app IS false",
WHERE app.workspace_id = $1 AND app_version.id = app.versions[array_upper(app.versions, 1)] AND app_version.raw_app IS false
AND (app.draft_only IS NULL OR app.draft_only = false)",
)
.bind(&w_id)
.fetch_all(&mut *tx)

View File

@@ -140,13 +140,26 @@ pub struct JWTAuthClaims {
pub groups: Vec<String>,
pub folders: Vec<(String, bool, bool)>,
pub label: Option<String>,
pub workspace_id: String,
pub workspace_id: Option<String>,
pub workspace_ids: Option<Vec<String>>,
pub exp: usize,
pub job_id: Option<String>,
pub scopes: Option<Vec<String>>,
pub audit_span: Option<String>,
}
impl JWTAuthClaims {
pub fn allowed_in_workspace(&self, w_id: &str) -> bool {
self.workspace_id
.as_ref()
.is_some_and(|token_w_id| w_id == token_w_id)
|| self
.workspace_ids
.as_ref()
.is_some_and(|token_w_ids| token_w_ids.iter().any(|token_w_id| w_id == token_w_id))
}
}
#[derive(Deserialize, Debug)]
pub struct JobPerms {
pub email: String,
@@ -411,7 +424,8 @@ pub async fn create_jwt_token(
groups: authed.groups.clone(),
folders: authed.folders.clone(),
label,
workspace_id: workspace_id.to_string(),
workspace_id: Some(workspace_id.to_string()),
workspace_ids: None,
exp: (chrono::Utc::now() + chrono::Duration::seconds(expires_in_seconds as i64)).timestamp()
as usize,
job_id: job_id.map(|id| id.to_string()),

View File

@@ -45,6 +45,10 @@ pub struct FlowStatus {
pub restarted_from: Option<RestartedFrom>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_job: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub chat_input_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_id: Option<Uuid>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
@@ -508,6 +512,8 @@ impl FlowStatus {
restarted_from: None,
user_states: HashMap::new(),
stream_job: None,
chat_input_enabled: f.chat_input_enabled,
memory_id: None,
}
}

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