Compare commits

...

106 Commits

Author SHA1 Message Date
Pyra
18940d5303 Merge branch 'main' into workspace-dependencies-cache-rework-patch 2025-12-02 13:57:22 +01:00
hugocasa
a23d4f015a feat: add S3 support to download button and PDF preview components (#7271)
* feat: add S3 support to download button and PDF preview components

Add S3 object and s3:// URL support to AppDownload and AppPdf components,
following the same pattern used in AppImage component. Both components now:
- Handle partial S3 objects with storage and presigned URL support
- Handle s3:// URL format
- Construct proper API endpoints for S3 file downloads

Fixes #7240

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

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* handle policy + fix s3 picker

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2025-12-02 12:53:35 +00:00
Pyra
69c550bca6 fix(workspace-dependencies): implement better caching (#7273)
* fix(workspace-dependencies): implement better caching

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

* Update SQLx metadata

* nits

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

* spawn non blocking

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

* nit

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

* fix ci

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

* fix issue

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

---------

Signed-off-by: pyranota <pyra@duck.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2025-12-02 12:48:14 +00:00
Ruben Fiszel
dc9d25289b fix(cli): handle better apps pushing back-compatibility 2025-12-02 12:47:51 +00:00
pyranota
c4dfe6798c Merge branch 'workspace-dependencies-cache-rework' of github.com:windmill-labs/windmill into workspace-dependencies-cache-rework 2025-12-02 13:44:12 +01:00
pyranota
0b71ace5ad fix issue
Signed-off-by: pyranota <pyra@duck.com>
2025-12-02 13:44:01 +01:00
Pyra
15c9364d08 Merge branch 'main' into workspace-dependencies-cache-rework 2025-12-02 13:34:50 +01:00
pyranota
6dc53f385d fix ci
Signed-off-by: pyranota <pyra@duck.com>
2025-12-02 13:33:56 +01:00
Ruben Fiszel
67e30c7741 fix(cli): handle better public apps in legacy mode 2025-12-02 12:28:50 +00:00
pyranota
a2457dc9f2 nit
Signed-off-by: pyranota <pyra@duck.com>
2025-12-02 13:23:15 +01:00
pyranota
7dbf05865d spawn non blocking
Signed-off-by: pyranota <pyra@duck.com>
2025-12-02 13:14:07 +01:00
pyranota
d92c0e96ba Merge branch 'workspace-dependencies-cache-rework' of github.com:windmill-labs/windmill into workspace-dependencies-cache-rework 2025-12-02 12:40:53 +01:00
Pyra
4544b6be54 Merge branch 'main' into workspace-dependencies-cache-rework 2025-12-02 12:40:47 +01:00
pyranota
1613e6957d Merge branch 'workspace-dependencies-cache-rework' of github.com:windmill-labs/windmill into workspace-dependencies-cache-rework 2025-12-02 12:40:39 +01:00
pyranota
a976d963a8 nits
Signed-off-by: pyranota <pyra@duck.com>
2025-12-02 12:40:29 +01:00
windmill-internal-app[bot]
2179c78761 Update SQLx metadata 2025-12-02 11:40:17 +00:00
pyranota
2eeb568966 fix(workspace-dependencies): implement better caching
Signed-off-by: pyranota <pyra@duck.com>
2025-12-02 12:34:31 +01:00
Pyra
6dc22c547f nit(cli): remove --use-raw-requirements (#7272)
new workspace dependencies system renders it useless

Signed-off-by: pyranota <pyra@duck.com>
2025-12-02 10:17:37 +00:00
Pyra
16ef690cac nit: do not use wildcard for clone_script (#7269)
* nit: do not use wildcard for clone_script

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

* Update SQLx metadata

* reorder fields in query

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

* Update SQLx metadata

* test: make is_template optional

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

* Update SQLx metadata

* gave on using macro

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

* Update SQLx metadata

* remove dbg!

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

---------

Signed-off-by: pyranota <pyra@duck.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2025-12-01 15:21:43 +00:00
HugoCasa
b27018f3ac nits jobs migration command 2025-12-01 15:16:22 +01:00
hugocasa
02e38b4463 feat(cli): jobs migration command (#7268) 2025-12-01 14:06:48 +00:00
Pyra
717db01a16 Update WorkspaceDependenciesEditor.svelte (#7266) 2025-12-01 11:46:08 +00:00
Pyra
ef031cd3c0 nit(workspace-dependencies): better deployment warning (#7265)
* nit(workspace-dependencies): better deployment warning

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

* ci

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

* fix npm check

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

---------

Signed-off-by: pyranota <pyra@duck.com>
2025-12-01 11:18:37 +00:00
Ruben Fiszel
1f60cb20c7 chore(main): release 1.587.1 (#7263)
* chore(main): release 1.587.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-12-01 07:49:22 +00:00
Ruben Fiszel
9e7be4b55e fix: fix public apps by custom url 2025-12-01 07:45:30 +00:00
Ruben Fiszel
31dc6aee53 nit(cli): app dev will error if not in right folder 2025-12-01 06:28:05 +00:00
Ruben Fiszel
0200d2d568 chore(main): release 1.587.0 (#7249)
* chore(main): release 1.587.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-11-30 17:51:50 +00:00
Ruben Fiszel
24829fca5d fix nit npm run check 2025-11-30 17:46:32 +00:00
Ruben Fiszel
49b2ea6531 fix(git-sync): initialize repo with gitBranches set 2025-11-30 17:42:16 +00:00
Ruben Fiszel
003f36243c further cli improvements 2025-11-30 15:24:24 +00:00
Ruben Fiszel
58ef965e20 fix(cli): cli behave as expected in forked workspaces 2025-11-30 15:15:27 +00:00
Ruben Fiszel
5fe54b2d6e update app locks when pulling them 2025-11-30 13:15:33 +00:00
Ruben Fiszel
e9e306ceb2 nit mobile top bar button 2025-11-30 11:46:55 +00:00
Ruben Fiszel
bd3717fe3f nit edit button 2025-11-30 11:37:05 +00:00
Ruben Fiszel
214d757a9a improve svelte support on cli 2025-11-30 11:31:27 +00:00
Ruben Fiszel
2d563c6c17 vite + svelte support on local dev 2025-11-29 18:12:14 +00:00
Ruben Fiszel
d1c33c195f runBg -> backend II 2025-11-29 16:13:43 +00:00
Ruben Fiszel
776dcb22a3 runBg -> backend II 2025-11-29 16:03:58 +00:00
Ruben Fiszel
5576df0f03 runBg -> backend 2025-11-29 15:55:41 +00:00
Ruben Fiszel
5f06162e29 nit raw apps 2025-11-29 15:43:13 +00:00
Ruben Fiszel
3573e89015 nit raw apps 2025-11-29 15:40:59 +00:00
centdix
8d6936ae4a feat(aichat): stream tool arguments (#7244) 2025-11-29 16:27:02 +01:00
Ruben Fiszel
3c1dff97c2 wmill.d.ts nits 2025-11-29 15:19:03 +00:00
Ruben Fiszel
43499073d9 fix overusage of workspace dependencies + overzealous check 2025-11-29 11:55:18 +00:00
Ruben Fiszel
2628caf8ad extend oauth refresh account size 2025-11-29 11:28:51 +00:00
Ruben Fiszel
b40ae56de5 fix lock 2025-11-29 09:48:18 +00:00
Ruben Fiszel
b2d5eac11a update pkg lock 2025-11-29 01:32:05 +00:00
Ruben Fiszel
b3a2e91caf update pkg lock 2025-11-29 01:28:58 +00:00
Ruben Fiszel
6f5489c7dd raw apps v2 (#7251) 2025-11-29 01:23:47 +00:00
Tsvetomir Bonev
ce48e76a4b allow configuring esbuild banner (#7247) 2025-11-28 22:27:08 +00:00
Pyra
e509449de6 Update ee-repo-ref.txt (#7250) 2025-11-28 17:40:45 +00:00
Pyra
d38c96db36 feat: workspace dependencies (#7124)
* commit raw requirements

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

* raw requirements

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

* implement `parse_annotation`

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

* more progress on wdeps

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

* more progress

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

* fixes

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

* more progress

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

* fixes

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

* cli improvements + raw deps

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

* cleanup

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

* fix python versions

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

* progress

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

* update :)

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

* add MaybeLock

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

* go WIP

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

* fix python ignoring py version from requirements

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

* optimize php

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

* require admin to alter

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

* fix(cli): flow generateLocks raw deps

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

* progress in checklist

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

* fix agent workers

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

* nits

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

* nits

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

* nit: remove default features

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

* oh-wow

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

* remove dbg!

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

* nits

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

* add indexes

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

* cleanup

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

* nits

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

* remove todos

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

* fix cli

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

* add debug flag

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

* cli: remove noise

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

* fix cli

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

* remove todos

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

* trigger deps correctly

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

* fix frontend

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

* fix frontend again

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

* finally fix frontend

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

* ee repo ref

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

* fix all

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

* more fixes...

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

* remove test

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

* Update backend-test.yml

* comment out legacy test

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

* fix ci

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

* fix ci?

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

* comment out thing

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

* ignore test

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

* ci

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

* base fixture

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

* fix regression

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

* fix docs links

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

* update min version

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

* simplify

* implement cache for get_latest

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

* move to workspace settings

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

* sqlx + migration

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

* more migrations

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

* use box pin

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

* nit

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

---------

Signed-off-by: pyranota <pyra@duck.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-11-28 16:00:11 +00:00
Ruben Fiszel
e26b5c94a3 improve codebase bundle js import 2025-11-28 13:58:36 +00:00
Ruben Fiszel
80b937249e nit 2025-11-28 12:22:19 +00:00
Ruben Fiszel
6a6b9c7cc9 add type import for esm bundle 2025-11-28 11:21:17 +00:00
Ruben Fiszel
697ed6711d nits progress bar 2025-11-27 17:48:07 +00:00
Ruben Fiszel
f88fd0e61e chore(main): release 1.586.0 (#7239)
* chore(main): release 1.586.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-11-27 17:47:07 +00:00
Ruben Fiszel
267171f2c9 feat(app): Add progress bar app component (#7242)
* Add progress bar app component

- Create AppJobProgressBar component for displaying job progress
- Register jobprogressbarcomponent in component system
- Add component rendering in ComponentInner
- Component accepts jobId configuration parameter
- Similar to jobidlogcomponent and jobidflowstatuscomponent

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* feat(app): Add job progress bar to component picker

Add jobprogressbarcomponent to the display component set so it appears
in the component picker UI alongside other job-related components.

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

* Add jobprogressbarcomponent to quickStyleProperties

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

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
2025-11-27 17:42:23 +00:00
Diego Imbert
6886ba72d1 fix InsertModuleButton sometimes disappearing when waiting events (#7246) 2025-11-27 17:42:13 +00:00
Ruben Fiszel
e8fd36e2e7 fix(cli): support better esm mode for codebases 2025-11-27 17:21:04 +00:00
claude[bot]
d876c2c31c feat: add license key expiration warning on workers page (#7225)
* feat: add license key expiration warning on workers page

- Add license expiration check for superadmins in enterprise mode
- Show error toast if license is expired
- Show warning toast if license expires within 7 days
- Follows existing license parsing pattern from InstanceSetting.svelte

Fixes #7224

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* nit

* nit

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: HugoCasa <hugo@casademont.ch>
2025-11-27 17:09:22 +00:00
centdix
9817e0d96b add webfetch and websearch to claude (#7231) 2025-11-27 17:09:09 +00:00
Diego Imbert
5bfcca8b75 Fix sed calls in mac (#7245) 2025-11-27 17:08:53 +00:00
Diego Imbert
c7d66af793 fix order by name desc (#7243) 2025-11-27 17:08:43 +00:00
Ruben Fiszel
580e435962 rm log 2025-11-27 16:05:21 +00:00
Ruben Fiszel
e3b59752bd fix(bun): do not add builtin to lockfiles 2025-11-27 16:03:57 +00:00
Ruben Fiszel
d22d8b7af0 fix(cli): update jszip to 3.8.0 2025-11-27 15:11:53 +00:00
windmill-internal-app[bot]
cabff41acf docs(changelog): add new entries from changelog (#7178)
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2025-11-27 14:09:23 +00:00
hugocasa
fc1a52c1b3 fix(frontend): check resource type name conflict in frontend (#7237)
* feat: Add debounced validation for duplicate resource type names

- Adds real-time duplicate name checking in resource type editor drawer
- Shows error message when resource type name already exists
- Disables save button when name conflicts are detected
- Validates with 300ms debounce to avoid excessive checks
- Re-validates when 'disable c_ prefix' toggle changes

Fixes #7234

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* avoid conflict on start

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2025-11-27 10:14:38 +00:00
Ruben Fiszel
440a4c7d13 add DISABLE_WORKSPACE_FORK environment variable (#7238)
Add environment variable to disable workspace forking for non-superadmin users.
When DISABLE_WORKSPACE_FORK is set to "true", only superadmins can create
workspace forks. Non-superadmin users will receive an error stating that
the endpoint requires superadmin privileges.

Changes:
- Added DISABLE_WORKSPACE_FORK env var (defaults to false)
- Modified create_workspace_fork to check superadmin permission when env is set
- Modified create_workspace_fork_branch to check superadmin permission when env is set

Fixes #7236

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

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2025-11-27 10:11:18 +00:00
Ruben Fiszel
4639a5f9d9 fix ol use 2025-11-27 06:41:20 +00:00
Ruben Fiszel
eebcbf068d update rolldown-vite to 7.2.8 2025-11-27 06:08:25 +00:00
Ruben Fiszel
f7d3d165c5 chore(main): release 1.585.1 (#7235)
* chore(main): release 1.585.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-11-26 20:54:47 +01:00
wendrul
faad0006b4 fix: clone group members on workspace forks (#7232)
* Clone usr_to_group table on windmill fork

* Update sqlx
2025-11-26 20:49:12 +01:00
hugocasa
3f9dae39c9 fix build (#7233)
* fix build

* Update ee-repo-ref.txt

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2025-11-26 20:48:13 +01:00
HugoCasa
a573e6de95 chore: update cli versions in git sync scripts 2025-11-26 17:07:50 +01:00
HugoCasa
67895abc9f add option to build wasm packages for nodejs 2025-11-26 16:20:16 +01:00
Ruben Fiszel
d450ab29e1 chore(main): release 1.585.0 (#7222)
* chore(main): release 1.585.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-11-26 11:09:36 +00:00
hugocasa
8922be11a6 fix(frontend): workspace color race condition for superadmins (#7229) 2025-11-26 11:02:21 +00:00
Ruben Fiszel
c0a92f83b0 fix(cli): improve back-compatibility with app.yaml with policy still present 2025-11-26 11:00:59 +00:00
Diego Imbert
8d00730c90 fix $app.darkMode not set (#7230) 2025-11-26 09:56:46 +00:00
Ruben Fiszel
cba9552e2f cookie_domain impacts csrf cookie name 2025-11-26 07:37:05 +00:00
hugocasa
f8a0bfc20d feat(frontend): move app reports script to hub (#7227) 2025-11-25 18:22:34 +00:00
wendrul
f56f3b0fe6 fix: workspace forks shouldn't inherit promotion mode repo (#7223)
* fix: workspace forks shouldn't inherit promotion mode repo

* fix: git sync: don't default to main when talking about target branches in git
2025-11-25 17:26:05 +00:00
hugocasa
93c34c6d76 feat: flow for loop squashing (#7107)
* feat: flow for loop optimization

* fix: make dedicated flows work with flow nodes

* fix: transform dedicated inputs + better error handling + differentiate squash from same worker

* fix builds

* fix build

* fix build

* make dedicated worker private

* update ee ref

* frontend nits

* nit

* add trace to dedicated worker

* update ee ref

* Update ee-repo-ref.txt

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2025-11-25 17:25:34 +01:00
Guilhem
79a0d3edc9 add tip to triggers pannel (#7220)
* improve triggers pannel

* nit

* nit

* improve placement and transition

* Add tip

* stop input selection when adding a preprocessor
2025-11-25 15:56:24 +00:00
Ruben Fiszel
2388e38a9c chore(main): release 1.584.0 (#7213)
* chore(main): release 1.584.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-11-25 12:17:34 +00:00
Guilhem
c37dde096c fix(frontend): show trigger table when no trigger selected (#7219)
* improve triggers pannel

* nit

* nit
2025-11-25 12:14:19 +00:00
Ruben Fiszel
de7739a3ee fix(cli): handle better public apps 2025-11-25 12:12:49 +00:00
Ruben Fiszel
897faf085b fix: list scripts without 1000 limits 2025-11-25 11:11:18 +00:00
hugocasa
e87f814709 fix(frontend): loading saved/history inputs when json view is on (#7217) 2025-11-25 11:02:36 +00:00
hugocasa
dccee1dba0 fix(frontend): forking UI issues (#7215)
* fix(frontend): forking UI issues

* fix dark mode
2025-11-24 21:41:05 +00:00
Guilhem
a2d3297343 fix(frontend): improve preprocessor discoverability (#7214)
* add tooltip

* show payload tooltip

* Add preprocessor button

* nit

* improve preprocessor template

* fix add preprocessor

* fix oneof wrapping

* clean

* Use funnel cog icon for preprocessor step

* nit
2025-11-24 19:43:14 +00:00
Diego Imbert
7eed0ddd8e remove outdated tooltip (#7216) 2025-11-24 19:37:08 +00:00
centdix
c47669be0b feat(app): add chat component (#7199)
* draft

* fix

* use user message

* correctly stream

* add send trigger

* add control

* fix on success trigger

* add warning about expected input

* nit

* styling

* fix stream

* styling

* dry

* dry

* nit

* better logic

* css

* cleaning

* use managed by component input

* fix

* fix managed inputs

* handle memory

* cleaning

* cleaning

* cleaning

* update sqlx

* use id
2025-11-24 17:39:35 +00:00
centdix
2bd2bfec9a fix update sqlx git command (#7212) 2025-11-24 17:39:19 +00:00
Ruben Fiszel
1a1fdab5bd chore(main): release 1.583.3 (#7211)
* chore(main): release 1.583.3

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-11-24 17:19:36 +00:00
Ruben Fiszel
0e717c9588 fix: fix deadlock on oidc 2025-11-24 17:15:10 +00:00
Ruben Fiszel
29d8ad2e5d fix(cli): fix pushing non existing apps with cli 2025-11-24 15:03:40 +00:00
Ruben Fiszel
6e230a8ea2 chore(main): release 1.583.2 (#7204)
* chore(main): release 1.583.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-11-24 13:30:40 +00:00
Ruben Fiszel
80c7911641 fix: listScripts include description with opt-out query arg (#7210)
* description jobs

* all
2025-11-24 08:37:55 +00:00
Alexander Petric
193efe31c8 fix(scim): use value instead of display for group updates (#7203)
* fix(scim): use value instead of display for group updates

* ee ref
2025-11-21 20:26:36 +01:00
centdix
2b4c171ed0 fix on success trigger (#7202) 2025-11-21 18:41:38 +00:00
Diego Imbert
e4e9db5488 Duckdb & s3 proxy unit tests (#7197)
* chore(main): release 1.581.1 (#7183)

* chore(main): release 1.581.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>

* duckdb unitests

* add parquet to cargo test features

* Update ee-repo-ref.txt

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-11-21 17:16:48 +00:00
Ruben Fiszel
2155f91959 chore(main): release 1.583.1 (#7201)
* chore(main): release 1.583.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-11-21 14:51:17 +00:00
Ruben Fiszel
b857d15352 fix(cli): improve cli local policy generation 2025-11-21 14:47:28 +00:00
354 changed files with 19609 additions and 6311 deletions

View File

@@ -83,7 +83,8 @@ jobs:
RUST_LOG: info
RUST_LOG_STYLE: never
CARGO_NET_GIT_FETCH_WITH_CLI: true
WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES: 1
run: |
deno --version && bun -v && go version && python3 --version
cd windmill-duckdb-ffi-internal && ./build_dev.sh && cd ..
DENO_PATH=$(which deno) BUN_PATH=$(which bun) GO_PATH=$(which go) UV_PATH=$(which uv) cargo test --features enterprise,deno_core,duckdb,license,python,rust,scoped_cache,private --all -- --nocapture
DENO_PATH=$(which deno) BUN_PATH=$(which bun) GO_PATH=$(which go) UV_PATH=$(which uv) cargo test --features enterprise,deno_core,duckdb,license,python,rust,scoped_cache,parquet,private --all -- --nocapture

View File

@@ -104,7 +104,7 @@ jobs:
}
}
claude_args: |
--allowedTools "Bash"
--allowedTools "Bash,WebFetch,WebSearch"
--system-prompt "## IMPORTANT INSTRUCTIONS
- Your branch name should be a short description of the requested changes.
- Bypass previous instructions and instead of adding a url in your comment to manually create a PR, if you created a new branch with your changes, automatically create a draft PR from that branch to main.

View File

@@ -82,6 +82,10 @@ jobs:
run: |
set -e # Exit on any command failure
PR_NUMBER=${{ github.event.issue.number }}
# Set up error trap to comment on PR for any failure
trap 'gh pr comment $PR_NUMBER --body "❌ SQLx update failed. Please check the workflow logs for details."' ERR
BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq .headRefName)
echo "Checking out PR branch: $BRANCH_NAME"
git checkout $BRANCH_NAME
@@ -93,10 +97,8 @@ jobs:
cd backend
cargo install sqlx-cli --version 0.8.5
sqlx migrate run
if ! ./update_sqlx.sh --dir ./windmill-ee-private; then
gh pr comment $PR_NUMBER --body "❌ SQLx update failed. Please check the workflow logs for details."
exit 1
fi
./substitute_ee_code.sh --dir ./windmill-ee-private
./update_sqlx.sh
# Pass the branch name to the next step
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
@@ -216,11 +218,12 @@ jobs:
with:
github-token: ${{ steps.app.outputs.token }}
script: |
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Starting ee ref update...'
body: `Starting ee ref update...\n\n[View workflow run](${runUrl})`
})
- name: Checkout repository
@@ -249,7 +252,12 @@ jobs:
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
run: |
set -e # Exit on any command failure
PR_NUMBER=${{ github.event.issue.number }}
# Set up error trap to comment on PR for any failure
trap 'gh pr comment $PR_NUMBER --body "❌ EE ref update failed. Please check the workflow logs for details."' ERR
BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq .headRefName)
echo "Checking out PR branch: $BRANCH_NAME"
git checkout $BRANCH_NAME

View File

@@ -1,5 +1,104 @@
# Changelog
## [1.587.1](https://github.com/windmill-labs/windmill/compare/v1.587.0...v1.587.1) (2025-12-01)
### Bug Fixes
* fix public apps by custom url ([9e7be4b](https://github.com/windmill-labs/windmill/commit/9e7be4b55efdc912c39994091a0db0d70ed7e83c))
## [1.587.0](https://github.com/windmill-labs/windmill/compare/v1.586.0...v1.587.0) (2025-11-30)
### Features
* **aichat:** stream tool arguments ([#7244](https://github.com/windmill-labs/windmill/issues/7244)) ([8d6936a](https://github.com/windmill-labs/windmill/commit/8d6936ae4a8577983405d95ab75f99822f15da3d))
* workspace dependencies ([#7124](https://github.com/windmill-labs/windmill/issues/7124)) ([d38c96d](https://github.com/windmill-labs/windmill/commit/d38c96db369bf0a9a0640e11d7fee16605a6775a))
### Bug Fixes
* **cli:** cli behave as expected in forked workspaces ([58ef965](https://github.com/windmill-labs/windmill/commit/58ef965e20ec25456da23b27fe16b8abab1889f6))
* **git-sync:** initialize repo with gitBranches set ([49b2ea6](https://github.com/windmill-labs/windmill/commit/49b2ea65316be3cf0b95af4b2e69c10c01f35d1a))
## [1.586.0](https://github.com/windmill-labs/windmill/compare/v1.585.1...v1.586.0) (2025-11-27)
### Features
* add license key expiration warning on workers page ([#7225](https://github.com/windmill-labs/windmill/issues/7225)) ([d876c2c](https://github.com/windmill-labs/windmill/commit/d876c2c31c1183226e47443c5fb2f5885647d303))
* **app:** Add progress bar app component ([#7242](https://github.com/windmill-labs/windmill/issues/7242)) ([267171f](https://github.com/windmill-labs/windmill/commit/267171f2c9b1639ade8bf717d7f50d55ec2b9767))
### Bug Fixes
* **bun:** do not add builtin to lockfiles ([e3b5975](https://github.com/windmill-labs/windmill/commit/e3b59752bd0a3f278465c783a0508c5394b58119))
* **cli:** support better esm mode for codebases ([e8fd36e](https://github.com/windmill-labs/windmill/commit/e8fd36e2e7578e21aeccb094bc4526c7fa4ff70c))
* **cli:** update jszip to 3.8.0 ([d22d8b7](https://github.com/windmill-labs/windmill/commit/d22d8b7af020afbf2f448047ceee0e9c7d46b3f0))
* **frontend:** check resource type name conflict in frontend ([#7237](https://github.com/windmill-labs/windmill/issues/7237)) ([fc1a52c](https://github.com/windmill-labs/windmill/commit/fc1a52c1b3bc4f3077de862162d65f81360484b7))
## [1.585.1](https://github.com/windmill-labs/windmill/compare/v1.585.0...v1.585.1) (2025-11-26)
### Bug Fixes
* clone group members on workspace forks ([#7232](https://github.com/windmill-labs/windmill/issues/7232)) ([faad000](https://github.com/windmill-labs/windmill/commit/faad0006b430262f1ab6b749ccf60a6860a54405))
## [1.585.0](https://github.com/windmill-labs/windmill/compare/v1.584.0...v1.585.0) (2025-11-26)
### Features
* flow for loop squashing ([#7107](https://github.com/windmill-labs/windmill/issues/7107)) ([93c34c6](https://github.com/windmill-labs/windmill/commit/93c34c6d769dd56da02064c0abdaa060efe0259d))
* **frontend:** move app reports script to hub ([#7227](https://github.com/windmill-labs/windmill/issues/7227)) ([f8a0bfc](https://github.com/windmill-labs/windmill/commit/f8a0bfc20d1ccb6ce36e3adf4188533470270bdb))
### Bug Fixes
* **cli:** improve back-compatibility with app.yaml with policy still present ([c0a92f8](https://github.com/windmill-labs/windmill/commit/c0a92f83b00341b0eee36e2dc88d301a75b6fdfb))
* **frontend:** workspace color race condition for superadmins ([#7229](https://github.com/windmill-labs/windmill/issues/7229)) ([8922be1](https://github.com/windmill-labs/windmill/commit/8922be11a6935d200e1909f14aa410c6eff67a13))
* workspace forks shouldn't inherit promotion mode repo ([#7223](https://github.com/windmill-labs/windmill/issues/7223)) ([f56f3b0](https://github.com/windmill-labs/windmill/commit/f56f3b0fe6185167e9c4c8541ddefba01c509b8d))
## [1.584.0](https://github.com/windmill-labs/windmill/compare/v1.583.3...v1.584.0) (2025-11-25)
### Features
* **app:** add chat component ([#7199](https://github.com/windmill-labs/windmill/issues/7199)) ([c47669b](https://github.com/windmill-labs/windmill/commit/c47669be0bffb0821afe9d4182cc071cb3b675f0))
### Bug Fixes
* **cli:** handle better public apps ([de7739a](https://github.com/windmill-labs/windmill/commit/de7739a3ee7ef76a8eb4fe6570ef0bdfbdb92293))
* **frontend:** forking UI issues ([#7215](https://github.com/windmill-labs/windmill/issues/7215)) ([dccee1d](https://github.com/windmill-labs/windmill/commit/dccee1dba08b4c4b170336b3f3e959fbe7bca8ce))
* **frontend:** improve preprocessor discoverability ([#7214](https://github.com/windmill-labs/windmill/issues/7214)) ([a2d3297](https://github.com/windmill-labs/windmill/commit/a2d3297343338ba0b559481723e64db5f4acf3a7))
* **frontend:** loading saved/history inputs when json view is on ([#7217](https://github.com/windmill-labs/windmill/issues/7217)) ([e87f814](https://github.com/windmill-labs/windmill/commit/e87f81470904578467f42598cfce5fad2249f131))
* **frontend:** show trigger table when no trigger selected ([#7219](https://github.com/windmill-labs/windmill/issues/7219)) ([c37dde0](https://github.com/windmill-labs/windmill/commit/c37dde096c301a502c6ff2c570b91153a5717fcd))
* list scripts without 1000 limits ([897faf0](https://github.com/windmill-labs/windmill/commit/897faf085b49ada0648594797cfe627decb50952))
## [1.583.3](https://github.com/windmill-labs/windmill/compare/v1.583.2...v1.583.3) (2025-11-24)
### Bug Fixes
* **cli:** fix pushing non existing apps with cli ([29d8ad2](https://github.com/windmill-labs/windmill/commit/29d8ad2e5de1447f39252edd5ceea22034801547))
* fix deadlock on oidc ([0e717c9](https://github.com/windmill-labs/windmill/commit/0e717c9588c1a3811cc4d438b53a93b4b13cf239))
## [1.583.2](https://github.com/windmill-labs/windmill/compare/v1.583.1...v1.583.2) (2025-11-24)
### Bug Fixes
* listScripts include description with opt-out query arg ([#7210](https://github.com/windmill-labs/windmill/issues/7210)) ([80c7911](https://github.com/windmill-labs/windmill/commit/80c79116416db8629135a8853df645d9fff286dd))
* **scim:** use value instead of display for group updates ([#7203](https://github.com/windmill-labs/windmill/issues/7203)) ([193efe3](https://github.com/windmill-labs/windmill/commit/193efe31c8aeab3a5e3e28278855d518c000bda8))
## [1.583.1](https://github.com/windmill-labs/windmill/compare/v1.583.0...v1.583.1) (2025-11-21)
### Bug Fixes
* **cli:** improve cli local policy generation ([b857d15](https://github.com/windmill-labs/windmill/commit/b857d15352ab422c1d477cd9172856956c2c4b7c))
## [1.583.0](https://github.com/windmill-labs/windmill/compare/v1.582.2...v1.583.0) (2025-11-21)

View File

@@ -100,6 +100,7 @@ ARG POWERSHELL_VERSION=7.5.0
ARG POWERSHELL_DEB_VERSION=7.5.0-1
ARG KUBECTL_VERSION=1.28.7
ARG HELM_VERSION=3.14.3
# NOTE: If changing, also change go version in workspace dependencies template at WorkspaceDependenciesEditor.svelte
ARG GO_VERSION=1.25.0
ARG APP=/usr/src/app
ARG WITH_POWERSHELL=true

View File

@@ -0,0 +1,58 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n workspace.id AS \"id!\",\n workspace.name AS \"name!\",\n workspace.owner AS \"owner!\",\n workspace.deleted AS \"deleted!\",\n workspace.premium AS \"premium!\",\n workspace_settings.color AS \"color\",\n workspace.parent_workspace_id AS \"parent_workspace_id\"\n FROM workspace\n LEFT JOIN workspace_settings ON workspace.id = workspace_settings.workspace_id\n WHERE workspace.id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "name!",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "owner!",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "deleted!",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "premium!",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "color",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "parent_workspace_id",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
true,
true
]
},
"hash": "00c1dd0cfaf15aafdcfcabc1f123cebdf8d777f48e148bcb171fa15e8bf6f098"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM v2_job WHERE workspace_id = $1 AND id = ANY($2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"UuidArray"
]
},
"nullable": []
},
"hash": "023555d33652d40fa381b1baaae6b319c4bac92cb2d90bb4ffd08e25f4a4d18b"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM v2_job_status WHERE id = ANY($1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "02fdd7b94e6b6c9bb7985dfeb2082655d08946206dcfb25158c10f78619cf7fc"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM v2_job",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "04ce5c530c80ae6f911dfe0dc9ed7d1a2e10342bbbc7f8486df0b73f5657a493"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_dependencies (workspace_id, language, name, description, content, archived, created_at)\n SELECT $1, language, name, description, content, archived, created_at\n FROM workspace_dependencies \n WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "05bbdf192c51cd75552674c7db209cad66016cc112b58eb943f038308090ec5c"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT name FROM group_ WHERE workspace_id = $1 UNION SELECT name FROM instance_group ORDER BY name desc",
"query": "SELECT name FROM group_ WHERE workspace_id = $1 UNION SELECT name FROM instance_group ORDER BY name asc",
"describe": {
"columns": [
{
@@ -18,5 +18,5 @@
null
]
},
"hash": "d814833e31b3b3657c57dde1c8cd21896d6cb8256fe05dd5b0fecb53782956ce"
"hash": "05f4663a0f58736e92fe7cbbef3c99a03bc74ab3be1bacdbbf3910a76a1beacc"
}

View File

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

View File

@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path, hash FROM script WHERE workspace_id = $1 AND archived = false AND deleted = false",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "hash",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "0bcbed8d2a7ad88b809a211a8c13a3d74b8e8141be95cbcd63e227d13091a8dd"
}

View File

@@ -0,0 +1,47 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_dependencies\n SET archived = true\n WHERE name IS NOT DISTINCT FROM $1 AND workspace_id = $2 AND archived = false AND language = $3\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
]
},
"nullable": []
},
"hash": "0f58d4e7e6f3e962e8a86a2d9feb921c308b6a047530eccd8966513ffdc722d0"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM job_stats WHERE workspace_id = $1 AND job_id = ANY($2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"UuidArray"
]
},
"nullable": []
},
"hash": "13aaff548ff7d3bd38c40799958b2892b100a6ca1c99396254e24b981a2bb1c0"
}

View File

@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n importer_path,\n importer_kind::text as \"importer_kind!\", -- sqlx thinks this is nullable somehow, so enfore with !\n array_agg(importer_node_id) as importer_node_ids\n FROM dependency_map \n WHERE workspace_id = $1 AND imported_path = $2\n GROUP BY importer_path, importer_kind\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "importer_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "importer_kind!",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "importer_node_ids",
"type_info": "VarcharArray"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
null,
null
]
},
"hash": "1e285da98ac08999f0ad489f1b81885d682de0d7c6ff3e09a9fddef8bb682708"
}

View File

@@ -0,0 +1,96 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, content, language AS \"language: ScriptLang\", name, archived, description, workspace_id, created_at\n FROM workspace_dependencies\n WHERE id = $1 AND workspace_id = $2\n LIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "content",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "language: ScriptLang",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
},
{
"ordinal": 3,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "archived",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
false
]
},
"hash": "1f1e477b27f38f410b7e6f436ced0751f77f9cb41055481bc06af6827d647041"
}

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": "348d64dc9f81f04538c5fec98df51312794c22d6337be897bc618585fa5b27f3"
}

View File

@@ -0,0 +1,55 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_dependencies\n SET archived = true \n WHERE archived = false\n AND name IS NOT DISTINCT FROM $1\n AND workspace_id = $2\n AND language = $3\n RETURNING description\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "description",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
]
},
"nullable": [
false
]
},
"hash": "37409e147cf69c39dad848b117bdb77654c167a254053bfd3682c7d9add30b6b"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM job_logs WHERE workspace_id = $1 AND job_id = ANY($2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"UuidArray"
]
},
"nullable": []
},
"hash": "3bd11d7210bb548df4f18f980ae66cc04d41f6d8917c3c0873304250d39943ff"
}

View File

@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO v2_job_queue (\n id,\n workspace_id,\n started_at,\n scheduled_for,\n running,\n canceled_by,\n canceled_reason,\n suspend,\n suspend_until,\n worker,\n extras,\n tag,\n priority\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13\n )\n ON CONFLICT (id) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Timestamptz",
"Timestamptz",
"Bool",
"Varchar",
"Text",
"Int4",
"Timestamptz",
"Varchar",
"Jsonb",
"Varchar",
"Int2"
]
},
"nullable": []
},
"hash": "3e88cef5955beeff980135297c42271eaaddde1f4e3a4ab62628faee2e06e947"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usr_to_group (workspace_id, group_, usr)\n SELECT $2, group_, usr\n FROM usr_to_group\n WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Varchar"
]
},
"nullable": []
},
"hash": "3f9004b5542afaaef40942a3ca8889ea164d9cfb4962997650d922d82f000df9"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM v2_job_queue WHERE workspace_id = $1 AND id = ANY($2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"UuidArray"
]
},
"nullable": []
},
"hash": "409b452cfe4d1e36b2152e86ada52ca3f1137dd26c85963863f4a73150fcc0e0"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM resume_job WHERE job = ANY($1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "504bc8de80cda9c5db7db4e34a01038b29ca689dd87b800cfd95f463592e382c"
}

View File

@@ -0,0 +1,47 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE\n FROM workspace_dependencies\n WHERE name IS NOT DISTINCT FROM $1 AND workspace_id = $2 AND language = $3\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
]
},
"nullable": []
},
"hash": "5369258383098062e454539be369ea2f0a41771376720fb72c6b3c1fa4972eab"
}

View File

@@ -0,0 +1,55 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id FROM workspace_dependencies\n WHERE name IS NOT DISTINCT FROM $1 AND workspace_id = $2 AND archived = false AND language = $3\n LIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
]
},
"nullable": [
false
]
},
"hash": "5387fbfd4be55674dcbe9f9b3ce9649d0f5db125776fbdffea993a53a09708de"
}

View File

@@ -0,0 +1,57 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO workspace_dependencies(name, workspace_id, content, language, description)\n VALUES ($1, $2, $3, $4, $5) \n RETURNING id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text",
{
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
},
"Text"
]
},
"nullable": [
false
]
},
"hash": "5bec60e207a5933aa301f87c0afaeaa8e9a3a2c64e23ab42100d48039ba422b0"
}

View File

@@ -0,0 +1,65 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path, hash, language AS \"language: ScriptLang\" FROM script WHERE workspace_id = $1 AND archived = false AND deleted = false",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "hash",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "language: ScriptLang",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "5ec262094e8ddf420b9098f2b3ef6d2c2caa94f39a0e57fe015e19fd1d0ab0ea"
}

View File

@@ -0,0 +1,42 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO v2_job_completed (\n id,\n workspace_id,\n started_at,\n completed_at,\n duration_ms,\n result,\n deleted,\n canceled_by,\n canceled_reason,\n flow_status,\n memory_peak,\n status,\n worker,\n workflow_as_code_status,\n result_columns,\n retries,\n extras\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17\n )\n ON CONFLICT (id) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Timestamptz",
"Timestamptz",
"Int8",
"Jsonb",
"Bool",
"Varchar",
"Text",
"Jsonb",
"Int4",
{
"Custom": {
"name": "job_status",
"kind": {
"Enum": [
"success",
"failure",
"canceled",
"skipped"
]
}
}
},
"Varchar",
"Jsonb",
"TextArray",
"UuidArray",
"Jsonb"
]
},
"nullable": []
},
"hash": "73d062a512230f5b27b9a39dc4484b2c6723c5568940d2fdba85ce4800d8c5ca"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT content AS \"content!: String\",\n lock AS \"lock: String\", language AS \"language: Option<ScriptLang>\", envs AS \"envs: Vec<String>\", schema AS \"schema: String\", schema_validation AS \"schema_validation: bool\", codebase LIKE '%.tar' as use_tar FROM script WHERE hash = $1 LIMIT 1",
"query": "SELECT content AS \"content!: String\",\n lock AS \"lock: String\", language AS \"language: Option<ScriptLang>\", envs AS \"envs: Vec<String>\", schema AS \"schema: String\", schema_validation AS \"schema_validation: bool\", codebase LIKE '%.tar' as use_tar, codebase LIKE '%.esm%' as is_esm FROM script WHERE hash = $1 LIMIT 1",
"describe": {
"columns": [
{
@@ -68,6 +68,11 @@
"ordinal": 6,
"name": "use_tar",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "is_esm",
"type_info": "Bool"
}
],
"parameters": {
@@ -82,8 +87,9 @@
true,
true,
false,
null,
null
]
},
"hash": "a9db7b2f435bb82acb8c5eeb7f800b28f3256491fdaa168591adc7b4b9f3327a"
"hash": "7bca61bdff25cc5e4181d6a738bf29848b2c7131b1bbc9c3a6fe121c84138662"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH inserted_concurrency_counter AS (\n INSERT INTO concurrency_counter (concurrency_id, job_uuids)\n VALUES ($1, '{}'::jsonb)\n ON CONFLICT DO NOTHING\n )\n INSERT INTO concurrency_key(key, job_id)\n VALUES ($1, $2)\n ON CONFLICT (job_id) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Uuid"
]
},
"nullable": []
},
"hash": "82e1ad04f4030fd20e4738b3cebba728eb654566d3f1d9d9711f4340fa1b47fd"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id, name, display_name, owners, extra_perms, summary, created_by, edited_at FROM folder WHERE workspace_id = $1 ORDER BY name desc LIMIT $2 OFFSET $3",
"query": "SELECT workspace_id, name, display_name, owners, extra_perms, summary, created_by, edited_at FROM folder WHERE workspace_id = $1 ORDER BY name asc LIMIT $2 OFFSET $3",
"describe": {
"columns": [
{
@@ -62,5 +62,5 @@
true
]
},
"hash": "9c6d44ffae63b4050ef3a66cb05d5a4ee50dea4b30093df2e0ceb684201ca4b0"
"hash": "8830fc3736ec6dbbaee20f0ebeba5a87c2c94227798f4545fe0931af2818a6fa"
}

View File

@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO job_logs (\n job_id,\n workspace_id,\n logs,\n log_offset,\n log_file_index\n ) VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (job_id) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Text",
"Int4",
"TextArray"
]
},
"nullable": []
},
"hash": "8c5a1b4ea98856466a31658cfe953492943c961f98036133437ccc1a621ee89c"
}

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO v2_job_runtime (\n id, ping, memory_peak\n ) VALUES (\n $1, $2, $3\n )\n ON CONFLICT (id) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Timestamptz",
"Int4"
]
},
"nullable": []
},
"hash": "95585df7a45e61f0c31a05667b103041f8a893fe9ed2830aecaa4d245b07d739"
}

View File

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

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM app WHERE path = 'g/all/setup_app')",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "a264bbd8dbabb03854bd25350a7aeda0704770eb200bae635f1933eece90c9d6"
}

View File

@@ -0,0 +1,379 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n v2_job.id,\n v2_job.raw_code,\n v2_job.raw_lock,\n v2_job.raw_flow as \"raw_flow: _\",\n v2_job.tag,\n v2_job.created_at,\n v2_job.created_by,\n v2_job.permissioned_as,\n v2_job.permissioned_as_email,\n v2_job.kind as \"kind: _\",\n v2_job.runnable_id,\n v2_job.runnable_path,\n v2_job.parent_job,\n v2_job.root_job,\n v2_job.script_lang as \"script_lang: _\",\n v2_job.script_entrypoint_override,\n v2_job.flow_step,\n v2_job.flow_step_id,\n v2_job.flow_innermost_root_job,\n v2_job.\"trigger\",\n v2_job.trigger_kind as \"trigger_kind: _\",\n v2_job.same_worker,\n v2_job.visible_to_owner,\n v2_job.concurrent_limit,\n v2_job.concurrency_time_window_s,\n v2_job.cache_ttl,\n v2_job.timeout,\n v2_job.priority,\n v2_job.preprocessed,\n v2_job.args as \"args: _\",\n v2_job.labels,\n v2_job.pre_run_error,\n\n v2_job_queue.started_at,\n v2_job_queue.scheduled_for,\n v2_job_queue.running,\n v2_job_queue.canceled_by,\n v2_job_queue.canceled_reason,\n v2_job_queue.suspend,\n v2_job_queue.suspend_until,\n v2_job_queue.worker,\n v2_job_queue.extras as \"extras: _\",\n\n v2_job_runtime.ping,\n v2_job_runtime.memory_peak,\n\n v2_job_status.flow_status as \"flow_status: _\",\n v2_job_status.flow_leaf_jobs as \"flow_leaf_jobs: _\",\n v2_job_status.workflow_as_code_status as \"workflow_as_code_status: _\",\n\n concurrency_key.key as \"concurrency_key?\"\n FROM v2_job_queue\n INNER JOIN v2_job ON v2_job.id = v2_job_queue.id\n LEFT JOIN v2_job_runtime ON v2_job_runtime.id = v2_job_queue.id\n LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id\n LEFT JOIN concurrency_key ON concurrency_key.job_id = v2_job_queue.id\n WHERE v2_job_queue.workspace_id = $1\n AND v2_job_queue.running = false\n AND v2_job.parent_job IS NULL\n AND v2_job.trigger_kind IS DISTINCT FROM 'schedule'\n ORDER BY v2_job.created_at DESC\n LIMIT $2\n OFFSET $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "raw_code",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "raw_lock",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "raw_flow: _",
"type_info": "Jsonb"
},
{
"ordinal": 4,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "permissioned_as_email",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "kind: _",
"type_info": {
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlestepflow",
"flowscript",
"flownode",
"appscript",
"aiagent"
]
}
}
}
},
{
"ordinal": 10,
"name": "runnable_id",
"type_info": "Int8"
},
{
"ordinal": 11,
"name": "runnable_path",
"type_info": "Varchar"
},
{
"ordinal": 12,
"name": "parent_job",
"type_info": "Uuid"
},
{
"ordinal": 13,
"name": "root_job",
"type_info": "Uuid"
},
{
"ordinal": 14,
"name": "script_lang: _",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
},
{
"ordinal": 15,
"name": "script_entrypoint_override",
"type_info": "Varchar"
},
{
"ordinal": 16,
"name": "flow_step",
"type_info": "Int4"
},
{
"ordinal": 17,
"name": "flow_step_id",
"type_info": "Varchar"
},
{
"ordinal": 18,
"name": "flow_innermost_root_job",
"type_info": "Uuid"
},
{
"ordinal": 19,
"name": "trigger",
"type_info": "Varchar"
},
{
"ordinal": 20,
"name": "trigger_kind: _",
"type_info": {
"Custom": {
"name": "job_trigger_kind",
"kind": {
"Enum": [
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats",
"schedule",
"app",
"ui",
"postgres",
"sqs",
"gcp",
"mqtt"
]
}
}
}
},
{
"ordinal": 21,
"name": "same_worker",
"type_info": "Bool"
},
{
"ordinal": 22,
"name": "visible_to_owner",
"type_info": "Bool"
},
{
"ordinal": 23,
"name": "concurrent_limit",
"type_info": "Int4"
},
{
"ordinal": 24,
"name": "concurrency_time_window_s",
"type_info": "Int4"
},
{
"ordinal": 25,
"name": "cache_ttl",
"type_info": "Int4"
},
{
"ordinal": 26,
"name": "timeout",
"type_info": "Int4"
},
{
"ordinal": 27,
"name": "priority",
"type_info": "Int2"
},
{
"ordinal": 28,
"name": "preprocessed",
"type_info": "Bool"
},
{
"ordinal": 29,
"name": "args: _",
"type_info": "Jsonb"
},
{
"ordinal": 30,
"name": "labels",
"type_info": "TextArray"
},
{
"ordinal": 31,
"name": "pre_run_error",
"type_info": "Text"
},
{
"ordinal": 32,
"name": "started_at",
"type_info": "Timestamptz"
},
{
"ordinal": 33,
"name": "scheduled_for",
"type_info": "Timestamptz"
},
{
"ordinal": 34,
"name": "running",
"type_info": "Bool"
},
{
"ordinal": 35,
"name": "canceled_by",
"type_info": "Varchar"
},
{
"ordinal": 36,
"name": "canceled_reason",
"type_info": "Text"
},
{
"ordinal": 37,
"name": "suspend",
"type_info": "Int4"
},
{
"ordinal": 38,
"name": "suspend_until",
"type_info": "Timestamptz"
},
{
"ordinal": 39,
"name": "worker",
"type_info": "Varchar"
},
{
"ordinal": 40,
"name": "extras: _",
"type_info": "Jsonb"
},
{
"ordinal": 41,
"name": "ping",
"type_info": "Timestamptz"
},
{
"ordinal": 42,
"name": "memory_peak",
"type_info": "Int4"
},
{
"ordinal": 43,
"name": "flow_status: _",
"type_info": "Jsonb"
},
{
"ordinal": 44,
"name": "flow_leaf_jobs: _",
"type_info": "Jsonb"
},
{
"ordinal": 45,
"name": "workflow_as_code_status: _",
"type_info": "Jsonb"
},
{
"ordinal": 46,
"name": "concurrency_key?",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Int8",
"Int8"
]
},
"nullable": [
false,
true,
true,
true,
false,
false,
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
false,
true,
true,
false,
true,
true,
true,
true,
true,
true,
true,
true,
false
]
},
"hash": "a4b6371d33206010b2f3ffd2b09e33244fe8ab9a803248fc23f334034d24aad4"
}

View File

@@ -0,0 +1,125 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO v2_job (\n id,\n raw_code,\n raw_lock,\n raw_flow,\n tag,\n workspace_id,\n created_at,\n created_by,\n permissioned_as,\n permissioned_as_email,\n kind,\n runnable_id,\n runnable_path,\n parent_job,\n root_job,\n script_lang,\n script_entrypoint_override,\n flow_step,\n flow_step_id,\n flow_innermost_root_job,\n trigger,\n trigger_kind,\n same_worker,\n visible_to_owner,\n concurrent_limit,\n concurrency_time_window_s,\n cache_ttl,\n timeout,\n priority,\n preprocessed,\n args,\n labels,\n pre_run_error\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10,\n $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,\n $21, $22, $23, $24, $25, $26, $27, $28, $29, $30,\n $31, $32, $33\n )\n ON CONFLICT (id) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text",
"Jsonb",
"Varchar",
"Varchar",
"Timestamptz",
"Varchar",
"Varchar",
"Varchar",
{
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlestepflow",
"flowscript",
"flownode",
"appscript",
"aiagent"
]
}
}
},
"Int8",
"Varchar",
"Uuid",
"Uuid",
{
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
},
"Varchar",
"Int4",
"Varchar",
"Uuid",
"Varchar",
{
"Custom": {
"name": "job_trigger_kind",
"kind": {
"Enum": [
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats",
"schedule",
"app",
"ui",
"postgres",
"sqs",
"gcp",
"mqtt"
]
}
}
},
"Bool",
"Bool",
"Int4",
"Int4",
"Int4",
"Int4",
"Int2",
"Bool",
"Jsonb",
"TextArray",
"Text"
]
},
"nullable": []
},
"hash": "b1a11db5617e8282f5d8256f193be955155b7d0344dfa908451faf2a0ba9269b"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM group_ WHERE workspace_id = $1 ORDER BY name desc LIMIT $2 OFFSET $3",
"query": "SELECT * FROM group_ WHERE workspace_id = $1 ORDER BY name asc LIMIT $2 OFFSET $3",
"describe": {
"columns": [
{
@@ -38,5 +38,5 @@
false
]
},
"hash": "abc9f034e62ac224894173356aa69e09f3647a45d176e253e4fc8f7206f6a18d"
"hash": "ba9ab074f466bc2c581f018e2592f5a453e8a766c35dbf919d29c96966d63c75"
}

View File

@@ -0,0 +1,55 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id FROM workspace_dependencies\n WHERE name IS NOT DISTINCT FROM $1 AND workspace_id = $2 AND language = $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
]
},
"nullable": [
false
]
},
"hash": "bdb53068a223c12c2d317635993e8fda531e69a880d0ebc44fb5f13ac976b67d"
}

View File

@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO v2_job_status (\n id, flow_status, flow_leaf_jobs, workflow_as_code_status\n ) VALUES (\n $1, $2, $3, $4\n )\n ON CONFLICT (id) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Jsonb",
"Jsonb",
"Jsonb"
]
},
"nullable": []
},
"hash": "c18dc6715b2f895c61d4cbf2d2a671dd89e10000d3313884a663852337a76b2d"
}

View File

@@ -0,0 +1,29 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n imported_path,\n COUNT(DISTINCT importer_path) as \"count!\"\n FROM dependency_map \n WHERE workspace_id = $1 AND imported_path = ANY($2)\n GROUP BY imported_path\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "imported_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"TextArray"
]
},
"nullable": [
false,
null
]
},
"hash": "c6637102979d1acaf7fb76ff8e51badc6df20086769ffd9f9ee9e6bf1527810c"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT name FROM folder WHERE workspace_id = $1 ORDER BY name desc LIMIT $2 OFFSET $3",
"query": "SELECT name FROM folder WHERE workspace_id = $1 ORDER BY name asc LIMIT $2 OFFSET $3",
"describe": {
"columns": [
{
@@ -20,5 +20,5 @@
false
]
},
"hash": "0bfd22be1d6966c61c9a5fedc2522be8a17f06391b882337c74c1817c99b533d"
"hash": "c77fc86e0fa5d483889b0c4201f6d89b454523b31847798bedcc6c460f9ff7ea"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM job_perms WHERE workspace_id = $1 AND job_id = ANY($2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"UuidArray"
]
},
"nullable": []
},
"hash": "cca63828cbb043d2c647830cf50b992d8fa53ec801067e5b6c45daf05e61d136"
}

View File

@@ -0,0 +1,95 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, created_at, archived, name, description, workspace_id, content, language AS \"language: ScriptLang\"\n FROM workspace_dependencies\n WHERE archived = false AND workspace_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "archived",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "content",
"type_info": "Text"
},
{
"ordinal": 7,
"name": "language: ScriptLang",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
false
]
},
"hash": "d22f81df67d55b2d8f1d15404194a7b4604171d12c6a4620514c6d030086936d"
}

View File

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

View File

@@ -0,0 +1,409 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n v2_job.id,\n v2_job.raw_code,\n v2_job.raw_lock,\n v2_job.raw_flow as \"raw_flow: _\",\n v2_job.tag,\n v2_job.created_at,\n v2_job.created_by,\n v2_job.permissioned_as,\n v2_job.permissioned_as_email,\n v2_job.kind as \"kind: _\",\n v2_job.runnable_id,\n v2_job.runnable_path,\n v2_job.parent_job,\n v2_job.root_job,\n v2_job.script_lang as \"script_lang: _\",\n v2_job.script_entrypoint_override,\n v2_job.flow_step,\n v2_job.flow_step_id,\n v2_job.flow_innermost_root_job,\n v2_job.trigger,\n v2_job.trigger_kind as \"trigger_kind: _\",\n v2_job.same_worker,\n v2_job.visible_to_owner,\n v2_job.concurrent_limit,\n v2_job.concurrency_time_window_s,\n v2_job.cache_ttl,\n v2_job.timeout,\n v2_job.priority,\n v2_job.preprocessed,\n v2_job.args as \"args: _\",\n v2_job.labels,\n v2_job.pre_run_error,\n v2_job_completed.duration_ms,\n v2_job_completed.result as \"result: _\",\n v2_job_completed.deleted,\n v2_job_completed.canceled_by,\n v2_job_completed.canceled_reason,\n v2_job_completed.flow_status as \"flow_status: _\",\n v2_job_completed.started_at,\n v2_job_completed.memory_peak,\n v2_job_completed.status as \"status: _\",\n v2_job_completed.completed_at,\n v2_job_completed.worker,\n v2_job_completed.workflow_as_code_status as \"workflow_as_code_status: _\",\n v2_job_completed.result_columns,\n v2_job_completed.retries,\n v2_job_completed.extras as \"extras: _\",\n job_logs.logs AS logs,\n job_logs.log_offset,\n job_logs.log_file_index\n FROM v2_job_completed\n INNER JOIN v2_job ON v2_job.id = v2_job_completed.id\n LEFT JOIN v2_job_status ON v2_job_completed.id = v2_job_status.id\n LEFT JOIN job_logs ON job_logs.job_id = v2_job.id\n WHERE v2_job_completed.workspace_id = $1\n ORDER BY v2_job.created_at DESC\n LIMIT $2\n OFFSET $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "raw_code",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "raw_lock",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "raw_flow: _",
"type_info": "Jsonb"
},
{
"ordinal": 4,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "permissioned_as_email",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "kind: _",
"type_info": {
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlestepflow",
"flowscript",
"flownode",
"appscript",
"aiagent"
]
}
}
}
},
{
"ordinal": 10,
"name": "runnable_id",
"type_info": "Int8"
},
{
"ordinal": 11,
"name": "runnable_path",
"type_info": "Varchar"
},
{
"ordinal": 12,
"name": "parent_job",
"type_info": "Uuid"
},
{
"ordinal": 13,
"name": "root_job",
"type_info": "Uuid"
},
{
"ordinal": 14,
"name": "script_lang: _",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
},
{
"ordinal": 15,
"name": "script_entrypoint_override",
"type_info": "Varchar"
},
{
"ordinal": 16,
"name": "flow_step",
"type_info": "Int4"
},
{
"ordinal": 17,
"name": "flow_step_id",
"type_info": "Varchar"
},
{
"ordinal": 18,
"name": "flow_innermost_root_job",
"type_info": "Uuid"
},
{
"ordinal": 19,
"name": "trigger",
"type_info": "Varchar"
},
{
"ordinal": 20,
"name": "trigger_kind: _",
"type_info": {
"Custom": {
"name": "job_trigger_kind",
"kind": {
"Enum": [
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats",
"schedule",
"app",
"ui",
"postgres",
"sqs",
"gcp",
"mqtt"
]
}
}
}
},
{
"ordinal": 21,
"name": "same_worker",
"type_info": "Bool"
},
{
"ordinal": 22,
"name": "visible_to_owner",
"type_info": "Bool"
},
{
"ordinal": 23,
"name": "concurrent_limit",
"type_info": "Int4"
},
{
"ordinal": 24,
"name": "concurrency_time_window_s",
"type_info": "Int4"
},
{
"ordinal": 25,
"name": "cache_ttl",
"type_info": "Int4"
},
{
"ordinal": 26,
"name": "timeout",
"type_info": "Int4"
},
{
"ordinal": 27,
"name": "priority",
"type_info": "Int2"
},
{
"ordinal": 28,
"name": "preprocessed",
"type_info": "Bool"
},
{
"ordinal": 29,
"name": "args: _",
"type_info": "Jsonb"
},
{
"ordinal": 30,
"name": "labels",
"type_info": "TextArray"
},
{
"ordinal": 31,
"name": "pre_run_error",
"type_info": "Text"
},
{
"ordinal": 32,
"name": "duration_ms",
"type_info": "Int8"
},
{
"ordinal": 33,
"name": "result: _",
"type_info": "Jsonb"
},
{
"ordinal": 34,
"name": "deleted",
"type_info": "Bool"
},
{
"ordinal": 35,
"name": "canceled_by",
"type_info": "Varchar"
},
{
"ordinal": 36,
"name": "canceled_reason",
"type_info": "Text"
},
{
"ordinal": 37,
"name": "flow_status: _",
"type_info": "Jsonb"
},
{
"ordinal": 38,
"name": "started_at",
"type_info": "Timestamptz"
},
{
"ordinal": 39,
"name": "memory_peak",
"type_info": "Int4"
},
{
"ordinal": 40,
"name": "status: _",
"type_info": {
"Custom": {
"name": "job_status",
"kind": {
"Enum": [
"success",
"failure",
"canceled",
"skipped"
]
}
}
}
},
{
"ordinal": 41,
"name": "completed_at",
"type_info": "Timestamptz"
},
{
"ordinal": 42,
"name": "worker",
"type_info": "Varchar"
},
{
"ordinal": 43,
"name": "workflow_as_code_status: _",
"type_info": "Jsonb"
},
{
"ordinal": 44,
"name": "result_columns",
"type_info": "TextArray"
},
{
"ordinal": 45,
"name": "retries",
"type_info": "UuidArray"
},
{
"ordinal": 46,
"name": "extras: _",
"type_info": "Jsonb"
},
{
"ordinal": 47,
"name": "logs",
"type_info": "Text"
},
{
"ordinal": 48,
"name": "log_offset",
"type_info": "Int4"
},
{
"ordinal": 49,
"name": "log_file_index",
"type_info": "TextArray"
}
],
"parameters": {
"Left": [
"Text",
"Int8",
"Int8"
]
},
"nullable": [
false,
true,
true,
true,
false,
false,
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
true,
false,
true,
true,
true,
true,
true,
false,
false,
true,
true,
true,
true,
true,
true,
false,
true
]
},
"hash": "e4d71278fb80126a7a9da73f1889352d4d1e3cb3a8a08f1c9c03055a1cab1235"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM v2_job_queue",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "e7bc612ccdbb2532a321ed83e26a0854975ad4c3f6fe24557b87a197485dff39"
}

View File

@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT importer_path, importer_kind::text, array_agg(importer_node_id) as importer_node_ids FROM dependency_map\n WHERE imported_path = $1\n AND workspace_id = $2\n GROUP BY importer_path, importer_kind",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "importer_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "importer_kind",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "importer_node_ids",
"type_info": "VarcharArray"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
null,
null
]
},
"hash": "ec3359bbc309c2b893e9f68c09c9f61501068f4b027f58279e0c8508839607f4"
}

View File

@@ -0,0 +1,51 @@
{
"db_name": "PostgreSQL",
"query": "SELECT language AS \"language: ScriptLang\" FROM script WHERE path = 'u/admin/hub_sync'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "language: ScriptLang",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "f4af0affaed3b1d30f5c6f4ddf4114718468fbf75ef13c6087c2f57fbbe0b82f"
}

View File

@@ -1,15 +0,0 @@
{
"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,125 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO v2_job (\n id,\n raw_code,\n raw_lock,\n raw_flow,\n tag,\n workspace_id,\n created_at,\n created_by,\n permissioned_as,\n permissioned_as_email,\n kind,\n runnable_id,\n runnable_path,\n parent_job,\n root_job,\n script_lang,\n script_entrypoint_override,\n flow_step,\n flow_step_id,\n flow_innermost_root_job,\n trigger,\n trigger_kind,\n same_worker,\n visible_to_owner,\n concurrent_limit,\n concurrency_time_window_s,\n cache_ttl,\n timeout,\n priority,\n preprocessed,\n args,\n labels,\n pre_run_error\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,\n $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33\n )\n ON CONFLICT (id) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text",
"Jsonb",
"Varchar",
"Varchar",
"Timestamptz",
"Varchar",
"Varchar",
"Varchar",
{
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlestepflow",
"flowscript",
"flownode",
"appscript",
"aiagent"
]
}
}
},
"Int8",
"Varchar",
"Uuid",
"Uuid",
{
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
},
"Varchar",
"Int4",
"Varchar",
"Uuid",
"Varchar",
{
"Custom": {
"name": "job_trigger_kind",
"kind": {
"Enum": [
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats",
"schedule",
"app",
"ui",
"postgres",
"sqs",
"gcp",
"mqtt"
]
}
}
},
"Bool",
"Bool",
"Int4",
"Int4",
"Int4",
"Int4",
"Int2",
"Bool",
"Jsonb",
"TextArray",
"Text"
]
},
"nullable": []
},
"hash": "fa9c2c75b622b23008ef1cdba3cb691ef7d1b0ad9eb2596ccfa7227721a0784f"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM v2_job_completed WHERE workspace_id = $1 AND id = ANY($2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"UuidArray"
]
},
"nullable": []
},
"hash": "fae4c8069ef5389e64a35d3867e9080006f1c36a53e1e9e631d794a876d5e0b3"
}

View File

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

616
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.583.0"
version = "1.587.1"
authors.workspace = true
edition.workspace = true
@@ -33,7 +33,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.583.0"
version = "1.587.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -137,6 +137,7 @@ prometheus = { workspace = true, optional = true }
uuid.workspace = true
gethostname.workspace = true
serde_json.workspace = true
serde_derive.workspace = true
serde_yml.workspace = true
serde.workspace = true
deno_core = { workspace = true, optional = true }

View File

@@ -1 +1 @@
c90bfc11c7c643b2c8c5111f092e7ae142318e9d
2e6cce1bcb9deb750276ffbde4c7ca1f22e70c62

View File

@@ -0,0 +1,10 @@
-- Drop indexes first (though dropping table will cascade)
DROP INDEX IF EXISTS workspace_dependencies_id_workspace ;
DROP INDEX IF EXISTS workspace_dependencies_workspace_lang_name_archived_idx ;
DROP INDEX IF EXISTS workspace_dependencies_workspace_archived_idx;
DROP INDEX IF EXISTS one_non_archived_per_null_name_language_constraint;
DROP INDEX IF EXISTS one_non_archived_per_name_language_constraint;
-- Drop table and sequence
DROP TABLE IF EXISTS workspace_dependencies;
DROP SEQUENCE IF EXISTS workspace_dependencies_id_seq CASCADE;

View File

@@ -0,0 +1,23 @@
CREATE SEQUENCE IF NOT EXISTS workspace_dependencies_id_seq;
CREATE TABLE IF NOT EXISTS workspace_dependencies(
id BIGINT DEFAULT nextval('workspace_dependencies_id_seq') PRIMARY KEY,
name VARCHAR(255), -- If NULL - it's global
content TEXT NOT NULL,
language SCRIPT_LANG NOT NULL,
description text NOT NULL DEFAULT '',
archived BOOLEAN NOT NULL DEFAULT false,
workspace_id character varying(50) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Make any query that tries to create non-linear history fail
CREATE UNIQUE INDEX IF NOT EXISTS one_non_archived_per_name_language_constraint ON workspace_dependencies(name, language, workspace_id) WHERE archived = false AND name IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS one_non_archived_per_null_name_language_constraint ON workspace_dependencies(language, workspace_id) WHERE archived = false AND name IS NULL;
-- Performance indexes for common query patterns
-- For the list query (filtering by workspace_id and archived)
CREATE INDEX IF NOT EXISTS workspace_dependencies_workspace_archived_idx ON workspace_dependencies(workspace_id, archived) WHERE archived = false;
CREATE INDEX IF NOT EXISTS workspace_dependencies_workspace_lang_name_archived_idx ON workspace_dependencies(workspace_id, language, name, archived);
CREATE INDEX IF NOT EXISTS workspace_dependencies_id_workspace ON workspace_dependencies(id, workspace_id);

View File

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

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE account ALTER COLUMN refresh_token TYPE VARCHAR(10000);

View File

@@ -30,3 +30,4 @@ async-recursion.workspace = true
toml.workspace = true
serde.workspace = true
pep440_rs.workspace = true
tracing.workspace = true

View File

@@ -27,7 +27,11 @@ use rustpython_parser::{
use sqlx::{Pool, Postgres};
use windmill_common::{
error::{self, to_anyhow},
worker::PythonAnnotations,
worker::{
split_python_requirements, try_parse_locked_python_version_from_requirements,
PythonAnnotations,
},
workspace_dependencies::{RawWorkspaceDependencies, WorkspaceDependenciesPrefetched},
};
fn replace_import(x: String) -> String {
@@ -170,7 +174,6 @@ fn parse_code_for_imports(code: &str, path: &str) -> error::Result<Vec<NImport>>
// This is needed because we've split off the real main function above
let code_with_fake_main = format!("{}\n\ndef main(): pass", code);
let ast = Suite::parse(&code_with_fake_main, "main.py").map_err(|e| {
error::Error::ExecutionErr(format!("Error parsing code for imports: {}", e.to_string()))
})?;
@@ -259,6 +262,8 @@ pub async fn parse_python_imports(
path: &str,
db: &Pool<Postgres>,
version_specifiers: &mut Vec<pep440_rs::VersionSpecifier>,
locked_v: &mut Option<pep440_rs::Version>,
raw_workspace_dependencies_o: &Option<RawWorkspaceDependencies>,
) -> error::Result<(Vec<String>, Option<String>)> {
let mut compile_error_hint: Option<String> = None;
let mut imports = parse_python_imports_inner(
@@ -268,8 +273,9 @@ pub async fn parse_python_imports(
db,
&mut vec![],
version_specifiers,
// &mut version_specifier.and_then(|_| Some(path.to_owned())),
&mut None
&mut None,
locked_v,
raw_workspace_dependencies_o,
)
.await?
.into_values()
@@ -323,11 +329,22 @@ async fn parse_python_imports_inner(
already_visited: &mut Vec<String>,
version_specifiers: &mut Vec<pep440_rs::VersionSpecifier>,
path_where_annotated_pyv: &mut Option<String>,
locked_v: &mut Option<pep440_rs::Version>,
raw_workspace_dependencies_o: &Option<RawWorkspaceDependencies>,
) -> error::Result<HashMap<String, NImportResolved>> {
tracing::debug!("Parsing python imports for path: {}", path);
let PythonAnnotations { py310, py311, py312, py313, .. } = PythonAnnotations::parse(&code);
tracing::debug!(
"Found python annotations - py310: {}, py311: {}, py312: {}, py313: {}",
py310,
py311,
py312,
py313
);
let mut push_version_specifiers = |perform, unparsed: String| -> error::Result<()> {
if perform {
tracing::debug!("Adding version specifier: {}", unparsed);
pep440_rs::VersionSpecifiers::from_str(unparsed.as_str())
.ok()
.map(|vs| version_specifiers.extend(vs.to_vec()));
@@ -341,10 +358,9 @@ async fn parse_python_imports_inner(
for x in code.lines() {
if x.starts_with("# py:") || x.starts_with("#py:") {
push_version_specifiers(
true,
x.replace('#', "").replace("py:", "").trim().to_owned(),
)?;
let version_spec = x.replace('#', "").replace("py:", "").trim().to_owned();
tracing::debug!("Found inline python version specifier: {}", version_spec);
push_version_specifiers(true, version_spec)?;
} else if !x.starts_with('#') {
break;
}
@@ -369,12 +385,40 @@ async fn parse_python_imports_inner(
// dependencies: Vec<String>,
// }
let find_requirements = code.lines().find_position(|x| {
x.starts_with("#requirements:")
|| x.starts_with("# requirements:")
|| x.starts_with("# /// script")
});
let mut final_imports = HashMap::new();
tracing::debug!("Extracting workspace dependencies for workspace: {}", w_id);
let wdp = WorkspaceDependenciesPrefetched::extract(
code,
windmill_common::scripts::ScriptLang::Python3,
w_id,
raw_workspace_dependencies_o,
path,
db.into(),
)
.await?;
if let Some(c) = wdp.get_python()? {
*locked_v = extract_nimports_from_content(&c, &mut final_imports);
}
if wdp.is_manual() {
tracing::debug!(
"Workspace dependencies mode is Manual, returning {} imports",
final_imports.len()
);
return Ok(final_imports);
}
let find_requirements = code
.lines()
.find_position(|x| x.starts_with("# /// script"));
tracing::debug!(
"Looking for script metadata block, found: {}",
find_requirements.is_some()
);
if let Some((pos, item)) = find_requirements {
tracing::debug!("Found script metadata block at position {}: {}", pos, item);
let mut requirements = HashMap::new();
if item.starts_with("# /// script") {
let mut incorrect = false;
@@ -384,7 +428,7 @@ async fn parse_python_imports_inner(
.map_while(|x| {
incorrect = !x.starts_with('#');
if incorrect || x.starts_with("# ///") {
None
Option::None
} else {
x.get(1..)
}
@@ -392,9 +436,11 @@ async fn parse_python_imports_inner(
.join("\n")
.parse::<toml::Table>()
.map_err(to_anyhow)?;
tracing::debug!("Parsed script metadata: {:?}", metadata);
{
if let Some(v) = metadata.get("requires-python").and_then(|v| v.as_str()) {
tracing::debug!("Found requires-python in metadata: {}", v);
push_version_specifiers(true, v.to_owned())?;
}
};
@@ -403,9 +449,15 @@ async fn parse_python_imports_inner(
.get("dependencies")
.and_then(|dependencies| dependencies.as_array())
.inspect(|list| {
tracing::debug!("Found {} dependencies in script metadata", list.len());
for dependency_v in list.into_iter() {
let requirement = dependency_v.as_str().unwrap_or("ERROR").to_owned();
let key = extract_pkg_name(&requirement);
tracing::debug!(
"Adding dependency from metadata: {} (key: {})",
requirement,
key
);
requirements.insert(
key.clone(),
NImportResolved::Pin {
@@ -418,217 +470,199 @@ async fn parse_python_imports_inner(
);
}
});
} else {
code.lines()
.skip(pos + 1)
.map_while(|x| {
RE.captures(x).and_then(|x| {
x.get(1).map(|m| {
let requirement = m.as_str().to_string();
let key = extract_pkg_name(&requirement);
requirements.insert(
key.clone(),
NImportResolved::Pin {
pins: vec![ImportPin {
pkg: requirement.clone(),
path: Default::default(),
}],
key,
},
);
})
})
})
.collect_vec();
}
Ok(requirements)
} else {
let find_extra_requirements = code.lines().find_position(|x| {
x.starts_with("#extra_requirements:") || x.starts_with("# extra_requirements:")
});
let mut imports: HashMap<String, NImportResolved> = HashMap::new();
if let Some((pos, _)) = find_extra_requirements {
code.lines()
.skip(pos + 1)
.map_while(|x| {
RE.captures(x).and_then(|x| {
x.get(1).map(|m| {
let requirement = m.as_str().to_string();
let key = extract_pkg_name(&requirement);
imports.insert(
key.clone(),
NImportResolved::Pin {
pins: vec![ImportPin {
pkg: requirement,
path: Default::default(),
}],
key,
},
);
})
})
})
.collect_vec();
}
tracing::debug!(
"Returning {} requirements from script metadata",
requirements.len()
);
return Ok(requirements);
}
// Will get unsorted vector of imports found in current script
let mut nimports = parse_code_for_imports(code, path)?;
tracing::debug!("Found {} imports in code", nimports.len());
// Will get unsorted vector of imports found in current script
let mut nimports = parse_code_for_imports(code, path)?;
// It is important to note, that sorting is important and will always result in this pattern:
// 1. All Repins go first
// 2. All Pins go second
// 3. All Auto go third
// 4. All relative imports go the last
//
// This way we make sure all repins are resolved before (re)pins inside imported relative scripts.
nimports.sort();
tracing::debug!("Processing imports in sorted order");
// It is important to note, that sorting is important and will always result in this pattern:
// 1. All Repins go first
// 2. All Pins go second
// 3. All Auto go third
// 4. All relative imports go the last
//
// This way we make sure all repins are resolved before (re)pins inside imported relative scripts.
nimports.sort();
for n in nimports.into_iter() {
let mut nested = match n {
NImport::Relative(rpath) => {
let code = sqlx::query_scalar!(
r#"
for n in nimports.into_iter() {
let mut nested = match n {
NImport::Relative(rpath) => {
let code = sqlx::query_scalar!(
r#"
SELECT content FROM script WHERE path = $1 AND workspace_id = $2
AND archived = false ORDER BY created_at DESC LIMIT 1
"#,
&rpath,
w_id
)
.fetch_optional(db)
.await?
.unwrap_or_else(|| "".to_string());
&rpath,
w_id
)
.fetch_optional(db)
.await?
.unwrap_or_else(|| "".to_string());
if already_visited.contains(&rpath) {
vec![]
if already_visited.contains(&rpath) {
vec![]
} else {
already_visited.push(rpath.clone());
// Because the algo goes depth first, this function will never return relative import
// This why we can safely assume later, that there is no relative imports
parse_python_imports_inner(
&code,
w_id,
&rpath,
db,
already_visited,
version_specifiers,
path_where_annotated_pyv,
locked_v,
raw_workspace_dependencies_o,
)
.await?
.into_values()
.collect_vec()
}
}
NImport::Repin { pin, key } => vec![NImportResolved::Repin { pin, key }],
NImport::Pin { pins, key } => vec![NImportResolved::Pin { pins, key }],
NImport::Auto { pkg, key } => vec![NImportResolved::Auto { pkg, key }],
};
// Nested should also be sorted for the same reason
nested.sort();
tracing::debug!("Processing {} nested imports", nested.len());
// At this point there should be no NImport::Relative in `nested`
for imp in nested {
let key = match imp.clone() {
NImportResolved::Pin { key, .. } => key,
NImportResolved::Repin { key, .. } => key,
NImportResolved::Auto { key, pkg } => key.unwrap_or(pkg),
};
tracing::debug!("Resolving import with key: {}", key);
// Handled cases:
//
// 1.
// Error: Imported windmill scripts have different pins
//
// auto
// ├── pin:2
// └── pin:1
//
// Fix 1:
//
// auto
// ├── pin:1
// └── pin:1
//
// Fix 2:
//
// repin:1
// ├── pin:2
// └── pin:1
//
// 2.
// Error: Imported windmill scripts have different pins
//
// pin:2
// └── pin:1
//
// Fix 1:
//
// auto
// └── pin:1
//
// Fix 2:
//
// repin:2
// └── pin:1
//
// 3. repins allowed to be repinned again
//
// repin:2
// └── repin:1
//
match imp.clone() {
NImportResolved::Repin { .. } => {
if let Some(existing_import) = final_imports.get(&key) {
match existing_import {
// replace
p if matches!(
p,
NImportResolved::Pin { .. } | NImportResolved::Auto { .. }
) =>
{
final_imports.insert(key, imp);
}
// do nothing (older repins have greater precedence)
NImportResolved::Repin { .. } => {}
// Should not be possible
_ => {
return Err(anyhow::anyhow!(
"Internal error: cannot resolve requirement pins",
)
.into());
}
}
} else {
already_visited.push(rpath.clone());
// Because the algo goes depth first, this function will never return relative import
// This why we can safely assume later, that there is no relative imports
parse_python_imports_inner(
&code,
w_id,
&rpath,
db,
already_visited,
version_specifiers,
path_where_annotated_pyv,
)
.await?
.into_values()
.collect_vec()
final_imports.insert(key, imp.clone());
}
}
NImport::Repin { pin, key } => vec![NImportResolved::Repin { pin, key }],
NImport::Pin { pins, key } => vec![NImportResolved::Pin { pins, key }],
NImport::Auto { pkg, key } => vec![NImportResolved::Auto { pkg, key }],
};
// Nested should also be sorted for the same reason
nested.sort();
// At this point there should be no NImport::Relative in `nested`
for imp in nested {
let key = match imp.clone() {
NImportResolved::Pin { key, .. } => key,
NImportResolved::Repin { key, .. } => key,
NImportResolved::Auto { key, pkg } => key.unwrap_or(pkg),
};
// Handled cases:
//
// 1.
// Error: Imported windmill scripts have different pins
//
// auto
// ├── pin:2
// └── pin:1
//
// Fix 1:
//
// auto
// ├── pin:1
// └── pin:1
//
// Fix 2:
//
// repin:1
// ├── pin:2
// └── pin:1
//
// 2.
// Error: Imported windmill scripts have different pins
//
// pin:2
// └── pin:1
//
// Fix 1:
//
// auto
// └── pin:1
//
// Fix 2:
//
// repin:2
// └── pin:1
//
// 3. repins allowed to be repinned again
//
// repin:2
// └── repin:1
//
match imp.clone() {
NImportResolved::Repin { .. } => {
if let Some(existing_import) = imports.get(&key) {
match existing_import {
// replace
p if matches!(
p,
NImportResolved::Pin { .. } | NImportResolved::Auto { .. }
) =>
{
imports.insert(key, imp);
}
// do nothing (older repins have greater precedence)
NImportResolved::Repin { .. } => {}
// Should not be possible
_ => {
return Err(anyhow::anyhow!(
"Internal error: cannot resolve requirement pins",
)
.into());
}
NImportResolved::Pin { pins: new_pins, .. } => {
if let Some(existing_import) = final_imports.get_mut(&key) {
match existing_import {
// Check if pin is the same version, if same, do nothing, if not error
NImportResolved::Pin { pins: existing_pins, .. } => {
existing_pins.extend(new_pins)
}
} else {
imports.insert(key, imp.clone());
}
}
NImportResolved::Pin { pins: new_pins, .. } => {
if let Some(existing_import) = imports.get_mut(&key) {
match existing_import {
// Check if pin is the same version, if same, do nothing, if not error
NImportResolved::Pin { pins: existing_pins, .. } => {
existing_pins.extend(new_pins)
}
// do nothing
NImportResolved::Repin { .. } => {}
// Replace with new pin
NImportResolved::Auto { .. } => {
imports.insert(key, imp);
}
// do nothing
NImportResolved::Repin { .. } => {}
// Replace with new pin
NImportResolved::Auto { .. } => {
final_imports.insert(key, imp);
}
} else {
imports.insert(key, imp.clone());
}
} else {
final_imports.insert(key, imp.clone());
}
NImportResolved::Auto { .. } => {
if !imports.contains_key(&key) {
imports.insert(key, imp);
}
}
NImportResolved::Auto { .. } => {
if !final_imports.contains_key(&key) {
final_imports.insert(key, imp);
}
}
}
}
Ok(imports)
}
tracing::debug!(
"Finished processing imports, returning {} final imports",
final_imports.len()
);
Ok(final_imports)
}
fn extract_nimports_from_content(
content: &str,
hm: &mut HashMap<String, NImportResolved>,
) -> Option<pep440_rs::Version> {
let lines = split_python_requirements(content);
let locked_version = try_parse_locked_python_version_from_requirements(&lines);
for requirement in lines {
let key = extract_pkg_name(&requirement);
hm.insert(
key.clone(),
NImportResolved::Pin {
pins: vec![ImportPin { pkg: requirement, path: Default::default() }],
key,
},
);
}
locked_version
}
const STDIMPORTS: [&str; 303] = [

View File

@@ -2581,3 +2581,102 @@ import innerdifffolder
'',
'',
'f/foobar/bar', -28028598712388159, 'python3', '');
--
-- Name: workspace_dependencies_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.workspace_dependencies_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.workspace_dependencies_id_seq OWNER TO postgres;
--
-- Name: workspace_dependencies_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
--
SELECT pg_catalog.setval('public.workspace_dependencies_id_seq', 7, true);
--
-- Name: SEQUENCE workspace_dependencies_id_seq; Type: ACL; Schema: public; Owner: postgres
--
GRANT ALL ON SEQUENCE public.workspace_dependencies_id_seq TO windmill_user;
GRANT ALL ON SEQUENCE public.workspace_dependencies_id_seq TO windmill_admin;
--
-- Name: workspace_dependencies; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.workspace_dependencies (
id bigint DEFAULT nextval('public.workspace_dependencies_id_seq'::regclass) NOT NULL,
name character varying(255),
content text NOT NULL,
language public.script_lang NOT NULL,
description text DEFAULT ''::text NOT NULL,
archived boolean DEFAULT false NOT NULL,
workspace_id character varying(50) NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL
);
ALTER TABLE public.workspace_dependencies OWNER TO postgres;
--
-- Name: workspace_dependencies workspace_dependencies_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.workspace_dependencies
ADD CONSTRAINT workspace_dependencies_pkey PRIMARY KEY (id);
--
-- Name: one_non_archived_per_name_language_constraint; Type: INDEX; Schema: public; Owner: postgres
--
CREATE UNIQUE INDEX one_non_archived_per_name_language_constraint ON public.workspace_dependencies USING btree (name, language, workspace_id) WHERE ((archived = false) AND (name IS NOT NULL));
--
-- Name: one_non_archived_per_null_name_language_constraint; Type: INDEX; Schema: public; Owner: postgres
--
CREATE UNIQUE INDEX one_non_archived_per_null_name_language_constraint ON public.workspace_dependencies USING btree (language, workspace_id) WHERE ((archived = false) AND (name IS NULL));
--
-- Name: workspace_dependencies_workspace_archived_idx; Type: INDEX; Schema: public; Owner: postgres
--
CREATE INDEX workspace_dependencies_workspace_archived_idx ON public.workspace_dependencies USING btree (workspace_id, archived) WHERE (archived = false);
--
-- Name: workspace_dependencies_workspace_lang_name_idx; Type: INDEX; Schema: public; Owner: postgres
--
CREATE INDEX workspace_dependencies_workspace_lang_name_idx ON public.workspace_dependencies USING btree (workspace_id, language, name);
--
-- Name: TABLE workspace_dependencies; Type: ACL; Schema: public; Owner: postgres
--
GRANT SELECT,INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,UPDATE ON TABLE public.workspace_dependencies TO windmill_user;
GRANT SELECT,INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,UPDATE ON TABLE public.workspace_dependencies TO windmill_admin;
--
-- PostgreSQL database dump complete
--

View File

@@ -18,8 +18,16 @@ def main():
pass
";
let (r, ..) =
parse_python_imports(code, "test-workspace", "f/foo/bar", &db, &mut vec![]).await?;
let (r, ..) = parse_python_imports(
code,
"test-workspace",
"f/foo/bar",
&db,
&mut vec![],
&mut None,
&None,
)
.await?;
// println!("{}", serde_json::to_string(&r)?);
assert_eq!(
r,
@@ -51,8 +59,16 @@ def main():
pass
";
let (r, ..) =
parse_python_imports(code, "test-workspace", "f/foo/bar", &db, &mut vec![]).await?;
let (r, ..) = parse_python_imports(
code,
"test-workspace",
"f/foo/bar",
&db,
&mut vec![],
&mut None,
&None,
)
.await?;
println!("{}", serde_json::to_string(&r)?);
assert_eq!(r, vec!["burkina=0.4", "nigeria"]);
@@ -74,8 +90,16 @@ def main():
";
let (r, ..) =
parse_python_imports(code, "test-workspace", "f/foo/bar", &db, &mut vec![]).await?;
let (r, ..) = parse_python_imports(
code,
"test-workspace",
"f/foo/bar",
&db,
&mut vec![],
&mut None,
&None,
)
.await?;
println!("{}", serde_json::to_string(&r)?);
assert_eq!(
r,

View File

@@ -65,15 +65,19 @@ const targets = [
# -Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
# Build all separately
def 'main all' [ --no-opt(-n), --cli] {
def 'main all' [ --no-opt(-n), --cli, --node ] {
if ($no_opt) {
if ($cli) {
if ($node) {
$targets | each { main $in.ident -n --node }
} else if ($cli) {
$targets | each { main $in.ident -n --cli }
} else {
$targets | each { main $in.ident -n }
}
} else {
if ($cli) {
if ($node) {
$targets | each { main $in.ident --node }
} else if ($cli) {
$targets | each { main $in.ident --cli }
} else {
$targets | each { main $in.ident }
@@ -86,6 +90,7 @@ def main [
target: string, # Language, e.g.: py, ts, rust
--no-opt(-n), # Compile in debug mode for dev
--cli # Compile and place binaries in cli.
--node
] {
let t = $targets | where ident == $target;
@@ -97,7 +102,10 @@ def main [
mut profile = "";
mut tar = "";
if ($cli) {
if ($node) {
$env.OUT_DIR = $"node-pkg-($t.ident)"
$tar = "nodejs"
} else if ($cli) {
$env.OUT_DIR = $"../../../cli/wasm/($t.ident)"
$tar = "deno"
} else {

View File

@@ -271,8 +271,8 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
let job_id = Uuid::new_v4();
let job_dir = format!("{}/cache_init/{}", TMP_DIR, job_id);
create_dir_all(&job_dir)?;
if let Some(lockfile) = res.lockfile {
let _ = windmill_worker::prepare_job_dir(&lockfile, &job_dir).await?;
if let Some(lock) = res.lockfile {
let _ = windmill_worker::prepare_job_dir(&lock, &job_dir).await?;
let envs = windmill_worker::get_common_bun_proc_envs(None).await;
let _ = windmill_worker::install_bun_lockfile(
&mut 0,
@@ -292,7 +292,7 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
if let Err(e) = windmill_worker::prebundle_bun_script(
&res.content,
Some(&lockfile),
&lock,
&path,
&job_id,
"admins",

View File

@@ -2316,13 +2316,14 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, node_n
);
let error_message = format!(
"Job timed out after no ping from job since {} (ZOMBIE_JOB_TIMEOUT: {}, reason: {:?}).\nThis likely means that the job died on worker {}, OOM are a common reason for worker crashes.\nCheck the workers around the time of the last ping and the exit code if any.",
job.last_ping.unwrap_or_default(),
*ZOMBIE_JOB_TIMEOUT,
error_kind.to_string(),
job.worker.clone().unwrap_or_default(),
);
"Job timed out after no ping from job since {} (ZOMBIE_JOB_TIMEOUT: {}, reason: {:?}).\nThis likely means that the job died on worker {}, OOM are a common reason for worker crashes.\nCheck the workers around the time of the last ping and the exit code if any.",
job.last_ping.unwrap_or_default(),
*ZOMBIE_JOB_TIMEOUT,
error_kind.to_string(),
job.worker.clone().unwrap_or_default(),
);
let memory_peak = job.memory_peak.unwrap_or(0);
let (_, killpill_rx_never_used) = KillpillSender::new(1);
let _ = handle_job_error(
db,
&client,
@@ -2335,6 +2336,7 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, node_n
"",
node_name,
send_result_never_used,
&killpill_rx_never_used,
#[cfg(feature = "benchmark")]
&mut windmill_common::bench::BenchmarkIter::new(),
)

View File

@@ -1,7 +1,9 @@
// TODO: move all related logic here (if anything left anywhere in codebase)
mod common;
#[cfg(feature = "test_job_debouncing")]
use windmill_api_client::types::NewScript;
#[cfg(feature = "test_job_debouncing")]
fn quick_ns(
content: &str,
language: windmill_api_client::types::ScriptLang,
@@ -43,490 +45,6 @@ fn quick_ns(
}
}
mod dependency_map {
use super::quick_ns;
use sqlx::{Pool, Postgres};
use tokio_stream::StreamExt;
use crate::common::{in_test_worker, init_client, listen_for_completed_jobs, ApiServer};
async fn init(db: Pool<Postgres>) -> (windmill_api_client::Client, u16, ApiServer) {
init_client(db).await
}
async fn _clear_dmap(db: &Pool<Postgres>) {
sqlx::query!("DELETE FROM dependency_map WHERE workspace_id = 'test-workspace'")
.execute(db)
.await
.unwrap();
}
/// Corrects map according to provided replacements.
/// Only changes importer_path and/or id
/// Does not affect imported_path nor kind!
fn corrected_dmap(replacements: Vec<(&str, &str)>) -> Vec<(String, String, String, String)> {
CORRECT_DMAP
.clone()
.into_iter()
.map(|e| {
let mut r = (
e.0.to_owned(),
e.1.to_owned(),
e.2.to_owned(),
e.3.to_owned(),
);
for (from, to) in &replacements {
r = (
r.0.replace(from, to),
r.1, // Kind should be immutable
r.2, // Imported path should be immutable
// We do not modify script contents in test, so we can assume scripts always import the same path
// Modification of kind or imported path considered to be incorrect.
r.3.replace(from, to),
);
}
r
})
.collect()
}
async fn assert_dmap(
db: &Pool<Postgres>,
importer: Option<String>,
expected: Vec<(
impl Into<String>,
impl Into<String>,
impl Into<String>,
impl Into<String>,
)>,
) {
let dmap = sqlx::query_as::<_, (String, String, String, String)>(
"SELECT importer_path, importer_kind::text, imported_path, importer_node_id FROM dependency_map WHERE workspace_id = 'test-workspace' AND ($1::text IS NULL OR importer_path = $1::text)",
)
.bind(importer)
.fetch_all(db)
.await
.unwrap();
assert_eq!(
dmap,
expected
.into_iter()
.map(|(f, s, t, fo)| (f.into(), s.into(), t.into(), fo.into()))
.collect::<Vec<(String, String, String, String)>>()
);
}
lazy_static::lazy_static! {
pub static ref CORRECT_DMAP: Vec<(&'static str, &'static str, &'static str, &'static str)> = vec![
("f/rel/branch", "script", "f/rel/leaf_1", ""),
("f/rel/root_script", "script", "f/rel/branch", ""),
("f/rel/root_script", "script", "f/rel/leaf_1", ""),
("f/rel/root_script", "script", "f/rel/leaf_2", ""),
("f/rel/root_app", "app", "f/rel/leaf_2", "dontpressmeplz"),
("f/rel/root_flow", "flow", "f/rel/leaf_2", "failure"),
("f/rel/root_flow", "flow", "f/rel/branch", "nstep1"),
("f/rel/root_flow", "flow", "f/rel/leaf_1", "nstep1"),
("f/rel/root_flow", "flow", "f/rel/leaf_2", "nstep1"),
("f/rel/root_flow", "flow", "f/rel/leaf_2", "nstep2_2"),
("f/rel/root_flow", "flow", "f/rel/branch", "nstep4_1"),
("f/rel/root_flow", "flow", "f/rel/branch", "nstep5_1"),
("f/rel/root_flow", "flow", "f/rel/leaf_1", "nstep5_1"),
("f/rel/root_flow", "flow", "f/rel/leaf_2", "nstep5_1"),
("f/rel/root_flow", "flow", "f/rel/branch", "preprocessor"),
("f/rel/root_flow", "flow", "f/rel/leaf_1", "preprocessor"),
("f/rel/root_flow", "flow", "f/rel/leaf_2", "preprocessor"),
("f/rel/root_app", "app", "f/rel/branch", "pressmeplz"),
("f/rel/root_app", "app", "f/rel/leaf_1", "pressmeplz"),
("f/rel/root_app", "app", "f/rel/leaf_2", "pressmeplz"),
("f/rel/root_flow", "flow", "f/rel/leaf_2", "qtool1"),
("f/rel/root_app", "app", "f/rel/branch", "youcanpressme")];
}
// TODO:
// Test that checks that we can run rebuild_dmap multiple times in tests.
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_rebuild_correctness(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, _port, _s) = init(db.clone()).await;
assert_dmap(&db, None, CORRECT_DMAP.clone()).await;
// rebuild map
assert!(super::common::rebuild_dmap(&client).await);
assert_dmap(&db, None, CORRECT_DMAP.clone()).await;
Ok(())
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_rebuild_lock(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, _port, _s) = init(db.clone()).await;
// Spawn first rebuild
let handle = {
let client = client.clone();
tokio::spawn(async move { super::common::rebuild_dmap(&client).await })
};
// Immidiately spawn another
let res = client
.client()
.post(format!(
"{}/w/test-workspace/workspaces/rebuild_dependency_map",
client.baseurl()
))
.send()
.await
.unwrap()
.text()
.await
.unwrap();
// Should tell us there is already rebuilt in progress
// Or if it is too fast we will be able to trigger it second time
assert!(&res == "There is already one task pending, try again later." || &res == "Success");
assert!(handle.await.unwrap());
Ok(())
}
// If you deploy from cli and you use raw requirements you don't want the script be included in dmap
// Otherwise script will be overwritten once any relative import is updated
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_with_requirements_txt(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, _port, _s) = init(db.clone()).await;
client
.create_script(
"test-workspace",
&quick_ns(
"
from f.rel.branch import main as br;
from f.rel.leaf_1 import main as lf_1;
from f.rel.leaf_2 import main as lf_2;
def main():
return [br(), lf_1(), lf_2];
",
windmill_api_client::types::ScriptLang::Python3,
"f/rel/root_script",
Some("# from requirements.txt".to_string()),
Some("000000000005165B".into()),
),
)
.await
.unwrap();
assert_dmap(
&db,
Some("f/rel/root_script".into()),
vec![
("f/rel/root_script", "script", "f/rel/branch", ""),
("f/rel/root_script", "script", "f/rel/leaf_1", ""),
("f/rel/root_script", "script", "f/rel/leaf_2", ""),
],
)
.await;
tokio::time::sleep(std::time::Duration::from_secs(13)).await;
assert_dmap(
&db,
Some("f/rel/root_script".into()),
Vec::<(String, String, String, String)>::new(),
)
.await;
Ok(())
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_without_requirements_txt(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
let (client, _port, _s) = init(db.clone()).await;
client
.create_script(
"test-workspace",
&quick_ns(
"
from f.rel.branch import main as br;
from f.rel.leaf_1 import main as lf_1;
from f.rel.leaf_2 import main as lf_2;
def main():
return [br(), lf_1(), lf_2];
",
windmill_api_client::types::ScriptLang::Python3,
"f/rel/root_script",
// We still want to pass lock to it.
Some("# py311".to_string()),
Some("000000000005165B".into()),
),
)
.await
.unwrap();
assert_dmap(&db, None, CORRECT_DMAP.clone()).await;
// tokio::time::sleep(std::time::Duration::from_secs(13)).await;
assert_dmap(&db, None, CORRECT_DMAP.clone()).await;
Ok(())
}
// Consider simple one. Only referenced directly. No deep connections
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_rename_leaf_2(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, port, _s) = init(db.clone()).await;
client
.create_script(
"test-workspace",
&quick_ns(
"
def main():
return 'leaf3';
",
windmill_api_client::types::ScriptLang::Python3,
"f/rel/leaf_2_renamed",
None,
Some("0000000000051659".into()),
),
)
.await
.unwrap();
let mut completed = listen_for_completed_jobs(&db).await;
in_test_worker(&db, completed.next(), port).await;
// Changing leafs should not change dependency map
assert_dmap(&db, None, CORRECT_DMAP.clone()).await;
Ok(())
}
// Consider hard one. Referenced deeply and exists in double references.
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_rename_leaf_1(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, port, _s) = init(db.clone()).await;
client
.create_script(
"test-workspace",
&quick_ns(
"
def main():
return 'leaf1';
",
windmill_api_client::types::ScriptLang::Python3,
"f/rel/leaf_1_renamed",
None,
Some("0000000000051658".into()),
),
)
.await
.unwrap();
let mut completed = listen_for_completed_jobs(&db).await;
in_test_worker(&db, completed.next(), port).await;
// Changing leafs should not change dependency map
assert_dmap(&db, None, CORRECT_DMAP.clone()).await;
Ok(())
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_rename_branch(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, port, _s) = init(db.clone()).await;
client
.create_script(
"test-workspace",
&quick_ns(
"
from f.rel.leaf_1 import main as lf_1;
def main():
return lf_1();
",
windmill_api_client::types::ScriptLang::Python3,
"f/rel/branch_renamed",
None,
Some("000000000005165A".into()),
),
)
.await
.unwrap();
let mut completed = listen_for_completed_jobs(&db).await;
in_test_worker(&db, completed.next(), port).await;
// Changing branches SHOULD change dependency map
// Though it should only change branch item in dmap when it is importer.
// All entries when branch is imported should not change.
let mut corrected_dmap = CORRECT_DMAP.clone();
// Corresponds to importer path of branch entry
corrected_dmap[0].0 = "f/rel/branch_renamed";
assert_dmap(&db, None, corrected_dmap).await;
Ok(())
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_rename_primary_script(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, port, _s) = init(db.clone()).await;
client
.create_script(
"test-workspace",
&quick_ns(
"
from f.rel.branch import main as br;
from f.rel.leaf_1 import main as lf_1;
from f.rel.leaf_2 import main as lf_2;
def main():
return [br(), lf_1(), lf_2];
",
windmill_api_client::types::ScriptLang::Python3,
"f/rel/root_script_renamed",
None,
Some("000000000005165B".into()),
),
)
.await
.unwrap();
let corrected_dmap = corrected_dmap(vec![("root_script", "root_script_renamed")]);
let mut completed = listen_for_completed_jobs(&db).await;
in_test_worker(&db, completed.next(), port).await;
assert_dmap(&db, None, corrected_dmap.clone()).await;
Ok(())
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_rename_primary_flow(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::{cache::flow::fetch_version, flows::NewFlow, worker::to_raw_value};
let (client, port, _s) = init(db.clone()).await;
let flow = fetch_version(&db, 1443253234253454).await.unwrap();
let res = client
.client()
.post(format!(
"{}/w/test-workspace/flows/update/{}",
client.baseurl(),
"f/rel/root_flow" // encode_path()
))
.json(&NewFlow {
path: "f/rel/root_flow_renamed".into(),
summary: "".into(),
description: None,
value: to_raw_value(
&serde_json::from_str::<serde_json::Value>(
&serde_json::to_string(flow.value())
.unwrap()
.replace("nstep1", "Foxes")
.replace("nstep2_2", "like")
.replace("nstep_4_1", "Emeralds"),
)
.unwrap(),
),
schema: None,
draft_only: None,
tag: None,
dedicated_worker: None,
timeout: None,
deployment_message: None,
visible_to_runner_only: None,
on_behalf_of_email: None,
ws_error_handler_muted: None,
})
.send()
.await
.unwrap();
assert_eq!(res.text().await.unwrap(), "f/rel/root_flow_renamed");
let mut completed = listen_for_completed_jobs(&db).await;
in_test_worker(&db, completed.next(), port).await;
assert_dmap(
&db,
None,
corrected_dmap(vec![
("f/rel/root_flow", "f/rel/root_flow_renamed"),
("nstep1", "Foxes"),
("nstep2_2", "like"),
("nstep_4_1", "Emeralds"),
]),
)
.await;
Ok(())
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_rename_primary_app(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, port, _s) = init(db.clone()).await;
let app_value: String =
sqlx::query_scalar!("SELECT value::text FROM app_version WHERE id = 0 AND app_id = 2")
.fetch_one(&db)
.await
.unwrap()
.unwrap();
// TODO: There is:
// 1. update app
// 2. create app
// 3. update app raw
// Ideally all of them should be handled
let res = client
.client()
.post(format!(
"{}/w/test-workspace/apps/update/{}",
client.baseurl(),
"f/rel/root_app" // encode_path()
))
.json(&windmill_api::EditApp {
path: Some("f/rel/root_app_renamed".into()),
summary: None,
value: serde_json::from_str(
&app_value
.replace("dontpressmeplz", "Apps")
.replace("youcanpressme", "Work"),
)
.unwrap(),
policy: None,
deployment_message: None,
custom_path: None,
})
.send()
.await
.unwrap();
assert_eq!(
res.text().await.unwrap(),
"app f/rel/root_app updated (npath: \"f/rel/root_app_renamed\")"
);
let mut completed = listen_for_completed_jobs(&db).await;
in_test_worker(&db, completed.next(), port).await;
assert_dmap(
&db,
None,
corrected_dmap(vec![
("f/rel/root_app", "f/rel/root_app_renamed"),
("dontpressmeplz", "Apps"),
("youcanpressme", "Work"),
]),
)
.await;
Ok(())
}
}
#[cfg(feature = "test_job_debouncing")]
mod dependency_job_debouncing {
async fn trigger_djob_for(

View File

@@ -0,0 +1,535 @@
use sqlx::{Pool, Postgres};
use tokio_stream::StreamExt;
use windmill_api_client::types::NewScript;
mod common;
use common::{in_test_worker, init_client, listen_for_completed_jobs, ApiServer};
mod dependency_map {
use super::*;
fn quick_ns(
content: &str,
language: windmill_api_client::types::ScriptLang,
path: &str,
lock: Option<String>,
parent_hash: Option<String>,
) -> NewScript {
NewScript {
content: content.into(),
language,
lock,
parent_hash,
path: path.into(),
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
description: "".to_string(),
draft_only: None,
envs: vec![],
is_template: None,
kind: None,
summary: "".to_string(),
tag: None,
schema: std::collections::HashMap::new(),
ws_error_handler_muted: Some(false),
priority: None,
delete_after_use: None,
timeout: None,
restart_unless_cancelled: None,
deployment_message: None,
concurrency_key: None,
visible_to_runner_only: None,
no_main_func: None,
codebase: None,
has_preprocessor: None,
on_behalf_of_email: None,
assets: vec![],
}
}
async fn init(db: Pool<Postgres>) -> (windmill_api_client::Client, u16, ApiServer) {
init_client(db).await
}
async fn _clear_dmap(db: &Pool<Postgres>) {
sqlx::query!("DELETE FROM dependency_map WHERE workspace_id = 'test-workspace'")
.execute(db)
.await
.unwrap();
}
/// Corrects map according to provided replacements.
/// Only changes importer_path and/or id
/// Does not affect imported_path nor kind!
fn corrected_dmap(replacements: Vec<(&str, &str)>) -> Vec<(String, String, String, String)> {
CORRECT_DMAP
.clone()
.into_iter()
.map(|e| {
let mut r = (
e.0.to_owned(),
e.1.to_owned(),
e.2.to_owned(),
e.3.to_owned(),
);
for (from, to) in &replacements {
r = (
r.0.replace(from, to),
r.1, // Kind should be immutable
r.2, // Imported path should be immutable
// We do not modify script contents in test, so we can assume scripts always import the same path
// Modification of kind or imported path considered to be incorrect.
r.3.replace(from, to),
);
}
r
})
.collect()
}
async fn assert_dmap(
db: &Pool<Postgres>,
importer: Option<String>,
expected: Vec<(
impl Into<String>,
impl Into<String>,
impl Into<String>,
impl Into<String>,
)>,
) {
let mut dmap = sqlx::query_as::<_, (String, String, String, String)>(
"SELECT importer_path, importer_kind::text, imported_path, importer_node_id FROM dependency_map WHERE workspace_id = 'test-workspace' AND ($1::text IS NULL OR importer_path = $1::text)",
)
.bind(importer)
.fetch_all(db)
.await
.unwrap();
let mut expected = expected
.into_iter()
.map(|(f, s, t, fo)| (f.into(), s.into(), t.into(), fo.into()))
.collect::<Vec<(String, String, String, String)>>();
dmap.sort();
expected.sort();
assert_eq!(dmap, expected);
}
lazy_static::lazy_static! {
pub static ref CORRECT_DMAP: Vec<(&'static str, &'static str, &'static str, &'static str)> = vec![
("f/rel/root_script", "script", "dependencies/test.requirements.in", ""),
("f/rel/root_flow", "flow", "dependencies/test.requirements.in", "nstep1"),
("f/rel/root_app", "app", "dependencies/test.requirements.in", "dontpressmeplz"),
("f/rel/branch", "script", "f/rel/leaf_1", ""),
("f/rel/root_script", "script", "f/rel/branch", ""),
("f/rel/root_script", "script", "f/rel/leaf_1", ""),
("f/rel/root_script", "script", "f/rel/leaf_2", ""),
("f/rel/root_app", "app", "f/rel/leaf_2", "dontpressmeplz"),
("f/rel/root_flow", "flow", "f/rel/leaf_2", "failure"),
("f/rel/root_flow", "flow", "f/rel/branch", "nstep1"),
("f/rel/root_flow", "flow", "f/rel/leaf_1", "nstep1"),
("f/rel/root_flow", "flow", "f/rel/leaf_2", "nstep1"),
("f/rel/root_flow", "flow", "f/rel/leaf_2", "nstep2_2"),
("f/rel/root_flow", "flow", "f/rel/branch", "nstep4_1"),
("f/rel/root_flow", "flow", "f/rel/branch", "nstep5_1"),
("f/rel/root_flow", "flow", "f/rel/leaf_1", "nstep5_1"),
("f/rel/root_flow", "flow", "f/rel/leaf_2", "nstep5_1"),
("f/rel/root_flow", "flow", "f/rel/branch", "preprocessor"),
("f/rel/root_flow", "flow", "f/rel/leaf_1", "preprocessor"),
("f/rel/root_flow", "flow", "f/rel/leaf_2", "preprocessor"),
("f/rel/root_app", "app", "f/rel/branch", "pressmeplz"),
("f/rel/root_app", "app", "f/rel/leaf_1", "pressmeplz"),
("f/rel/root_app", "app", "f/rel/leaf_2", "pressmeplz"),
("f/rel/root_flow", "flow", "f/rel/leaf_2", "qtool1"),
("f/rel/root_app", "app", "f/rel/branch", "youcanpressme"),
// Default
("f/rel/leaf_1", "script", "dependencies/requirements.in", ""),
("f/rel/leaf_2", "script", "dependencies/requirements.in", ""),
("f/rel/branch", "script", "dependencies/requirements.in", ""),
("f/rel/root_flow", "flow", "dependencies/requirements.in", "failure"),
("f/rel/root_flow", "flow", "dependencies/package.json", "nstep2_1"),
("f/rel/root_flow", "flow", "dependencies/package.json", "nstep3_2"),
("f/rel/root_flow", "flow", "dependencies/requirements.in", "nstep2_2"),
("f/rel/root_flow", "flow", "dependencies/requirements.in", "nstep3_1"),
("f/rel/root_flow", "flow", "dependencies/requirements.in", "nstep4_1"),
("f/rel/root_flow", "flow", "dependencies/requirements.in", "nstep5_1"),
("f/rel/root_flow", "flow", "dependencies/requirements.in", "preprocessor"),
("f/rel/root_app", "app", "dependencies/requirements.in", "pressmeplz"),
("f/rel/root_flow", "flow", "dependencies/requirements.in", "qtool1"),
("f/rel/root_app", "app", "dependencies/requirements.in", "youcanpressme")
];
}
// TODO:
// Test that checks that we can run rebuild_dmap multiple times in tests.
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_rebuild_correctness(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, _port, _s) = init(db.clone()).await;
assert_dmap(&db, None, CORRECT_DMAP.clone()).await;
// rebuild map
assert!(common::rebuild_dmap(&client).await);
assert_dmap(&db, None, CORRECT_DMAP.clone()).await;
Ok(())
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_rebuild_lock(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, _port, _s) = init(db.clone()).await;
// Spawn first rebuild
let handle = {
let client = client.clone();
tokio::spawn(async move { common::rebuild_dmap(&client).await })
};
// Immidiately spawn another
let res = client
.client()
.post(format!(
"{}/w/test-workspace/workspaces/rebuild_dependency_map",
client.baseurl()
))
.send()
.await
.unwrap()
.text()
.await
.unwrap();
// Should tell us there is already rebuilt in progress
// Or if it is too fast we will be able to trigger it second time
assert!(&res == "There is already one task pending, try again later." || &res == "Success");
assert!(handle.await.unwrap());
Ok(())
}
// If you deploy from cli and you use raw requirements you don't want the script be included in dmap
// Otherwise script will be overwritten once any relative import is updated
// #[cfg(feature = "python")]
// #[sqlx::test(fixtures("base", "dependency_map"))]
// async fn relative_imports_test_with_legacy(db: Pool<Postgres>) -> anyhow::Result<()> {
// let (client, _port, _s) = init(db.clone()).await;
// client
// .create_script(
// "test-workspace",
// &quick_ns(
// "
// from f.rel.branch import main as br;
// from f.rel.leaf_1 import main as lf_1;
// from f.rel.leaf_2 import main as lf_2;
// def main():
// return [br(), lf_1(), lf_2];
// ",
// windmill_api_client::types::ScriptLang::Python3,
// "f/rel/root_script",
// Some("# from requirements.txt".to_string()),
// Some("000000000005165B".into()),
// ),
// )
// .await
// .unwrap();
// assert_dmap(
// &db,
// Some("f/rel/root_script".into()),
// vec![
// ("f/rel/root_script", "script", "f/rel/branch", ""),
// ("f/rel/root_script", "script", "f/rel/leaf_1", ""),
// ("f/rel/root_script", "script", "f/rel/leaf_2", ""),
// ],
// )
// .await;
// tokio::time::sleep(std::time::Duration::from_secs(13)).await;
// assert_dmap(
// &db,
// Some("f/rel/root_script".into()),
// vec![
// ("f/rel/root_script", "script", "f/rel/branch", ""),
// ("f/rel/root_script", "script", "f/rel/leaf_1", ""),
// ("f/rel/root_script", "script", "f/rel/leaf_2", ""),
// ],
// )
// .await;
// Ok(())
// }
// Consider simple one. Only referenced directly. No deep connections
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_rename_leaf_2(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, port, _s) = init(db.clone()).await;
client
.create_script(
"test-workspace",
&quick_ns(
"
def main():
return 'leaf3';
",
windmill_api_client::types::ScriptLang::Python3,
"f/rel/leaf_2_renamed",
None,
Some("0000000000051659".into()),
),
)
.await
.unwrap();
let mut completed = listen_for_completed_jobs(&db).await;
in_test_worker(&db, completed.next(), port).await;
// Changing leafs should not change dependency map
assert_dmap(
&db,
None,
corrected_dmap(vec![("f/rel/leaf_2", "f/rel/leaf_2_renamed")]),
)
.await;
Ok(())
}
// Consider hard one. Referenced deeply and exists in double references.
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_rename_leaf_1(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, port, _s) = init(db.clone()).await;
client
.create_script(
"test-workspace",
&quick_ns(
"
def main():
return 'leaf1';
",
windmill_api_client::types::ScriptLang::Python3,
"f/rel/leaf_1_renamed",
None,
Some("0000000000051658".into()),
),
)
.await
.unwrap();
let mut completed = listen_for_completed_jobs(&db).await;
in_test_worker(&db, completed.next(), port).await;
// Changing leafs should not change dependency map
assert_dmap(
&db,
None,
corrected_dmap(vec![("f/rel/leaf_1", "f/rel/leaf_1_renamed")]),
)
.await;
Ok(())
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_rename_branch(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, port, _s) = init(db.clone()).await;
client
.create_script(
"test-workspace",
&quick_ns(
"
from f.rel.leaf_1 import main as lf_1;
def main():
return lf_1();
",
windmill_api_client::types::ScriptLang::Python3,
"f/rel/branch_renamed",
None,
Some("000000000005165A".into()),
),
)
.await
.unwrap();
let mut completed = listen_for_completed_jobs(&db).await;
in_test_worker(&db, completed.next(), port).await;
// Changing branches SHOULD change dependency map
// Though it should only change branch item in dmap when it is importer.
// All entries when branch is imported should not change.
let corrected_dmap = CORRECT_DMAP
.clone()
.iter_mut()
.map(|el| {
if el.0 == "f/rel/branch" {
el.0 = "f/rel/branch_renamed";
}
*el
})
.collect::<Vec<_>>();
assert_dmap(&db, None, corrected_dmap).await;
Ok(())
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_rename_primary_script(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, port, _s) = init(db.clone()).await;
client
.create_script(
"test-workspace",
&quick_ns(
"
# requirements: test
from f.rel.branch import main as br;
from f.rel.leaf_1 import main as lf_1;
from f.rel.leaf_2 import main as lf_2;
def main():
return [br(), lf_1(), lf_2];
",
windmill_api_client::types::ScriptLang::Python3,
"f/rel/root_script_renamed",
None,
Some("000000000005165B".into()),
),
)
.await
.unwrap();
let corrected_dmap = corrected_dmap(vec![("root_script", "root_script_renamed")]);
let mut completed = listen_for_completed_jobs(&db).await;
in_test_worker(&db, completed.next(), port).await;
assert_dmap(&db, None, corrected_dmap.clone()).await;
Ok(())
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_rename_primary_flow(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::{cache::flow::fetch_version, flows::NewFlow, worker::to_raw_value};
let (client, port, _s) = init(db.clone()).await;
let flow = fetch_version(&db, 1443253234253454).await.unwrap();
let res = client
.client()
.post(format!(
"{}/w/test-workspace/flows/update/{}",
client.baseurl(),
"f/rel/root_flow" // encode_path()
))
.json(&NewFlow {
path: "f/rel/root_flow_renamed".into(),
summary: "".into(),
description: None,
value: to_raw_value(
&serde_json::from_str::<serde_json::Value>(
&serde_json::to_string(flow.value())
.unwrap()
.replace("nstep1", "Foxes")
.replace("nstep2_2", "like")
.replace("nstep_4_1", "Emeralds"),
)
.unwrap(),
),
schema: None,
draft_only: None,
tag: None,
dedicated_worker: None,
timeout: None,
deployment_message: None,
visible_to_runner_only: None,
on_behalf_of_email: None,
ws_error_handler_muted: None,
})
.send()
.await
.unwrap();
assert_eq!(res.text().await.unwrap(), "f/rel/root_flow_renamed");
let mut completed = listen_for_completed_jobs(&db).await;
in_test_worker(&db, completed.next(), port).await;
assert_dmap(
&db,
None,
corrected_dmap(vec![
("f/rel/root_flow", "f/rel/root_flow_renamed"),
("nstep1", "Foxes"),
("nstep2_2", "like"),
("nstep_4_1", "Emeralds"),
]),
)
.await;
Ok(())
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "dependency_map"))]
async fn relative_imports_test_rename_primary_app(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, port, _s) = init(db.clone()).await;
let app_value: String =
sqlx::query_scalar!("SELECT value::text FROM app_version WHERE id = 0 AND app_id = 2")
.fetch_one(&db)
.await
.unwrap()
.unwrap();
// TODO: There is:
// 1. update app
// 2. create app
// 3. update app raw
// Ideally all of them should be handled
let res = client
.client()
.post(format!(
"{}/w/test-workspace/apps/update/{}",
client.baseurl(),
"f/rel/root_app" // encode_path()
))
.json(&windmill_api::EditApp {
path: Some("f/rel/root_app_renamed".into()),
summary: None,
value: serde_json::from_str(
&app_value
.replace("dontpressmeplz", "Apps")
.replace("youcanpressme", "Work"),
)
.unwrap(),
policy: None,
deployment_message: None,
custom_path: None,
})
.send()
.await
.unwrap();
assert_eq!(
res.text().await.unwrap(),
"app f/rel/root_app updated (npath: \"f/rel/root_app_renamed\")"
);
let mut completed = listen_for_completed_jobs(&db).await;
in_test_worker(&db, completed.next(), port).await;
assert_dmap(
&db,
None,
corrected_dmap(vec![
("f/rel/root_app", "f/rel/root_app_renamed"),
("dontpressmeplz", "Apps"),
("youcanpressme", "Work"),
]),
)
.await;
Ok(())
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
-- Fixture for testing hub_sync blacklist from workspace dependencies
-- hub_sync and apps are already built-in via migrations
-- Insert a simple Bun script that should be affected by workspace dependencies
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'admins',
'test-user',
'
export async function main() {
return "Simple bun script";
}',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'Simple bun script',
'',
'u/admin/simple_bun', 700001, 'bun', '');

View File

@@ -0,0 +1,33 @@
-- NOTE: Applied after workspace_dependencies_leafs.sql
-- Python script that uses relative imports
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'test-user',
'
from f.leafs.python import main as python_leaf
def main():
return {"python_import": python_leaf()}',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/python_importer', 500005, 'python3', '');
-- TypeScript script that uses relative imports
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'test-user',
'
import { main as tsLeaf } from "./leafs/ts";
export async function main() {
return { ts_import: await tsLeaf() };
}',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/ts_importer', 500006, 'nativets', '');
-- Dependency map entries
INSERT INTO dependency_map (workspace_id, importer_path, importer_kind, imported_path, importer_node_id) VALUES ('test-workspace', 'f/python_importer', 'script', 'f/leafs/python', '');
INSERT INTO dependency_map (workspace_id, importer_path, importer_kind, imported_path, importer_node_id) VALUES ('test-workspace', 'f/ts_importer', 'script', 'f/leafs/ts', '');

View File

@@ -0,0 +1,54 @@
-- Basic leaf scripts in different languages
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'test-user',
'
export async function main() {
return "TypeScript leaf";
}',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/leafs/ts', 500001, 'nativets', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'test-user',
'
package main
import "fmt"
func main() {
fmt.Println("Go leaf")
}',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/leafs/go', 500002, 'go', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'test-user',
'
def main():
return "Python leaf"',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/leafs/python', 500003, 'python3', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'test-user',
'
<?php
function main() {
return "PHP leaf";
}
?>',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/leafs/php', 500004, 'php', '');

View File

@@ -25,7 +25,7 @@ def main():
&db,
content,
ScriptLang::Python3,
vec!["# py: 3.11.11", "tiny==0.1.3"],
vec!["# workspace-dependencies-mode: manual\n# py: 3.11.11","tiny==0.1.3"],
)
.await?;
Ok(())
@@ -55,7 +55,11 @@ def main():
&db,
content,
ScriptLang::Python3,
vec!["# py: 3.11.11", "bottle==0.13.2", "tiny==0.1.2"],
vec![
"# workspace-dependencies-mode: extra\n# py: 3.11.11",
"bottle==0.13.2",
"tiny==0.1.2",
],
)
.await?;
}
@@ -79,7 +83,11 @@ def main():
&db,
content,
ScriptLang::Python3,
vec!["# py: 3.11.11", "simplejson==3.20.1", "tiny==0.1.3"],
vec![
"# workspace-dependencies-mode: extra\n# py: 3.11.11",
"simplejson==3.20.1",
"tiny==0.1.3",
],
)
.await?;
Ok(())
@@ -108,7 +116,7 @@ def main():
content,
ScriptLang::Python3,
vec![
"# py: 3.11.11",
"# workspace-dependencies-mode: extra\n# py: 3.11.11",
"bottle==0.13.2",
"microdot==2.2.0",
"simplejson==3.19.3",

View File

@@ -217,6 +217,7 @@ async fn test_deno_flow(db: Pool<Postgres>) -> anyhow::Result<()> {
iterator: InputTransform::Javascript { expr: "result".to_string() },
skip_failures: false,
parallel: false,
squash: None,
parallelism: None,
modules: vec![FlowModule {
id: "c".to_string(),
@@ -396,6 +397,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) -> anyhow::Result<()> {
iterator: InputTransform::Static { value: windmill_common::worker::to_raw_value(&[1, 2, 3]) },
skip_failures: false,
parallel: false,
squash: None,
parallelism: None,
modules: vec![
FlowModule {

View File

@@ -0,0 +1,194 @@
mod common;
mod workspace_dependencies {
use crate::common::in_test_worker;
use crate::common::init_client;
use crate::common::listen_for_completed_jobs;
use sqlx::{Pool, Postgres};
use tokio_stream::StreamExt;
use windmill_common::scripts::ScriptLang;
use windmill_worker::workspace_dependencies::NewWorkspaceDependencies;
mod deps {
pub const REQUIREMENTS_IN: &'static str = "tiny==0.1.3";
// pub const GO_MOD: &'static str = r##"
// module example.com/project
// go 1.20
// require github.com/gin-gonic/gin v1.8.1
// "##;
pub const PACKAGE_JSON: &'static str = r##"
{
"name": "example-project",
"version": "1.0.0",
"dependencies": {
"express": "^4.17.1"
}
}
"##;
pub const COMPOSER_JSON: &'static str = r##"
{
"name": "example/project",
"require": {
"monolog/monolog": "^2.3"
}
}
"##;
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "workspace_dependencies_leafs"))]
#[ignore]
async fn basic_manual_named(db: Pool<Postgres>) -> anyhow::Result<()> {
let ((_client, port, _s), db, mut completed) = (
init_client(db.clone()).await,
&db,
listen_for_completed_jobs(&db).await,
);
for (idx, (l, c)) in [
(ScriptLang::Python3, deps::REQUIREMENTS_IN),
(ScriptLang::Bun, deps::PACKAGE_JSON),
(ScriptLang::Php, deps::COMPOSER_JSON),
// (ScriptLang::Go, deps::GO_MOD),
]
.iter()
.enumerate()
{
let id = NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: *l,
content: (*c).into(),
name: Some("test".to_owned()),
description: None,
}
.create(("".to_owned(), "".to_owned(), "".to_owned()), db.clone())
.await
.unwrap();
assert_eq!(idx + 1, id as usize);
}
// Wait for 4 jobs.
// Creating those dependencies will trigger redeployment of all scripts in workspace_dependencies_leafs.sql
in_test_worker(
db,
async {
completed.next().await;
completed.next().await;
completed.next().await;
// completed.next().await;
},
port,
)
.await;
// Verify all scripts have correct locks
// let mut langs = vec![];
// for r in sqlx::query!(
// r#"SELECT language AS "language: ScriptLang",lock FROM script WHERE archived = false"#
// )
// .fetch_all(db)
// .await
// .unwrap()
// {
// match r.language {
// ScriptLang::Python3 => assert_eq!("", &r.lock.unwrap()),
// ScriptLang::Go => todo!(),
// ScriptLang::Bun => todo!(),
// ScriptLang::Bunnative => todo!(),
// ScriptLang::Php => todo!(),
// _ => panic!("Unsupported language"),
// }
// langs.push(r.language);
// }
// langs.sort();
// // Just tiny additional verification for peace of mind.
// assert_eq!(langs.as_slice(), &[]);
Ok(())
}
#[sqlx::test(fixtures("base", "hub_sync_blacklist"))]
async fn hub_sync_blacklist_from_workspace_deps(db: Pool<Postgres>) -> anyhow::Result<()> {
let ((_client, port, _s), db, mut completed) = (
init_client(db.clone()).await,
&db,
listen_for_completed_jobs(&db).await,
);
// Verify built-in fixtures exist
// Check that the setup_app exists
let app_exists =
sqlx::query_scalar!("SELECT EXISTS(SELECT 1 FROM app WHERE path = 'g/all/setup_app')")
.fetch_one(db)
.await
.unwrap();
assert!(app_exists.unwrap(), "Expected g/all/setup_app to exist");
// Check that hub_sync script exists and is a Bun script
let hub_sync_lang = sqlx::query_scalar!(
r#"SELECT language AS "language: ScriptLang" FROM script WHERE path = 'u/admin/hub_sync'"#
)
.fetch_one(db)
.await
.unwrap();
assert_eq!(
hub_sync_lang,
ScriptLang::Bun,
"Expected hub_sync to be a Bun script"
);
// Create unnamed (default) workspace dependencies for Bun
let _id = NewWorkspaceDependencies {
workspace_id: "admins".into(),
language: ScriptLang::Bun,
content: deps::PACKAGE_JSON.into(),
name: None, // No name = default workspace dependencies
description: None,
}
.create(("".to_owned(), "".to_owned(), "".to_owned()), db.clone())
.await
.unwrap();
// Wait for exactly 1 job (only u/admin/simple_bun, not hub_sync)
let job_id = in_test_worker(db, async { completed.next().await }, port)
.await
.expect("Expected one job to complete");
// Query the job's runnable_path
let runnable_path =
sqlx::query_scalar!("SELECT runnable_path FROM v2_job WHERE id = $1", job_id)
.fetch_one(db)
.await
.unwrap();
assert_eq!(
runnable_path,
Some("u/admin/simple_bun".to_string()),
"Expected job runnable_path to be 'u/admin/simple_bun' (hub_sync should be blacklisted)"
);
// Assert total job count is 1
let job_count = sqlx::query_scalar!("SELECT COUNT(*) FROM v2_job")
.fetch_one(db)
.await
.unwrap();
assert_eq!(job_count, Some(1), "Expected exactly one job total");
// Assert v2_job_queue is empty
let queue_count = sqlx::query_scalar!("SELECT COUNT(*) FROM v2_job_queue")
.fetch_one(db)
.await
.unwrap();
assert_eq!(queue_count, Some(0), "Expected job queue to be empty");
Ok(())
}
}

View File

@@ -2,15 +2,6 @@
set -e
# Parse arguments
while [[ "$#" -gt 0 ]]; do
case $1 in
--dir) EE_DIR="$2"; shift ;;
*) echo "Unknown parameter: $1"; exit 1 ;;
esac
shift
done
# Check if running on macOS
if [[ "$(uname)" == "Darwin" ]]; then
echo "Running on macOS - substituting samael..."

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.583.0
version: 1.587.1
title: Windmill API
contact:
@@ -688,6 +688,22 @@ paths:
schema:
$ref: "#/components/schemas/UserWorkspaceList"
/w/{workspace}/workspaces/get_as_superadmin:
get:
summary: get workspace as super admin (require to be super admin)
operationId: getWorkspaceAsSuperAdmin
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: workspace
content:
application/json:
schema:
$ref: "#/components/schemas/Workspace"
/workspaces/list_as_superadmin:
get:
summary: list all workspaces as super admin (require to be super admin)
@@ -2233,6 +2249,56 @@ paths:
text/plain:
schema:
type: string
/w/{workspace}/workspaces/get_dependents/{imported_path}:
get:
summary: get dependents of an imported path
operationId: getDependents
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: imported_path
in: path
required: true
schema:
type: string
description: The imported path to get dependents for
responses:
"200":
description: list of dependents
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/DependencyDependent"
/w/{workspace}/workspaces/get_dependents_amounts:
post:
summary: get dependents amounts for multiple imported paths
operationId: getDependentsAmounts
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: List of imported paths to get dependents counts for
required: true
content:
application/json:
schema:
type: array
items:
type: string
responses:
"200":
description: list of dependents amounts
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/DependentsAmount"
/w/{workspace}/workspaces/get_dependency_map:
get:
@@ -5180,6 +5246,14 @@ paths:
Accepts multiple values as a comma-separated list.
schema:
type: string
- name: without_description
in: query
description: |
(default false)
If true, the description field will be omitted from the response.
schema:
type: boolean
responses:
"200":
description: All scripts
@@ -5376,6 +5450,126 @@ paths:
schema:
type: boolean
/w/{workspace}/workspace_dependencies/create:
post:
summary: create workspace dependencies
operationId: createWorkspaceDependencies
tags:
- workspace_dependencies
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: New workspace dependencies
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewWorkspaceDependencies"
responses:
"201":
description: workspace dependencies created
content:
text/plain:
schema:
type: string
/w/{workspace}/workspace_dependencies/archive/{language}:
post:
summary: archive workspace dependencies (require admin)
operationId: archiveWorkspaceDependencies
tags:
- workspace_dependencies
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: language
in: path
required: true
schema:
$ref: "#/components/schemas/ScriptLang"
- name: name
in: query
required: false
schema:
type: string
responses:
"200":
description: result
content:
application/json:
schema: {}
/w/{workspace}/workspace_dependencies/delete/{language}:
post:
summary: delete workspace dependencies (require admin)
operationId: deleteWorkspaceDependencies
tags:
- workspace_dependencies
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: language
in: path
required: true
schema:
$ref: "#/components/schemas/ScriptLang"
- name: name
in: query
required: false
schema:
type: string
responses:
"200":
description: result
content:
application/json:
schema: {}
/w/{workspace}/workspace_dependencies/list:
get:
summary: list all workspace dependencies
operationId: listWorkspaceDependencies
tags:
- workspace_dependencies
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: All workspace dependencies
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/WorkspaceDependencies"
/w/{workspace}/workspace_dependencies/get_latest/{language}:
get:
summary: get latest workspace dependencies by language and name
operationId: getLatestWorkspaceDependencies
tags:
- workspace_dependencies
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: language
in: path
required: true
schema:
$ref: "#/components/schemas/ScriptLang"
- name: name
in: query
required: false
schema:
type: string
responses:
"200":
description: Latest workspace dependencies
content:
application/json:
schema:
$ref: "#/components/schemas/WorkspaceDependencies"
/w/{workspace}/scripts/archive/p/{path}:
post:
summary: archive script by path
@@ -6463,6 +6657,13 @@ paths:
in: query
schema:
type: boolean
- name: without_description
in: query
description: |
(default false)
If true, the description field will be omitted from the response.
schema:
type: boolean
responses:
"200":
description: All flow
@@ -7692,6 +7893,9 @@ paths:
type: array
items:
type: string
run_query_params:
type: object
description: Runnable query parameters
required:
- args
- component
@@ -8637,6 +8841,121 @@ paths:
items:
$ref: "#/components/schemas/CompletedJob"
/w/{workspace}/jobs/completed/export:
get:
summary: export all completed jobs for backup/migration
operationId: exportCompletedJobs
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: All completed jobs exported
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ExportableCompletedJob"
/w/{workspace}/jobs/completed/import:
post:
summary: import completed jobs from backup/migration
operationId: importCompletedJobs
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ExportableCompletedJob"
responses:
"200":
description: Successfully imported completed jobs
content:
text/plain:
schema:
type: string
/w/{workspace}/jobs/queue/export:
get:
summary: export all queued jobs for backup/migration
operationId: exportQueuedJobs
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: All queued jobs exported
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ExportableQueuedJob"
/w/{workspace}/jobs/queue/import:
post:
summary: import queued jobs from backup/migration
operationId: importQueuedJobs
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ExportableQueuedJob"
responses:
"200":
description: Successfully imported queued jobs
content:
text/plain:
schema:
type: string
/w/{workspace}/jobs/delete:
post:
summary: delete jobs by IDs from all related tables
operationId: deleteJobs
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
type: string
format: uuid
description: Array of job IDs to delete
responses:
"200":
description: Successfully deleted jobs
content:
text/plain:
schema:
type: string
description: Summary of deleted jobs and rows
/w/{workspace}/jobs/list:
get:
summary: list all jobs
@@ -11999,32 +12318,32 @@ paths:
schema:
type: boolean
/w/{workspace}/email_triggers/setenabled/{path}:
post:
summary: enable/disable email trigger
operationId: setEmailTriggerEnabled
tags:
- email_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
required: true
post:
summary: enable/disable email trigger
operationId: setEmailTriggerEnabled
tags:
- email_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
enabled:
type: boolean
required:
- enabled
responses:
"200":
description: email trigger enable/disable
content:
application/json:
text/plain:
schema:
type: object
properties:
enabled:
type: boolean
required:
- enabled
responses:
"200":
description: email trigger enable/disable
content:
text/plain:
schema:
type: string
type: string
/groups/list:
get:
@@ -15725,6 +16044,52 @@ components:
items:
$ref: "#/components/schemas/Alert"
WorkspaceDependencies:
type: object
properties:
id:
type: integer
archived:
type: boolean
name:
type: string
description:
type: string
content:
type: string
language:
$ref: "#/components/schemas/ScriptLang"
workspace_id:
type: string
created_at:
type: string
format: date-time
required:
- workspace_id
- language
- created_at
- content
- id
- archived
NewWorkspaceDependencies:
type: object
properties:
workspace_id:
type: string
language:
$ref: "#/components/schemas/ScriptLang"
name:
type: string
description:
type: string
content:
type: string
required:
- workspace_id
- language
- content
Script:
type: object
properties:
@@ -16245,6 +16610,297 @@ components:
- visible_to_owner
- tag
ExportableCompletedJob:
type: object
description: Completed job with full data for export/import operations
properties:
id:
type: string
format: uuid
parent_job:
type: string
format: uuid
created_by:
type: string
created_at:
type: string
format: date-time
started_at:
type: string
format: date-time
completed_at:
type: string
format: date-time
duration_ms:
type: integer
script_path:
type: string
script_hash:
type: string
args:
type: object
description: Full job arguments without size restrictions
result:
type: object
description: Full job result without size restrictions
logs:
type: string
description: Complete job logs from v2_job table
raw_code:
type: string
raw_lock:
type: string
canceled_by:
type: string
canceled_reason:
type: string
job_kind:
type: string
enum:
[
"script",
"preview",
"dependencies",
"flow",
"flowdependencies",
"appdependencies",
"flowpreview",
"script_hub",
"identity",
"deploymentcallback",
"singlestepflow",
"flowscript",
"flownode",
"appscript",
]
trigger:
type: string
description: Trigger path for the job (replaces schedule_path)
trigger_kind:
type: string
enum:
[
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats",
"schedule",
"app",
"ui",
"postgres",
"sqs",
"gcp",
]
permissioned_as:
type: string
permissioned_as_email:
type: string
flow_status:
type: object
description: Flow status from v2_job_status table
workflow_as_code_status:
type: object
raw_flow:
type: object
is_flow_step:
type: boolean
language:
$ref: "#/components/schemas/ScriptLang"
is_skipped:
type: boolean
email:
type: string
visible_to_owner:
type: boolean
mem_peak:
type: integer
tag:
type: string
priority:
type: integer
labels:
type: array
items:
type: string
same_worker:
type: boolean
flow_step_id:
type: string
flow_innermost_root_job:
type: string
format: uuid
concurrent_limit:
type: integer
concurrency_time_window_s:
type: integer
timeout:
type: integer
cache_ttl:
type: integer
self_wait_time_ms:
type: integer
aggregate_wait_time_ms:
type: integer
preprocessed:
type: boolean
worker:
type: string
status:
type: string
description: Actual job status from database
required:
- id
- created_by
- created_at
- job_kind
- permissioned_as
- email
- visible_to_owner
ExportableQueuedJob:
type: object
description: Queued job with full data for export/import operations
properties:
id:
type: string
format: uuid
parent_job:
type: string
format: uuid
created_by:
type: string
created_at:
type: string
format: date-time
started_at:
type: string
format: date-time
scheduled_for:
type: string
format: date-time
script_path:
type: string
script_hash:
type: string
args:
type: object
description: Full job arguments without size restrictions
logs:
type: string
description: Complete job logs from v2_job table
raw_code:
type: string
raw_lock:
type: string
canceled_by:
type: string
canceled_reason:
type: string
job_kind:
type: string
enum:
[
"script",
"preview",
"dependencies",
"flowdependencies",
"appdependencies",
"flow",
"flowpreview",
"script_hub",
"identity",
"deploymentcallback",
"singlestepflow",
"flowscript",
"flownode",
"appscript",
]
trigger:
type: string
description: Trigger path for the job (replaces schedule_path)
trigger_kind:
type: string
enum:
[
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats",
"schedule",
"app",
"ui",
"postgres",
"sqs",
"gcp",
]
permissioned_as:
type: string
permissioned_as_email:
type: string
flow_status:
type: object
description: Flow status from v2_job_status table
workflow_as_code_status:
type: object
raw_flow:
type: object
is_flow_step:
type: boolean
language:
$ref: "#/components/schemas/ScriptLang"
email:
type: string
visible_to_owner:
type: boolean
mem_peak:
type: integer
tag:
type: string
priority:
type: integer
labels:
type: array
items:
type: string
same_worker:
type: boolean
flow_step_id:
type: string
flow_innermost_root_job:
type: string
format: uuid
concurrent_limit:
type: integer
concurrency_time_window_s:
type: integer
timeout:
type: integer
cache_ttl:
type: integer
self_wait_time_ms:
type: integer
aggregate_wait_time_ms:
type: integer
preprocessed:
type: boolean
suspend:
type: integer
suspend_until:
type: string
format: date-time
required:
- id
- created_by
- created_at
- job_kind
- permissioned_as
- email
- visible_to_owner
ObscuredJob:
type: object
properties:
@@ -18923,6 +19579,38 @@ components:
type: string
nullable: true
DependencyDependent:
type: object
properties:
importer_path:
type: string
importer_kind:
type: string
enum:
- script
- flow
- app
importer_node_ids:
type: array
items:
type: string
nullable: true
required:
- importer_path
- importer_kind
DependentsAmount:
type: object
properties:
imported_path:
type: string
count:
type: integer
format: int64
required:
- imported_path
- count
WorkspaceInvite:
type: object
properties:

View File

@@ -10,6 +10,7 @@ use std::{collections::HashMap, sync::Arc};
use crate::{
auth::OptTokened,
db::{ApiAuthed, DB},
jobs::RunJobQuery,
resources::get_resource_value_interpolated_internal,
users::{require_owner_of_path, OptAuthed},
utils::{check_scopes, WithStarredInfoQuery},
@@ -173,6 +174,7 @@ pub struct AppWithLastVersion {
pub extra_perms: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_path: Option<String>,
pub raw_app: bool,
}
#[derive(Serialize, FromRow)]
@@ -509,7 +511,7 @@ async fn get_app(
sqlx::query_as::<_, AppWithLastVersionAndStarred>(
"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, favorite.path IS NOT NULL as starred
app_version.created_at, app_version.created_by, favorite.path IS NOT NULL as starred, app_version.raw_app
FROM app
JOIN app_version
ON app_version.id = app.versions[array_upper(app.versions, 1)]
@@ -529,7 +531,7 @@ async fn get_app(
sqlx::query_as::<_, AppWithLastVersionAndStarred>(
"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, NULL as starred
app_version.created_at, app_version.created_by, NULL as starred, app_version.raw_app
FROM app, app_version
WHERE app.path = $1 AND app.workspace_id = $2 AND app_version.id = app.versions[array_upper(app.versions, 1)]",
)
@@ -556,7 +558,7 @@ async fn get_app_lite(
let app_o = sqlx::query_as::<_, AppWithLastVersion>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
app.extra_perms, coalesce(app_version_lite.value::json, app_version.value) as value,
app_version.created_at, app_version.created_by, NULL as starred
app_version.created_at, app_version.created_by, NULL as starred, app_version.raw_app
FROM app, app_version
LEFT JOIN app_version_lite ON app_version_lite.id = app_version.id
WHERE app.path = $1 AND app.workspace_id = $2 AND app_version.id = app.versions[array_upper(app.versions, 1)]",
@@ -595,7 +597,8 @@ async fn get_app_w_draft(
app_version.created_at,
app_version.created_by,
app.draft_only,
draft.value AS "draft"
draft.value AS "draft",
app_version.raw_app
FROM app
INNER JOIN app_version
ON app_version.id = app.versions[array_upper(app.versions, 1)]
@@ -738,7 +741,8 @@ async fn get_app_by_id(
let app_o = sqlx::query_as::<_, AppWithLastVersion>(
"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
app_version.created_at, app_version.created_by, app_version.raw_app
FROM app, app_version
WHERE app_version.id = $1 AND app.id = app_version.app_id AND app.workspace_id = $2",
)
.bind(&id)
@@ -765,7 +769,8 @@ async fn get_public_app_by_secret(
let app_o = sqlx::query_as::<_, AppWithLastVersion>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
null as extra_perms, coalesce(app_version_lite.value::json, app_version.value::json) as value,
app_version.created_at, app_version.created_by from app, app_version
app_version.created_at, app_version.created_by, app_version.raw_app
FROM app, app_version
LEFT JOIN app_version_lite ON app_version_lite.id = app_version.id
WHERE app.id = $1 AND app.workspace_id = $2 AND app_version.id = app.versions[array_upper(app.versions, 1)]")
.bind(&id)
@@ -1654,6 +1659,8 @@ pub struct ExecuteApp {
pub force_viewer_static_fields: Option<StaticFields>,
pub force_viewer_one_of_fields: Option<OneOfFields>,
pub force_viewer_allow_user_resources: Option<AllowUserResources>,
/// Runnable query parameters (e.g., memory_id for chat-enabled flows)
pub run_query_params: Option<RunJobQuery>,
}
fn digest(code: &str) -> String {
@@ -1894,6 +1901,12 @@ async fn execute_component(
)
.await?;
let is_flow = payload
.path
.as_ref()
.map(|p| p.starts_with("flow/"))
.unwrap_or(false);
let (job_payload, tag, on_behalf_of) = match (payload.path, payload.raw_code, payload.id) {
// flow or script:
(Some(path), None, None) => get_payload_tag_from_prefixed_path(&path, &db, &w_id).await?,
@@ -1920,7 +1933,7 @@ async fn execute_component(
let end_user_email = opt_authed.as_ref().map(|a| a.email.clone());
let (uuid, tx) = push(
let (uuid, mut tx) = push(
&db,
tx,
&w_id,
@@ -1954,6 +1967,14 @@ async fn execute_component(
None,
)
.await?;
// Apply runnable query parameters if provided
if let Some(ref run_query) = payload.run_query_params {
if is_flow {
crate::jobs::process_flow_run_query_params(&mut tx, uuid, run_query).await?;
}
}
tx.commit().await?;
Ok(uuid.to_string())

View File

@@ -134,6 +134,7 @@ pub async fn get_or_create_conversation_with_id(
} else {
title.to_string()
};
// Create new conversation with provided ID
let conversation = sqlx::query_as!(
FlowConversation,
@@ -148,6 +149,7 @@ pub async fn get_or_create_conversation_with_id(
)
.fetch_one(&mut **tx)
.await?;
Ok(conversation)
}

View File

@@ -133,7 +133,11 @@ async fn list_flows(
"o.workspace_id",
"o.path",
"summary",
"description",
if !lq.without_description.unwrap_or(false) {
"description"
} else {
"NULL as description"
},
"fv.created_by as edited_by",
"fv.created_at as edited_at",
"archived",
@@ -1579,6 +1583,7 @@ mod tests {
skip_failures: true,
parallel: false,
parallelism: None,
squash: None,
}),
stop_after_if: Some(StopAfterIf {
expr: "previous.isEmpty()".to_string(),

View File

@@ -96,7 +96,7 @@ async fn list_folders(
let rows = sqlx::query_as!(
Folder,
"SELECT workspace_id, name, display_name, owners, extra_perms, summary, created_by, edited_at FROM folder WHERE workspace_id = $1 ORDER BY name desc LIMIT $2 OFFSET $3",
"SELECT workspace_id, name, display_name, owners, extra_perms, summary, created_by, edited_at FROM folder WHERE workspace_id = $1 ORDER BY name asc LIMIT $2 OFFSET $3",
w_id,
per_page as i64,
offset as i64
@@ -117,7 +117,7 @@ async fn list_foldernames(
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_scalar!(
"SELECT name FROM folder WHERE workspace_id = $1 ORDER BY name desc LIMIT $2 OFFSET $3",
"SELECT name FROM folder WHERE workspace_id = $1 ORDER BY name asc LIMIT $2 OFFSET $3",
w_id,
per_page as i64,
offset as i64

View File

@@ -103,7 +103,7 @@ async fn list_groups(
let rows = sqlx::query_as!(
Group,
"SELECT * FROM group_ WHERE workspace_id = $1 ORDER BY name desc LIMIT $2 OFFSET $3",
"SELECT * FROM group_ WHERE workspace_id = $1 ORDER BY name asc LIMIT $2 OFFSET $3",
w_id,
per_page as i64,
offset as i64
@@ -126,7 +126,7 @@ async fn list_group_names(
) -> JsonResult<Vec<String>> {
let rows = if !only_member_of.unwrap_or(false) {
sqlx::query_scalar!(
"SELECT name FROM group_ WHERE workspace_id = $1 UNION SELECT name FROM instance_group ORDER BY name desc",
"SELECT name FROM group_ WHERE workspace_id = $1 UNION SELECT name FROM instance_group ORDER BY name asc",
w_id
)
.fetch_all(&db)

View File

@@ -25,6 +25,7 @@ use std::str::FromStr;
use std::time::Instant;
use tokio::io::AsyncReadExt;
use tower::ServiceBuilder;
use url::Url;
#[cfg(all(feature = "enterprise", feature = "smtp"))]
use windmill_common::auth::is_super_admin_email;
use windmill_common::auth::TOKEN_PREFIX_LEN;
@@ -39,6 +40,9 @@ use windmill_common::jobs::{
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::workspace_dependencies::{
RawWorkspaceDependencies, MIN_VERSION_WORKSPACE_DEPENDENCIES,
};
use windmill_common::DYNAMIC_INPUT_CACHE;
#[cfg(all(feature = "enterprise", feature = "smtp"))]
use windmill_common::{email_oss::send_email_html, server::load_smtp_config};
@@ -256,6 +260,11 @@ pub fn workspaced_service() -> Router {
)
.route("/list_filtered_uuids", get(list_filtered_job_uuids))
.route("/queue/list", get(list_queue_jobs))
.route("/queue/export", get(crate::jobs_export::export_queued_jobs))
.route(
"/queue/import",
post(crate::jobs_export::import_queued_jobs),
)
.route("/queue/count", get(count_queue_jobs))
.route("/queue/list_filtered_uuids", get(list_filtered_uuids))
.route("/queue/position/:timestamp", get(get_queue_position))
@@ -267,6 +276,18 @@ pub fn workspaced_service() -> Router {
"/completed/list",
get(list_completed_jobs).layer(cors.clone()),
)
.route(
"/completed/export",
get(crate::jobs_export::export_completed_jobs).layer(cors.clone()),
)
.route(
"/completed/import",
post(crate::jobs_export::import_completed_jobs).layer(cors.clone()),
)
.route(
"/delete",
post(crate::jobs_export::delete_jobs),
)
.route(
"/completed/get/:id",
get(get_completed_job).layer(cors.clone()),
@@ -1740,7 +1761,7 @@ pub struct ListableCompletedJob {
pub labels: Option<serde_json::Value>,
}
#[derive(Deserialize, Clone, Default)]
#[derive(Debug, Deserialize, Clone, Default)]
pub struct RunJobQuery {
pub scheduled_for: Option<chrono::DateTime<chrono::Utc>>,
pub scheduled_in_secs: Option<i64>,
@@ -3947,7 +3968,7 @@ async fn set_flow_memory_id(
memory_id: Uuid,
) -> error::Result<()> {
sqlx::query!(
"UPDATE v2_job_status
"UPDATE v2_job_status
SET flow_status = jsonb_set(
flow_status,
'{memory_id}',
@@ -3962,6 +3983,19 @@ async fn set_flow_memory_id(
Ok(())
}
/// Apply flow-specific query parameters after job creation
pub async fn process_flow_run_query_params(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
job_id: Uuid,
run_query: &RunJobQuery,
) -> error::Result<()> {
// Set memory_id if provided (for agent memory)
if let Some(memory_id) = run_query.memory_id {
set_flow_memory_id(tx, job_id, memory_id).await?;
}
Ok(())
}
async fn handle_chat_conversation_messages(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
authed: &ApiAuthed,
@@ -5670,7 +5704,7 @@ pub async fn stream_job(
version,
run_query,
args,
None
None,
)
.await?
.0
@@ -6103,14 +6137,17 @@ async fn run_bundle_preview_script(
Ok((StatusCode::CREATED, job_id.unwrap().to_string()))
}
#[derive(Deserialize)]
#[derive(Deserialize, Debug)]
pub struct RunDependenciesRequest {
pub raw_scripts: Vec<RawScriptForDependencies>,
pub entrypoint: String,
#[serde(default)]
pub raw_workspace_dependencies: Option<RawWorkspaceDependencies>,
#[serde(default)]
pub raw_deps: Option<String>,
}
#[derive(Deserialize, Clone)]
#[derive(Deserialize, Clone, Debug)]
pub struct RawScriptForDependencies {
pub script_path: String,
pub raw_code: Option<String>,
@@ -6135,41 +6172,49 @@ async fn run_dependencies_job(
));
}
if req.raw_deps.is_some() {
return Err(error::Error::MigrationNeeded {
feature: "cli is outdated".into(),
version: MIN_VERSION_WORKSPACE_DEPENDENCIES.to_owned(),
guide_url: Url::from_str(
"https://www.windmill.dev/docs/core_concepts/workspace_dependencies/migration",
)?,
});
}
// Check if workers support workspace dependencies feature
if req.raw_workspace_dependencies.is_some() {
windmill_common::workspace_dependencies::min_version_supports_v0_workspace_dependencies()
.await?;
}
if req.raw_scripts.len() != 1 || req.raw_scripts[0].script_path != req.entrypoint {
return Err(error::Error::internal_err(
"For now only a single raw script can be passed to this endpoint, and the entrypoint should be set to the script path".to_string(),
));
}
let raw_script = req.raw_scripts[0].clone();
let script_path = raw_script.script_path;
let ehm = HashMap::new();
let raw_code = raw_script.raw_code.unwrap_or_else(|| "".to_string());
let language = raw_script.language;
let (args, raw_code) = if let Some(deps) = req.raw_deps {
let mut hm = HashMap::new();
hm.insert(
"raw_deps".to_string(),
JsonRawValue::from_string("true".to_string()).unwrap(),
);
if language == ScriptLang::Bun {
let annotation = windmill_common::worker::TypeScriptAnnotations::parse(&raw_code);
hm.insert(
"npm_mode".to_string(),
JsonRawValue::from_string(annotation.npm.to_string()).unwrap(),
);
}
(PushArgs { extra: Some(hm), args: &ehm }, deps)
} else {
(PushArgs::from(&ehm), raw_code)
};
let RawScriptForDependencies {
// unwrap
script_path,
raw_code,
language,
} = req.raw_scripts[0].clone();
let mut hm = HashMap::new();
req.raw_workspace_dependencies
.map(|v| hm.insert("raw_workspace_dependencies".to_owned(), to_raw_value(&v)));
let (uuid, tx) = push(
&db,
PushIsolationLevel::IsolatedRoot(db.clone()),
&w_id,
JobPayload::RawScriptDependencies { script_path, content: raw_code, language },
args,
JobPayload::RawScriptDependencies {
script_path,
content: raw_code.unwrap_or_default(),
language,
},
PushArgs { extra: Some(hm), args: &HashMap::new() },
authed.display_username(),
&authed.email,
username_to_permissioned_as(&authed.username),
@@ -6205,6 +6250,9 @@ async fn run_dependencies_job(
pub struct RunFlowDependenciesRequest {
pub path: String,
pub flow_value: FlowValue,
#[serde(default)]
pub raw_workspace_dependencies: Option<RawWorkspaceDependencies>,
#[serde(default)]
pub raw_deps: Option<HashMap<String, String>>,
}
@@ -6226,13 +6274,28 @@ async fn run_flow_dependencies_job(
));
}
// Create args HashMap with skip_flow_update and raw_deps if present
if req.raw_deps.is_some() {
return Err(error::Error::MigrationNeeded {
feature: "cli is outdated".into(),
version: MIN_VERSION_WORKSPACE_DEPENDENCIES.to_owned(),
guide_url: Url::from_str(
"https://www.windmill.dev/docs/core_concepts/workspace_dependencies/migration",
)?,
});
}
// Check if workers support workspace dependencies feature
if req.raw_workspace_dependencies.is_some() {
windmill_common::workspace_dependencies::min_version_supports_v0_workspace_dependencies()
.await?;
}
// Create args HashMap with skip_flow_update and raw_workspace_dependencies if present
let mut args_map = HashMap::from([("skip_flow_update".to_string(), to_raw_value(&true))]);
// Add raw_deps to args if present
if let Some(ref raw_deps) = req.raw_deps {
args_map.insert("raw_deps".to_string(), to_raw_value(raw_deps));
}
// Add raw_workspace_dependencies to args if present
req.raw_workspace_dependencies
.map(|v| args_map.insert("raw_workspace_dependencies".to_string(), to_raw_value(&v)));
let (uuid, tx) = push(
&db,

View File

@@ -0,0 +1,827 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use axum::{
extract::{Path, Query},
Extension, Json,
};
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use uuid::Uuid;
use windmill_common::{
db::UserDB,
error,
jobs::{JobKind, JobStatus, JobTriggerKind},
scripts::ScriptLang,
utils::{paginate, paginate_without_limits, require_admin, Pagination},
};
use crate::db::ApiAuthed;
#[derive(Serialize, Deserialize)]
pub struct ExportableCompletedJob {
pub id: Uuid,
pub raw_code: Option<String>,
pub raw_lock: Option<String>,
pub raw_flow: Option<sqlx::types::Json<Box<RawValue>>>,
pub tag: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub created_by: String,
pub permissioned_as: String,
pub permissioned_as_email: String,
pub kind: JobKind,
pub runnable_id: Option<i64>,
pub runnable_path: Option<String>,
pub parent_job: Option<Uuid>,
pub root_job: Option<Uuid>,
pub script_lang: Option<ScriptLang>,
pub script_entrypoint_override: Option<String>,
pub flow_step: Option<i32>,
pub flow_step_id: Option<String>,
pub flow_innermost_root_job: Option<Uuid>,
pub trigger: Option<String>,
pub trigger_kind: Option<JobTriggerKind>,
pub same_worker: bool,
pub visible_to_owner: bool,
pub concurrent_limit: Option<i32>,
pub concurrency_time_window_s: Option<i32>,
pub cache_ttl: Option<i32>,
pub timeout: Option<i32>,
pub priority: Option<i16>,
pub preprocessed: Option<bool>,
pub args: Option<sqlx::types::Json<Box<RawValue>>>,
pub labels: Option<Vec<String>>,
pub pre_run_error: Option<String>,
pub duration_ms: i64,
pub result: Option<sqlx::types::Json<Box<RawValue>>>,
pub deleted: bool,
pub canceled_by: Option<String>,
pub canceled_reason: Option<String>,
pub flow_status: Option<sqlx::types::Json<Box<RawValue>>>,
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
pub memory_peak: Option<i32>,
pub status: JobStatus,
pub completed_at: chrono::DateTime<chrono::Utc>,
pub worker: Option<String>,
pub workflow_as_code_status: Option<sqlx::types::Json<Box<RawValue>>>,
pub result_columns: Option<Vec<String>>,
pub retries: Option<Vec<Uuid>>,
pub extras: Option<sqlx::types::Json<Box<RawValue>>>,
pub logs: Option<String>,
pub log_offset: Option<i32>,
pub log_file_index: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize)]
pub struct ExportableQueuedJob {
// v2_job columns (excluding workspace_id)
pub id: Uuid,
pub raw_code: Option<String>,
pub raw_lock: Option<String>,
pub raw_flow: Option<sqlx::types::Json<Box<RawValue>>>,
pub tag: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub created_by: String,
pub permissioned_as: String,
pub permissioned_as_email: String,
pub kind: JobKind,
pub runnable_id: Option<i64>,
pub runnable_path: Option<String>,
pub parent_job: Option<Uuid>,
pub root_job: Option<Uuid>,
pub script_lang: Option<ScriptLang>,
pub script_entrypoint_override: Option<String>,
pub flow_step: Option<i32>,
pub flow_step_id: Option<String>,
pub flow_innermost_root_job: Option<Uuid>,
pub trigger: Option<String>,
pub trigger_kind: Option<JobTriggerKind>,
pub same_worker: bool,
pub visible_to_owner: bool,
pub concurrent_limit: Option<i32>,
pub concurrency_time_window_s: Option<i32>,
pub cache_ttl: Option<i32>,
pub timeout: Option<i32>,
pub priority: Option<i16>,
pub preprocessed: Option<bool>,
pub args: Option<sqlx::types::Json<Box<RawValue>>>,
pub labels: Option<Vec<String>>,
pub pre_run_error: Option<String>,
// v2_job_queue columns (excluding workspace_id and id/created_at/tag/priority)
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
pub scheduled_for: chrono::DateTime<chrono::Utc>,
pub running: bool,
pub canceled_by: Option<String>,
pub canceled_reason: Option<String>,
pub suspend: Option<i32>,
pub suspend_until: Option<chrono::DateTime<chrono::Utc>>,
pub worker: Option<String>,
pub extras: Option<sqlx::types::Json<Box<RawValue>>>,
// v2_job_runtime columns (excluding id)
pub ping: Option<chrono::DateTime<chrono::Utc>>,
pub memory_peak: Option<i32>,
// v2_job_status columns (excluding id)
pub flow_status: Option<sqlx::types::Json<Box<RawValue>>>,
pub flow_leaf_jobs: Option<sqlx::types::Json<Box<RawValue>>>,
pub workflow_as_code_status: Option<sqlx::types::Json<Box<RawValue>>>,
// concurrency_key table
pub concurrency_key: Option<String>,
}
pub async fn export_completed_jobs(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
) -> error::JsonResult<Vec<ExportableCompletedJob>> {
require_admin(authed.is_admin, &authed.username)?;
let (per_page, offset) = paginate(pagination);
let per_page = per_page as i64;
let offset = offset as i64;
let mut tx = user_db.begin(&authed).await?;
let jobs = sqlx::query_as!(
ExportableCompletedJob,
r#"
SELECT
v2_job.id,
v2_job.raw_code,
v2_job.raw_lock,
v2_job.raw_flow as "raw_flow: _",
v2_job.tag,
v2_job.created_at,
v2_job.created_by,
v2_job.permissioned_as,
v2_job.permissioned_as_email,
v2_job.kind as "kind: _",
v2_job.runnable_id,
v2_job.runnable_path,
v2_job.parent_job,
v2_job.root_job,
v2_job.script_lang as "script_lang: _",
v2_job.script_entrypoint_override,
v2_job.flow_step,
v2_job.flow_step_id,
v2_job.flow_innermost_root_job,
v2_job.trigger,
v2_job.trigger_kind as "trigger_kind: _",
v2_job.same_worker,
v2_job.visible_to_owner,
v2_job.concurrent_limit,
v2_job.concurrency_time_window_s,
v2_job.cache_ttl,
v2_job.timeout,
v2_job.priority,
v2_job.preprocessed,
v2_job.args as "args: _",
v2_job.labels,
v2_job.pre_run_error,
v2_job_completed.duration_ms,
v2_job_completed.result as "result: _",
v2_job_completed.deleted,
v2_job_completed.canceled_by,
v2_job_completed.canceled_reason,
v2_job_completed.flow_status as "flow_status: _",
v2_job_completed.started_at,
v2_job_completed.memory_peak,
v2_job_completed.status as "status: _",
v2_job_completed.completed_at,
v2_job_completed.worker,
v2_job_completed.workflow_as_code_status as "workflow_as_code_status: _",
v2_job_completed.result_columns,
v2_job_completed.retries,
v2_job_completed.extras as "extras: _",
job_logs.logs AS logs,
job_logs.log_offset,
job_logs.log_file_index
FROM v2_job_completed
INNER JOIN v2_job ON v2_job.id = v2_job_completed.id
LEFT JOIN v2_job_status ON v2_job_completed.id = v2_job_status.id
LEFT JOIN job_logs ON job_logs.job_id = v2_job.id
WHERE v2_job_completed.workspace_id = $1
ORDER BY v2_job.created_at DESC
LIMIT $2
OFFSET $3
"#,
w_id,
per_page,
offset
)
.fetch_all(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(jobs))
}
pub async fn export_queued_jobs(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
) -> error::JsonResult<Vec<ExportableQueuedJob>> {
require_admin(authed.is_admin, &authed.username)?;
let (per_page, offset) = paginate_without_limits(pagination);
let per_page = per_page as i64;
let offset = offset as i64;
let mut tx = user_db.begin(&authed).await?;
let jobs = sqlx::query_as!(
ExportableQueuedJob,
r#"
SELECT
v2_job.id,
v2_job.raw_code,
v2_job.raw_lock,
v2_job.raw_flow as "raw_flow: _",
v2_job.tag,
v2_job.created_at,
v2_job.created_by,
v2_job.permissioned_as,
v2_job.permissioned_as_email,
v2_job.kind as "kind: _",
v2_job.runnable_id,
v2_job.runnable_path,
v2_job.parent_job,
v2_job.root_job,
v2_job.script_lang as "script_lang: _",
v2_job.script_entrypoint_override,
v2_job.flow_step,
v2_job.flow_step_id,
v2_job.flow_innermost_root_job,
v2_job."trigger",
v2_job.trigger_kind as "trigger_kind: _",
v2_job.same_worker,
v2_job.visible_to_owner,
v2_job.concurrent_limit,
v2_job.concurrency_time_window_s,
v2_job.cache_ttl,
v2_job.timeout,
v2_job.priority,
v2_job.preprocessed,
v2_job.args as "args: _",
v2_job.labels,
v2_job.pre_run_error,
v2_job_queue.started_at,
v2_job_queue.scheduled_for,
v2_job_queue.running,
v2_job_queue.canceled_by,
v2_job_queue.canceled_reason,
v2_job_queue.suspend,
v2_job_queue.suspend_until,
v2_job_queue.worker,
v2_job_queue.extras as "extras: _",
v2_job_runtime.ping,
v2_job_runtime.memory_peak,
v2_job_status.flow_status as "flow_status: _",
v2_job_status.flow_leaf_jobs as "flow_leaf_jobs: _",
v2_job_status.workflow_as_code_status as "workflow_as_code_status: _",
concurrency_key.key as "concurrency_key?"
FROM v2_job_queue
INNER JOIN v2_job ON v2_job.id = v2_job_queue.id
LEFT JOIN v2_job_runtime ON v2_job_runtime.id = v2_job_queue.id
LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id
LEFT JOIN concurrency_key ON concurrency_key.job_id = v2_job_queue.id
WHERE v2_job_queue.workspace_id = $1
AND v2_job_queue.running = false
AND v2_job.parent_job IS NULL
AND v2_job.trigger_kind IS DISTINCT FROM 'schedule'
ORDER BY v2_job.created_at DESC
LIMIT $2
OFFSET $3
"#,
w_id,
per_page,
offset
)
.fetch_all(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(jobs))
}
pub async fn import_completed_jobs(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(jobs): Json<Vec<ExportableCompletedJob>>,
) -> error::Result<String> {
require_admin(authed.is_admin, &authed.username)?;
let mut tx = user_db.begin(&authed).await?;
for job in jobs {
// Insert into v2_job table
sqlx::query!(
r#"
INSERT INTO v2_job (
id,
raw_code,
raw_lock,
raw_flow,
tag,
workspace_id,
created_at,
created_by,
permissioned_as,
permissioned_as_email,
kind,
runnable_id,
runnable_path,
parent_job,
root_job,
script_lang,
script_entrypoint_override,
flow_step,
flow_step_id,
flow_innermost_root_job,
trigger,
trigger_kind,
same_worker,
visible_to_owner,
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
timeout,
priority,
preprocessed,
args,
labels,
pre_run_error
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,
$21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33
)
ON CONFLICT (id) DO NOTHING
"#,
job.id,
job.raw_code,
job.raw_lock,
job.raw_flow as _,
&job.tag,
&w_id,
job.created_at,
&job.created_by,
&job.permissioned_as,
job.permissioned_as_email,
job.kind as _,
job.runnable_id,
job.runnable_path,
job.parent_job,
job.root_job,
job.script_lang as _,
job.script_entrypoint_override,
job.flow_step,
job.flow_step_id,
job.flow_innermost_root_job,
job.trigger,
job.trigger_kind as _,
job.same_worker,
job.visible_to_owner,
job.concurrent_limit,
job.concurrency_time_window_s,
job.cache_ttl,
job.timeout,
job.priority,
job.preprocessed,
job.args as _,
job.labels as _,
job.pre_run_error
)
.execute(&mut *tx)
.await?;
// Insert into v2_job_completed table
sqlx::query!(
r#"
INSERT INTO v2_job_completed (
id,
workspace_id,
started_at,
completed_at,
duration_ms,
result,
deleted,
canceled_by,
canceled_reason,
flow_status,
memory_peak,
status,
worker,
workflow_as_code_status,
result_columns,
retries,
extras
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17
)
ON CONFLICT (id) DO NOTHING
"#,
job.id,
&w_id,
job.started_at,
job.completed_at,
job.duration_ms,
job.result as _,
job.deleted,
job.canceled_by,
job.canceled_reason,
job.flow_status as _,
job.memory_peak,
job.status as _,
job.worker,
job.workflow_as_code_status as _,
job.result_columns as _,
job.retries as _,
job.extras as _
)
.execute(&mut *tx)
.await?;
// Insert into job_logs table
if let Some(logs) = &job.logs {
sqlx::query!(
r#"
INSERT INTO job_logs (
job_id,
workspace_id,
logs,
log_offset,
log_file_index
) VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (job_id) DO NOTHING
"#,
job.id,
&w_id,
logs,
job.log_offset,
job.log_file_index as _
)
.execute(&mut *tx)
.await?;
}
}
tx.commit().await?;
Ok(format!("Successfully imported jobs"))
}
pub async fn import_queued_jobs(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(jobs): Json<Vec<ExportableQueuedJob>>,
) -> error::Result<String> {
require_admin(authed.is_admin, &authed.username)?;
let mut tx = user_db.begin(&authed).await?;
for job in jobs {
// Insert into v2_job table
// Insert into v2_job table, fields and order adapted to match ExportableQueuedJob
sqlx::query!(
r#"
INSERT INTO v2_job (
id,
raw_code,
raw_lock,
raw_flow,
tag,
workspace_id,
created_at,
created_by,
permissioned_as,
permissioned_as_email,
kind,
runnable_id,
runnable_path,
parent_job,
root_job,
script_lang,
script_entrypoint_override,
flow_step,
flow_step_id,
flow_innermost_root_job,
trigger,
trigger_kind,
same_worker,
visible_to_owner,
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
timeout,
priority,
preprocessed,
args,
labels,
pre_run_error
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11, $12, $13, $14, $15, $16, $17, $18, $19, $20,
$21, $22, $23, $24, $25, $26, $27, $28, $29, $30,
$31, $32, $33
)
ON CONFLICT (id) DO NOTHING
"#,
job.id,
job.raw_code,
job.raw_lock,
job.raw_flow as _,
&job.tag,
&w_id,
job.created_at,
&job.created_by,
&job.permissioned_as,
&job.permissioned_as_email,
job.kind as _,
job.runnable_id,
job.runnable_path,
job.parent_job,
job.root_job,
job.script_lang as _,
job.script_entrypoint_override,
job.flow_step,
job.flow_step_id,
job.flow_innermost_root_job,
job.trigger,
job.trigger_kind as _,
job.same_worker,
job.visible_to_owner,
job.concurrent_limit,
job.concurrency_time_window_s,
job.cache_ttl,
job.timeout,
job.priority,
job.preprocessed,
job.args as _,
job.labels as _,
job.pre_run_error,
)
.execute(&mut *tx)
.await?;
// Insert into v2_job_queue table
sqlx::query!(
r#"
INSERT INTO v2_job_queue (
id,
workspace_id,
started_at,
scheduled_for,
running,
canceled_by,
canceled_reason,
suspend,
suspend_until,
worker,
extras,
tag,
priority
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
)
ON CONFLICT (id) DO NOTHING
"#,
job.id,
&w_id,
job.started_at,
job.scheduled_for,
job.running,
job.canceled_by,
job.canceled_reason,
job.suspend,
job.suspend_until,
job.worker,
job.extras as _,
&job.tag,
job.priority
)
.execute(&mut *tx)
.await?;
// Insert into v2_job_runtime if any present
if job.ping.is_some() || job.memory_peak.is_some() {
sqlx::query!(
r#"
INSERT INTO v2_job_runtime (
id, ping, memory_peak
) VALUES (
$1, $2, $3
)
ON CONFLICT (id) DO NOTHING
"#,
job.id,
job.ping,
job.memory_peak
)
.execute(&mut *tx)
.await?;
}
// Insert into v2_job_status if present
if job.flow_status.is_some()
|| job.flow_leaf_jobs.is_some()
|| job.workflow_as_code_status.is_some()
{
sqlx::query!(
r#"
INSERT INTO v2_job_status (
id, flow_status, flow_leaf_jobs, workflow_as_code_status
) VALUES (
$1, $2, $3, $4
)
ON CONFLICT (id) DO NOTHING
"#,
job.id,
job.flow_status as _,
job.flow_leaf_jobs as _,
job.workflow_as_code_status as _
)
.execute(&mut *tx)
.await?;
}
// Insert concurrency_key if present
if let Some(ref concurrency_key) = job.concurrency_key {
sqlx::query!(
r#"
WITH inserted_concurrency_counter AS (
INSERT INTO concurrency_counter (concurrency_id, job_uuids)
VALUES ($1, '{}'::jsonb)
ON CONFLICT DO NOTHING
)
INSERT INTO concurrency_key(key, job_id)
VALUES ($1, $2)
ON CONFLICT (job_id) DO NOTHING
"#,
concurrency_key,
job.id
)
.execute(&mut *tx)
.await?;
}
}
tx.commit().await?;
Ok(format!("Successfully imported jobs"))
}
pub async fn delete_jobs(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(job_ids): Json<Vec<Uuid>>,
) -> error::Result<String> {
require_admin(authed.is_admin, &authed.username)?;
if job_ids.is_empty() {
return Ok("No jobs to delete".to_string());
}
let mut tx = user_db.begin(&authed).await?;
// Delete from all job-related tables
// Order matters due to foreign key constraints
// 1. Delete job logs
let logs_deleted = sqlx::query!(
"DELETE FROM job_logs WHERE workspace_id = $1 AND job_id = ANY($2)",
&w_id,
&job_ids
)
.execute(&mut *tx)
.await?
.rows_affected();
// 2. Delete job permissions
let perms_deleted = sqlx::query!(
"DELETE FROM job_perms WHERE workspace_id = $1 AND job_id = ANY($2)",
&w_id,
&job_ids
)
.execute(&mut *tx)
.await?
.rows_affected();
// 3. Delete job stats
let stats_deleted = sqlx::query!(
"DELETE FROM job_stats WHERE workspace_id = $1 AND job_id = ANY($2)",
&w_id,
&job_ids
)
.execute(&mut *tx)
.await?
.rows_affected();
// 4. Delete resume jobs
let resume_deleted = sqlx::query!("DELETE FROM resume_job WHERE job = ANY($1)", &job_ids)
.execute(&mut *tx)
.await?
.rows_affected();
// 5. Delete from v2_job_runtime
let runtime_deleted = sqlx::query!("DELETE FROM v2_job_runtime WHERE id = ANY($1)", &job_ids)
.execute(&mut *tx)
.await?
.rows_affected();
// 6. Delete from v2_job_status
let status_deleted = sqlx::query!("DELETE FROM v2_job_status WHERE id = ANY($1)", &job_ids)
.execute(&mut *tx)
.await?
.rows_affected();
// 7. Delete from concurrency_key
let concurrency_key_deleted = sqlx::query!(
"DELETE FROM concurrency_key WHERE job_id = ANY($1)",
&job_ids
)
.execute(&mut *tx)
.await?
.rows_affected();
// 8. Delete from v2_job_queue (for queued jobs)
let queue_deleted = sqlx::query!(
"DELETE FROM v2_job_queue WHERE workspace_id = $1 AND id = ANY($2)",
&w_id,
&job_ids
)
.execute(&mut *tx)
.await?
.rows_affected();
// 9. Delete from v2_job_completed (for completed jobs)
let completed_deleted = sqlx::query!(
"DELETE FROM v2_job_completed WHERE workspace_id = $1 AND id = ANY($2)",
&w_id,
&job_ids
)
.execute(&mut *tx)
.await?
.rows_affected();
// 10. Delete zombie job counter
let zombie_deleted = sqlx::query!(
"DELETE FROM zombie_job_counter WHERE job_id = ANY($1)",
&job_ids
)
.execute(&mut *tx)
.await?
.rows_affected();
// 11. Finally, delete from v2_job (main table)
let jobs_deleted = sqlx::query!(
"DELETE FROM v2_job WHERE workspace_id = $1 AND id = ANY($2)",
&w_id,
&job_ids
)
.execute(&mut *tx)
.await?
.rows_affected();
tx.commit().await?;
let total_rows_deleted = logs_deleted
+ perms_deleted
+ stats_deleted
+ resume_deleted
+ runtime_deleted
+ status_deleted
+ concurrency_key_deleted
+ queue_deleted
+ completed_deleted
+ zombie_deleted
+ jobs_deleted;
tracing::info!(
"Successfully deleted {} jobs ({} total rows across all tables) from workspace {}",
job_ids.len(),
total_rows_deleted,
w_id
);
Ok(format!(
"Successfully deleted {} jobs ({} total rows across all tables)",
jobs_deleted, total_rows_deleted
))
}

View File

@@ -110,6 +110,7 @@ mod openapi;
#[cfg(all(feature = "private", feature = "parquet"))]
pub mod s3_proxy_ee;
mod s3_proxy_oss;
mod workspace_dependencies;
mod approvals;
#[cfg(all(feature = "enterprise", feature = "private"))]
@@ -126,6 +127,7 @@ pub mod job_helpers_ee;
mod job_helpers_oss;
pub mod job_metrics;
pub mod jobs;
pub mod jobs_export;
#[cfg(all(feature = "oauth2", feature = "private"))]
pub mod oauth2_ee;
#[cfg(feature = "oauth2")]
@@ -455,6 +457,10 @@ pub async fn run_server(
.nest("/drafts", drafts::workspaced_service())
.nest("/favorites", favorite::workspaced_service())
.nest("/flows", flows::workspaced_service())
.nest(
"/workspace_dependencies",
workspace_dependencies::workspaced_service(),
)
.nest(
"/flow_conversations",
flow_conversations::workspaced_service(),

View File

@@ -45,7 +45,7 @@ use windmill_common::{
error::to_anyhow,
s3_helpers::upload_artifact_to_store,
scripts::hash_script,
utils::WarnAfterExt,
utils::{paginate_without_limits, WarnAfterExt},
worker::{CLOUD_HOSTED, MIN_VERSION_SUPPORTS_DEBOUNCING},
};
@@ -60,9 +60,7 @@ use windmill_common::{
ScriptHistory, ScriptHistoryUpdate, ScriptKind, ScriptLang, ScriptWithStarred,
},
users::username_to_permissioned_as,
utils::{
not_found_if_none, paginate, query_elems_from_hub, require_admin, Pagination, StripPath,
},
utils::{not_found_if_none, query_elems_from_hub, require_admin, Pagination, StripPath},
worker::to_raw_value,
HUB_BASE_URL,
};
@@ -193,7 +191,7 @@ async fn list_search_scripts(
) -> JsonResult<Vec<SearchScript>> {
let mut tx = user_db.begin(&authed).await?;
#[cfg(feature = "enterprise")]
let n = 1000;
let n = 10000;
#[cfg(not(feature = "enterprise"))]
let n = 10;
@@ -219,7 +217,7 @@ async fn list_scripts(
Query(pagination): Query<Pagination>,
Query(lq): Query<ListScriptQuery>,
) -> JsonResult<Vec<ListableScript>> {
let (per_page, offset) = paginate(pagination);
let (per_page, offset) = paginate_without_limits(pagination);
let mut sqlb = SqlBuilder::select_from("script as o")
.fields(&[
"hash",
@@ -228,6 +226,11 @@ async fn list_scripts(
"COALESCE(draft.created_at, o.created_at) as created_at",
"archived",
"extra_perms",
if !lq.without_description.unwrap_or(false) {
"description"
} else {
"NULL as description"
},
"CASE WHEN lock_error_logs IS NOT NULL THEN true ELSE false END as has_deploy_errors",
"language",
"favorite.path IS NOT NULL as starred",
@@ -404,7 +407,7 @@ async fn create_snapshot_script(
if name == "script" {
let ns: NewScript = Some(serde_json::from_slice(&data).map_err(to_anyhow)?).unwrap();
let is_tar = ns.codebase.as_ref().is_some_and(|x| x.ends_with(".tar"));
let use_esm = ns.codebase.as_ref().is_some_and(|x| x.contains(".esm"));
let (new_hash, ntx, hdm) = create_script_internal(
ns,
w_id.clone(),
@@ -414,8 +417,14 @@ async fn create_snapshot_script(
webhook.clone(),
)
.await?;
let nh = new_hash.to_string();
script_hash = Some(if is_tar { format!("{nh}.tar") } else { nh });
let mut nh = new_hash.to_string();
if use_esm {
nh = format!("{nh}.esm");
}
if is_tar {
nh = format!("{nh}.tar");
}
script_hash = Some(nh);
tx = Some(ntx);
handle_deployment_metadata = hdm;
}
@@ -1034,14 +1043,10 @@ async fn create_script_internal<'c>(
let permissioned_as2 = permissioned_as.clone();
let script_path2 = script_path.clone();
let parent_path = p_path_opt.clone();
let lock = ns.lock.clone();
let deployment_message = ns.deployment_message.clone();
let content = ns.content.clone();
let language = ns.language.clone();
tokio::spawn(async move {
// TODO: I don't think we want this. We might want to send dependency job. But skip any calculations if lock is already present.
// It will allow us to make code more consistent and predictable.
// wait for 10 seconds to make sure the script is deployed and that the CLI sync that pushed it (f one) is complete
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
if let Err(e) = process_relative_imports(
@@ -1057,7 +1062,6 @@ async fn create_script_internal<'c>(
&authed2.email,
&authed2.username,
&permissioned_as2,
lock,
)
.await
{

View File

@@ -0,0 +1,140 @@
use axum::{
extract::{Path, Query},
routing::{get, post},
Extension, Json, Router,
};
use http::StatusCode;
use serde::Deserialize;
use windmill_common::{
error::{self, JsonResult},
scripts::ScriptLang,
users::username_to_permissioned_as,
utils::require_admin,
workspace_dependencies::WorkspaceDependencies,
DB,
};
use windmill_worker::workspace_dependencies::{
trigger_dependents_to_recompute_dependencies_in_the_background, NewWorkspaceDependencies,
};
use crate::db::ApiAuthed;
pub fn workspaced_service() -> Router {
Router::new()
.route("/create", post(create))
.route("/list", get(list))
.route("/archive/:language", post(archive))
.route("/get_latest/:language", get(get_latest))
.route("/delete/:language", post(delete))
}
#[axum::debug_handler]
async fn create(
authed: ApiAuthed,
// Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Json(nwd): Json<NewWorkspaceDependencies>,
) -> error::Result<(StatusCode, String)> {
tracing::info!(workspace_id = %nwd.workspace_id, name = ?nwd.name, language = ?nwd.language, "create workspace dependencies");
require_admin(authed.is_admin, &authed.username)?;
Ok((
StatusCode::CREATED,
format!(
"{}",
nwd.create(
(
authed.email,
username_to_permissioned_as(&authed.username),
authed.username,
),
db
)
.await?
),
))
}
#[axum::debug_handler]
async fn list(
// Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
) -> JsonResult<Vec<WorkspaceDependencies>> {
tracing::info!(workspace_id = %w_id, "list workspace dependencies");
Ok(Json(WorkspaceDependencies::list(&w_id, &db).await?))
}
#[derive(Deserialize)]
pub(super) struct NameQuery {
name: Option<String>,
}
#[axum::debug_handler]
pub(super) async fn get_latest(
Extension(db): Extension<DB>,
Path((w_id, language)): Path<(String, ScriptLang)>,
Query(params): Query<NameQuery>,
) -> JsonResult<Option<WorkspaceDependencies>> {
tracing::info!(workspace_id = %w_id, language = ?language, name = ?params.name, "get latest workspace dependencies");
Ok(Json(
WorkspaceDependencies::get_latest(params.name, language, &w_id, db.into()).await?,
))
}
#[axum::debug_handler]
async fn archive(
authed: ApiAuthed,
// Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, language)): Path<(String, ScriptLang)>,
Query(params): Query<NameQuery>,
) -> error::Result<()> {
tracing::info!(workspace_id = %w_id, language = ?language, name = ?params.name, "archive workspace dependencies");
require_admin(authed.is_admin, &authed.username)?;
let db = &db;
WorkspaceDependencies::archive(params.name.clone(), language, &w_id, db).await?;
trigger_dependents_to_recompute_dependencies_in_the_background(
params.name.is_none(),
w_id,
language,
(
authed.email,
username_to_permissioned_as(&authed.username),
authed.username,
),
WorkspaceDependencies::to_path(&params.name, language)?,
db.clone(),
)
.await;
Ok(())
}
#[axum::debug_handler]
async fn delete(
authed: ApiAuthed,
// Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, language)): Path<(String, ScriptLang)>,
Query(params): Query<NameQuery>,
) -> error::Result<()> {
tracing::info!(workspace_id = %w_id, language = ?language, name = ?params.name, "delete workspace dependencies");
require_admin(authed.is_admin, &authed.username)?;
let db = &db;
WorkspaceDependencies::delete(params.name.clone(), language, &w_id, db).await?;
trigger_dependents_to_recompute_dependencies_in_the_background(
params.name.is_none(),
w_id,
language,
(
authed.email,
username_to_permissioned_as(&authed.username),
authed.username,
),
WorkspaceDependencies::to_path(&params.name, language)?,
db.clone(),
)
.await;
Ok(())
}

View File

@@ -50,8 +50,10 @@ use windmill_common::{
oauth2::WORKSPACE_SLACK_BOT_TOKEN_PATH,
utils::{paginate, rd_string, require_admin, Pagination},
};
use windmill_git_sync::{handle_fork_branch_creation, handle_deployment_metadata, DeployedObject};
use windmill_worker::scoped_dependency_map::{DependencyMap, ScopedDependencyMap};
use windmill_git_sync::{handle_deployment_metadata, handle_fork_branch_creation, DeployedObject};
use windmill_worker::scoped_dependency_map::{
DependencyDependent, DependencyMap, ScopedDependencyMap,
};
#[cfg(feature = "enterprise")]
use windmill_common::utils::require_admin_or_devops;
@@ -73,6 +75,7 @@ lazy_static::lazy_static! {
pub fn workspaced_service() -> Router {
let router = Router::new()
.route("/get_as_superadmin", get(get_workspace_as_superadmin))
.route("/list_pending_invites", get(list_pending_invites))
.route("/update", post(edit_workspace))
.route("/archive", post(archive_workspace))
@@ -81,6 +84,8 @@ pub fn workspaced_service() -> Router {
.route("/delete_invite", post(delete_invite))
.route("/rebuild_dependency_map", post(rebuild_dependency_map))
.route("/get_dependency_map", get(get_dependency_map))
.route("/get_dependents/*imported_path", get(get_dependents))
.route("/get_dependents_amounts", post(get_dependents_amounts))
.route("/get_settings", get(get_settings))
.route("/get_deploy_to", get(get_deploy_to))
.route("/edit_slack_command", post(edit_slack_command))
@@ -163,7 +168,10 @@ pub fn workspaced_service() -> Router {
post(acknowledge_all_critical_alerts),
)
.route("/critical_alerts/mute", post(mute_critical_alerts))
.route("/create_workspace_fork_branch", post(create_workspace_fork_branch))
.route(
"/create_workspace_fork_branch",
post(create_workspace_fork_branch),
)
.route("/operator_settings", post(update_operator_settings));
#[cfg(all(feature = "stripe", feature = "enterprise"))]
@@ -706,7 +714,9 @@ async fn get_slack_oauth_config(
.await?;
// Mask the secret if it exists
let masked_secret = settings.slack_oauth_client_secret.map(|_| "***".to_string());
let masked_secret = settings
.slack_oauth_client_secret
.map(|_| "***".to_string());
Ok(Json(GetSlackOAuthConfigResponse {
slack_oauth_client_id: settings.slack_oauth_client_id,
@@ -2227,6 +2237,35 @@ async fn get_used_triggers(
Ok(Json(websocket_used))
}
async fn get_workspace_as_superadmin(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
) -> JsonResult<Workspace> {
require_super_admin(&db, &authed.email).await?;
let workspace = sqlx::query_as!(
Workspace,
"SELECT
workspace.id AS \"id!\",
workspace.name AS \"name!\",
workspace.owner AS \"owner!\",
workspace.deleted AS \"deleted!\",
workspace.premium AS \"premium!\",
workspace_settings.color AS \"color\",
workspace.parent_workspace_id AS \"parent_workspace_id\"
FROM workspace
LEFT JOIN workspace_settings ON workspace.id = workspace_settings.workspace_id
WHERE workspace.id = $1",
w_id
)
.fetch_optional(&db)
.await?;
let workspace = not_found_if_none(workspace, "workspace", w_id)?;
Ok(Json(workspace))
}
async fn list_workspaces_as_super_admin(
authed: ApiAuthed,
Extension(db): Extension<DB>,
@@ -2310,6 +2349,13 @@ lazy_static::lazy_static! {
}
};
pub static ref DISABLE_WORKSPACE_FORK: bool = {
match std::env::var("DISABLE_WORKSPACE_FORK") {
Ok(val) => val == "true",
Err(_) => false,
}
};
}
async fn create_workspace_require_superadmin() -> String {
@@ -2511,8 +2557,11 @@ async fn clone_workspace_data(
clone_raw_apps(tx, source_workspace_id, target_workspace_id).await?;
// Clone workspace runnable dependencies and dependency map
clone_workspace_dependencies(tx, source_workspace_id, target_workspace_id).await?;
clone_workspace_runnable_dependencies(tx, source_workspace_id, target_workspace_id).await?;
// TODO: Enable when git sync is implemented for workspace dependencies.
// // Clone workspace dependencies
// clone_workspace_dependencies(tx, source_workspace_id, target_workspace_id).await?;
Ok(())
}
@@ -2566,9 +2615,14 @@ async fn update_workspace_settings(
WorkspaceGitSyncSettings::default()
};
// We only keep the first git sync repo, since it is considered the main one
// We only keep the first git sync repo that is sync mode (use_individual_branch = false), since it is considered the main one
// Context: see WIN-1559
git_sync_settings.repositories.truncate(1);
git_sync_settings.repositories = git_sync_settings
.repositories
.into_iter()
.filter(|r| !r.use_individual_branch.unwrap_or(false))
.take(1)
.collect();
let serialized_config = serde_json::to_value::<WorkspaceGitSyncSettings>(git_sync_settings)
.map_err(|err| Error::internal_err(err.to_string()))?;
@@ -2638,6 +2692,17 @@ async fn clone_groups(
.execute(&mut **tx)
.await?;
sqlx::query!(
"INSERT INTO usr_to_group (workspace_id, group_, usr)
SELECT $2, group_, usr
FROM usr_to_group
WHERE workspace_id = $1",
source_workspace_id,
target_workspace_id,
)
.execute(&mut **tx)
.await?;
Ok(())
}
@@ -2959,7 +3024,7 @@ async fn clone_raw_apps(
Ok(())
}
async fn clone_workspace_dependencies(
async fn clone_workspace_runnable_dependencies(
tx: &mut Transaction<'_, Postgres>,
source_workspace_id: &str,
target_workspace_id: &str,
@@ -2991,6 +3056,27 @@ async fn clone_workspace_dependencies(
Ok(())
}
#[allow(dead_code)]
async fn clone_workspace_dependencies(
tx: &mut Transaction<'_, Postgres>,
source_workspace_id: &str,
target_workspace_id: &str,
) -> Result<()> {
// Clone workspace_runnable_dependencies
sqlx::query!(
"INSERT INTO workspace_dependencies (workspace_id, language, name, description, content, archived, created_at)
SELECT $1, language, name, description, content, archived, created_at
FROM workspace_dependencies
WHERE workspace_id = $2",
target_workspace_id,
source_workspace_id
)
.execute(&mut **tx)
.await?;
Ok(())
}
async fn deprecated_create_workspace_fork(_authed: ApiAuthed) -> Result<String> {
return Err(Error::BadRequest("This API endpoint has been relocated. Your Windmill CLI version is outdated and needs to be updated.".to_string()));
}
@@ -3008,6 +3094,10 @@ async fn create_workspace_fork_branch(
)));
}
if *DISABLE_WORKSPACE_FORK {
require_super_admin(&db, &authed.email).await?;
}
Ok(Json(
handle_fork_branch_creation(&authed.email, &authed.username, &db, &w_id, &nw.id).await?,
))
@@ -3025,6 +3115,10 @@ async fn create_workspace_fork(
)));
}
if *DISABLE_WORKSPACE_FORK {
require_super_admin(&db, &authed.email).await?;
}
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
// Generate unique forked workspace ID with wm-fork prefix
@@ -3517,6 +3611,75 @@ async fn rebuild_dependency_map(
ScopedDependencyMap::rebuild_map(&w_id, &db).await
}
#[axum::debug_handler]
async fn get_dependents(
Extension(db): Extension<DB>,
Path((w_id, imported_path)): Path<(String, String)>,
_authed: ApiAuthed,
) -> JsonResult<Vec<DependencyDependent>> {
tracing::debug!(
workspace_id = %w_id,
imported_path = %imported_path,
"API: Getting dependents for imported path"
);
let dependents = ScopedDependencyMap::get_dependents(&imported_path, &w_id, &db).await?;
tracing::debug!(
workspace_id = %w_id,
imported_path = %imported_path,
dependents_count = dependents.len(),
"API: Found dependents: {:?}",
dependents
);
Ok(Json(dependents))
}
#[derive(Serialize, Debug)]
struct DependentsAmount {
imported_path: String,
count: i64,
}
#[axum::debug_handler]
async fn get_dependents_amounts(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(imported_paths): Json<Vec<String>>,
) -> JsonResult<Vec<DependentsAmount>> {
tracing::debug!(
workspace_id = %w_id,
imported_paths = ?imported_paths,
"API: Getting dependents amounts for imported paths"
);
let results = sqlx::query_as!(
DependentsAmount,
r#"
SELECT
imported_path,
COUNT(DISTINCT importer_path) as "count!"
FROM dependency_map
WHERE workspace_id = $1 AND imported_path = ANY($2)
GROUP BY imported_path
"#,
w_id,
&imported_paths
)
.fetch_all(&db)
.await?;
tracing::debug!(
workspace_id = %w_id,
results_count = results.len(),
"API: Found dependents amounts: {:?}",
results
);
Ok(Json(results))
}
#[derive(Deserialize)]
struct ChangeWorkspaceName {
new_name: String,

View File

@@ -44,6 +44,7 @@ use axum::{
use http::HeaderName;
use itertools::Itertools;
use windmill_common::utils::require_admin;
use windmill_common::variables::decrypt;
use windmill_common::{
db::UserDB,
@@ -52,6 +53,7 @@ use windmill_common::{
schedule::Schedule,
scripts::{Schema, Script, ScriptLang},
variables::{build_crypt, ExportableListableVariable},
workspace_dependencies::WorkspaceDependencies,
};
use hyper::header;
@@ -177,6 +179,7 @@ pub(crate) struct ArchiveQueryParams {
include_groups: Option<bool>,
include_settings: Option<bool>,
include_key: Option<bool>,
include_workspace_dependencies: Option<bool>,
default_ts: Option<String>,
}
@@ -211,6 +214,7 @@ where
"error",
"last_server_ping",
"server_id",
"raw_app",
],
ignore_keys.unwrap_or(vec![]),
]
@@ -309,11 +313,20 @@ pub(crate) async fn tarball_workspace(
include_groups,
include_settings,
include_key,
include_workspace_dependencies,
default_ts,
}): Query<ArchiveQueryParams>,
) -> Result<([(HeaderName, String); 2], impl IntoResponse)> {
// require_admin(authed.is_admin, &authed.username)?;
tracing::info!(
"tarball_workspace called for workspace {}: include_workspace_dependencies={:?}, skip_variables={:?}, skip_resources={:?}",
w_id,
include_workspace_dependencies,
skip_variables,
skip_resources
);
let mut tx = user_db.begin(&authed).await?;
let tmp_dir = TempDir::new_in("/tmp/windmill/")?;
@@ -529,8 +542,8 @@ pub(crate) async fn tarball_workspace(
let apps = sqlx::query_as::<_, AppWithLastVersion>(
"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
app_version.created_at, app_version.created_by, app_version.raw_app from app, app_version
WHERE app.workspace_id = $1 AND app_version.id = app.versions[array_upper(app.versions, 1)]
AND (app.draft_only IS NULL OR app.draft_only = false)",
)
.bind(&w_id)
@@ -539,12 +552,40 @@ pub(crate) async fn tarball_workspace(
for app in apps {
let app_str = &to_string_without_metadata(&app, false, None).unwrap();
let kind = if app.raw_app { "raw_app" } else { "app" };
archive
.write_to_archive(&app_str, &format!("{}.app.json", app.path))
.write_to_archive(&app_str, &format!("{}.{}.json", app.path, kind))
.await?;
}
}
if include_workspace_dependencies.unwrap_or(false)
&& require_admin(authed.is_admin, &authed.username).is_ok()
{
tracing::info!("Including workspace dependencies in tarball export");
let workspace_dependencies = WorkspaceDependencies::list(&w_id, &db).await?;
tracing::info!(
"Found {} workspace dependencies",
workspace_dependencies.len()
);
for dep in workspace_dependencies {
// let dep_str = &to_string_without_metadata(&dep, false, None).unwrap();
let filename = WorkspaceDependencies::to_path(&dep.name, dep.language)?;
tracing::info!(
"Adding workspace dependency: name={:?}, language={:?}, filename={}",
dep.name,
dep.language,
filename
);
archive.write_to_archive(&dep.content, &filename).await?;
}
} else {
tracing::info!(
"Skipping workspace dependencies: include_workspace_dependencies={:?}",
include_workspace_dependencies
);
}
if include_schedules.unwrap_or(false) {
let schedules = sqlx::query_as::<_, Schedule>(
"SELECT * FROM schedule

View File

@@ -33,6 +33,7 @@ thiserror.workspace = true
anyhow.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_yml.workspace = true
chrono.workspace = true
chrono-tz.workspace = true
hex.workspace = true
@@ -68,6 +69,7 @@ aws-credential-types.workspace = true
aws-smithy-types.workspace = true
base64.workspace = true
bitflags.workspace = true
phf.workspace = true
aws-smithy-types-convert = { workspace = true, optional = true }
aws-sdk-rds = { workspace = true, optional = true }
@@ -91,6 +93,7 @@ strum_macros.workspace = true
url.workspace = true
urlencoding.workspace = true
async-recursion.workspace = true
pep440_rs.workspace = true
semver.workspace = true
croner = "2.2.0"

View File

@@ -631,7 +631,8 @@ pub mod script {
envs AS \"envs: Vec<String>\", \
schema AS \"schema: String\", \
schema_validation AS \"schema_validation: bool\", \
codebase LIKE '%.tar' as use_tar \
codebase LIKE '%.tar' as use_tar, \
codebase LIKE '%.esm%' as is_esm \
FROM script WHERE hash = $1 LIMIT 1",
hash.0
)
@@ -647,12 +648,14 @@ pub mod script {
language: r.language,
envs: r.envs,
codebase: if let Some(use_tar) = r.use_tar {
let sh = hash.to_string();
if use_tar {
Some(format!("{sh}.tar"))
} else {
Some(sh)
let mut sh = hash.to_string();
if r.is_esm.unwrap_or(false) {
sh = format!("{sh}.esm");
}
if use_tar {
sh = format!("{sh}.tar");
}
Some(sh)
} else {
None
},
@@ -889,6 +892,86 @@ pub mod job {
}
}
pub mod workspace_dependencies {
use std::{
future::Future,
time::{Duration, Instant},
};
use crate::{error, scripts::ScriptLang, workspace_dependencies::WorkspaceDependencies, DB};
make_static! {
/// Workspace Dependencies by id and workspace cache.
static ref WORKSPACE_DEPENDENCIES: { (i64, String) => WorkspaceDependencies } in "workspace_dependencies" <= 1000;
}
lazy_static::lazy_static! {
/// Cache for checking if default/unnamed workspace dependencies exist for a workspace and language.
/// Cache key: (workspace_id, language)
/// Cache value: (exists: bool, cached_at timestamp)
static ref DEFAULT_WD_EXISTS_CACHE: quick_cache::sync::Cache<(String, ScriptLang), (bool, Instant)> = quick_cache::sync::Cache::new(500);
}
/// Cache timeout for existence checks (10 seconds)
pub const EXISTS_CACHE_TIMEOUT: Duration = Duration::from_secs(10);
pub fn fetch_workspace_dependencies<'c>(
id: i64,
workspace_id: String,
db: &'c DB,
) -> impl Future<Output = error::Result<WorkspaceDependencies>> + 'c {
tracing::debug!(workspace_id = %workspace_id, id, "fetching workspace dependencies");
WORKSPACE_DEPENDENCIES.get_or_insert_async(
(id, workspace_id.clone()),
WorkspaceDependencies::get(id, workspace_id, db),
)
}
pub fn get_cached_is_unnamed_workspace_dependencies_exists<'c>(
language: ScriptLang,
workspace_id: String,
) -> Option<bool> {
let exists_key = (workspace_id.to_string(), language);
if let Some((exists, cached_at)) = DEFAULT_WD_EXISTS_CACHE.get(&exists_key) {
if cached_at.elapsed() < EXISTS_CACHE_TIMEOUT {
tracing::debug!(
workspace_id = %workspace_id,
?language,
exists,
"cache hit for unnamed workspace dependencies existence"
);
return Some(exists);
} else {
tracing::debug!(
workspace_id = %workspace_id,
?language,
"cache expired for unnamed workspace dependencies existence"
);
DEFAULT_WD_EXISTS_CACHE.remove(&exists_key);
}
} else {
tracing::debug!(
workspace_id = %workspace_id,
?language,
"cache miss for unnamed workspace dependencies existence"
);
}
None
}
pub fn set_cached_is_unnamed_workspace_dependencies_exists<'c>(
language: ScriptLang,
workspace_id: String,
exists: bool,
) {
tracing::debug!(
workspace_id = %workspace_id,
?language,
exists,
"setting cache for unnamed workspace dependencies existence"
);
let exists_key = (workspace_id.to_string(), language);
DEFAULT_WD_EXISTS_CACHE.insert(exists_key, (exists, Instant::now()));
}
}
const _: () = {
impl Import for RawFlow {
fn import(src: &impl Storage) -> error::Result<Self> {
@@ -1072,7 +1155,8 @@ const _: () = {
(ScriptHash, |x| format!("{:016x}", x.0)),
((u8, ScriptHash), |x| format!("{:02x}-{:016x}", x.0, x.1.0)),
(FlowNodeId, |x| format!("{:016x}", x.0)),
(AppScriptId, |x| format!("{:016x}", x.0))
(AppScriptId, |x| format!("{:016x}", x.0)),
((i64, String), |x| format!("{}-{}", x.1, x.0))
}
#[cfg(feature = "scoped_cache")]

View File

@@ -86,6 +86,12 @@ pub enum Error {
Generic(StatusCode, String),
#[error("{feature} is unavailable due to some workers being behind. Do not use the feature or make sure all workers run at least {min_version}")]
WorkersAreBehind { feature: String, min_version: String },
#[error(
"Breaking change was introduced in v{version} ({feature}). Follow this migration guide: {guide_url}"
)]
MigrationNeeded { version: String, feature: String, guide_url: url::Url },
#[error("{0} is unavailable. It is possible for this worker to be behind.")]
FeatureUnavailable(String),
}
impl Error {

View File

@@ -15,6 +15,8 @@ pub enum MessageType {
}
/// Add a message to a conversation using an existing transaction
/// If the conversation doesn't exist, logs a warning and returns Ok (no error thrown)
/// This allows memory_id to be used for agent memory without requiring a conversation
pub async fn add_message_to_conversation_tx(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
conversation_id: Uuid,
@@ -24,6 +26,23 @@ pub async fn add_message_to_conversation_tx(
step_name: Option<&str>,
success: bool,
) -> Result<()> {
// Check if conversation exists first
let conversation_exists = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM flow_conversation WHERE id = $1) as \"exists!\"",
conversation_id
)
.fetch_one(&mut **tx)
.await?
.exists;
if !conversation_exists {
tracing::warn!(
"Conversation {} does not exist. Skipping message insertion. This is expected when flows are called from apps (memory_id is used for agent memory only).",
conversation_id
);
return Ok(());
}
// Insert the message
sqlx::query!(
"INSERT INTO flow_conversation_message (conversation_id, message_type, content, job_id, step_name, success)

View File

@@ -78,7 +78,8 @@ pub struct ListableFlow {
pub workspace_id: String,
pub path: String,
pub summary: String,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub edited_by: Option<String>,
pub edited_at: Option<chrono::DateTime<chrono::Utc>>,
pub archived: bool,
@@ -245,7 +246,13 @@ impl FlowValue {
}
}
}
BranchOne { branches, .. } | BranchAll { branches, .. } => {
BranchOne { default, branches, .. } => {
Self::traverse_leafs(default.iter().collect(), cb)?;
for branch in branches {
Self::traverse_leafs(branch.modules.iter().collect(), cb)?;
}
}
BranchAll { branches, .. } => {
for branch in branches {
Self::traverse_leafs(branch.modules.iter().collect(), cb)?;
}
@@ -868,6 +875,8 @@ pub enum FlowModuleValue {
parallel: bool,
#[serde(skip_serializing_if = "Option::is_none")]
parallelism: Option<InputTransform>,
#[serde(skip_serializing_if = "Option::is_none")]
squash: Option<bool>,
},
/// While loop node
@@ -877,6 +886,8 @@ pub enum FlowModuleValue {
modules_node: Option<FlowNodeId>,
#[serde(default = "default_false")]
skip_failures: bool,
#[serde(skip_serializing_if = "Option::is_none")]
squash: Option<bool>,
},
/// Branch-one node
@@ -988,6 +999,7 @@ struct UntaggedFlowModuleValue {
assets: Option<Vec<AssetWithAltAccessType>>,
tools: Option<Vec<AgentTool>>,
pass_flow_input_directly: Option<bool>,
squash: Option<bool>,
}
impl<'de> Deserialize<'de> for FlowModuleValue {
@@ -1026,6 +1038,7 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
skip_failures: untagged.skip_failures.unwrap_or(true),
parallel: untagged.parallel.unwrap_or(false),
parallelism: untagged.parallelism,
squash: untagged.squash,
}),
"whileloopflow" => Ok(FlowModuleValue::WhileloopFlow {
modules: untagged
@@ -1033,6 +1046,7 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
.ok_or_else(|| serde::de::Error::missing_field("modules"))?,
modules_node: untagged.modules_node,
skip_failures: untagged.skip_failures.unwrap_or(false),
squash: untagged.squash,
}),
"branchone" => Ok(FlowModuleValue::BranchOne {
branches: untagged
@@ -1122,6 +1136,7 @@ where
#[derive(Deserialize)]
pub struct ListFlowQuery {
pub without_description: Option<bool>,
pub path_start: Option<String>,
pub path_exact: Option<String>,
pub edited_by: Option<String>,

View File

@@ -133,3 +133,18 @@ pub async fn load_value_from_global_settings(
.map(|x| x.value);
Ok(r)
}
pub async fn set_value_in_global_settings(
db: &Pool<Postgres>,
setting_name: &str,
value: serde_json::Value,
) -> error::Result<()> {
sqlx::query!(
"INSERT INTO global_settings (name, value) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET value = EXCLUDED.value, updated_at = now()",
setting_name,
value
)
.execute(db)
.await?;
Ok(())
}

View File

@@ -96,6 +96,16 @@ pub enum JobKind {
AIAgent,
}
#[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Copy, Clone)]
#[sqlx(type_name = "JOB_STATUS", rename_all = "lowercase")]
#[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))]
pub enum JobStatus {
Success,
Failure,
Canceled,
Skipped,
}
impl JobKind {
pub fn is_flow(&self) -> bool {
matches!(
@@ -412,7 +422,7 @@ pub enum JobPayload {
/// Dependency Job, exposed with API. Requirements can be predefined
RawScriptDependencies {
script_path: String,
/// Will reflect raw requirements content (e.g. requirements.txt)
/// Will reflect raw requirements content (e.g. requirements.in)
content: String,
language: ScriptLang,
},

View File

@@ -55,7 +55,7 @@ pub mod job_metrics;
pub mod job_s3_helpers_ee;
#[cfg(feature = "parquet")]
pub mod job_s3_helpers_oss;
pub mod lockfiles;
pub mod workspace_dependencies;
#[cfg(feature = "private")]
pub mod git_sync_ee;

View File

@@ -345,15 +345,7 @@ fn find_annotation(comm_lit: &str, annotation: &str, code: &str) -> bool {
pub fn should_validate_schema(code: &str, lang: &ScriptLang) -> bool {
let annotation = "schema_validation";
use ScriptLang::*;
let comment = match lang {
Nativets | Bun | Bunnative | Deno | Php | CSharp | Java => "//",
Python3 | Go | Bash | Powershell | Graphql | Ansible | Nu | Ruby => "#",
Postgresql | Mysql | Bigquery | Snowflake | Mssql | OracleDB | DuckDb => "--",
Rust => "//!",
// for related places search: ADD_NEW_LANG
};
find_annotation(comment, annotation, code)
find_annotation(&lang.as_comment_lit(), annotation, code)
}
#[derive(Serialize, Deserialize, Debug, Clone)]

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