Compare commits

...

40 Commits

Author SHA1 Message Date
Guilhem Lemouel
632c8868bc feat(cli): add localhost reverse proxy to wmill dev for Claude Desktop preview
Adds a reverse proxy to `wmill dev` that serves the Windmill UI on localhost,
enabling Claude Desktop/Preview to open dev pages. Each connected dev page can
watch a specific file via the `path` URL param and `setWatch` WebSocket message.

Key changes:
- CLI: single-port proxy (default :3100) that forwards HTTP to remote Windmill,
  handles /ws_dev locally for dev file changes, and proxies /ws/* to remote
- CLI: per-client watch filtering so multiple tabs can watch different files
- CLI: flow broadcasts now include a `path` field
- Frontend: Dev.svelte connects to same-origin /ws_dev when no port param,
  sends setWatch on connect, filters messages client-side as safety net
- CLI init: generates .claude/skills/dev-preview/SKILL.md and .claude/launch.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:45:37 +01:00
Alexander Petric
911df958e7 fix(cli): add Svelte 5 event delegation guidance and safe push to raw-app skill (#8466)
- Add documentation about the $.delegated runtime error that occurs when
  the Svelte runtime version in node_modules doesn't match the compiler
  version used by wmill sync push.
- Change the push command in CLI reference to use --extra-includes for
  targeted pushes instead of blanket wmill sync push.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 17:18:41 +00:00
Ruben Fiszel
fb2bdc6a53 SSRF protection for SAML and MCP OAuth endpoints (#8473)
* fix: add SSRF protection to SAML and MCP OAuth endpoints

- Add shared SSRF URL validation utility (windmill-common/ssrf.rs) that blocks private/loopback/link-local IPs and validates DNS resolution
- Move test_metadata to authed service requiring superadmin access
- Strip response body from SAML metadata parsing errors
- Add SSRF blocklist to MCP OAuth discover, start, and client registration endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref.txt for SSRF fix

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref.txt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 563877bf1c8b4184f638bab51be89b1c0aec6dad

This commit updates the EE repository reference after PR #471 was merged in windmill-ee-private.

Previous ee-repo-ref: a600fe1807ea267f87a57360f4b48bf917776723

New ee-repo-ref: 563877bf1c8b4184f638bab51be89b1c0aec6dad

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-03-22 17:18:22 +00:00
hugocasa
1503bf948e fix: stop_after_if with empty error_message prevents flow from stopping (#8464)
* fix: stop_after_if with empty error_message no longer prevents flow from stopping

When skip_if_stopped=true and error_message="" were both set, the flow
would continue executing instead of stopping because the empty string
was converted to a default error message, which triggered the error
handler path. Now skip_if_stopped takes precedence and the two options
are treated as mutually exclusive in both backend and frontend.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate system prompts after openflow schema change

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 20:04:51 +01:00
Ruben Fiszel
039b79dfe6 chore(main): release 1.662.0 (#8463)
* chore(main): release 1.662.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-03-20 17:47:23 +00:00
hugocasa
efb4a27d51 fix: replace email with permissioned_as for triggers/schedules (#8439)
* refactor: replace email with permissioned_as for triggers/schedules

Add a new `permissioned_as` column (format: `u/{username}`, `g/{group}`,
or raw email) to all trigger tables and schedule. This value is used
directly for job permission checks, removing the need for email lookups
when creating/updating triggers.

- Migration: add permissioned_as to all 9 trigger tables + schedule,
  drop email from trigger tables (schedule keeps it for backwards compat)
- Backend: resolve_email() (async, DB) -> resolve_permissioned_as() (sync)
- Email cache: get_email_from_permissioned_as() with quick_cache for
  places that still need email (fetch_api_authed, schedule backwards compat)
- Frontend: rename email/preserve_email -> permissioned_as/preserve_permissioned_as
  in deploy data and OpenAPI schemas
- Tests updated for new field names and u/{username} format

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix sqlx/build

* update ee ref

* refactor: simplify resolve_edited_by to always use authed username

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix compile + migration

* update ref

* test: add trigger trait method tests for permissioned_as queries

Add tests that call TriggerCrud and Listener trait methods directly
to verify dynamic SQL correctly references the permissioned_as column.
Covers get_trigger_by_path, list_triggers, set_trigger_mode, and
fetch_enabled_unlistened_triggers for all trigger types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update sqlx

* fix: use permissioned_as directly for schedules and fix audit RLS for groups

- Schedule: permissioned_as only set on create, not on edit/set_enabled
- Schedule: stop reading email column, use get_email_from_permissioned_as
- Triggers: use fetch_api_authed_from_permissioned_as instead of edited_by
- Triggers: rename listener fields for clarity (username -> edited_by)
- Fix audit author username for group permissioned_as (g/test -> group-test)
  to match session.user, preventing RLS policy violations on audit_partitioned
- OpenAPI: remove permissioned_as/preserve_permissioned_as from EditSchedule
- Add backwards-compat comments for schedule email writes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate system prompts for permissioned_as field

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix build

* refactor: generalize onBehalfOf naming, add permissioned_as to EditSchedule

- Frontend: rename onBehalfOfPermissionedAs -> onBehalfOf with comments
  explaining it carries emails for flows/scripts and permissioned_as for
  triggers/schedules
- Frontend: rename getOnBehalfOfEmail -> getOnBehalfOf,
  getOnBehalfOfPermissionedAsForDeploy -> getOnBehalfOfForDeploy,
  customOnBehalfOfEmails -> customOnBehalfOf
- Backend: add optional permissioned_as/preserve_permissioned_as to
  EditSchedule with COALESCE (only updates when provided)
- Backend: add on_behalf_of audit log for schedule edit
- Backend: remove unused resolve_on_behalf_of_permissioned_as
- Tests: remove email assertions from schedule update test (email is
  just backwards compat, only permissioned_as matters)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: preserve email column when permissioned_as is preserved on schedule edit

Derive email from the preserved permissioned_as via cache lookup instead
of always writing authed.email. This keeps the email column consistent
with the old behavior for backwards compat with old workers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update deploy UI labels from "edited by" to "run as" for triggers

Triggers now use permissioned_as (not edited_by) for permissions, so
update the deploy UI wording to reflect this. Also update wm_deployers
group description to mention schedules and permissioned_as.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use u/username format for custom trigger/schedule deploy selection

When picking a custom user for trigger/schedule deployment, store
u/${username} (permissioned_as format) instead of the email. Flows/scripts
continue to use email format for on_behalf_of_email.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: show u/username format for "me" option in trigger deploy selector

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: simplify OnBehalfOfSelector to return the right format per kind

OnBehalfOfSelector now handles the email vs permissioned_as format
internally based on kind:
- triggers: returns u/username, displays u/username in all options
- flows/scripts/apps: returns email, displays username

The onSelect callback now takes (choice, value?) where value is already
in the correct format. Parent components just store it directly without
needing to know about the format difference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: always show u/username format in OnBehalfOfSelector for all kinds

Display is now consistent: all kinds show u/username in the selector.
The returned value still differs (email for flows/scripts, u/username
for triggers) since the backend APIs expect different formats.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace email with permissioned_as in http_trigger test insert

The email column was dropped from trigger tables in the migration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: review fixes — migration, app policy, capture cleanup, naming

- Migration: remove DEFAULT '', use nullable → populate → SET NOT NULL
- App policy: set both on_behalf_of and on_behalf_of_email for all choices
- OnBehalfOfSelector: return OnBehalfOfDetails {email, permissionedAs} instead of ambiguous value
- Remove unused email field from Capture struct and query
- Rename getSourceEmail/getTargetEmail → getSourceOnBehalfOf/getTargetOnBehalfOf
- Rename test functions from preserve_email to preserve_permissioned_as

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add permissioned_as to all test schedule INSERTs

Since the migration no longer uses DEFAULT '', all INSERTs must
explicitly provide permissioned_as. Updated test fixtures and
schedule_push tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: strip permissioned_as from exports/sync, fix OpenAPI required field

- Add permissioned_as to workspace export strip list (like edited_by)
- Add permissioned_as to CLI TriggerFile Omit list
- Fix TriggerExtraProperty.required: email → permissioned_as
- Regenerate frontend and CLI types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove accidentally committed generated files

These directories are gitignored and should not be tracked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate system prompts for permissioned_as schema changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove permissioned_as from CLI TriggerFile Omit list

Already stripped in workspace export, no need to also omit from the type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: optimize email cache key and revert TriggerFile Omit change

- Use single concatenated string for cache key instead of (String, String) tuple
- Remove permissioned_as from CLI TriggerFile Omit (already stripped in export)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: zero-allocation email cache lookups using Equivalent trait

Use a borrowed EmailCacheKey(&str, &str) for cache lookups via
quick_cache's Equivalent support. Only allocates (String, String)
on cache miss for insert. This is called on every trigger fire
and schedule push.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add permissioned_as to Schedule required fields in OpenAPI spec

The backend always returns permissioned_as (non-optional String),
so the schema should reflect that.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: handle group- prefix in migration UPDATE statements

edited_by can be 'group-{name}' for group-owned triggers/schedules.
The migration now correctly maps these to 'g/{name}' format instead
of incorrectly producing 'u/group-{name}'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "fix: handle group- prefix in migration UPDATE statements"

This reverts commit 0971392b38.

* fix: use superadmin email to resolve permissioned_as in schedule migration

For users upgrading from older versions where edited_by may not reflect
the actual schedule owner, check if the email belongs to a superadmin
and look up their username. Otherwise fall back to edited_by.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: fall back to superadmin email when not in workspace usr table

If the superadmin isn't a member of the workspace, use their email
as raw permissioned_as instead of falling back to edited_by.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: always update permissioned_as and email on schedule edit

Consistent with pre-refactor behavior where email and edited_by
were always updated on every edit. permissioned_as is now always
set (to editing user or preserved value), removing the COALESCE
that previously preserved it when not provided.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add schedule permission tests and centralize group prefix constants

Tests: schedule create/update for normal user, workspace admin, and
superadmin not in workspace. Verifies schedule fields (email,
permissioned_as, edited_by) and pushed job fields (permissioned_as,
permissioned_as_email).

Constants: centralize "u/", "g/", "group-" as PERMISSIONED_AS_USER_PREFIX,
PERMISSIONED_AS_GROUP_PREFIX, USERNAME_GROUP_PREFIX.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use @unknown.windmill.dev for synthetic email fallback

Prevents privilege escalation: a user with username like
'superadmin_secret' would get superadmin via the synthetic
email matching SUPERADMIN_SECRET_EMAIL. Using a different
subdomain avoids any collision with hardcoded @windmill.dev emails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update ee ref

* sqlx

* chore: regenerate system prompts after main merge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to bda51bc33bcb573659e7ff07d0a23ff6e23b8148

This commit updates the EE repository reference after PR #468 was merged in windmill-ee-private.

Previous ee-repo-ref: 8cf1802f8fe183f430830590b4f3172a50207843

New ee-repo-ref: bda51bc33bcb573659e7ff07d0a23ff6e23b8148

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-03-20 16:28:38 +00:00
Alexander Petric
51957f7d92 feat: mcp oauth gateway (#8443)
* feat: extract McpScopeSelector into reusable component

Extract scope selection UI from CreateToken.svelte and mcp_authorize page
into a shared McpScopeSelector.svelte component to reduce duplication.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add MCP gateway endpoint for workspace-agnostic access

Add /api/mcp/gateway endpoint that allows MCP clients to connect without
knowing the workspace ID upfront. During OAuth, the user picks their
workspace on the consent page. The token is then scoped to that workspace.

This enables a single URL for the Anthropic connectors directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review nits

- Use onClick prop instead of legacy on:click directive in McpScopeSelector
- Remove unused catch variable in workspace loading

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: deduplicate gateway OAuth handlers into shared inner functions

Extract build_oauth_metadata, build_protected_resource_metadata,
oauth_authorize_inner, and oauth_approve_inner so gateway handlers
are thin wrappers. Also revert formatting-only changes in auth.rs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: gate run_inline test helpers behind cfg(feature = "run_inline")

Imports and helper functions were not gated, causing unused-import and
dead-code errors when compiling without the run_inline feature.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update SQLx metadata

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-03-20 16:12:59 +00:00
centdix
533609989f handle OSS onboarding error gracefully (#8459)
* fix: handle OSS onboarding error gracefully in setup wizard

When creating a custom admin account fails on OSS builds (Enterprise-only
feature), show a helpful dialog instead of a generic error, guiding the
user to continue with default credentials.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use more precise error check for OSS account creation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: correct error message — not an EE feature, just not implemented in OSS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove misleading "change from user settings" since set_password is also OSS-stubbed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: move default credentials info to frontend dialog only

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 12:37:05 +00:00
centdix
88ad376791 fix: strip invalid enum values from MCP schemas (#8462)
* fix: harden MCP tool schemas for Claude compatibility

* fix: strip invalid enum values from MCP schemas
2026-03-20 12:36:43 +00:00
centdix
f2f178eb31 chore: remove dead users_oss module (#8458) 2026-03-19 17:21:07 +00:00
Ruben Fiszel
c4be206c5a chore(main): release 1.661.0 (#8448)
* chore(main): release 1.661.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-03-19 17:09:33 +00:00
wendrul
0e022b14fd fix: full code apps deployable on merge UI and deploy UI (#8451)
* fix: full code apps deployable on merge UI and deploy UI

* update ee repo ref

* preapare sqlx

* split app and raw_app

* update eereporef

* fix displayy showing raw apps appropriately

* chore: update ee-repo-ref to b3b8005d45e3f2aa7228c61d2e4ae86a17d89a30

This commit updates the EE repository reference after PR #470 was merged in windmill-ee-private.

Previous ee-repo-ref: 78d1f6cc4b15ec4c0768969635ba6b8f166a7742

New ee-repo-ref: b3b8005d45e3f2aa7228c61d2e4ae86a17d89a30

Automated by sync-ee-ref workflow.

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-03-19 17:01:56 +00:00
Ruben Fiszel
b2c1e3de0a fix: resolve blank inline script panel for components with underscores in ID (#8457)
* fix: resolve blank inline script panel for components with underscores in ID

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: compute matched grid item once per selection instead of per-item

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 16:54:30 +00:00
Ruben Fiszel
ff78f448be webmux nits 2026-03-19 16:06:07 +00:00
Ruben Fiszel
4e0b6db4ea webmux nits 2026-03-19 16:04:42 +00:00
Ruben Fiszel
041e1dcf82 simplify webmux ports 2026-03-19 16:03:21 +00:00
Ruben Fiszel
49f943b51d use BACKEND_PORT/FRONTEND_PORT as port fallbacks in backend and vite (#8454)
* feat: use WM_BACKEND_PORT/WM_FRONTEND_PORT env vars as port fallbacks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: use BACKEND_PORT/FRONTEND_PORT instead of WM_ prefixed vars

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: keep app.windmill.dev as ws proxy fallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:16:11 +00:00
Ruben Fiszel
75b191b3ad add gateway reverse proxy for extra services (#8456)
* feat: add gateway reverse proxy for extra services

Add a lightweight Node.js gateway on port 3000 that routes requests
by URL prefix (/ws/*, /ws_mp/*, /ws_debug/*) to the correct backend
service, stripping the prefix before forwarding. This allows all
extra services to be accessed through a single port.

Also makes the multiplayer server more tolerant by generically
stripping /ws_mp/ prefix on HTTP requests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: enable gateway by default for extra services

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: add REMOTE_EXTRA env var for unified extra services proxy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: make gateway port configurable via PORT env var

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: simplify Caddyfile extra services routing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:11:05 +00:00
Ruben Fiszel
4e59a1a166 fix: prevent raw app iframe reload on userStore refresh (#8455)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 14:17:06 +00:00
centdix
278c8fe416 chore: restore backend/.env copy in worktree setup (#8452)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 12:16:01 +00:00
Diego Imbert
446afb5b36 fix: fix datatable setup on RDS (#8450)
* Fix Datatable setup on RDS

* nit

* unused import

* add replication
2026-03-19 10:02:41 +00:00
Ruben Fiszel
fd7f0d3da9 fix: improve DND drag feedback in EditableSchemaForm (#8449)
Three issues fixed:
- Dragged element clone was invisible because morphDraggedElementToBeLike
  ran before the clone was in the DOM, copying 0-height from the
  uninitialized ResizeTransitionWrapper shadow. Fixed with morphDisabled.
- Shadow placeholder was inconsistently hidden because the DND library's
  inline visibility:hidden was overwritten by RTW's reactive style binding.
  Fixed with !visible CSS class that overrides inline styles.
- Small cursor movements immediately triggered field reordering. Added a
  200ms grace period after drag start before processing reorder events.

The shadow element now shows a dashed blue drop-target indicator instead
of being fully hidden.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 09:15:37 +00:00
Ruben Fiszel
7de98c0df4 feat: add OTel metrics support (#8442)
* [ee] feat: add OTel metrics support

Add OpenTelemetry metrics export for Windmill operational metrics.
When the OTel metrics toggle is enabled in instance settings (EE),
Windmill exports 16 metrics to any OTLP-compatible collector, letting
users observe queue depths, worker execution, DB pool state, and health
without a separate Prometheus setup.

Changes:
- otel_oss.rs: no-op stubs for OSS builds
- monitor.rs: queue count/running count gauges, zombie counters, DB pool
  monitoring (shared single DB query and loop with Prometheus)
- worker.rs: execution count/duration, worker busy, pull duration
- jobs.rs: queue push/delete/pull counters
- health.rs: DB latency gauge
- main.rs: call monitor_pool_otel unconditionally
- InstanceSetting.svelte: enable metrics toggle for EE licenses

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref.txt for OTel metrics

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add worker_started, worker_uptime, health_status, health_db_unresponsive OTel metrics

Wire up 5 additional metrics to reach parity with Prometheus:
- worker_execution_failed: wired in add_completed_job_error (was defined but unused)
- worker.started: incremented on worker startup
- worker.uptime: recorded each loop iteration
- health.status: phase gauge (healthy/degraded/unhealthy)
- health.db_unresponsive: flag (0/1)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref.txt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to fbe68e4aa621e30378995cfd328a6ccf74176614

This commit updates the EE repository reference after PR #469 was merged in windmill-ee-private.

Previous ee-repo-ref: 6fa1881aafdfb60f4abf11a37f01f6fedaecb3ec

New ee-repo-ref: fbe68e4aa621e30378995cfd328a6ccf74176614

Automated by sync-ee-ref workflow.

* fix: remove duplicate cfg attr and duplicate OTel pool reporting

- Remove duplicate #[cfg(feature = "prometheus")] on monitor_pool
- Remove OTel block from monitor_pool; monitor_pool_otel is the sole
  OTel reporter, eliminating duplicate windmill.db.pool.* metrics in
  EE builds
- Simplify monitor_pool back to its original Prometheus-only structure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-03-19 08:10:37 +00:00
Ruben Fiszel
1bca2e931b chore(main): release 1.660.1 (#8445)
* chore(main): release 1.660.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-03-19 07:00:38 +00:00
Ruben Fiszel
0ab9a81e20 field reorder on rename in EditableSchemaForm (#8447)
* fix: track schema.properties reference not keys in EditableSchemaForm

Object.keys() tracked key enumeration, so renaming a field triggered
onSchemaChange -> alignOrderWithProperties -> reorder. schema?.order
created a feedback loop since alignOrderWithProperties writes to it.

Only schema?.properties (the object reference) is needed to detect
when inferArgs replaces properties (schema.properties = {}).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: revert EditableSchemaForm effect to original

The added schema?.order and Object.keys(schema?.properties) tracking
caused field reordering on rename — Object.keys returns the renamed
key at the end (JS insertion order after delete+add), and schema?.order
created a feedback loop with alignOrderWithProperties. Revert to the
original schema reference-only tracking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 06:35:02 +00:00
Ruben Fiszel
c0edbe4317 fix: schema inference not updating on reset and language switch (#8446)
Three root causes:

1. Editor.setCode() never dispatched `change` — it pre-set `code = ncode`
   before the Monaco edit, so the debounced updateCode() saw code == ncode
   and skipped dispatch. The Reset button, copilot accept, and other
   setCode callers never triggered schema inference. Fixed by capturing
   `changed` before the pre-set and dispatching directly when true.

2. EditableSchemaForm's $effect only tracked the schema reference, not
   its properties. Since inferArgs mutates schema in-place through the
   Svelte 5 proxy, the reference never changes and the effect never
   re-ran. Added schema?.order and Object.keys(schema?.properties ?? {})
   reads to detect in-place mutations (matching SchemaForm's pattern).

3. ScriptEditor's $effect depended on both selectedTab and code, causing
   a redundant double inferSchema call on every code change (racing with
   the on:change handler and initContent's explicit call). Moved code
   into untrack() so the effect only fires on tab switches.

Also removed the no-op `testPanelSchema = testPanelSchema` in
inferModuleSchema.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 06:23:38 +00:00
Ruben Fiszel
a8fa0cccef fix: prevent S3 file browser crash when selecting storage (#8444)
VirtualList crashes with 'Requested index 0 is outside of range 0..0'
when it mounts with itemCount=0 and a positive height. This happened
because the old condition allowed VirtualList to remount during loading
with zero items but a stale listDivHeight from a previous mount.

Change the guard to displayedFileKeys.length === 0 so VirtualList is
never rendered when there are no items. Show a centered loading spinner
or "no files" message instead.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 05:49:44 +00:00
Ruben Fiszel
f2334e6564 chore(main): release 1.660.0 (#8428)
* chore(main): release 1.660.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-03-18 22:33:26 +00:00
hugocasa
f4489cbe64 fix: prevent AI agent tool jobs from becoming zombies on cancellation (#8437)
* fix: prevent AI agent tool jobs from becoming zombies on cancellation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* kill tool tasks on cancel timeout

* fix: address review feedback and update sqlx cache

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 18:30:31 +00:00
Ruben Fiszel
2171cc8e0a chore: separate csharp publish output dir from source dir to fix flaky build (#8441)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 18:30:09 +00:00
Alexander Petric
1cfb40bdaa feat: MCP server readiness for Anthropic connectors directory (#8438)
* feat: MCP server readiness for Anthropic integrations directory

- Add CORS layer to MCP streamable HTTP endpoint for browser clients
- Add tool result truncation (25K token limit) to prevent oversized responses
- Add HEAD method support on OAuth authorize endpoint
- Skip workspace selection redirect during MCP OAuth flow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR #8438 review feedback

- Add DELETE to CORS allowed methods (MCP spec requires DELETE for session termination)
- Add CORS layer to deprecated /sse endpoint for completeness
- Remove redundant .head() on OAuth authorize (axum auto-handles HEAD via GET)
- Fix comment: "chars/token" → "bytes/token" since len() returns bytes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 17:19:43 +00:00
Ruben Fiszel
bee928276e fix: show cancelled WAC jobs as done in workflow timeline (#8436)
* fix: show cancelled WAC jobs as done in workflow timeline

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: keep polling after cancel so WAC timeline updates to completed state

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: treat WAC as done in LogPanel when loader stops after cancel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: show preview badge and hide _MODULES arg in run history

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: show preview badge alongside status dot, not instead of it

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 12:50:50 +00:00
Ruben Fiszel
391da1d5af add cloud quota usage display and version pruning (#8433)
* feat: add cloud quota usage display and version pruning

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: hard-delete pruned scripts so quota actually decreases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: update quota error messages to reference workspace settings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 12:03:01 +00:00
centdix
9ca86f7a11 bump utils internal version (#8435) 2026-03-18 12:02:47 +00:00
Ruben Fiszel
19129aa019 nit serve_ui + workmux 2026-03-18 11:38:22 +00:00
centdix
435de95e7d feat(cli): use local scripts when previewing flows (#8365)
* feat(cli): use local scripts when previewing flows

When previewing a flow, PathScript modules (type: "script") now resolve
to local file content instead of remote versions. This ensures flow
preview and dev mode test the actual local changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(cli): add tests for PathScript local replacement in flow preview

Unit tests for replacePathScriptsWithLocal covering:
- basic PathScript→RawScript conversion
- tag_override preservation
- missing local file fallback
- mixed module types
- nested structures (loops, branches)

Integration test verifying flow preview with a PathScript step
uses the local script file content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(cli): extract shared helpers and add aiagent support for PathScript replacement

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(cli): replace `as any` casts with proper type assertions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): preserve local flow preview script context

* fix(cli): normalize inline flow preview bundles for bun

* fix(cli): make local flow path scripts opt-in

* fix(cli): only merge flow preview config for local mode

* chore(system-prompts): regenerate cli command guidance

* fix(cli): skip deno defaultTs test in CI without deno runtime

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(cli): clean up local path script helpers

* feat(cli): make flow preview use local path scripts

* fix(cli): ignore normalized preview metadata drift

* chore(cli): address review follow-ups

* test(cli): cover custom bundler path quoting

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:29:30 +00:00
Ruben Fiszel
997dd6ac3a windows volume mount symlinks for integration tests (#8431)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-18 10:27:41 +00:00
Ruben Fiszel
9a6ce44c84 fix: exclude wm_deployers group from CE group limit check (#8429)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-18 10:21:52 +00:00
Sascha Egerer
66a8e844a6 perf: cache composer vendor dir to skip reinstall on repeated php executions (#8330)
* perf: cache composer vendor dir to skip reinstall on repeated php executions

* feat: add COMPOSER_VENDOR_CACHE_DISABLED env var to opt out of vendor caching

---------

Co-authored-by: hugocasa <hugo@casademont.ch>
2026-03-18 09:41:37 +00:00
Ruben Fiszel
e0857421aa handle /ws_debug/health in debugger and add request logging (#8426)
- Fix debugger HTTP health endpoint to also match /ws_debug/health
  (ingress forwards the full path, not just /health)
- Add request logging to all three extra services (LSP, multiplayer,
  debugger) for HTTP and WebSocket ping/upgrade events

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 09:21:38 +00:00
220 changed files with 7191 additions and 2303 deletions

View File

@@ -44,7 +44,7 @@ profiles:
- Pane 1: backend (cargo watch -x run)
- Pane 2: frontend (npm run dev)
To check logs, use: \`tmux capture-pane -t .1 -p -S -50\` (backend) or \`tmux capture-pane -t .2 -p -S -50\` (frontend).
When restarting backend or frontend, make sure to use ${BACKEND_PORT} and ${FRONTEND_PORT}.
For this window specifically, backend is running on: ${BACKEND_PORT} and frontend is running on: ${FRONTEND_PORT}.
To connect to the database, use this connection string: ${DATABASE_URL}
Because we are running backend with cargo watch, to verify your changes, just check the logs in the backend pane. No need for cargo check.
IMPORTANT: Read docs/autonomous-mode.md before starting any work.
@@ -55,11 +55,11 @@ profiles:
- id: backend
kind: command
split: right
command: ROOT="$(git rev-parse --show-toplevel)"; cd "$ROOT/backend" && PORT=${BACKEND_PORT:-8000} cargo watch -x "run ${CARGO_FEATURES:+--features $CARGO_FEATURES}"
command: ROOT="$(git rev-parse --show-toplevel)"; cd "$ROOT/backend" && cargo watch -x "run ${CARGO_FEATURES:+--features $CARGO_FEATURES}"
- id: frontend
kind: command
split: bottom
command: ROOT="$(git rev-parse --show-toplevel)"; cd "$ROOT/frontend" && npm run generate-backend-client && REMOTE=${REMOTE:-http://localhost:${BACKEND_PORT:-8000}} npm run dev -- --port ${FRONTEND_PORT:-3000} --host 0.0.0.0
command: ROOT="$(git rev-parse --show-toplevel)"; cd "$ROOT/frontend" && npm run generate-backend-client && npm run dev -- --host 0.0.0.0
frontendOnly:
runtime: host
@@ -71,7 +71,7 @@ profiles:
- Pane 0: this pane (claude agent)
- Pane 1: frontend (npm run dev)
To check logs, use: \`tmux capture-pane -t .1 -p -S -50\` (frontend).
When restarting frontend, make sure to use ${FRONTEND_PORT}.
On this window specifically, frontend is running on: ${FRONTEND_PORT}.
To connect to the database, use this connection string: ${DATABASE_URL}
Because we are running frontend with npm run dev, to verify your changes, just check the logs in the frontend pane. No need for npm run build.
IMPORTANT: Read docs/autonomous-mode.md before starting any work.
@@ -82,7 +82,7 @@ profiles:
- id: frontend
kind: command
split: right
command: ROOT="$(git rev-parse --show-toplevel)"; cd "$ROOT/frontend" && npm run generate-backend-client && npm run dev -- --port ${FRONTEND_PORT:-3000} --host 0.0.0.0
command: ROOT="$(git rev-parse --show-toplevel)"; cd "$ROOT/frontend" && npm run generate-backend-client && npm run dev -- --host 0.0.0.0
agentOnly:
runtime: host

View File

@@ -1,74 +0,0 @@
main_branch: main
merge_strategy: rebase
# worktree_dir: .worktrees
worktree_naming: basename
worktree_prefix: ""
# Default: "wm-"
window_prefix: "wm-"
auto_name:
model: "gemini-2.5-flash-lite"
system_prompt: |
Generate a concise git branch name based on the task description.
Rules:
- Use kebab-case (lowercase with hyphens)
- Keep it short: 1-3 words, max 4 if necessary
- Focus on the core task/feature, not implementation details
- No prefixes like feat/, fix/, chore/
Examples of good branch names:
- "Add dark mode toggle" → dark-mode
- "Fix the search results not showing" → fix-search
- "Refactor the authentication module" → auth-refactor
- "Add CSV export to reports" → export-csv
- "Shell completion is broken" → shell-completion
Output ONLY the branch name, nothing else.
background: true
# Commands to run in new worktree before tmux window opens.
# These block window creation - use for short tasks only.
# Use "<global>" to inherit from global config.
# Set to empty list to disable: `post_create: []`
# post_create:
# - "<global>"
# - mise use
post_create:
- ./scripts/worktree-env
pre_remove:
- ./scripts/worktree-cleanup
panes:
- command: >-
claude --dangerously-skip-permissions --append-system-prompt
"You are running inside a tmux session with other panes running services.\n
Pane layout (current window):\n
- Pane 0: this pane (claude agent)\n
- Pane 1: backend (cargo watch -x run)\n
- Pane 2: frontend (npm run dev)\n\n
To check logs, use: \`tmux capture-pane -t .1 -p -S -50\` (backend) or \`tmux capture-pane -t .2 -p -S -50\` (frontend).\n
When restarting backend or frontend, make sure to use the ports listed in .env.local.\n
Because we are running backend with cargo watch, to verify your changes, just check the logs in the backend pane. No need for cargo check.\n\n
IMPORTANT: Read docs/autonomous-mode.md before starting any work."
focus: true
- command: 'ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/backend" && PORT=${BACKEND_PORT:-8000} cargo watch -x "run ${CARGO_FEATURES:+--features $CARGO_FEATURES}"'
split: horizontal
- command: 'ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/frontend" && npm run generate-backend-client && REMOTE=${REMOTE:-http://localhost:${BACKEND_PORT:-8000}} npm run dev -- --port ${FRONTEND_PORT:-3000} --host 0.0.0.0'
split: vertical
files:
copy:
- backend/.env
- scripts/
- wm-ts-nav/target/release/wm-ts-nav
sandbox:
enabled: false
toolchain: off

View File

@@ -1,5 +1,62 @@
# Changelog
## [1.662.0](https://github.com/windmill-labs/windmill/compare/v1.661.0...v1.662.0) (2026-03-20)
### Features
* mcp oauth gateway ([#8443](https://github.com/windmill-labs/windmill/issues/8443)) ([51957f7](https://github.com/windmill-labs/windmill/commit/51957f7d921b624fc132ca9ea03cdd30a5810e51))
### Bug Fixes
* replace email with permissioned_as for triggers/schedules ([#8439](https://github.com/windmill-labs/windmill/issues/8439)) ([efb4a27](https://github.com/windmill-labs/windmill/commit/efb4a27d5181bf9db3deb5e8100ec60adbe45e7f))
* strip invalid enum values from MCP schemas ([#8462](https://github.com/windmill-labs/windmill/issues/8462)) ([88ad376](https://github.com/windmill-labs/windmill/commit/88ad3767916b86c4e0b272d040ee0b75a0580d76))
## [1.661.0](https://github.com/windmill-labs/windmill/compare/v1.660.1...v1.661.0) (2026-03-19)
### Features
* add OTel metrics support ([#8442](https://github.com/windmill-labs/windmill/issues/8442)) ([7de98c0](https://github.com/windmill-labs/windmill/commit/7de98c0df464d8a7c9cf5d04228753294183f759))
### Bug Fixes
* fix datatable setup on RDS ([#8450](https://github.com/windmill-labs/windmill/issues/8450)) ([446afb5](https://github.com/windmill-labs/windmill/commit/446afb5b36211e5cbe8a279ce68f2f790a5953b9))
* full code apps deployable on merge UI and deploy UI ([#8451](https://github.com/windmill-labs/windmill/issues/8451)) ([0e022b1](https://github.com/windmill-labs/windmill/commit/0e022b14fd36e897106219010917bd7ceabf4078))
* improve DND drag feedback in EditableSchemaForm ([#8449](https://github.com/windmill-labs/windmill/issues/8449)) ([fd7f0d3](https://github.com/windmill-labs/windmill/commit/fd7f0d3da9153d91c15df5847aaae51e67479cde))
* prevent raw app iframe reload on userStore refresh ([#8455](https://github.com/windmill-labs/windmill/issues/8455)) ([4e59a1a](https://github.com/windmill-labs/windmill/commit/4e59a1a166847045897a6b576812bb53546e683b))
* resolve blank inline script panel for components with underscores in ID ([#8457](https://github.com/windmill-labs/windmill/issues/8457)) ([b2c1e3d](https://github.com/windmill-labs/windmill/commit/b2c1e3de0a263f606127f0decedb11a2ce0b822b))
## [1.660.1](https://github.com/windmill-labs/windmill/compare/v1.660.0...v1.660.1) (2026-03-19)
### Bug Fixes
* prevent S3 file browser crash when selecting storage ([#8444](https://github.com/windmill-labs/windmill/issues/8444)) ([a8fa0cc](https://github.com/windmill-labs/windmill/commit/a8fa0cccef870f841c68be77832d9be12109badb))
* schema inference not updating on reset and language switch ([#8446](https://github.com/windmill-labs/windmill/issues/8446)) ([c0edbe4](https://github.com/windmill-labs/windmill/commit/c0edbe431773f878201e96a79ce291d4b37a10bb))
## [1.660.0](https://github.com/windmill-labs/windmill/compare/v1.659.1...v1.660.0) (2026-03-18)
### Features
* **cli:** use local scripts when previewing flows ([#8365](https://github.com/windmill-labs/windmill/issues/8365)) ([435de95](https://github.com/windmill-labs/windmill/commit/435de95e7d5c9433dafac5369cfc533fd738fc22))
* MCP server readiness for Anthropic connectors directory ([#8438](https://github.com/windmill-labs/windmill/issues/8438)) ([1cfb40b](https://github.com/windmill-labs/windmill/commit/1cfb40bdaa877f1616fc1c1cf5fb6b6aa1832b86))
### Bug Fixes
* exclude wm_deployers group from CE group limit check ([#8429](https://github.com/windmill-labs/windmill/issues/8429)) ([9a6ce44](https://github.com/windmill-labs/windmill/commit/9a6ce44c8414810292ebc8a1ae64950ee2c76307))
* prevent AI agent tool jobs from becoming zombies on cancellation ([#8437](https://github.com/windmill-labs/windmill/issues/8437)) ([f4489cb](https://github.com/windmill-labs/windmill/commit/f4489cbe645489a892994c70d17df2284b494568))
* show cancelled WAC jobs as done in workflow timeline ([#8436](https://github.com/windmill-labs/windmill/issues/8436)) ([bee9282](https://github.com/windmill-labs/windmill/commit/bee928276e098ce7b17e20af74e34458e5c5353e))
### Performance Improvements
* cache composer vendor dir to skip reinstall on repeated php executions ([#8330](https://github.com/windmill-labs/windmill/issues/8330)) ([66a8e84](https://github.com/windmill-labs/windmill/commit/66a8e844a64d91d57dcabb7ad31d9308dec99032))
## [1.659.1](https://github.com/windmill-labs/windmill/compare/v1.659.0...v1.659.1) (2026-03-18)

View File

@@ -11,18 +11,8 @@
{$BASE_URL} {
bind {$ADDRESS}
# LSP - Language Server Protocol for code intelligence (windmill_extra:3001)
reverse_proxy /ws/* http://windmill_extra:3001
# Multiplayer - Real-time collaboration, Enterprise Edition (windmill_extra:3002)
# Uncomment and set ENABLE_MULTIPLAYER=true in docker-compose.yml
# reverse_proxy /ws_mp/* http://windmill_extra:3002
# Debugger - Interactive debugging via DAP WebSocket (windmill_extra:3003)
# Set ENABLE_DEBUGGER=true in docker-compose.yml to enable
handle_path /ws_debug/* {
reverse_proxy http://windmill_extra:3003
}
# Extra services: LSP, Multiplayer, Debugger (windmill_extra gateway)
reverse_proxy /ws/* /ws_mp/* /ws_debug/* http://windmill_extra:3000
# Search indexer, Enterprise Edition (windmill_indexer:8002)
# reverse_proxy /api/srch/* http://windmill_indexer:8002

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, email, server_id, error\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, permissioned_as, server_id, error\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -18,5 +18,5 @@
},
"nullable": []
},
"hash": "1d4bb4f53574ef95ef1016b760f849ec2372ac6a21bb2556d17a96dc72ea4980"
"hash": "02748cae17e8966dbd57a33017ccb747c84fcc12fbfd93c6c749570b94d35696"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, email, mode\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8::trigger_mode)\n ",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, permissioned_as, mode\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8::trigger_mode)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -28,5 +28,5 @@
},
"nullable": []
},
"hash": "7c1ae9cac13d1387cfa94149f039054dd8c30c16b4657e73cdb0d7c7f1cb3b6d"
"hash": "02e04f9ebc0e14f98f290bf2dc3eb00bc613ba7d29f8dd5ff31a4acd0ef3adfd"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id, script_path,\n is_flow, workspace_id, edited_by, email\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id, script_path,\n is_flow, workspace_id, edited_by, permissioned_as\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -18,5 +18,5 @@
},
"nullable": []
},
"hash": "1cad2ebfbdc46f9c0d93329897a71701f17a33b708b334d909563c9a0dcc9c23"
"hash": "066c9690d1606bf889879b7e3c686529c37db0d5f18c83706bfbc63c8c3e4315"
}

View File

@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, permissioned_as, edited_by FROM schedule WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "edited_by",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "0f26c74f604e1c3c613de8ba654cac1a41b20b1d3ea0f1a1c4ea2fcbbd314d7e"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO http_trigger (\n path, route_path, route_path_key, script_path, is_flow,\n workspace_id, edited_by, email, http_method,\n authentication_method, is_static_website, workspaced_route,\n wrap_body, raw_string\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::http_method,\n $10::authentication_method, $11, $12, $13, $14)\n ",
"query": "\n INSERT INTO http_trigger (\n path, route_path, route_path_key, script_path, is_flow,\n workspace_id, edited_by, permissioned_as, http_method,\n authentication_method, is_static_website, workspaced_route,\n wrap_body, raw_string\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::http_method,\n $10::authentication_method, $11, $12, $13, $14)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -50,5 +50,5 @@
},
"nullable": []
},
"hash": "6afa076744233fc5e92188ff978990fa3a704afe3eec523f4e203f7f6e247261"
"hash": "13d60d85694b5a5fcfc7687a07b78a54ff53245271466b1f3a9d9edf43cdaa1f"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE email_trigger\n SET\n script_path = $1,\n path = $2,\n is_flow = $3,\n local_part = $4,\n workspaced_local_part = $5,\n edited_by = $6,\n email = $7,\n edited_at = now(),\n error_handler_path = $8,\n error_handler_args = $9,\n retry = $10,\n mode = $11\n WHERE\n workspace_id = $12 AND path = $13\n ",
"query": "\n UPDATE email_trigger\n SET\n script_path = $1,\n path = $2,\n is_flow = $3,\n local_part = $4,\n workspaced_local_part = $5,\n edited_by = $6,\n permissioned_as = $7,\n edited_at = now(),\n error_handler_path = $8,\n error_handler_args = $9,\n retry = $10,\n mode = $11\n WHERE\n workspace_id = $12 AND path = $13\n ",
"describe": {
"columns": [],
"parameters": {
@@ -33,5 +33,5 @@
},
"nullable": []
},
"hash": "388ff2abd495cf71e87cf0c4ddc73b6c84867fb966df91b320c54acdd5e61315"
"hash": "1b7803a2060a19cb6e71f1e97619891ea8449b4a9433908cf717738846f7eec5"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE sqs_trigger\n SET\n queue_url = $1,\n aws_resource_path = $2,\n message_attributes = $3,\n aws_auth_resource_type = $4,\n script_path = $5,\n path = $6,\n is_flow = $7,\n edited_by = $8,\n email = $9,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $12,\n error_handler_args = $13,\n retry = $14\n WHERE\n workspace_id = $10 AND path = $11\n ",
"query": "\n UPDATE sqs_trigger\n SET\n queue_url = $1,\n aws_resource_path = $2,\n message_attributes = $3,\n aws_auth_resource_type = $4,\n script_path = $5,\n path = $6,\n is_flow = $7,\n edited_by = $8,\n permissioned_as = $9,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $12,\n error_handler_args = $13,\n retry = $14\n WHERE\n workspace_id = $10 AND path = $11\n ",
"describe": {
"columns": [],
"parameters": {
@@ -33,5 +33,5 @@
},
"nullable": []
},
"hash": "c723c3a5066a487b93e2642993f3bf624a1f50d06c7de75157420d97cf144763"
"hash": "1d2514b3d75ffb6cc0eb09ceb8fde974a07881eb08164e885448d9a17c4ca6db"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO email_trigger (\n path, local_part, workspaced_local_part, script_path,\n is_flow, workspace_id, edited_by, email\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
"query": "\n INSERT INTO email_trigger (\n path, local_part, workspaced_local_part, script_path,\n is_flow, workspace_id, edited_by, permissioned_as\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -17,5 +17,5 @@
},
"nullable": []
},
"hash": "1074c6c98e6a0c83ac04172a39abea21c793f58947051d39931d4da0868a1d77"
"hash": "1e28751bb98a1c477c0e582a2a39f81bf34e2d72f35ef1ea5d8c057ec9e694d8"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id, script_path,\n is_flow, workspace_id, edited_by, email, auto_commit\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ",
"query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id, script_path,\n is_flow, workspace_id, edited_by, permissioned_as, auto_commit\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -19,5 +19,5 @@
},
"nullable": []
},
"hash": "45fc21026fa76e5d69f00a68a7be81abb3ec627578f2d14f0ce33896dc6ab4cf"
"hash": "1ef63255389bdc47d5392a84aad38adf1ccc3a4923f988d8abaafc9749307c0e"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n mqtt_trigger\n SET\n mqtt_resource_path = $1,\n subscribe_topics = $2,\n client_version = $3,\n client_id = $4,\n v3_config = $5,\n v5_config = $6,\n is_flow = $7,\n edited_by = $8,\n email = $9,\n script_path = $10,\n path = $11,\n edited_at = now(),\n error = NULL,\n server_id = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE\n workspace_id = $12 AND\n path = $13\n ",
"query": "\n UPDATE\n mqtt_trigger\n SET\n mqtt_resource_path = $1,\n subscribe_topics = $2,\n client_version = $3,\n client_id = $4,\n v3_config = $5,\n v5_config = $6,\n is_flow = $7,\n edited_by = $8,\n permissioned_as = $9,\n script_path = $10,\n path = $11,\n edited_at = now(),\n error = NULL,\n server_id = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE\n workspace_id = $12 AND\n path = $13\n ",
"describe": {
"columns": [],
"parameters": {
@@ -35,5 +35,5 @@
},
"nullable": []
},
"hash": "e486a64b76da5de97e404c81dd6e29d333ada2dcfbbddb028f37794b85778ca8"
"hash": "1f693e2fba9885f7fc49bd2994240c4421fdae4aab496a3397c5c07d960582f2"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO postgres_trigger (\n workspace_id,\n path,\n postgres_resource_path,\n replication_slot_name,\n publication_name,\n script_path,\n is_flow,\n mode,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, now(), $11, $12, $13\n )\n ",
"query": "\n INSERT INTO postgres_trigger (\n workspace_id,\n path,\n postgres_resource_path,\n replication_slot_name,\n publication_name,\n script_path,\n is_flow,\n mode,\n edited_by,\n permissioned_as,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, now(), $11, $12, $13\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -33,5 +33,5 @@
},
"nullable": []
},
"hash": "fb942aa7894b4ae904f0233405f62f201e3f5deed593128017b886342ef6d210"
"hash": "21d7ce033b5f67499f579aeae98806599401fcfa9765a58b8cd45a20b411d0ca"
}

View File

@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "SELECT created_by, permissioned_as, permissioned_as_email\n FROM v2_job\n WHERE workspace_id = 'test-workspace'\n AND trigger_kind = 'schedule'\n AND trigger = $1\n ORDER BY created_at DESC\n LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "permissioned_as_email",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "270cfaea4f888e73e21a957e0328ec1f990fce409160eb7b0e807bff56defff4"
}

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_completed SET\n workflow_as_code_status = jsonb_set(\n jsonb_set(\n workflow_as_code_status,\n array[$1],\n COALESCE(workflow_as_code_status->$1, '{}'::jsonb)\n ),\n array[$1, 'duration_ms'],\n to_jsonb($2::bigint)\n )\n WHERE id = $3 AND workflow_as_code_status IS NOT NULL",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int8",
"Uuid"
]
},
"nullable": []
},
"hash": "29935e89475f637d765c516f1aa2be2f0f31fb50d519b42a056d0d73417599a3"
}

View File

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

View File

@@ -1,11 +1,11 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, edited_by FROM websocket_trigger WHERE path = $1 AND workspace_id = $2",
"query": "SELECT permissioned_as, edited_by FROM websocket_trigger WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"name": "permissioned_as",
"type_info": "Varchar"
},
{
@@ -25,5 +25,5 @@
false
]
},
"hash": "075d4749299af2cb81162bf396bec6aa89de43ec201c911196763e03e644ca7a"
"hash": "39062cdb183b97906c25602000321a76d4e629ba027364190a2487cc9bd93235"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO email_trigger (\n workspace_id,\n path,\n script_path,\n is_flow,\n local_part,\n workspaced_local_part,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry,\n mode\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, now(), $9, $10, $11, $12\n )\n ",
"query": "\n INSERT INTO email_trigger (\n workspace_id,\n path,\n script_path,\n is_flow,\n local_part,\n workspaced_local_part,\n edited_by,\n permissioned_as,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry,\n mode\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, now(), $9, $10, $11, $12\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -32,5 +32,5 @@
},
"nullable": []
},
"hash": "3d763dbb411e28ce026cc9ab525b20b409cfe17bf1cdef22aaffc719cf6c53e3"
"hash": "3aa3d0362fa8ed97dd034454b05f14880c9bb43d0c3ce4ea9b4511aa2d092d0c"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO http_trigger (\n workspace_id,\n path,\n route_path,\n route_path_key,\n workspaced_route,\n authentication_resource_path,\n wrap_body,\n raw_string,\n script_path,\n summary,\n description,\n is_flow,\n mode,\n request_type,\n authentication_method,\n http_method,\n static_asset_config,\n edited_by,\n email,\n edited_at,\n is_static_website,\n error_handler_path,\n error_handler_args,\n retry\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, now(), $20, $21, $22, $23\n )\n ",
"query": "\n INSERT INTO http_trigger (\n workspace_id,\n path,\n route_path,\n route_path_key,\n workspaced_route,\n authentication_resource_path,\n wrap_body,\n raw_string,\n script_path,\n summary,\n description,\n is_flow,\n mode,\n request_type,\n authentication_method,\n http_method,\n static_asset_config,\n edited_by,\n permissioned_as,\n edited_at,\n is_static_website,\n error_handler_path,\n error_handler_args,\n retry\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, now(), $20, $21, $22, $23\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -81,5 +81,5 @@
},
"nullable": []
},
"hash": "ee9ee0fbf5dd72d190e18c56622244b984fdb36fbf70197d9f8cb6306c9670db"
"hash": "41ad9954bebe31b0545837147b1616da2e6743f3a45775cd556a7414bc8f9726"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE nats_trigger\n SET\n nats_resource_path = $1,\n subjects = $2,\n stream_name = $3,\n consumer_name = $4,\n use_jetstream = $5,\n script_path = $6,\n path = $7,\n is_flow = $8,\n edited_by = $9,\n email = $10,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $13,\n error_handler_args = $14,\n retry = $15\n WHERE\n workspace_id = $11 AND path = $12\n ",
"query": "\n UPDATE nats_trigger\n SET\n nats_resource_path = $1,\n subjects = $2,\n stream_name = $3,\n consumer_name = $4,\n use_jetstream = $5,\n script_path = $6,\n path = $7,\n is_flow = $8,\n edited_by = $9,\n permissioned_as = $10,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $13,\n error_handler_args = $14,\n retry = $15\n WHERE\n workspace_id = $11 AND path = $12\n ",
"describe": {
"columns": [],
"parameters": {
@@ -24,5 +24,5 @@
},
"nullable": []
},
"hash": "9f41ea5cbe4cffa74e4a283fe8f023c813e349956487f7b6599da452c068e9b9"
"hash": "4ba114f54ed88c27dd4aed05a273c91e98913397b310ff760c1dbb9077764e9b"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE schedule SET\n schedule = $1,\n timezone = $2,\n args = $3,\n on_failure = $4,\n on_failure_times = $5,\n on_failure_exact = $6,\n on_failure_extra_args = $7,\n on_recovery = $8,\n on_recovery_times = $9,\n on_recovery_extra_args = $10,\n on_success = $11,\n on_success_extra_args = $12,\n ws_error_handler_muted = $13,\n retry = $14,\n summary = $15,\n no_flow_overlap = $16,\n tag = $17,\n paused_until = $18,\n path = $19,\n workspace_id = $20,\n cron_version = COALESCE($21, cron_version),\n description = $22,\n dynamic_skip = $23,\n email = COALESCE($24, email),\n edited_by = $25\n WHERE path = $19 AND workspace_id = $20\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"query": "\n UPDATE schedule SET\n schedule = $1,\n timezone = $2,\n args = $3,\n on_failure = $4,\n on_failure_times = $5,\n on_failure_exact = $6,\n on_failure_extra_args = $7,\n on_recovery = $8,\n on_recovery_times = $9,\n on_recovery_extra_args = $10,\n on_success = $11,\n on_success_extra_args = $12,\n ws_error_handler_muted = $13,\n retry = $14,\n summary = $15,\n no_flow_overlap = $16,\n tag = $17,\n paused_until = $18,\n path = $19,\n workspace_id = $20,\n cron_version = COALESCE($21, cron_version),\n description = $22,\n dynamic_skip = $23,\n email = $24,\n edited_by = $25,\n permissioned_as = $26\n WHERE path = $19 AND workspace_id = $20\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n permissioned_as,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"describe": {
"columns": [
{
@@ -65,96 +65,101 @@
},
{
"ordinal": 12,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 13,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "on_failure",
"type_info": "Varchar"
},
{
"ordinal": 14,
"ordinal": 15,
"name": "on_failure_times",
"type_info": "Int4"
},
{
"ordinal": 15,
"ordinal": 16,
"name": "on_failure_exact",
"type_info": "Bool"
},
{
"ordinal": 16,
"ordinal": 17,
"name": "on_failure_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 17,
"ordinal": 18,
"name": "on_recovery",
"type_info": "Varchar"
},
{
"ordinal": 18,
"ordinal": 19,
"name": "on_recovery_times",
"type_info": "Int4"
},
{
"ordinal": 19,
"ordinal": 20,
"name": "on_recovery_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 20,
"ordinal": 21,
"name": "on_success",
"type_info": "Varchar"
},
{
"ordinal": 21,
"ordinal": 22,
"name": "on_success_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"ordinal": 23,
"name": "ws_error_handler_muted",
"type_info": "Bool"
},
{
"ordinal": 23,
"ordinal": 24,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 24,
"ordinal": 25,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 25,
"ordinal": 26,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 26,
"ordinal": 27,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 27,
"ordinal": 28,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 28,
"ordinal": 29,
"name": "paused_until",
"type_info": "Timestamptz"
},
{
"ordinal": 29,
"ordinal": 30,
"name": "cron_version",
"type_info": "Text"
},
{
"ordinal": 30,
"ordinal": 31,
"name": "dynamic_skip",
"type_info": "Varchar"
}
@@ -185,6 +190,7 @@
"Text",
"Varchar",
"Varchar",
"Varchar",
"Varchar"
]
},
@@ -201,6 +207,7 @@
true,
false,
false,
false,
true,
true,
true,
@@ -222,5 +229,5 @@
true
]
},
"hash": "987d79f7c6d7bc148cc8aab67e47161cfca045966e995e28c7a7ad090cffeda0"
"hash": "54b4c762add9b1ebfdb2a6d5abd6d20e86dc0e6544f0bb22fa4ec68aa54a4dc8"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n http_trigger\n SET\n route_path = $1,\n route_path_key = $2,\n workspaced_route = $3,\n wrap_body = $4,\n raw_string = $5,\n authentication_resource_path = $6,\n script_path = $7,\n path = $8,\n is_flow = $9,\n mode = $10,\n http_method = $11,\n static_asset_config = $12,\n edited_by = $13,\n email = $14,\n request_type = $15,\n authentication_method = $16,\n summary = $17,\n description = $18,\n edited_at = now(),\n is_static_website = $19,\n error_handler_path = $20,\n error_handler_args = $21,\n retry = $22\n WHERE\n workspace_id = $23 AND\n path = $24\n ",
"query": "\n UPDATE\n http_trigger\n SET\n route_path = $1,\n route_path_key = $2,\n workspaced_route = $3,\n wrap_body = $4,\n raw_string = $5,\n authentication_resource_path = $6,\n script_path = $7,\n path = $8,\n is_flow = $9,\n mode = $10,\n http_method = $11,\n static_asset_config = $12,\n edited_by = $13,\n permissioned_as = $14,\n request_type = $15,\n authentication_method = $16,\n summary = $17,\n description = $18,\n edited_at = now(),\n is_static_website = $19,\n error_handler_path = $20,\n error_handler_args = $21,\n retry = $22\n WHERE\n workspace_id = $23 AND\n path = $24\n ",
"describe": {
"columns": [],
"parameters": {
@@ -82,5 +82,5 @@
},
"nullable": []
},
"hash": "2fd0d3224382b000028d98b0af4c431d3cadd54cca65d83c1ab7f2d2972e2282"
"hash": "5efbf92ac7347e73769c66ffdc4037c7e56a5939d9ffcbee13e0264cbb2a6dfe"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n gcp_resource_path,\n script_path,\n is_flow,\n mode as \"mode: _\",\n workspace_id,\n path,\n edited_by,\n email,\n delivery_config AS \"delivery_config: _\",\n retry as \"retry: _\",\n error_handler_path,\n error_handler_args as \"error_handler_args: _\"\n FROM\n gcp_trigger\n WHERE\n workspace_id = $1 AND\n path = $2 AND\n delivery_type = 'push'::DELIVERY_MODE\n ",
"query": "\n SELECT\n gcp_resource_path,\n script_path,\n is_flow,\n mode as \"mode: _\",\n workspace_id,\n path,\n edited_by,\n permissioned_as,\n delivery_config AS \"delivery_config: _\",\n retry as \"retry: _\",\n error_handler_path,\n error_handler_args as \"error_handler_args: _\"\n FROM\n gcp_trigger\n WHERE\n workspace_id = $1 AND\n path = $2 AND\n delivery_type = 'push'::DELIVERY_MODE\n ",
"describe": {
"columns": [
{
@@ -51,7 +51,7 @@
},
{
"ordinal": 7,
"name": "email",
"name": "permissioned_as",
"type_info": "Varchar"
},
{
@@ -96,5 +96,5 @@
true
]
},
"hash": "1cf2eb1426e8be89c3649272103bcd029e99b029b7f1b71eda4411d1e24e790d"
"hash": "6cfa6b5f16207863a03b77fffbb94ae91872a207ea2362e49e100ed1f0b35198"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, email\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, permissioned_as\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -16,5 +16,5 @@
},
"nullable": []
},
"hash": "57b7236cae0b6a1940f4c2d4b202692450ee231488d9a55ca59ff53a6f674626"
"hash": "72bc0b4acb3fb155436df74b0bb11600df8e55d50fa48d21ce2c5ae82eaf9f1c"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n path,\n script_path,\n is_flow,\n route_path,\n authentication_resource_path,\n workspace_id,\n request_type AS \"request_type: _\",\n authentication_method AS \"authentication_method: _\",\n edited_by,\n email,\n static_asset_config AS \"static_asset_config: _\",\n wrap_body,\n raw_string,\n workspaced_route,\n is_static_website,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\",\n mode as \"mode: _\"\n FROM\n http_trigger\n WHERE\n http_method = $1 AND\n (mode = 'enabled'::TRIGGER_MODE OR mode = 'suspended'::TRIGGER_MODE)\n ",
"query": "\n SELECT\n path,\n script_path,\n is_flow,\n route_path,\n authentication_resource_path,\n workspace_id,\n request_type AS \"request_type: _\",\n authentication_method AS \"authentication_method: _\",\n edited_by,\n permissioned_as,\n static_asset_config AS \"static_asset_config: _\",\n wrap_body,\n raw_string,\n workspaced_route,\n is_static_website,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\",\n mode as \"mode: _\"\n FROM\n http_trigger\n WHERE\n http_method = $1 AND\n (mode = 'enabled'::TRIGGER_MODE OR mode = 'suspended'::TRIGGER_MODE)\n ",
"describe": {
"columns": [
{
@@ -75,7 +75,7 @@
},
{
"ordinal": 9,
"name": "email",
"name": "permissioned_as",
"type_info": "Varchar"
},
{
@@ -175,5 +175,5 @@
false
]
},
"hash": "3cd37daa80bc3697d331c19e01a49916fc03fdf9eceff73fa153020b4a48f4a2"
"hash": "7402639802ba5f286db8436d21dcce24aa615fe1db7e8b4fc468963216aab69d"
}

View File

@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO nats_trigger (path, nats_resource_path, subjects, use_jetstream, script_path, is_flow, workspace_id, edited_by, permissioned_as) VALUES ($1, $2, $3, $4, $5, false, 'test-workspace', 'test-user', 'u/test-user')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"VarcharArray",
"Bool",
"Varchar"
]
},
"nullable": []
},
"hash": "75b9a2153ba12ef443212d83743605e0f9d8c1f0fbe3271f0e65b1dc5cd47987"
}

View File

@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "SELECT app_version_id, file_type, data FROM app_bundles\n WHERE app_version_id = ANY($1) AND w_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "app_version_id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "file_type",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "data",
"type_info": "Bytea"
}
],
"parameters": {
"Left": [
"Int8Array",
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "80844305a8339ca453c7afe8ffaa0aee765b15e0d4cb917f79d82a2fec60dc1e"
}

View File

@@ -1,11 +1,11 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, edited_by FROM schedule WHERE path = $1 AND workspace_id = $2",
"query": "SELECT permissioned_as, edited_by FROM schedule WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"name": "permissioned_as",
"type_info": "Varchar"
},
{
@@ -25,5 +25,5 @@
false
]
},
"hash": "17aafb72843659df9594d6d2466d2afaf26e666ffe52e0ea85792ea31b63410c"
"hash": "893ff34f2b22cf89a24a0b613ed390077fe6c75f56a3419f530e542bab0fb1a4"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO http_trigger (\n path, route_path, route_path_key, script_path, is_flow,\n workspace_id, edited_by, email, http_method,\n authentication_method, is_static_website, workspaced_route,\n wrap_body, raw_string, mode\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::http_method,\n $10::authentication_method, $11, $12, $13, $14, $15::trigger_mode)\n ",
"query": "\n INSERT INTO http_trigger (\n path, route_path, route_path_key, script_path, is_flow,\n workspace_id, edited_by, permissioned_as, http_method,\n authentication_method, is_static_website, workspaced_route,\n wrap_body, raw_string, mode\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::http_method,\n $10::authentication_method, $11, $12, $13, $14, $15::trigger_mode)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -62,5 +62,5 @@
},
"nullable": []
},
"hash": "57eca702e951f5303a74643c7ba64472e2c2a781fbb6366d998a0f1ca22fcdf2"
"hash": "8d53b0f2df5fdb6c43b9f5e92c8a97669676249dbf512ff39db94ecacc51e04e"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO schedule (\n workspace_id, path, edited_by, schedule, enabled,\n script_path, is_flow, email, timezone\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"query": "\n INSERT INTO schedule (\n workspace_id, path, edited_by, schedule, enabled,\n script_path, is_flow, email, timezone, permissioned_as\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -13,10 +13,11 @@
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "75ce9753a8acc8eccb3f2e0baaa5a871f866a8d21961fac8a003bc40c406ee79"
"hash": "906773b0de209734e9cf9c6421d4b3bf95ed2e00ac07942d8bba2b11b8462d3b"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO websocket_trigger (\n workspace_id,\n path,\n url,\n script_path,\n is_flow,\n mode,\n filters,\n initial_messages,\n url_runnable_args,\n edited_by,\n can_return_message,\n can_return_error_result,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14, $15, $16\n )\n ",
"query": "\n INSERT INTO websocket_trigger (\n workspace_id,\n path,\n url,\n script_path,\n is_flow,\n mode,\n filters,\n initial_messages,\n url_runnable_args,\n edited_by,\n can_return_message,\n can_return_error_result,\n permissioned_as,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14, $15, $16\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -36,5 +36,5 @@
},
"nullable": []
},
"hash": "e99c958628d83e3fea054eaa182b1301e0b04c7eb9801d9de13b37bd908d902f"
"hash": "942c0abb55c910862fd45d3fa56a4eb6729f1a658101bda2d0b0fca96b3cfee5"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM script s WHERE s.workspace_id = $1 AND s.hash NOT IN (\n SELECT DISTINCT ON (path) hash FROM script\n WHERE workspace_id = $1 AND deleted = false AND draft_only IS NOT TRUE\n ORDER BY path, created_at DESC\n )",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "9517395ac7230ab7c40c03ddd2a95fd6118b329a4421c9e8022df90ff7e775c8"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO nats_trigger (\n path, nats_resource_path, subjects, script_path,\n is_flow, workspace_id, edited_by, email, use_jetstream\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"query": "\n INSERT INTO nats_trigger (\n path, nats_resource_path, subjects, script_path,\n is_flow, workspace_id, edited_by, permissioned_as, use_jetstream\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -18,5 +18,5 @@
},
"nullable": []
},
"hash": "97c60a4193a75b611db41543e7fe6da81fc631cc6ac43576f8a18afedad4d4a4"
"hash": "9883cff4f988767aeea7bce8b66672029b2a4c90e06c05d4356b3ec6bd8d2748"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO kafka_trigger (\n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n filters,\n auto_offset_reset,\n auto_commit,\n script_path,\n is_flow,\n mode,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14, $15, $16\n )\n ",
"query": "\n INSERT INTO kafka_trigger (\n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n filters,\n auto_offset_reset,\n auto_commit,\n script_path,\n is_flow,\n mode,\n edited_by,\n permissioned_as,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14, $15, $16\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -36,5 +36,5 @@
},
"nullable": []
},
"hash": "5dd6315ec270c268e905262e4b0a920837354d91a0ae16b1236c1267da71765f"
"hash": "a0a545fda5f3ebea0113d5daaf13358c964d9fb0f41bf2a1c834305b4d2398f2"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE kafka_trigger\n SET\n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n filters = $4,\n auto_offset_reset = $5,\n auto_commit = $6,\n script_path = $7,\n path = $8,\n is_flow = $9,\n edited_by = $10,\n email = $11,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE\n workspace_id = $12 AND path = $13\n ",
"query": "\n UPDATE kafka_trigger\n SET\n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n filters = $4,\n auto_offset_reset = $5,\n auto_commit = $6,\n script_path = $7,\n path = $8,\n is_flow = $9,\n edited_by = $10,\n permissioned_as = $11,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE\n workspace_id = $12 AND path = $13\n ",
"describe": {
"columns": [],
"parameters": {
@@ -25,5 +25,5 @@
},
"nullable": []
},
"hash": "072e5ab78f929c6b7264f98c1588cb24cc635836276ee6faa2438f494bfbce04"
"hash": "a37cfc632dd37cf37c06743239b5ebc784e5da5ee25d47af187a75220d8fded7"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO gcp_trigger (\n gcp_resource_path,\n subscription_id,\n topic_id,\n delivery_type,\n delivery_config,\n workspace_id,\n path,\n script_path,\n is_flow,\n email,\n mode,\n edited_by,\n error_handler_path,\n error_handler_args,\n retry,\n auto_acknowledge_msg,\n ack_deadline\n )\n VALUES (\n $1,\n $2,\n $3,\n $4,\n $5,\n $6,\n $7,\n $8,\n $9,\n $10,\n $11,\n $12,\n $13,\n $14,\n $15,\n $16,\n $17\n )",
"query": "\n INSERT INTO gcp_trigger (\n gcp_resource_path,\n subscription_id,\n topic_id,\n delivery_type,\n delivery_config,\n workspace_id,\n path,\n script_path,\n is_flow,\n permissioned_as,\n mode,\n edited_by,\n error_handler_path,\n error_handler_args,\n retry,\n auto_acknowledge_msg,\n ack_deadline\n )\n VALUES (\n $1,\n $2,\n $3,\n $4,\n $5,\n $6,\n $7,\n $8,\n $9,\n $10,\n $11,\n $12,\n $13,\n $14,\n $15,\n $16,\n $17\n )",
"describe": {
"columns": [],
"parameters": {
@@ -47,5 +47,5 @@
},
"nullable": []
},
"hash": "aa59a96bf2d7edfa7c550e66c4d52ddc8e84eacc633e361e49a5219d0bec94b9"
"hash": "a5b6ca174b7a3be1b6507ef40b9cc67d85089199c900afc6668f2336b803562e"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO postgres_trigger (\n path, script_path, is_flow, workspace_id, edited_by, email,\n postgres_resource_path, replication_slot_name, publication_name\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"query": "\n INSERT INTO postgres_trigger (\n path, script_path, is_flow, workspace_id, edited_by, permissioned_as,\n postgres_resource_path, replication_slot_name, publication_name\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -18,5 +18,5 @@
},
"nullable": []
},
"hash": "0300afc35a880eef163dfdfd9d5299fac14562ee8595c792f3c30d042fa2d3eb"
"hash": "a8245a3b29927c26894be884c38e4d848674d5318e20bb5fc6c4261da25744ca"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE email_trigger\n SET\n script_path = $1,\n path = $2,\n is_flow = $3,\n edited_by = $4,\n email = $5,\n edited_at = now(),\n error_handler_path = $6,\n error_handler_args = $7,\n retry = $8,\n mode = $9\n WHERE\n workspace_id = $10 AND path = $11\n ",
"query": "\n UPDATE email_trigger\n SET\n script_path = $1,\n path = $2,\n is_flow = $3,\n edited_by = $4,\n permissioned_as = $5,\n edited_at = now(),\n error_handler_path = $6,\n error_handler_args = $7,\n retry = $8,\n mode = $9\n WHERE\n workspace_id = $10 AND path = $11\n ",
"describe": {
"columns": [],
"parameters": {
@@ -31,5 +31,5 @@
},
"nullable": []
},
"hash": "b5cda8eb32384f315689001f45676d4bf44cb4397dd0a722e7c9d035b58a09c1"
"hash": "aa7d3d159943250787a8ebc964cb46e5fbb157d1ec0684789a8d37eff1adf9a5"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE schedule SET\n enabled = $1,\n email = $2\n WHERE path = $3 AND workspace_id = $4\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"query": "\n UPDATE schedule SET\n enabled = $1,\n email = $2\n WHERE path = $3 AND workspace_id = $4\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n permissioned_as,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"describe": {
"columns": [
{
@@ -65,96 +65,101 @@
},
{
"ordinal": 12,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 13,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "on_failure",
"type_info": "Varchar"
},
{
"ordinal": 14,
"ordinal": 15,
"name": "on_failure_times",
"type_info": "Int4"
},
{
"ordinal": 15,
"ordinal": 16,
"name": "on_failure_exact",
"type_info": "Bool"
},
{
"ordinal": 16,
"ordinal": 17,
"name": "on_failure_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 17,
"ordinal": 18,
"name": "on_recovery",
"type_info": "Varchar"
},
{
"ordinal": 18,
"ordinal": 19,
"name": "on_recovery_times",
"type_info": "Int4"
},
{
"ordinal": 19,
"ordinal": 20,
"name": "on_recovery_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 20,
"ordinal": 21,
"name": "on_success",
"type_info": "Varchar"
},
{
"ordinal": 21,
"ordinal": 22,
"name": "on_success_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"ordinal": 23,
"name": "ws_error_handler_muted",
"type_info": "Bool"
},
{
"ordinal": 23,
"ordinal": 24,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 24,
"ordinal": 25,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 25,
"ordinal": 26,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 26,
"ordinal": 27,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 27,
"ordinal": 28,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 28,
"ordinal": 29,
"name": "paused_until",
"type_info": "Timestamptz"
},
{
"ordinal": 29,
"ordinal": 30,
"name": "cron_version",
"type_info": "Text"
},
{
"ordinal": 30,
"ordinal": 31,
"name": "dynamic_skip",
"type_info": "Varchar"
}
@@ -180,6 +185,7 @@
true,
false,
false,
false,
true,
true,
true,
@@ -201,5 +207,5 @@
true
]
},
"hash": "7927b80ce75d99b2a30f6b29196af000578a3c166509f032d14452cc637d884f"
"hash": "ad96768ff61fab1cfb9421683bb13b64f5f157fafe114d50fb910dc36ebe0f91"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO http_trigger (\n path, route_path, route_path_key, script_path, is_flow,\n workspace_id, edited_by, email, http_method,\n authentication_method, is_static_website, workspaced_route,\n wrap_body, raw_string\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::http_method,\n $10::authentication_method, $11, $12, $13, $14)\n ",
"query": "\n INSERT INTO http_trigger (\n path, route_path, route_path_key, script_path, is_flow,\n workspace_id, edited_by, permissioned_as, http_method,\n authentication_method, is_static_website, workspaced_route,\n wrap_body, raw_string\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::http_method,\n $10::authentication_method, $11, $12, $13, $14)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -50,5 +50,5 @@
},
"nullable": []
},
"hash": "74c11f5a0315424574fe3e7429f967f7b94f9d9db7be628f9cb411d789085711"
"hash": "ae8d7c07a4027bccf404951e75992bf3048c58bdf6fc67cb344eeb6cf72a9156"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id FROM token WHERE token_hash = $1 AND workspace_id IS NOT NULL AND (expiration > NOW() OR expiration IS NULL)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "b418f7ae7c0dc6fbe55a9b479e7304475fadab8613b0f541fec33a819e15807b"
}

View File

@@ -1,30 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "SELECT app_id, value, created_by, created_at, raw_app\n FROM app_version\n WHERE app_id = ANY(SELECT id FROM app WHERE workspace_id = $1)\n ORDER BY app_id, created_at",
"query": "SELECT id, app_id, value, created_by, created_at, raw_app\n FROM app_version\n WHERE app_id = ANY(SELECT id FROM app WHERE workspace_id = $1)\n ORDER BY app_id, created_at",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "app_id",
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "app_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "value",
"type_info": "Json"
},
{
"ordinal": 2,
"ordinal": 3,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 3,
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"ordinal": 5,
"name": "raw_app",
"type_info": "Bool"
}
@@ -39,8 +44,9 @@
false,
false,
false,
false,
false
]
},
"hash": "5d621d9d2bb37c3115e10a90452c42e563d1c7f2c4d27e9386fe9ed06fe3607a"
"hash": "bb37254ed68d9644e75b4d177ed96b46ad0c8ef31c8f4f44a5a66817c5460733"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM group_ WHERE name != 'all' AND name != 'error_handler' AND name != 'slack'",
"query": "SELECT COUNT(*) FROM group_ WHERE name != 'all' AND name != 'error_handler' AND name != 'slack' AND name != 'wm_deployers'",
"describe": {
"columns": [
{
@@ -16,5 +16,5 @@
null
]
},
"hash": "cec906e3bcfe5b477f23e8bb44500b97166c789ab1cf6487679d092eebf3a48b"
"hash": "bc6ebef9d41aba232f115f95404922c6054df01fa7f38f5d15d6d4af6c726a3c"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO mqtt_trigger (\n mqtt_resource_path,\n subscribe_topics,\n client_version,\n client_id,\n v3_config,\n v5_config,\n workspace_id,\n path,\n script_path,\n is_flow,\n email,\n mode,\n edited_by,\n error_handler_path,\n error_handler_args,\n retry\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16\n )",
"query": "\n INSERT INTO mqtt_trigger (\n mqtt_resource_path,\n subscribe_topics,\n client_version,\n client_id,\n v3_config,\n v5_config,\n workspace_id,\n path,\n script_path,\n is_flow,\n permissioned_as,\n mode,\n edited_by,\n error_handler_path,\n error_handler_args,\n retry\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16\n )",
"describe": {
"columns": [],
"parameters": {
@@ -46,5 +46,5 @@
},
"nullable": []
},
"hash": "e25aa749cfc9f0bb1649d162e36f2c0ce2187e47d745aa5ba96f9453c722750c"
"hash": "c301e668a5b88741e6c20700909be55518fabafcadaf6458fdcf1095643534c5"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n http_trigger\n SET\n wrap_body = $1,\n raw_string = $2,\n authentication_resource_path = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n mode = $7,\n http_method = $8,\n static_asset_config = $9,\n edited_by = $10,\n email = $11,\n request_type = $12,\n authentication_method = $13,\n summary = $14,\n description = $15,\n edited_at = now(),\n is_static_website = $16,\n error_handler_path = $17,\n error_handler_args = $18,\n retry = $19\n WHERE\n workspace_id = $20 AND\n path = $21\n ",
"query": "\n UPDATE\n http_trigger\n SET\n wrap_body = $1,\n raw_string = $2,\n authentication_resource_path = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n mode = $7,\n http_method = $8,\n static_asset_config = $9,\n edited_by = $10,\n permissioned_as = $11,\n request_type = $12,\n authentication_method = $13,\n summary = $14,\n description = $15,\n edited_at = now(),\n is_static_website = $16,\n error_handler_path = $17,\n error_handler_args = $18,\n retry = $19\n WHERE\n workspace_id = $20 AND\n path = $21\n ",
"describe": {
"columns": [],
"parameters": {
@@ -79,5 +79,5 @@
},
"nullable": []
},
"hash": "888f7e82c25b677172a276d042ae7066113f2a522068f04c8cee0895a49b787f"
"hash": "c5b650748a4ac0d0a04e287d1b3c89a5cc2af0439fe3c3c5829fd0776ec0734e"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n websocket_trigger\n SET\n url = $1,\n script_path = $2,\n path = $3,\n is_flow = $4,\n filters = $5,\n initial_messages = $6,\n url_runnable_args = $7,\n edited_by = $8,\n email = $9,\n can_return_message = $10,\n can_return_error_result = $11,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE\n workspace_id = $12 AND path = $13\n ",
"query": "\n UPDATE\n websocket_trigger\n SET\n url = $1,\n script_path = $2,\n path = $3,\n is_flow = $4,\n filters = $5,\n initial_messages = $6,\n url_runnable_args = $7,\n edited_by = $8,\n permissioned_as = $9,\n can_return_message = $10,\n can_return_error_result = $11,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE\n workspace_id = $12 AND path = $13\n ",
"describe": {
"columns": [],
"parameters": {
@@ -25,5 +25,5 @@
},
"nullable": []
},
"hash": "6c3c38ed5a0e6de0c97954fe4581daa788a7551b4a00dd33f78d36e246b98dd7"
"hash": "c7aed7fe3b6774477d403bc3e7fcbce7cdbdd1feb553718cbde60bb8ccff4733"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id,\n script_path, is_flow, workspace_id, edited_by, email\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id,\n script_path, is_flow, workspace_id, edited_by, permissioned_as\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -18,5 +18,5 @@
},
"nullable": []
},
"hash": "8c3cc09c1bbb6209467c75723dd02e97dddb99789422012e85e86c3151a9f2e9"
"hash": "ccd76be88fa9c11b3dc2e6d7711437ab3e02d8c3c10c53decc664533b8d04bc0"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n script_path, \n is_flow, \n workspace_id, \n mode as \"mode: _\",\n edited_by, \n email, \n path, \n error_handler_path as \"error_handler_path: _\", \n error_handler_args as \"error_handler_args: _\", \n retry as \"retry: _\" \n FROM email_trigger \n WHERE local_part = $1 \n AND workspaced_local_part = FALSE\n AND (mode = 'enabled'::TRIGGER_MODE OR mode = 'suspended'::TRIGGER_MODE)\n ",
"query": "\n SELECT \n script_path, \n is_flow, \n workspace_id, \n mode as \"mode: _\",\n permissioned_as,\n path,\n error_handler_path as \"error_handler_path: _\",\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM email_trigger\n WHERE local_part = $1\n AND workspaced_local_part = FALSE\n AND (mode = 'enabled'::TRIGGER_MODE OR mode = 'suspended'::TRIGGER_MODE)\n ",
"describe": {
"columns": [
{
@@ -36,31 +36,26 @@
},
{
"ordinal": 4,
"name": "edited_by",
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 7,
"ordinal": 6,
"name": "error_handler_path: _",
"type_info": "Varchar"
},
{
"ordinal": 8,
"ordinal": 7,
"name": "error_handler_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"ordinal": 8,
"name": "retry: _",
"type_info": "Jsonb"
}
@@ -77,11 +72,10 @@
false,
false,
false,
false,
true,
true,
true
]
},
"hash": "de656102e898ebc90e53af8d36c882cf47e798567bb733e8b528dcedc48ad4bf"
"hash": "cd410aa458ad23d47945084a0b9614babda8e6f9fe1ce71224773b2d5f307161"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE postgres_trigger\n SET\n postgres_resource_path = $1,\n replication_slot_name = $2,\n publication_name = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n edited_by = $7,\n email = $8,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $11,\n error_handler_args = $12,\n retry = $13\n WHERE\n workspace_id = $9 AND path = $10\n ",
"query": "\n UPDATE postgres_trigger\n SET\n postgres_resource_path = $1,\n replication_slot_name = $2,\n publication_name = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n edited_by = $7,\n permissioned_as = $8,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $11,\n error_handler_args = $12,\n retry = $13\n WHERE\n workspace_id = $9 AND path = $10\n ",
"describe": {
"columns": [],
"parameters": {
@@ -22,5 +22,5 @@
},
"nullable": []
},
"hash": "f4340e31adddc9a37bab64957b2b1b9b29f978c73f003dfbf81d5c487f666358"
"hash": "d082ff0fcc2bb02257ab82376d87ff8535dfcb7c2b1143b7016fa17bc42a595d"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n gcp_trigger\n SET\n gcp_resource_path = $1,\n subscription_id = $2,\n topic_id = $3,\n delivery_type = $4,\n delivery_config = $5,\n is_flow = $6,\n edited_by = $7,\n email = $8,\n script_path = $9,\n path = $10,\n mode = $11,\n edited_at = now(),\n error = NULL,\n server_id = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16,\n auto_acknowledge_msg = $17,\n ack_deadline = $18\n WHERE\n workspace_id = $12 AND\n path = $13\n ",
"query": "\n UPDATE\n gcp_trigger\n SET\n gcp_resource_path = $1,\n subscription_id = $2,\n topic_id = $3,\n delivery_type = $4,\n delivery_config = $5,\n is_flow = $6,\n edited_by = $7,\n permissioned_as = $8,\n script_path = $9,\n path = $10,\n mode = $11,\n edited_at = now(),\n error = NULL,\n server_id = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16,\n auto_acknowledge_msg = $17,\n ack_deadline = $18\n WHERE\n workspace_id = $12 AND\n path = $13\n ",
"describe": {
"columns": [],
"parameters": {
@@ -48,5 +48,5 @@
},
"nullable": []
},
"hash": "8642e4fc3efb011a8b7f98d2080a165ec4046c3a7172b5635b818c0fa133411f"
"hash": "d35300ab94202c181ee5d1e1c76ec9cf1044ff173375a82c529babe053acb24f"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO nats_trigger (\n workspace_id,\n path,\n nats_resource_path,\n subjects,\n stream_name,\n consumer_name,\n use_jetstream,\n script_path,\n is_flow,\n mode,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13, $14, $15\n )\n ",
"query": "\n INSERT INTO nats_trigger (\n workspace_id,\n path,\n nats_resource_path,\n subjects,\n stream_name,\n consumer_name,\n use_jetstream,\n script_path,\n is_flow,\n mode,\n edited_by,\n permissioned_as,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13, $14, $15\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -35,5 +35,5 @@
},
"nullable": []
},
"hash": "1bf8dc01326ebf6b8faa04e418b781e37bb9cedd1a89bf71a969b6db8cace48e"
"hash": "d56a8a7291ce3141c06d79ef854ac2cf970c3e41a223e31d3991276742d2afe1"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM app_version av\n JOIN app a ON a.id = av.app_id\n WHERE a.workspace_id = $1 AND av.id != a.versions[array_upper(a.versions, 1)]",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "d592ba371d4ad6e7f1bcffc01749f4753b6fe4e42413cce3d55f92797c856f35"
}

View File

@@ -1,8 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO app_version (app_id, value, created_by, created_at, raw_app)\n VALUES ($1, $2, $3, $4, $5)",
"query": "INSERT INTO app_version (app_id, value, created_by, created_at, raw_app)\n VALUES ($1, $2, $3, $4, $5) RETURNING id",
"describe": {
"columns": [],
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8",
@@ -12,7 +18,9 @@
"Bool"
]
},
"nullable": []
"nullable": [
false
]
},
"hash": "0924c79aca648e5ec3fcc5e91ca71d524fe9d4b46c2e8ed36ae99b5810a896ab"
"hash": "da87e3c6678e4ada367dfe6bdaef2c99ea980fb89fdb1bd954c22ac3cf79624c"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sqs_trigger (\n path, queue_url, aws_resource_path, script_path,\n is_flow, workspace_id, edited_by, email\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
"query": "\n INSERT INTO sqs_trigger (\n path, queue_url, aws_resource_path, script_path,\n is_flow, workspace_id, edited_by, permissioned_as\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -17,5 +17,5 @@
},
"nullable": []
},
"hash": "c659cfe81441bb1b62a9f23f5d9b7f5b1f354b6bb0a78d007114fed084cfe292"
"hash": "db9957ea9b5b326c27ff62f96a3981787a91ac587b1936c774de7a2a2094bf91"
}

View File

@@ -1,11 +1,11 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, edited_by FROM http_trigger WHERE path = $1 AND workspace_id = $2",
"query": "SELECT permissioned_as, edited_by FROM http_trigger WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"name": "permissioned_as",
"type_info": "Varchar"
},
{
@@ -25,5 +25,5 @@
false
]
},
"hash": "8311a553c44221751ffdbbe6a997d6feba8d43292daf6c5433b66bd8450e8854"
"hash": "dbb16284b9dd98b9339816e43eebf0fef488102cd7b3fd38d8af3148545bf1a4"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO schedule (\n workspace_id, path, schedule, timezone, edited_by, script_path,\n is_flow, args, enabled, email,\n on_failure, on_failure_times, on_failure_exact, on_failure_extra_args,\n on_recovery, on_recovery_times, on_recovery_extra_args,\n on_success, on_success_extra_args,\n ws_error_handler_muted, retry, summary, no_flow_overlap,\n tag, paused_until, cron_version, description, dynamic_skip\n ) VALUES (\n $1, $2, $3, $4, $5, $6,\n $7, $8, $9, $10,\n $11, $12, $13, $14,\n $15, $16, $17,\n $18, $19,\n $20, $21, $22, $23,\n $24, $25, $26, $27, $28\n )\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"query": "\n INSERT INTO schedule (\n workspace_id, path, schedule, timezone, edited_by, script_path,\n is_flow, args, enabled, email, permissioned_as,\n on_failure, on_failure_times, on_failure_exact, on_failure_extra_args,\n on_recovery, on_recovery_times, on_recovery_extra_args,\n on_success, on_success_extra_args,\n ws_error_handler_muted, retry, summary, no_flow_overlap,\n tag, paused_until, cron_version, description, dynamic_skip\n ) VALUES (\n $1, $2, $3, $4, $5, $6,\n $7, $8, $9, $10, $11,\n $12, $13, $14, $15,\n $16, $17, $18,\n $19, $20,\n $21, $22, $23, $24,\n $25, $26, $27, $28, $29\n )\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n permissioned_as,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"describe": {
"columns": [
{
@@ -65,96 +65,101 @@
},
{
"ordinal": 12,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 13,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "on_failure",
"type_info": "Varchar"
},
{
"ordinal": 14,
"ordinal": 15,
"name": "on_failure_times",
"type_info": "Int4"
},
{
"ordinal": 15,
"ordinal": 16,
"name": "on_failure_exact",
"type_info": "Bool"
},
{
"ordinal": 16,
"ordinal": 17,
"name": "on_failure_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 17,
"ordinal": 18,
"name": "on_recovery",
"type_info": "Varchar"
},
{
"ordinal": 18,
"ordinal": 19,
"name": "on_recovery_times",
"type_info": "Int4"
},
{
"ordinal": 19,
"ordinal": 20,
"name": "on_recovery_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 20,
"ordinal": 21,
"name": "on_success",
"type_info": "Varchar"
},
{
"ordinal": 21,
"ordinal": 22,
"name": "on_success_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"ordinal": 23,
"name": "ws_error_handler_muted",
"type_info": "Bool"
},
{
"ordinal": 23,
"ordinal": 24,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 24,
"ordinal": 25,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 25,
"ordinal": 26,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 26,
"ordinal": 27,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 27,
"ordinal": 28,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 28,
"ordinal": 29,
"name": "paused_until",
"type_info": "Timestamptz"
},
{
"ordinal": 29,
"ordinal": 30,
"name": "cron_version",
"type_info": "Text"
},
{
"ordinal": 30,
"ordinal": 31,
"name": "dynamic_skip",
"type_info": "Varchar"
}
@@ -172,6 +177,7 @@
"Bool",
"Varchar",
"Varchar",
"Varchar",
"Int4",
"Bool",
"Jsonb",
@@ -204,6 +210,7 @@
true,
false,
false,
false,
true,
true,
true,
@@ -225,5 +232,5 @@
true
]
},
"hash": "23e4c6e3dc6a48f702c2b26a6b1f94668e086caaa0093a3b685f87483513b0d2"
"hash": "dd20f94d560238096390371c98ded1f80825a11cd61c0bb431678ad9ab4a138e"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sqs_trigger (\n workspace_id,\n path,\n queue_url,\n aws_resource_path,\n message_attributes,\n aws_auth_resource_type,\n script_path,\n is_flow,\n mode,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now(), $12, $13, $14\n )\n ",
"query": "\n INSERT INTO sqs_trigger (\n workspace_id,\n path,\n queue_url,\n aws_resource_path,\n message_attributes,\n aws_auth_resource_type,\n script_path,\n is_flow,\n mode,\n edited_by,\n permissioned_as,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now(), $12, $13, $14\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -44,5 +44,5 @@
},
"nullable": []
},
"hash": "cad914554762b6be72b289814f77072600f8f1bc1fa73ef6f63972775aedf044"
"hash": "de4879b8e3002ca48005406c4e107df2663aca35b60e3c528f564da3114f0af1"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM v2_job_debounce_batch WHERE debounce_batch = (\n SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = $1\n )",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "e050734d7642b26f8859982c55ec2c8b1fc14a8de665b15a2f2dfd6e0b5b7fdf"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n script_path, \n is_flow, \n workspace_id, \n mode as \"mode: _\",\n edited_by, \n email, \n path, \n error_handler_path as \"error_handler_path: _\", \n error_handler_args as \"error_handler_args: _\", \n retry as \"retry: _\" \n FROM email_trigger \n WHERE workspace_id = $1 \n AND local_part = $2 \n AND (workspaced_local_part = TRUE OR $3 IS TRUE)\n AND (mode = 'enabled'::TRIGGER_MODE OR mode = 'suspended'::TRIGGER_MODE)\n ",
"query": "\n SELECT \n script_path, \n is_flow, \n workspace_id, \n mode as \"mode: _\",\n permissioned_as,\n path,\n error_handler_path as \"error_handler_path: _\",\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM email_trigger\n WHERE workspace_id = $1\n AND local_part = $2\n AND (workspaced_local_part = TRUE OR $3 IS TRUE)\n AND (mode = 'enabled'::TRIGGER_MODE OR mode = 'suspended'::TRIGGER_MODE)\n ",
"describe": {
"columns": [
{
@@ -36,31 +36,26 @@
},
{
"ordinal": 4,
"name": "edited_by",
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 7,
"ordinal": 6,
"name": "error_handler_path: _",
"type_info": "Varchar"
},
{
"ordinal": 8,
"ordinal": 7,
"name": "error_handler_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"ordinal": 8,
"name": "retry: _",
"type_info": "Jsonb"
}
@@ -79,11 +74,10 @@
false,
false,
false,
false,
true,
true,
true
]
},
"hash": "3aad6340ea1f8dc8411742ef5de5a77d5de903845da9e686aef804bc09db1687"
"hash": "e3f09fe777cbbc009ca1af31989b542ec2d585ef672f0e1496944f4dd0d082f4"
}

View File

@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO kafka_trigger (path, kafka_resource_path, group_id, topics, script_path, is_flow, workspace_id, edited_by, permissioned_as) VALUES ($1, $2, $3, $4, $5, false, 'test-workspace', 'test-user', 'u/test-user')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"VarcharArray",
"Varchar"
]
},
"nullable": []
},
"hash": "e998bb8195b4fafe6de20fb0bf4315b3a10db9aca72447b8b8afd9474baa0614"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM flow_version fv\n JOIN flow f ON f.workspace_id = fv.workspace_id AND f.path = fv.path\n WHERE fv.workspace_id = $1 AND fv.id != f.versions[array_upper(f.versions, 1)]",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "ed202f69f5f0a8f21f2dbfcae98b9a94d039362537b050913ea2b7534f85a047"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, email, initial_messages\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, permissioned_as, initial_messages\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -17,5 +17,5 @@
},
"nullable": []
},
"hash": "e485c82978d10379c6d1b7cd850f3bf764e6c5bf775f3d710ba36a79f5e424eb"
"hash": "f57a891d1b507ec6767099a388be7deb97a89168fd7f74040e73033075140655"
}

View File

@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO sqs_trigger (path, queue_url, aws_resource_path, script_path, is_flow, workspace_id, edited_by, permissioned_as) VALUES ($1, $2, $3, $4, false, 'test-workspace', 'test-user', 'u/test-user')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "f5f18e6c5b7376b2760915ee3c859fb2306f16c0c601668adaf30baf66f0fd0d"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT j.id FROM v2_job j\n JOIN v2_job_queue q ON q.id = j.id\n WHERE j.parent_job = $1 AND j.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
false
]
},
"hash": "fc27f306a9399205eaa0946897d797e5b792674c353acb41bd4e7133e19e2a03"
}

View File

@@ -1,8 +0,0 @@
panes:
# Pane 1: Install dependencies, then start dev server
- command: cargo run
# Pane 2: AI agent
- command: <agent>
split: horizontal
focus: true

197
backend/Cargo.lock generated
View File

@@ -860,9 +860,9 @@ dependencies = [
[[package]]
name = "aws-lc-rs"
version = "1.16.1"
version = "1.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf"
checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
dependencies = [
"aws-lc-sys",
"zeroize",
@@ -870,9 +870,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
version = "0.38.0"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e"
checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a"
dependencies = [
"cc",
"cmake",
@@ -1885,19 +1885,20 @@ dependencies = [
[[package]]
name = "borsh"
version = "1.6.0"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f"
checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
dependencies = [
"borsh-derive",
"bytes",
"cfg_aliases 0.2.1",
]
[[package]]
name = "borsh-derive"
version = "1.6.0"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c"
checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59"
dependencies = [
"once_cell",
"proc-macro-crate",
@@ -7524,9 +7525,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.17"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jni"
@@ -10641,9 +10642,9 @@ dependencies = [
[[package]]
name = "quick_cache"
version = "0.6.20"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c901384fb8fb3d4510388129ce6e13ecd686eee610da778b6ea77219decd53"
checksum = "5a70b1b8b47e31d0498ecbc3c5470bb931399a8bfed1fd79d1717a61ce7f96e3"
dependencies = [
"ahash 0.8.12",
"equivalent",
@@ -13843,9 +13844,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tar"
version = "0.4.44"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
dependencies = [
"filetime",
"libc",
@@ -15741,7 +15742,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-nats",
@@ -15786,6 +15787,7 @@ dependencies = [
"uuid",
"windmill-api",
"windmill-api-agent-workers",
"windmill-api-auth",
"windmill-api-client",
"windmill-api-settings",
"windmill-autoscaling",
@@ -15799,6 +15801,14 @@ dependencies = [
"windmill-queue",
"windmill-runtime-nativets",
"windmill-test-utils",
"windmill-trigger",
"windmill-trigger-gcp",
"windmill-trigger-kafka",
"windmill-trigger-mqtt",
"windmill-trigger-nats",
"windmill-trigger-postgres",
"windmill-trigger-sqs",
"windmill-trigger-websocket",
"windmill-types",
"windmill-worker",
"windmill-worker-volumes",
@@ -15808,7 +15818,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15821,7 +15831,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"argon2",
@@ -15962,7 +15972,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15985,7 +15995,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15998,7 +16008,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16024,7 +16034,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -16034,7 +16044,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16051,7 +16061,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"axum 0.7.9",
"base64 0.22.1",
@@ -16074,7 +16084,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16097,7 +16107,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16113,7 +16123,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16133,7 +16143,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16153,7 +16163,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16167,7 +16177,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-nats",
@@ -16195,7 +16205,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16220,7 +16230,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"axum 0.7.9",
"flate2",
@@ -16238,7 +16248,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16259,7 +16269,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16279,7 +16289,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16309,7 +16319,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16336,7 +16346,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"lazy_static",
"serde",
@@ -16348,7 +16358,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"argon2",
"axum 0.7.9",
@@ -16371,7 +16381,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16385,7 +16395,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16416,7 +16426,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"chrono",
"lazy_static",
@@ -16430,7 +16440,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16449,7 +16459,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"aes-gcm",
"anyhow",
@@ -16476,6 +16486,7 @@ dependencies = [
"cron",
"croner",
"datafusion",
"equivalent",
"futures",
"futures-core",
"gethostname",
@@ -16548,7 +16559,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -16567,7 +16578,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"regex",
"serde",
@@ -16582,7 +16593,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -16606,7 +16617,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"futures",
@@ -16623,7 +16634,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -16639,7 +16650,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16660,7 +16671,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16691,7 +16702,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-oauth2",
@@ -16715,7 +16726,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-stream",
@@ -16749,7 +16760,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"futures",
@@ -16767,7 +16778,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -16776,7 +16787,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16788,7 +16799,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16800,7 +16811,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"gosyn",
@@ -16812,7 +16823,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16824,7 +16835,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16836,7 +16847,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -16847,7 +16858,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16858,7 +16869,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16870,7 +16881,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-asset"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -16881,7 +16892,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16905,7 +16916,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16919,7 +16930,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -16936,7 +16947,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16950,7 +16961,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql-asset"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"serde",
@@ -16962,7 +16973,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16980,7 +16991,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts-asset"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
@@ -16996,7 +17007,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wac"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -17012,7 +17023,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"serde",
@@ -17023,7 +17034,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17060,7 +17071,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"const_format",
@@ -17098,7 +17109,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
@@ -17109,7 +17120,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17138,7 +17149,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -17161,7 +17172,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17194,7 +17205,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17214,7 +17225,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17248,7 +17259,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17283,7 +17294,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17306,7 +17317,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17330,7 +17341,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-nats",
@@ -17354,7 +17365,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17389,7 +17400,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17417,7 +17428,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17440,7 +17451,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
@@ -17458,7 +17469,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"anyhow",
"async-once-cell",
@@ -17565,7 +17576,7 @@ dependencies = [
[[package]]
name = "windmill-worker-volumes"
version = "1.659.1"
version = "1.662.0"
dependencies = [
"bytes",
"futures",
@@ -18454,18 +18465,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.42"
version = "0.8.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3"
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.42"
version = "0.8.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.659.1"
version = "1.662.0"
authors.workspace = true
edition.workspace = true
@@ -82,7 +82,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.659.1"
version = "1.662.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -260,6 +260,15 @@ windmill-dep-map.workspace = true
windmill-test-utils.workspace = true
windmill-worker-volumes.workspace = true
windmill-types.workspace = true
windmill-trigger.workspace = true
windmill-trigger-websocket.workspace = true
windmill-trigger-postgres.workspace = true
windmill-trigger-mqtt.workspace = true
windmill-trigger-kafka.workspace = true
windmill-trigger-nats.workspace = true
windmill-trigger-sqs.workspace = true
windmill-trigger-gcp.workspace = true
windmill-api-auth.workspace = true
axum.workspace = true
serde.workspace = true
windmill-api-client.workspace = true

View File

@@ -1 +1 @@
878cc2044717e0177228529a50433fe2768e70b5
563877bf1c8b4184f638bab51be89b1c0aec6dad

View File

@@ -0,0 +1,30 @@
-- Revert: re-add email columns to trigger tables, drop permissioned_as
ALTER TABLE http_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE http_trigger DROP COLUMN permissioned_as;
ALTER TABLE websocket_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE websocket_trigger DROP COLUMN permissioned_as;
ALTER TABLE postgres_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE postgres_trigger DROP COLUMN permissioned_as;
ALTER TABLE mqtt_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE mqtt_trigger DROP COLUMN permissioned_as;
ALTER TABLE kafka_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE kafka_trigger DROP COLUMN permissioned_as;
ALTER TABLE nats_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE nats_trigger DROP COLUMN permissioned_as;
ALTER TABLE sqs_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE sqs_trigger DROP COLUMN permissioned_as;
ALTER TABLE gcp_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE gcp_trigger DROP COLUMN permissioned_as;
ALTER TABLE email_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE email_trigger DROP COLUMN permissioned_as;
ALTER TABLE schedule DROP COLUMN permissioned_as;

View File

@@ -0,0 +1,64 @@
-- Add permissioned_as column to all trigger tables and schedule
-- permissioned_as stores 'u/{username}', 'g/{group}', or raw email
-- We add nullable first, populate, then set NOT NULL to avoid a DEFAULT '' that could mask bugs.
-- Trigger tables: add permissioned_as, drop email
ALTER TABLE http_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE http_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE http_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE http_trigger DROP COLUMN email;
ALTER TABLE websocket_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE websocket_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE websocket_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE websocket_trigger DROP COLUMN email;
ALTER TABLE postgres_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE postgres_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE postgres_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE postgres_trigger DROP COLUMN email;
ALTER TABLE mqtt_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE mqtt_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE mqtt_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE mqtt_trigger DROP COLUMN email;
ALTER TABLE kafka_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE kafka_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE kafka_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE kafka_trigger DROP COLUMN email;
ALTER TABLE nats_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE nats_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE nats_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE nats_trigger DROP COLUMN email;
ALTER TABLE sqs_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE sqs_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE sqs_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE sqs_trigger DROP COLUMN email;
ALTER TABLE gcp_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE gcp_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE gcp_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE gcp_trigger DROP COLUMN email;
ALTER TABLE email_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE email_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE email_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE email_trigger DROP COLUMN email;
-- Schedule table: add permissioned_as, keep email for backwards compat with old workers
-- For superadmin-owned schedules, use the email to find the actual username (since edited_by
-- may have been overwritten by a later edit). Otherwise use edited_by as the source.
ALTER TABLE schedule ADD COLUMN permissioned_as VARCHAR(255);
UPDATE schedule SET permissioned_as = CASE
WHEN EXISTS (
SELECT 1 FROM password p WHERE p.email = schedule.email AND p.super_admin = true
) THEN COALESCE(
'u/' || (SELECT u.username FROM usr u WHERE u.email = schedule.email AND u.workspace_id = schedule.workspace_id LIMIT 1),
schedule.email
)
ELSE CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END
END;
ALTER TABLE schedule ALTER COLUMN permissioned_as SET NOT NULL;

View File

@@ -1035,7 +1035,10 @@ Windmill Community Edition {GIT_VERSION}
}
if server_mode || worker_mode || indexer_mode || mcp_mode {
let port_var = std::env::var("PORT").ok().and_then(|x| x.parse().ok());
let port_var = std::env::var("PORT")
.or_else(|_| std::env::var("BACKEND_PORT"))
.ok()
.and_then(|x| x.parse().ok());
let port = if server_mode || indexer_mode || mcp_mode {
port_var.unwrap_or(DEFAULT_PORT as u16)
@@ -1082,6 +1085,10 @@ Windmill Community Edition {GIT_VERSION}
crate::monitor::monitor_pool(&db).await;
}
if let Some(db) = conn.as_sql() {
crate::monitor::monitor_pool_otel(&db).await;
}
send_logs_to_object_store(&conn, &hostname, &mode);
#[cfg(all(not(target_env = "msvc"), feature = "jemalloc"))]

View File

@@ -39,6 +39,10 @@ use windmill_common::ee_oss::{jobs_waiting_alerts, worker_groups_alerts};
#[cfg(feature = "oauth2")]
use windmill_common::global_settings::OAUTH_SETTING;
use windmill_common::otel_oss::{
otel_incr_zombie_delete_count, otel_incr_zombie_restart_count, otel_set_db_pool,
otel_set_queue_count, otel_set_queue_running_count,
};
use windmill_common::{
agent_workers::DECODED_AGENT_TOKEN,
apps::APP_WORKSPACED_ROUTE,
@@ -1960,28 +1964,39 @@ pub async fn monitor_pool(db: &DB) {
if METRICS_ENABLED.load(Ordering::Relaxed) {
let db = db.clone();
tokio::spawn(async move {
let active_pool_connections: prometheus::IntGauge = prometheus::register_int_gauge!(
let active_gauge = prometheus::register_int_gauge!(
"pool_connections_active",
"Number of active postgresql connections in the pool"
)
.unwrap();
let idle_pool_connections: prometheus::IntGauge = prometheus::register_int_gauge!(
let idle_gauge = prometheus::register_int_gauge!(
"pool_connections_idle",
"Number of idle postgresql connections in the pool"
)
.unwrap();
let max_pool_connections: prometheus::IntGauge = prometheus::register_int_gauge!(
let max_gauge = prometheus::register_int_gauge!(
"pool_connections_max",
"Number of max postgresql connections in the pool"
)
.unwrap();
max_pool_connections.set(db.options().get_max_connections() as i64);
max_gauge.set(db.options().get_max_connections() as i64);
loop {
active_pool_connections.set(db.size() as i64);
idle_pool_connections.set(db.num_idle() as i64);
active_gauge.set(db.size() as i64);
idle_gauge.set(db.num_idle() as i64);
tokio::time::sleep(Duration::from_secs(30)).await;
}
});
}
}
pub async fn monitor_pool_otel(db: &DB) {
if OTEL_METRICS_ENABLED.load(Ordering::Relaxed) {
let db = db.clone();
tokio::spawn(async move {
let max = db.options().get_max_connections() as i64;
loop {
otel_set_db_pool(db.size() as i64, db.num_idle() as i64, max);
tokio::time::sleep(Duration::from_secs(30)).await;
}
});
@@ -2320,7 +2335,7 @@ pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
.map(|last_check| chrono::Utc::now() - last_check > chrono::Duration::seconds(25))
.unwrap_or(true);
if metrics_enabled || save_metrics {
if metrics_enabled || save_metrics || OTEL_METRICS_ENABLED.load(Ordering::Relaxed) {
let queue_counts = windmill_common::queue::get_queue_counts(db).await;
#[cfg(feature = "prometheus")]
@@ -2345,6 +2360,8 @@ pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
tags_to_watch.push(tag.to_string());
}
otel_set_queue_count(&tag, count as i64);
// save queue_count and delay metrics per tag
if save_metrics {
sqlx::query!(
@@ -2378,28 +2395,45 @@ pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
*w = tags_to_watch;
}
// Single DB query for running counts, shared by Prometheus and OTel
let otel_running = OTEL_METRICS_ENABLED.load(Ordering::Relaxed);
#[cfg(feature = "prometheus")]
if metrics_enabled {
// Handle queue running count metrics
let need_running_counts = metrics_enabled || otel_running;
#[cfg(not(feature = "prometheus"))]
let need_running_counts = otel_running;
if need_running_counts {
let queue_running_counts = windmill_common::queue::get_queue_running_counts(db).await;
for q in QUEUE_RUNNING_COUNT_TAGS.read().await.iter() {
if queue_running_counts.get(q).is_none() {
(*QUEUE_RUNNING_COUNT).with_label_values(&[q]).set(0);
#[cfg(feature = "prometheus")]
if metrics_enabled {
for q in QUEUE_RUNNING_COUNT_TAGS.read().await.iter() {
if queue_running_counts.get(q).is_none() {
(*QUEUE_RUNNING_COUNT).with_label_values(&[q]).set(0);
}
}
}
let mut running_tags_to_watch = vec![];
for q in queue_running_counts {
let count = q.1;
let tag = q.0;
#[allow(unused_mut, unused_variables)]
let mut running_tags_to_watch: Vec<String> = vec![];
for (tag, count) in &queue_running_counts {
#[cfg(feature = "prometheus")]
if metrics_enabled {
let metric = (*QUEUE_RUNNING_COUNT).with_label_values(&[tag]);
metric.set(*count as i64);
running_tags_to_watch.push(tag.to_string());
}
let metric = (*QUEUE_RUNNING_COUNT).with_label_values(&[&tag]);
metric.set(count as i64);
running_tags_to_watch.push(tag.to_string());
if otel_running {
otel_set_queue_running_count(tag, *count as i64);
}
}
#[cfg(feature = "prometheus")]
if metrics_enabled {
let mut w = QUEUE_RUNNING_COUNT_TAGS.write().await;
*w = running_tags_to_watch;
}
let mut w = QUEUE_RUNNING_COUNT_TAGS.write().await;
*w = running_tags_to_watch;
}
}
@@ -2693,6 +2727,8 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, node_n
QUEUE_ZOMBIE_RESTART_COUNT.inc_by(restarted.len() as _);
}
otel_incr_zombie_restart_count(restarted.len() as u64);
let base_url = BASE_URL.read().await.clone();
for r in restarted {
let last_ping = if let Some(x) = r.ping {
@@ -2861,6 +2897,8 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, node_n
QUEUE_ZOMBIE_DELETE_COUNT.inc_by(timeouts.len() as _);
}
otel_incr_zombie_delete_count(timeouts.len() as u64);
for (job_id, error_kind) in timeouts {
// since the job is unrecoverable, the same worker queue should never be sent anything
let (same_worker_tx_never_used, _same_worker_rx_never_used) =

View File

@@ -204,14 +204,14 @@ VALUES ('test-workspace', 'u/bob/my_variable', 'bob_value', false,
-- ============================================
-- Alice's personal schedule
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms)
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms, permissioned_as)
VALUES ('test-workspace', 'u/alice/my_schedule', 'alice', NOW(), '0 * * * *', false,
'u/alice/my_script', '{}', false, 'alice@windmill.dev', 'UTC', '{}');
'u/alice/my_script', '{}', false, 'alice@windmill.dev', 'UTC', '{}', 'u/alice');
-- Bob's personal schedule
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms)
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms, permissioned_as)
VALUES ('test-workspace', 'u/bob/my_schedule', 'bob', NOW(), '0 * * * *', false,
'u/bob/my_script', '{}', false, 'bob@windmill.dev', 'UTC', '{}');
'u/bob/my_script', '{}', false, 'bob@windmill.dev', 'UTC', '{}', 'u/bob');
-- ============================================
-- APPS (with app_versions)

View File

@@ -65,6 +65,11 @@ INSERT INTO usr_to_group(workspace_id, group_, usr) VALUES
('test-workspace', 'wm_deployers', 'deployer-user')
ON CONFLICT DO NOTHING;
-- Superadmin NOT in workspace (only in password table, not in usr)
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name, username)
VALUES ('superadmin-external@windmill.dev', 'not-a-real-hash', 'password', true, true, 'External Superadmin', 'superadmin-external')
ON CONFLICT DO NOTHING;
-- Tokens for all users (token_hash = sha256 hex, token_prefix = first 10 chars)
-- NOTE: plaintext `token` column is included for backward compat during transition.
-- Remove it once the `token` column is dropped from the schema.
@@ -80,6 +85,9 @@ ON CONFLICT DO NOTHING;
INSERT INTO token(token_hash, token_prefix, token, email, label, super_admin)
VALUES (encode(sha256('ORIGINAL_TOKEN'::bytea), 'hex'), 'ORIGINAL_T', 'ORIGINAL_TOKEN', 'original@windmill.dev', 'original token', false)
ON CONFLICT DO NOTHING;
INSERT INTO token(token_hash, token_prefix, token, email, label, super_admin)
VALUES (encode(sha256('EXTERNAL_SUPERADMIN_TOKEN'::bytea), 'hex'), 'EXTERNAL_S', 'EXTERNAL_SUPERADMIN_TOKEN', 'superadmin-external@windmill.dev', 'external superadmin token', true)
ON CONFLICT DO NOTHING;
GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_admin;
GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_user;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,385 @@
//! Tests that call trigger trait methods directly to verify
//! all dynamic SQL correctly references the permissioned_as column.
use serde_json::json;
use sqlx::{Pool, Postgres};
use windmill_common::DB;
use windmill_trigger::handler::TriggerCrud;
use windmill_trigger::listener::Listener;
use windmill_trigger::types::TriggerMode;
/// Helper to insert a minimal trigger row.
async fn insert_trigger(db: &DB, table: &str, path: &str, extra_cols: &str, extra_vals: &str) {
let sql = format!(
"INSERT INTO {} (path, script_path, is_flow, workspace_id, edited_by, permissioned_as{}) \
VALUES ($1, 'f/test/handler', false, 'test-workspace', 'test-user', 'u/test-user'{})",
table, extra_cols, extra_vals
);
sqlx::query(&sql).bind(path).execute(db).await.unwrap();
}
#[cfg(feature = "websocket")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_listener_query_websocket(db: Pool<Postgres>) -> anyhow::Result<()> {
insert_trigger(
&db,
"websocket_trigger",
"f/test/listener_ws",
", url",
", 'wss://example.com'",
)
.await;
let triggers = windmill_trigger_websocket::WebsocketTrigger
.fetch_enabled_unlistened_triggers(&db)
.await?;
assert!(triggers.iter().any(|t| t.path == "f/test/listener_ws"));
Ok(())
}
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_listener_query_postgres(db: Pool<Postgres>) -> anyhow::Result<()> {
insert_trigger(
&db,
"postgres_trigger",
"f/test/listener_pg",
", postgres_resource_path, replication_slot_name, publication_name",
", 'u/test/pg', 'slot', 'pub'",
)
.await;
let triggers = windmill_trigger_postgres::PostgresTrigger
.fetch_enabled_unlistened_triggers(&db)
.await?;
assert!(triggers.iter().any(|t| t.path == "f/test/listener_pg"));
Ok(())
}
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_listener_query_mqtt(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO mqtt_trigger (path, mqtt_resource_path, subscribe_topics, client_version, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, ARRAY[$3::jsonb], $4::mqtt_client_version, $5, false, 'test-workspace', 'test-user', 'u/test-user')"
)
.bind("f/test/listener_mqtt").bind("u/test/mqtt")
.bind(json!({"topic": "t", "qos": "qos0"})).bind("v5").bind("f/test/handler")
.execute(&db).await?;
let triggers = windmill_trigger_mqtt::MqttTrigger
.fetch_enabled_unlistened_triggers(&db)
.await?;
assert!(triggers.iter().any(|t| t.path == "f/test/listener_mqtt"));
Ok(())
}
#[cfg(feature = "private")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_listener_query_kafka(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
"INSERT INTO kafka_trigger (path, kafka_resource_path, group_id, topics, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, $3, $4, $5, false, 'test-workspace', 'test-user', 'u/test-user')",
"f/test/listener_kafka",
"u/test/kafka",
"grp",
&["topic"] as &[&str],
"f/test/handler"
)
.execute(&db)
.await?;
let triggers = windmill_trigger_kafka::KafkaTrigger
.fetch_enabled_unlistened_triggers(&db)
.await?;
assert!(triggers.iter().any(|t| t.path == "f/test/listener_kafka"));
Ok(())
}
#[cfg(feature = "private")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_listener_query_nats(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
"INSERT INTO nats_trigger (path, nats_resource_path, subjects, use_jetstream, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, $3, $4, $5, false, 'test-workspace', 'test-user', 'u/test-user')",
"f/test/listener_nats",
"u/test/nats",
&["subj"] as &[&str],
false,
"f/test/handler"
)
.execute(&db)
.await?;
let triggers = windmill_trigger_nats::NatsTrigger
.fetch_enabled_unlistened_triggers(&db)
.await?;
assert!(triggers.iter().any(|t| t.path == "f/test/listener_nats"));
Ok(())
}
#[cfg(feature = "private")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_listener_query_sqs(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
"INSERT INTO sqs_trigger (path, queue_url, aws_resource_path, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, $3, $4, false, 'test-workspace', 'test-user', 'u/test-user')",
"f/test/listener_sqs",
"https://sqs.example.com/q",
"u/test/aws",
"f/test/handler"
)
.execute(&db)
.await?;
let triggers = windmill_trigger_sqs::SqsTrigger
.fetch_enabled_unlistened_triggers(&db)
.await?;
assert!(triggers.iter().any(|t| t.path == "f/test/listener_sqs"));
Ok(())
}
#[cfg(feature = "private")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_listener_query_gcp(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO gcp_trigger (path, gcp_resource_path, topic_id, subscription_id, \
delivery_type, subscription_mode, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, $3, $4, $5::delivery_mode, $6::gcp_subscription_mode, $7, false, 'test-workspace', 'test-user', 'u/test-user')"
)
.bind("f/test/listener_gcp").bind("u/test/gcp").bind("topic").bind("sub")
.bind("pull").bind("existing").bind("f/test/handler")
.execute(&db).await?;
let triggers = windmill_trigger_gcp::GcpTrigger
.fetch_enabled_unlistened_triggers(&db)
.await?;
assert!(triggers.iter().any(|t| t.path == "f/test/listener_gcp"));
Ok(())
}
// ============================================================================
// Handler trait method tests (get_trigger_by_path, list_triggers, set_trigger_mode)
// ============================================================================
fn make_authed() -> windmill_api_auth::ApiAuthed {
windmill_api_auth::ApiAuthed {
email: "test@windmill.dev".to_string(),
username: "test-user".to_string(),
is_admin: true,
is_operator: false,
groups: vec![],
folders: vec![],
scopes: None,
username_override: None,
token_prefix: None,
}
}
/// Tests get_trigger_by_path, list_triggers, set_trigger_mode for websocket (server_state=true).
#[cfg(feature = "websocket")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_handler_queries_websocket(db: Pool<Postgres>) -> anyhow::Result<()> {
insert_trigger(
&db,
"websocket_trigger",
"f/test/handler_ws",
", url",
", 'wss://example.com'",
)
.await;
let handler = windmill_trigger_websocket::WebsocketTrigger;
let mut conn = db.acquire().await?;
let trigger = handler
.get_trigger_by_path(&mut *conn, "test-workspace", "f/test/handler_ws")
.await?;
assert_eq!(trigger.base.permissioned_as, "u/test-user");
let triggers = handler
.list_triggers(&mut *conn, "test-workspace", None)
.await?;
assert!(triggers.iter().any(|t| t.base.path == "f/test/handler_ws"));
let authed = make_authed();
let updated = handler
.set_trigger_mode(
&authed,
&mut *conn,
"test-workspace",
"f/test/handler_ws",
&TriggerMode::Disabled,
)
.await?;
assert!(updated);
Ok(())
}
/// Tests get_trigger_by_path, list_triggers, set_trigger_mode for mqtt (server_state=true).
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_handler_queries_mqtt(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO mqtt_trigger (path, mqtt_resource_path, subscribe_topics, client_version, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, ARRAY[$3::jsonb], $4::mqtt_client_version, $5, false, 'test-workspace', 'test-user', 'u/test-user')",
)
.bind("f/test/handler_mqtt").bind("u/test/mqtt")
.bind(json!({"topic": "t", "qos": "qos0"})).bind("v5").bind("f/test/handler")
.execute(&db).await?;
let handler = windmill_trigger_mqtt::MqttTrigger;
let mut conn = db.acquire().await?;
let trigger = handler
.get_trigger_by_path(&mut *conn, "test-workspace", "f/test/handler_mqtt")
.await?;
assert_eq!(trigger.base.permissioned_as, "u/test-user");
let authed = make_authed();
let updated = handler
.set_trigger_mode(
&authed,
&mut *conn,
"test-workspace",
"f/test/handler_mqtt",
&TriggerMode::Disabled,
)
.await?;
assert!(updated);
Ok(())
}
/// Tests handler queries for kafka trigger (EE only — OSS stub returns ()).
#[cfg(feature = "private")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_handler_queries_kafka(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
"INSERT INTO kafka_trigger (path, kafka_resource_path, group_id, topics, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, $3, $4, $5, false, 'test-workspace', 'test-user', 'u/test-user')",
"f/test/handler_kafka",
"u/test/kafka",
"grp",
&["topic"] as &[&str],
"f/test/handler"
)
.execute(&db)
.await?;
let handler = windmill_trigger_kafka::KafkaTrigger;
let mut conn = db.acquire().await?;
let trigger = handler
.get_trigger_by_path(&mut *conn, "test-workspace", "f/test/handler_kafka")
.await?;
assert_eq!(trigger.base.permissioned_as, "u/test-user");
Ok(())
}
/// Tests handler queries for postgres trigger.
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_handler_queries_postgres(db: Pool<Postgres>) -> anyhow::Result<()> {
insert_trigger(
&db,
"postgres_trigger",
"f/test/handler_pg",
", postgres_resource_path, replication_slot_name, publication_name",
", 'u/test/pg', 'slot', 'pub'",
)
.await;
let handler = windmill_trigger_postgres::PostgresTrigger;
let mut conn = db.acquire().await?;
let trigger = handler
.get_trigger_by_path(&mut *conn, "test-workspace", "f/test/handler_pg")
.await?;
assert_eq!(trigger.base.permissioned_as, "u/test-user");
Ok(())
}
/// Tests handler queries for nats trigger (EE only).
#[cfg(feature = "private")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_handler_queries_nats(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
"INSERT INTO nats_trigger (path, nats_resource_path, subjects, use_jetstream, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, $3, $4, $5, false, 'test-workspace', 'test-user', 'u/test-user')",
"f/test/handler_nats",
"u/test/nats",
&["subj"] as &[&str],
false,
"f/test/handler"
)
.execute(&db)
.await?;
let handler = windmill_trigger_nats::NatsTrigger;
let mut conn = db.acquire().await?;
let trigger = handler
.get_trigger_by_path(&mut *conn, "test-workspace", "f/test/handler_nats")
.await?;
assert_eq!(trigger.base.permissioned_as, "u/test-user");
Ok(())
}
/// Tests handler queries for sqs trigger (EE only).
#[cfg(feature = "private")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_handler_queries_sqs(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
"INSERT INTO sqs_trigger (path, queue_url, aws_resource_path, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, $3, $4, false, 'test-workspace', 'test-user', 'u/test-user')",
"f/test/handler_sqs",
"https://sqs.example.com/q",
"u/test/aws",
"f/test/handler"
)
.execute(&db)
.await?;
let handler = windmill_trigger_sqs::SqsTrigger;
let mut conn = db.acquire().await?;
let trigger = handler
.get_trigger_by_path(&mut *conn, "test-workspace", "f/test/handler_sqs")
.await?;
assert_eq!(trigger.base.permissioned_as, "u/test-user");
Ok(())
}
/// Tests handler queries for gcp trigger (EE only).
#[cfg(feature = "private")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_handler_queries_gcp(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO gcp_trigger (path, gcp_resource_path, topic_id, subscription_id, \
delivery_type, subscription_mode, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, $3, $4, $5::delivery_mode, $6::gcp_subscription_mode, $7, false, 'test-workspace', 'test-user', 'u/test-user')",
)
.bind("f/test/handler_gcp").bind("u/test/gcp").bind("topic").bind("sub")
.bind("pull").bind("existing").bind("f/test/handler")
.execute(&db).await?;
let handler = windmill_trigger_gcp::GcpTrigger;
let mut conn = db.acquire().await?;
let trigger = handler
.get_trigger_by_path(&mut *conn, "test-workspace", "f/test/handler_gcp")
.await?;
assert_eq!(trigger.base.permissioned_as, "u/test-user");
Ok(())
}

View File

@@ -571,16 +571,16 @@ async fn test_volume_sql_worker_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
std::fs::write(vol_dir.join("hello.txt"), b"hello from volume")?;
// 3. Push the job and run with SQL-connected worker
let code = r#"// volume: test-vol /tmp/data
let code = r#"// volume: test-vol data
import { readFileSync, writeFileSync, existsSync } from "fs";
export function main() {
const content = readFileSync("/tmp/data/hello.txt", "utf-8");
writeFileSync("/tmp/data/output.txt", "written by sql worker");
const content = readFileSync("data/hello.txt", "utf-8");
writeFileSync("data/output.txt", "written by sql worker");
return {
read_content: content,
output_exists: existsSync("/tmp/data/output.txt"),
output_exists: existsSync("data/output.txt"),
};
}"#;

View File

@@ -280,9 +280,7 @@ impl AuthCache {
folders,
scopes: None,
username_override,
token_prefix: Some(
safe_token_prefix(token),
),
token_prefix: Some(safe_token_prefix(token)),
})
} else {
let groups = vec![name.to_string()];
@@ -297,16 +295,17 @@ impl AuthCache {
.unwrap_or_default();
Some(ApiAuthed {
email: email,
username: format!("group-{name}"),
username: format!(
"{}{name}",
windmill_common::users::USERNAME_GROUP_PREFIX
),
is_admin: false,
groups,
is_operator: false,
folders,
scopes: None,
username_override,
token_prefix: Some(
safe_token_prefix(token),
),
token_prefix: Some(safe_token_prefix(token)),
})
}
} else {
@@ -369,9 +368,7 @@ impl AuthCache {
folders,
scopes,
username_override,
token_prefix: Some(
safe_token_prefix(token),
),
token_prefix: Some(safe_token_prefix(token)),
})
}
None if super_admin => Some(ApiAuthed {
@@ -383,9 +380,7 @@ impl AuthCache {
folders: vec![],
scopes,
username_override,
token_prefix: Some(
safe_token_prefix(token),
),
token_prefix: Some(safe_token_prefix(token)),
}),
None => None,
}
@@ -660,7 +655,12 @@ pub async fn resolve_opt_job_authed(
.map(|x| x.0)
.unwrap_or_default();
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = maybe_get_workspace_id_from_path(&path_vec);
let workspace_id = maybe_get_workspace_id_from_path(&path_vec).or_else(|| {
parts
.extensions
.get::<windmill_common::db::GatewayWorkspaceId>()
.map(|g| g.0.clone())
});
if let Some(mut opt_job_authed) =
cache.get_opt_job_authed(workspace_id.clone(), &token).await

View File

@@ -452,7 +452,7 @@ async fn create_flow(
.await?;
if nb_flows.unwrap_or(0) >= 1000 {
return Err(Error::BadRequest(
"You have reached the maximum number of flows (1000) on cloud. Contact support@windmill.dev to increase the limit"
"You have reached the maximum number of flows (1000) on cloud. Check your usage in Workspace Settings > General > Cloud Quotas. Contact support@windmill.dev to increase the limit"
.to_string(),
));
}

View File

@@ -215,12 +215,12 @@ pub async fn require_is_owner(
}
async fn _check_nb_of_groups(db: &DB) -> Result<()> {
let nb_groups = sqlx::query_scalar!("SELECT COUNT(*) FROM group_ WHERE name != 'all' AND name != 'error_handler' AND name != 'slack'",)
let nb_groups = sqlx::query_scalar!("SELECT COUNT(*) FROM group_ WHERE name != 'all' AND name != 'error_handler' AND name != 'slack' AND name != 'wm_deployers'",)
.fetch_one(db)
.await?;
if nb_groups.unwrap_or(0) >= 3 {
return Err(Error::BadRequest(
"You have reached the maximum number of groups (3 outside of native groups 'all', 'slack' and 'error_handler') without an enterprise license"
"You have reached the maximum number of groups (3 outside of native groups 'all', 'slack', 'error_handler' and 'wm_deployers') without an enterprise license"
.to_string(),
));
}

View File

@@ -104,6 +104,8 @@ pub struct Input {
created_by: String,
is_public: bool,
success: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
is_preview: bool,
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
@@ -113,6 +115,7 @@ pub struct CompletedJobMini {
args: Option<sqlx::types::Json<Box<serde_json::value::RawValue>>>,
created_by: String,
success: bool,
is_preview: bool,
}
#[derive(Deserialize)]
@@ -153,9 +156,10 @@ async fn get_input_history(
let inner_limit = 2 * (per_page + offset);
let sql = &format!(
"SELECT id, completed_at, created_by, args, success FROM (\
"SELECT id, completed_at, created_by, args, success, is_preview FROM (\
SELECT id, v2_job_completed.completed_at, created_by, 'null'::jsonb as args, \
status = 'success' as success \
status = 'success' as success, \
kind IN ('preview', 'flowpreview') as is_preview \
FROM v2_job JOIN v2_job_completed USING (id) \
WHERE v2_job.workspace_id = $3 AND {} = $1 AND kind = any($2) \
{args_query} AND v2_job_completed.status != 'skipped' {include_non_root} \
@@ -209,6 +213,7 @@ async fn get_input_history(
created_by: row.created_by,
is_public: true,
success: row.success,
is_preview: row.is_preview,
});
}
@@ -306,6 +311,7 @@ async fn list_saved_inputs(
created_at: row.created_at,
is_public: row.is_public,
success: true,
is_preview: false,
})
}

View File

@@ -204,14 +204,14 @@ VALUES ('test-workspace', 'u/bob/my_variable', 'bob_value', false,
-- ============================================
-- Alice's personal schedule
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms)
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms, permissioned_as)
VALUES ('test-workspace', 'u/alice/my_schedule', 'alice', NOW(), '0 * * * *', false,
'u/alice/my_script', '{}', false, 'alice@windmill.dev', 'UTC', '{}');
'u/alice/my_script', '{}', false, 'alice@windmill.dev', 'UTC', '{}', 'u/alice');
-- Bob's personal schedule
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms)
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms, permissioned_as)
VALUES ('test-workspace', 'u/bob/my_schedule', 'bob', NOW(), '0 * * * *', false,
'u/bob/my_script', '{}', false, 'bob@windmill.dev', 'UTC', '{}');
'u/bob/my_script', '{}', false, 'bob@windmill.dev', 'UTC', '{}', 'u/bob');
-- ============================================
-- APPS (with app_versions)

View File

@@ -1,6 +1,9 @@
#[cfg(feature = "run_inline")]
use serde_json::json;
#[cfg(feature = "run_inline")]
use sqlx::{Pool, Postgres};
#[cfg(feature = "run_inline")]
use windmill_test_utils::*;
#[cfg(feature = "run_inline")]
@@ -20,18 +23,22 @@ async fn init_inline_utils(port: u16) -> anyhow::Result<()> {
Ok(())
}
#[cfg(feature = "run_inline")]
fn run_inline_url(port: u16, endpoint: &str) -> String {
format!("http://localhost:{port}/api/w/test-workspace/jobs/run_inline/{endpoint}")
}
#[cfg(feature = "run_inline")]
fn client() -> reqwest::Client {
reqwest::Client::new()
}
#[cfg(feature = "run_inline")]
fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
builder.header("Authorization", "Bearer SECRET_TOKEN")
}
#[cfg(feature = "run_inline")]
fn new_script(
path: &str,
summary: &str,

View File

@@ -158,7 +158,7 @@ async fn test_mqtt_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO mqtt_trigger (
path, mqtt_resource_path, subscribe_topics, client_version,
script_path, is_flow, workspace_id, edited_by, email
script_path, is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, ARRAY[$3::jsonb], $4::mqtt_client_version, $5, $6, $7, $8, $9)
"#,
@@ -171,7 +171,7 @@ async fn test_mqtt_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await?;
@@ -240,7 +240,7 @@ async fn test_websocket_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO websocket_trigger (
path, url, script_path, is_flow, workspace_id,
edited_by, email, initial_messages
edited_by, permissioned_as, initial_messages
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
@@ -250,7 +250,7 @@ async fn test_websocket_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
&[json!({"type": "RawMessage", "content": "hello from e2e test"})]
as &[serde_json::Value],
)
@@ -338,7 +338,7 @@ async fn test_postgres_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
r#"
INSERT INTO postgres_trigger (
path, script_path, is_flow, workspace_id, edited_by, email,
path, script_path, is_flow, workspace_id, edited_by, permissioned_as,
postgres_resource_path, replication_slot_name, publication_name
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
@@ -349,7 +349,7 @@ async fn test_postgres_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.bind("u/test-user/pg_res")
.bind(&slot_name)
.bind(&pub_name)
@@ -410,7 +410,7 @@ async fn test_kafka_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO kafka_trigger (
path, kafka_resource_path, topics, group_id,
script_path, is_flow, workspace_id, edited_by, email
script_path, is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
"#,
@@ -422,7 +422,7 @@ async fn test_kafka_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
)
.execute(&db)
.await?;
@@ -502,7 +502,7 @@ async fn test_nats_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO nats_trigger (
path, nats_resource_path, subjects, script_path,
is_flow, workspace_id, edited_by, email, use_jetstream
is_flow, workspace_id, edited_by, permissioned_as, use_jetstream
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
"#,
@@ -513,7 +513,7 @@ async fn test_nats_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
false,
)
.execute(&db)
@@ -580,7 +580,7 @@ async fn test_sqs_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO sqs_trigger (
path, queue_url, aws_resource_path, script_path,
is_flow, workspace_id, edited_by, email
is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
@@ -591,7 +591,7 @@ async fn test_sqs_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
)
.execute(&db)
.await?;
@@ -675,7 +675,7 @@ async fn test_gcp_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
INSERT INTO gcp_trigger (
path, gcp_resource_path, topic_id, subscription_id,
delivery_type, subscription_mode, script_path, is_flow,
workspace_id, edited_by, email
workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5::delivery_mode, $6::gcp_subscription_mode, $7, $8, $9, $10, $11)
"#,
@@ -690,7 +690,7 @@ async fn test_gcp_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await?;

View File

@@ -557,7 +557,7 @@ async fn test_http_trigger_insert_and_query(db: Pool<Postgres>) -> anyhow::Resul
r#"
INSERT INTO http_trigger (
path, route_path, route_path_key, script_path, is_flow,
workspace_id, edited_by, email, http_method,
workspace_id, edited_by, permissioned_as, http_method,
authentication_method, is_static_website, workspaced_route,
wrap_body, raw_string
)
@@ -571,7 +571,7 @@ async fn test_http_trigger_insert_and_query(db: Pool<Postgres>) -> anyhow::Resul
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
"post" as _,
"none" as _,
false,
@@ -615,7 +615,7 @@ async fn test_http_trigger_multiple_methods(db: Pool<Postgres>) -> anyhow::Resul
r#"
INSERT INTO http_trigger (
path, route_path, route_path_key, script_path, is_flow,
workspace_id, edited_by, email, http_method,
workspace_id, edited_by, permissioned_as, http_method,
authentication_method, is_static_website, workspaced_route,
wrap_body, raw_string
)
@@ -629,7 +629,7 @@ async fn test_http_trigger_multiple_methods(db: Pool<Postgres>) -> anyhow::Resul
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
*method as _,
"none" as _,
false,
@@ -662,7 +662,7 @@ async fn test_http_trigger_authentication_methods(db: Pool<Postgres>) -> anyhow:
r#"
INSERT INTO http_trigger (
path, route_path, route_path_key, script_path, is_flow,
workspace_id, edited_by, email, http_method,
workspace_id, edited_by, permissioned_as, http_method,
authentication_method, is_static_website, workspaced_route,
wrap_body, raw_string
)
@@ -676,7 +676,7 @@ async fn test_http_trigger_authentication_methods(db: Pool<Postgres>) -> anyhow:
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
"get" as _,
*auth as _,
false,
@@ -710,7 +710,7 @@ async fn test_http_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO http_trigger (
path, route_path, route_path_key, script_path, is_flow,
workspace_id, edited_by, email, http_method,
workspace_id, edited_by, permissioned_as, http_method,
authentication_method, is_static_website, workspaced_route,
wrap_body, raw_string
)
@@ -724,7 +724,7 @@ async fn test_http_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
"get" as _,
"none" as _,
false,
@@ -763,7 +763,7 @@ async fn test_http_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO http_trigger (
path, route_path, route_path_key, script_path, is_flow,
workspace_id, edited_by, email, http_method,
workspace_id, edited_by, permissioned_as, http_method,
authentication_method, is_static_website, workspaced_route,
wrap_body, raw_string
)
@@ -777,7 +777,7 @@ async fn test_http_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
"get" as _,
"none" as _,
false,
@@ -818,7 +818,7 @@ async fn test_http_trigger_mode_filtering(db: Pool<Postgres>) -> anyhow::Result<
r#"
INSERT INTO http_trigger (
path, route_path, route_path_key, script_path, is_flow,
workspace_id, edited_by, email, http_method,
workspace_id, edited_by, permissioned_as, http_method,
authentication_method, is_static_website, workspaced_route,
wrap_body, raw_string, mode
)
@@ -832,7 +832,7 @@ async fn test_http_trigger_mode_filtering(db: Pool<Postgres>) -> anyhow::Result<
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
"get" as _,
"none" as _,
false,
@@ -872,7 +872,7 @@ async fn test_websocket_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()>
r#"
INSERT INTO websocket_trigger (
path, url, script_path, is_flow, workspace_id,
edited_by, email
edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
"#,
@@ -882,7 +882,7 @@ async fn test_websocket_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()>
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
)
.execute(&db)
.await?;
@@ -911,7 +911,7 @@ async fn test_kafka_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO kafka_trigger (
path, kafka_resource_path, topics, group_id, script_path,
is_flow, workspace_id, edited_by, email
is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
"#,
@@ -923,7 +923,7 @@ async fn test_kafka_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
)
.execute(&db)
.await?;
@@ -958,7 +958,7 @@ async fn test_kafka_trigger_insert_auto_commit_disabled(db: Pool<Postgres>) -> a
r#"
INSERT INTO kafka_trigger (
path, kafka_resource_path, topics, group_id, script_path,
is_flow, workspace_id, edited_by, email, auto_commit
is_flow, workspace_id, edited_by, permissioned_as, auto_commit
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
"#,
@@ -970,7 +970,7 @@ async fn test_kafka_trigger_insert_auto_commit_disabled(db: Pool<Postgres>) -> a
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
false,
)
.execute(&db)
@@ -998,7 +998,7 @@ async fn test_postgres_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()>
sqlx::query!(
r#"
INSERT INTO postgres_trigger (
path, script_path, is_flow, workspace_id, edited_by, email,
path, script_path, is_flow, workspace_id, edited_by, permissioned_as,
postgres_resource_path, replication_slot_name, publication_name
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
@@ -1008,7 +1008,7 @@ async fn test_postgres_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()>
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
"u/admin/pg_resource",
"test_slot",
"test_publication",
@@ -1042,7 +1042,7 @@ async fn test_nats_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO nats_trigger (
path, nats_resource_path, subjects, script_path,
is_flow, workspace_id, edited_by, email, use_jetstream
is_flow, workspace_id, edited_by, permissioned_as, use_jetstream
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
"#,
@@ -1053,7 +1053,7 @@ async fn test_nats_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
false,
)
.execute(&db)
@@ -1085,7 +1085,7 @@ async fn test_sqs_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO sqs_trigger (
path, queue_url, aws_resource_path, script_path,
is_flow, workspace_id, edited_by, email
is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
@@ -1096,7 +1096,7 @@ async fn test_sqs_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
)
.execute(&db)
.await?;
@@ -1136,7 +1136,7 @@ async fn test_trigger_server_state_tracking(db: Pool<Postgres>) -> anyhow::Resul
r#"
INSERT INTO websocket_trigger (
path, url, script_path, is_flow, workspace_id,
edited_by, email, server_id, error
edited_by, permissioned_as, server_id, error
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
"#,
@@ -1146,7 +1146,7 @@ async fn test_trigger_server_state_tracking(db: Pool<Postgres>) -> anyhow::Resul
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
"server-abc-123",
"connection refused",
)
@@ -1194,7 +1194,7 @@ async fn test_trigger_mode_filtering(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO websocket_trigger (
path, url, script_path, is_flow, workspace_id,
edited_by, email, mode
edited_by, permissioned_as, mode
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::trigger_mode)
"#,
@@ -1204,7 +1204,7 @@ async fn test_trigger_mode_filtering(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
*mode as _,
)
.execute(&db)
@@ -1276,9 +1276,9 @@ async fn test_schedule_insert_and_query(db: Pool<Postgres>) -> anyhow::Result<()
r#"
INSERT INTO schedule (
workspace_id, path, edited_by, schedule, enabled,
script_path, is_flow, email, timezone
script_path, is_flow, email, timezone, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
"#,
"test-workspace",
"f/test/my_schedule",
@@ -1289,6 +1289,7 @@ async fn test_schedule_insert_and_query(db: Pool<Postgres>) -> anyhow::Result<()
false,
"test@windmill.dev",
"UTC",
"u/test-user",
)
.execute(&db)
.await?;
@@ -1324,7 +1325,7 @@ async fn test_mqtt_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO mqtt_trigger (
path, mqtt_resource_path, subscribe_topics, client_version,
script_path, is_flow, workspace_id, edited_by, email
script_path, is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, ARRAY[$3::jsonb], $4::mqtt_client_version, $5, $6, $7, $8, $9)
"#,
@@ -1337,7 +1338,7 @@ async fn test_mqtt_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await?;
@@ -1368,7 +1369,7 @@ async fn test_mqtt_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO mqtt_trigger (
path, mqtt_resource_path, subscribe_topics, client_version,
script_path, is_flow, workspace_id, edited_by, email
script_path, is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, ARRAY[$3::jsonb], $4::mqtt_client_version, $5, $6, $7, $8, $9)
"#,
@@ -1381,7 +1382,7 @@ async fn test_mqtt_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await?;
@@ -1413,7 +1414,7 @@ async fn test_mqtt_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO mqtt_trigger (
path, mqtt_resource_path, subscribe_topics, client_version,
script_path, is_flow, workspace_id, edited_by, email
script_path, is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, ARRAY[$3::jsonb], $4::mqtt_client_version, $5, $6, $7, $8, $9)
"#,
@@ -1426,7 +1427,7 @@ async fn test_mqtt_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await?;
@@ -1462,7 +1463,7 @@ async fn test_gcp_trigger_insert_pull(db: Pool<Postgres>) -> anyhow::Result<()>
INSERT INTO gcp_trigger (
path, gcp_resource_path, topic_id, subscription_id,
delivery_type, subscription_mode, script_path, is_flow,
workspace_id, edited_by, email
workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5::delivery_mode, $6::gcp_subscription_mode, $7, $8, $9, $10, $11)
"#,
@@ -1477,7 +1478,7 @@ async fn test_gcp_trigger_insert_pull(db: Pool<Postgres>) -> anyhow::Result<()>
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await?;
@@ -1513,7 +1514,7 @@ async fn test_gcp_trigger_insert_push(db: Pool<Postgres>) -> anyhow::Result<()>
INSERT INTO gcp_trigger (
path, gcp_resource_path, topic_id, subscription_id,
delivery_type, delivery_config, subscription_mode,
script_path, is_flow, workspace_id, edited_by, email
script_path, is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5::delivery_mode, $6::jsonb, $7::gcp_subscription_mode, $8, $9, $10, $11, $12)
"#,
@@ -1529,7 +1530,7 @@ async fn test_gcp_trigger_insert_push(db: Pool<Postgres>) -> anyhow::Result<()>
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await?;
@@ -1562,7 +1563,7 @@ async fn test_gcp_trigger_unique_constraint(db: Pool<Postgres>) -> anyhow::Resul
INSERT INTO gcp_trigger (
path, gcp_resource_path, topic_id, subscription_id,
delivery_type, subscription_mode, script_path, is_flow,
workspace_id, edited_by, email
workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5::delivery_mode, $6::gcp_subscription_mode, $7, $8, $9, $10, $11)
"#;
@@ -1578,7 +1579,7 @@ async fn test_gcp_trigger_unique_constraint(db: Pool<Postgres>) -> anyhow::Resul
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await?;
@@ -1594,7 +1595,7 @@ async fn test_gcp_trigger_unique_constraint(db: Pool<Postgres>) -> anyhow::Resul
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await;
@@ -1616,7 +1617,7 @@ async fn test_email_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO email_trigger (
path, local_part, workspaced_local_part, script_path,
is_flow, workspace_id, edited_by, email
is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
@@ -1627,7 +1628,7 @@ async fn test_email_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
)
.execute(&db)
.await?;
@@ -1659,7 +1660,7 @@ async fn test_email_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO email_trigger (
path, local_part, workspaced_local_part, script_path,
is_flow, workspace_id, edited_by, email
is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
@@ -1670,7 +1671,7 @@ async fn test_email_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
)
.execute(&db)
.await?;
@@ -1705,7 +1706,7 @@ async fn test_email_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO email_trigger (
path, local_part, workspaced_local_part, script_path,
is_flow, workspace_id, edited_by, email
is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
@@ -1716,7 +1717,7 @@ async fn test_email_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
)
.execute(&db)
.await?;

View File

@@ -33,42 +33,22 @@ use windmill_common::{
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_queue::schedule::push_scheduled_job;
/// Resolves the email to use for a schedule based on preservation settings.
/// When preserving, looks up the email from the provided username.
async fn resolve_email(
username: Option<&String>,
preserve_email: Option<bool>,
authed: &ApiAuthed,
db: &DB,
w_id: &str,
) -> Result<String> {
if let Some(username) = username {
if preserve_email.unwrap_or(false) && can_preserve_on_behalf_of(authed) {
let email = sqlx::query_scalar!(
"SELECT email FROM usr WHERE username = $1 AND workspace_id = $2",
username,
w_id
)
.fetch_optional(db)
.await?;
if let Some(email) = email {
return Ok(email);
}
}
}
Ok(authed.email.clone())
}
fn resolve_edited_by(
username: Option<&String>,
preserve_edited_by: Option<bool>,
/// Resolves the permissioned_as value for a schedule.
/// When preserving, uses the provided permissioned_as value directly.
fn resolve_permissioned_as(
permissioned_as: Option<&String>,
preserve_permissioned_as: Option<bool>,
authed: &ApiAuthed,
) -> String {
if let Some(username) = username {
if preserve_edited_by.unwrap_or(false) && can_preserve_on_behalf_of(authed) {
return username.clone();
if let Some(permissioned_as) = permissioned_as {
if preserve_permissioned_as.unwrap_or(false) && can_preserve_on_behalf_of(authed) {
return permissioned_as.clone();
}
}
windmill_common::users::username_to_permissioned_as(&authed.username)
}
fn resolve_edited_by(authed: &ApiAuthed) -> String {
authed.username.clone()
}
@@ -117,8 +97,8 @@ pub struct NewSchedule {
pub paused_until: Option<DateTime<Utc>>,
pub cron_version: Option<String>,
pub dynamic_skip: Option<String>,
pub email: Option<String>,
pub preserve_email: Option<bool>,
pub permissioned_as: Option<String>,
pub preserve_permissioned_as: Option<bool>,
}
#[derive(Serialize, Deserialize)]
@@ -244,14 +224,26 @@ async fn create_schedule(
validate_dynamic_skip(&mut tx, &w_id, handler_path).await?;
}
let resolved_edited_by = resolve_edited_by(ns.email.as_ref(), ns.preserve_email, &authed);
let resolved_edited_by = resolve_edited_by(&authed);
let resolved_permissioned_as = resolve_permissioned_as(
ns.permissioned_as.as_ref(),
ns.preserve_permissioned_as,
&authed,
);
// email is still written for backwards compat with old workers that don't know about permissioned_as
let resolved_email = windmill_common::users::get_email_from_permissioned_as(
&resolved_permissioned_as,
&w_id,
&db,
)
.await?;
let schedule = sqlx::query_as!(
Schedule,
r#"
INSERT INTO schedule (
workspace_id, path, schedule, timezone, edited_by, script_path,
is_flow, args, enabled, email,
is_flow, args, enabled, email, permissioned_as,
on_failure, on_failure_times, on_failure_exact, on_failure_extra_args,
on_recovery, on_recovery_times, on_recovery_extra_args,
on_success, on_success_extra_args,
@@ -259,12 +251,12 @@ async fn create_schedule(
tag, paused_until, cron_version, description, dynamic_skip
) 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
$7, $8, $9, $10, $11,
$12, $13, $14, $15,
$16, $17, $18,
$19, $20,
$21, $22, $23, $24,
$25, $26, $27, $28, $29
)
RETURNING
workspace_id,
@@ -279,6 +271,7 @@ async fn create_schedule(
args AS "args: _",
extra_perms,
email,
permissioned_as,
error,
on_failure,
on_failure_times,
@@ -309,7 +302,8 @@ async fn create_schedule(
to_json_raw_opt(ns.args.as_ref())
as Option<sqlx::types::Json<Box<serde_json::value::RawValue>>>,
ns.enabled.unwrap_or(false),
resolve_email(ns.email.as_ref(), ns.preserve_email, &authed, &db, &w_id).await?,
resolved_email,
resolved_permissioned_as,
ns.on_failure,
ns.on_failure_times,
ns.on_failure_exact,
@@ -355,8 +349,8 @@ async fn create_schedule(
)
.await?;
if let Some(on_behalf_of) = windmill_common::check_on_behalf_of_preservation(
ns.email.as_deref(),
ns.preserve_email.unwrap_or(false),
ns.permissioned_as.as_deref(),
ns.preserve_permissioned_as.unwrap_or(false),
&authed,
&authed.username,
) {
@@ -421,7 +415,28 @@ async fn edit_schedule(
clear_schedule(&mut tx, path, &w_id).await?;
let resolved_edited_by = resolve_edited_by(es.email.as_ref(), es.preserve_email, &authed);
let resolved_edited_by = resolve_edited_by(&authed);
let resolved_permissioned_as = resolve_permissioned_as(
es.permissioned_as.as_ref(),
es.preserve_permissioned_as,
&authed,
);
// email is still written for backwards compat with old workers that don't know about permissioned_as.
// When permissioned_as is preserved to a different user, derive email from it.
let resolved_email = if resolved_permissioned_as
!= windmill_common::users::username_to_permissioned_as(&authed.username)
{
windmill_common::users::get_email_from_permissioned_as(
&resolved_permissioned_as,
&w_id,
&db,
)
.await?
} else {
authed.email.clone()
};
let schedule = sqlx::query_as!(
Schedule,
@@ -450,8 +465,9 @@ async fn edit_schedule(
cron_version = COALESCE($21, cron_version),
description = $22,
dynamic_skip = $23,
email = COALESCE($24, email),
edited_by = $25
email = $24,
edited_by = $25,
permissioned_as = $26
WHERE path = $19 AND workspace_id = $20
RETURNING
workspace_id,
@@ -466,6 +482,7 @@ async fn edit_schedule(
args AS "args: _",
extra_perms,
email,
permissioned_as,
error,
on_failure,
on_failure_times,
@@ -513,8 +530,9 @@ async fn edit_schedule(
es.cron_version,
es.description,
es.dynamic_skip,
Some(resolve_email(es.email.as_ref(), es.preserve_email, &authed, &db, &w_id).await?),
resolved_edited_by
resolved_email,
resolved_edited_by,
resolved_permissioned_as
)
.fetch_one(&mut *tx)
.await
@@ -535,9 +553,10 @@ async fn edit_schedule(
),
)
.await?;
if let Some(on_behalf_of) = windmill_common::check_on_behalf_of_preservation(
es.email.as_deref(),
es.preserve_email.unwrap_or(false),
es.permissioned_as.as_deref(),
es.preserve_permissioned_as.unwrap_or(false),
&authed,
&authed.username,
) {
@@ -547,14 +566,8 @@ async fn edit_schedule(
"schedule.on_behalf_of",
ActionKind::Update,
&w_id,
Some(path),
Some(
[
("on_behalf_of", on_behalf_of.as_str()),
("action", "update"),
]
.into(),
),
Some(&path.to_string()),
Some([("on_behalf_of", on_behalf_of.as_str()), ("action", "edit")].into()),
)
.await?;
}
@@ -788,6 +801,7 @@ pub async fn set_enabled(
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
check_scopes(&authed, || format!("schedules:write:{}", path))?;
// email is still written for backwards compat with old workers that don't know about permissioned_as
let schedule_o = sqlx::query_as!(
Schedule,
r#"
@@ -808,6 +822,7 @@ pub async fn set_enabled(
args AS "args: _",
extra_perms,
email,
permissioned_as,
error,
on_failure,
on_failure_times,
@@ -1179,8 +1194,8 @@ pub struct EditSchedule {
pub paused_until: Option<DateTime<Utc>>,
pub cron_version: Option<String>,
pub dynamic_skip: Option<String>,
pub email: Option<String>,
pub preserve_email: Option<bool>,
pub permissioned_as: Option<String>,
pub preserve_permissioned_as: Option<bool>,
}
pub use windmill_queue::schedule::clear_schedule;

View File

@@ -636,7 +636,7 @@ async fn create_script_internal<'c>(
.await?;
if nb_scripts.unwrap_or(0) >= 5000 {
return Err(Error::BadRequest(
"You have reached the maximum number of scripts (5000) on cloud. Contact support@windmill.dev to increase the limit"
"You have reached the maximum number of scripts (5000) on cloud. Check your usage in Workspace Settings > General > Cloud Quotas. Contact support@windmill.dev to increase the limit"
.to_string(),
));
}

View File

@@ -972,8 +972,7 @@ async fn setup_custom_instance_pg_database_inner(
GRANT CREATE ON DATABASE \"{dbname}\" TO custom_instance_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO custom_instance_user;
ALTER ROLE custom_instance_user CREATEROLE;
ALTER ROLE custom_instance_user REPLICATION;"
ALTER ROLE custom_instance_user CREATEROLE;"
))
.await
.map_err(|e| {
@@ -982,6 +981,14 @@ async fn setup_custom_instance_pg_database_inner(
e.to_string(),
))
})?;
if let Err(e) = client
.batch_execute(&format!("ALTER ROLE custom_instance_user REPLICATION;"))
.await
{
tracing::error!("Failed to grant replication permission to custom_instance_user: {e:#}");
}
logs.grant_permissions = "OK".to_string();
drop(client); // /!\ Drop before joining to avoid deadlock

View File

@@ -1,2 +1 @@
pub mod users;
pub mod users_oss;

View File

@@ -1,65 +0,0 @@
use std::sync::Arc;
use windmill_api_auth::ApiAuthed;
use crate::users::{EditPassword, NewUser};
use windmill_common::webhook::WebhookShared;
use windmill_common::DB;
use argon2::Argon2;
use axum::{extract::Extension, Json};
use http::StatusCode;
use serde::Deserialize;
use windmill_common::error::{Error, Result};
pub async fn create_user(
_authed: ApiAuthed,
_db: DB,
_webhook: WebhookShared,
_argon2: Arc<Argon2<'_>>,
mut _nu: NewUser,
) -> Result<(StatusCode, String)> {
Err(Error::internal_err(
"Not implemented in Windmill's Open Source repository".to_string(),
))
}
pub async fn set_password(
_db: DB,
_argon2: Arc<Argon2<'_>>,
_authed: ApiAuthed,
_user_email: &str,
_ep: EditPassword,
) -> Result<String> {
Err(Error::internal_err(
"Not implemented in Windmill's Open Source repository".to_string(),
))
}
pub fn hash_password(_argon2: Arc<Argon2<'_>>, _password: String) -> Result<String> {
Err(Error::internal_err(
"Not implemented in Windmill's Open Source repository".to_string(),
))
}
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
pub struct OnboardingData {
pub touch_point: String,
pub use_case: String,
}
pub async fn submit_onboarding_data(
_authed: ApiAuthed,
Extension(_db): Extension<DB>,
Json(_data): Json<OnboardingData>,
) -> Result<String> {
Err(Error::internal_err(
"Not implemented in Windmill's Open Source repository".to_string(),
))
}

View File

@@ -158,6 +158,8 @@ pub fn workspaced_service() -> Router {
post(update_protection_rule).delete(delete_protection_rule),
)
.route("/log_chat", post(log_ai_chat))
.route("/cloud_quotas", get(get_cloud_quotas))
.route("/prune_versions", post(prune_versions))
}
pub fn global_service() -> Router {
Router::new()
@@ -3249,10 +3251,12 @@ async fn clone_apps(
app_id_mapping.insert(app.id, new_app_id);
}
let mut version_id_mapping: HashMap<i64, i64> = HashMap::new();
{
// Clone app versions
let app_versions = sqlx::query!(
"SELECT app_id, value, created_by, created_at, raw_app
"SELECT id, app_id, value, created_by, created_at, raw_app
FROM app_version
WHERE app_id = ANY(SELECT id FROM app WHERE workspace_id = $1)
ORDER BY app_id, created_at",
@@ -3263,15 +3267,45 @@ async fn clone_apps(
for version in app_versions {
if let Some(&new_app_id) = app_id_mapping.get(&version.app_id) {
sqlx::query!(
let new_version_id = sqlx::query_scalar!(
"INSERT INTO app_version (app_id, value, created_by, created_at, raw_app)
VALUES ($1, $2, $3, $4, $5)",
VALUES ($1, $2, $3, $4, $5) RETURNING id",
new_app_id,
version.value,
version.created_by,
version.created_at,
version.raw_app,
)
.fetch_one(&mut **tx)
.await?;
version_id_mapping.insert(version.id, new_version_id);
}
}
}
// Clone app bundles for raw apps
if !version_id_mapping.is_empty() {
let old_ids: Vec<i64> = version_id_mapping.keys().copied().collect();
let bundles = sqlx::query!(
"SELECT app_version_id, file_type, data FROM app_bundles
WHERE app_version_id = ANY($1) AND w_id = $2",
&old_ids,
source_workspace_id
)
.fetch_all(&mut **tx)
.await?;
for bundle in bundles {
if let Some(&new_version_id) = version_id_mapping.get(&bundle.app_version_id) {
sqlx::query!(
"INSERT INTO app_bundles (app_version_id, w_id, file_type, data)
VALUES ($1, $2, $3, $4)",
new_version_id,
target_workspace_id,
bundle.file_type,
bundle.data,
)
.execute(&mut **tx)
.await?;
}
@@ -4739,7 +4773,7 @@ async fn compare_workspaces(
compare_two_flows(&db, &source_workspace_id, &fork_workspace_id, &item.path)
.await?,
),
"app" => Some(
"app" | "raw_app" => Some(
compare_two_apps(&db, &source_workspace_id, &fork_workspace_id, &item.path).await?,
),
"resource" => Some(
@@ -4834,7 +4868,10 @@ async fn compare_workspaces(
.fold(0, |acc, s| acc + s.try_into().unwrap_or(0)),
scripts_changed: visible_diffs.iter().filter(|s| s.kind == "script").count(),
flows_changed: visible_diffs.iter().filter(|s| s.kind == "flow").count(),
apps_changed: visible_diffs.iter().filter(|s| s.kind == "app").count(),
apps_changed: visible_diffs
.iter()
.filter(|s| s.kind == "app" || s.kind == "raw_app")
.count(),
resources_changed: visible_diffs
.iter()
.filter(|s| s.kind == "resource")
@@ -4943,7 +4980,7 @@ async fn query_visible_items<'c>(
.fetch_all(&mut **tx)
.await?
}
"app" => {
"app" | "raw_app" => {
sqlx::query_scalar!(
"SELECT path FROM app
WHERE workspace_id = $1 AND path = ANY($2)",
@@ -5398,3 +5435,199 @@ async fn log_ai_chat(
.await?;
Ok(StatusCode::NO_CONTENT)
}
#[derive(Serialize)]
struct QuotaInfo {
used: i64,
limit: i64,
prunable: i64,
}
#[derive(Serialize)]
struct CloudQuotas {
scripts: QuotaInfo,
flows: QuotaInfo,
apps: QuotaInfo,
variables: QuotaInfo,
resources: QuotaInfo,
}
async fn get_cloud_quotas(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
) -> JsonResult<CloudQuotas> {
require_admin(authed.is_admin, &authed.username)?;
if !*CLOUD_HOSTED {
return Err(Error::BadRequest(
"Cloud quotas are only available on cloud-hosted instances".to_string(),
));
}
let scripts_used =
sqlx::query_scalar!("SELECT COUNT(*) FROM script WHERE workspace_id = $1", &w_id)
.fetch_one(&db)
.await?
.unwrap_or(0);
let scripts_prunable = sqlx::query_scalar!(
"SELECT COUNT(*) FROM script s WHERE s.workspace_id = $1 AND s.hash NOT IN (
SELECT DISTINCT ON (path) hash FROM script
WHERE workspace_id = $1 AND deleted = false AND draft_only IS NOT TRUE
ORDER BY path, created_at DESC
)",
&w_id
)
.fetch_one(&db)
.await?
.unwrap_or(0);
let flows_used =
sqlx::query_scalar!("SELECT COUNT(*) FROM flow WHERE workspace_id = $1", &w_id)
.fetch_one(&db)
.await?
.unwrap_or(0);
let flows_prunable = sqlx::query_scalar!(
"SELECT COUNT(*) FROM flow_version fv
JOIN flow f ON f.workspace_id = fv.workspace_id AND f.path = fv.path
WHERE fv.workspace_id = $1 AND fv.id != f.versions[array_upper(f.versions, 1)]",
&w_id
)
.fetch_one(&db)
.await?
.unwrap_or(0);
let apps_used = sqlx::query_scalar!("SELECT COUNT(*) FROM app WHERE workspace_id = $1", &w_id)
.fetch_one(&db)
.await?
.unwrap_or(0);
let apps_prunable = sqlx::query_scalar!(
"SELECT COUNT(*) FROM app_version av
JOIN app a ON a.id = av.app_id
WHERE a.workspace_id = $1 AND av.id != a.versions[array_upper(a.versions, 1)]",
&w_id
)
.fetch_one(&db)
.await?
.unwrap_or(0);
let variables_used = sqlx::query_scalar!(
"SELECT COUNT(*) FROM variable WHERE workspace_id = $1",
&w_id
)
.fetch_one(&db)
.await?
.unwrap_or(0);
let resources_used = sqlx::query_scalar!(
"SELECT COUNT(*) FROM resource WHERE workspace_id = $1",
&w_id
)
.fetch_one(&db)
.await?
.unwrap_or(0);
Ok(Json(CloudQuotas {
scripts: QuotaInfo { used: scripts_used, limit: 5000, prunable: scripts_prunable },
flows: QuotaInfo { used: flows_used, limit: 1000, prunable: flows_prunable },
apps: QuotaInfo { used: apps_used, limit: 1000, prunable: apps_prunable },
variables: QuotaInfo { used: variables_used, limit: 10000, prunable: 0 },
resources: QuotaInfo { used: resources_used, limit: 10000, prunable: 0 },
}))
}
#[derive(Deserialize)]
struct PruneVersionsRequest {
resource_type: String,
}
#[derive(Serialize)]
struct PruneVersionsResponse {
pruned: u64,
}
async fn prune_versions(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(req): Json<PruneVersionsRequest>,
) -> JsonResult<PruneVersionsResponse> {
require_admin(authed.is_admin, &authed.username)?;
if !*CLOUD_HOSTED {
return Err(Error::BadRequest(
"Version pruning is only available on cloud-hosted instances".to_string(),
));
}
let pruned = match req.resource_type.as_str() {
"scripts" => {
let result = sqlx::query(
"DELETE FROM script
WHERE workspace_id = $1 AND hash NOT IN (
SELECT DISTINCT ON (path) hash FROM script
WHERE workspace_id = $1 AND deleted = false AND draft_only IS NOT TRUE
ORDER BY path, created_at DESC
)",
)
.bind(&w_id)
.execute(&db)
.await?;
result.rows_affected()
}
"flows" => {
let deleted = sqlx::query(
"DELETE FROM flow_version fv
USING flow f
WHERE fv.workspace_id = f.workspace_id AND fv.path = f.path
AND fv.workspace_id = $1
AND fv.id != f.versions[array_upper(f.versions, 1)]",
)
.bind(&w_id)
.execute(&db)
.await?;
sqlx::query(
"UPDATE flow SET versions = ARRAY[versions[array_upper(versions, 1)]]
WHERE workspace_id = $1 AND array_length(versions, 1) > 1",
)
.bind(&w_id)
.execute(&db)
.await?;
deleted.rows_affected()
}
"apps" => {
let deleted = sqlx::query(
"DELETE FROM app_version av
USING app a
WHERE av.app_id = a.id AND a.workspace_id = $1
AND av.id != a.versions[array_upper(a.versions, 1)]",
)
.bind(&w_id)
.execute(&db)
.await?;
sqlx::query(
"UPDATE app SET versions = ARRAY[versions[array_upper(versions, 1)]]
WHERE workspace_id = $1 AND array_length(versions, 1) > 1",
)
.bind(&w_id)
.execute(&db)
.await?;
deleted.rows_affected()
}
_ => {
return Err(Error::BadRequest(format!(
"Invalid resource type '{}'. Must be 'scripts', 'flows', or 'apps'",
req.resource_type
)));
}
};
Ok(Json(PruneVersionsResponse { pruned }))
}

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.659.1
version: 1.662.0
title: Windmill API
contact:
@@ -4491,6 +4491,76 @@ paths:
responses:
"204":
description: logged
/w/{workspace}/workspaces/cloud_quotas:
get:
summary: get cloud quota usage and limits for workspace
operationId: getCloudQuotas
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: cloud quota usage and limits
content:
application/json:
schema:
type: object
properties:
scripts:
$ref: "#/components/schemas/QuotaInfo"
flows:
$ref: "#/components/schemas/QuotaInfo"
apps:
$ref: "#/components/schemas/QuotaInfo"
variables:
$ref: "#/components/schemas/QuotaInfo"
resources:
$ref: "#/components/schemas/QuotaInfo"
required:
- scripts
- flows
- apps
- variables
- resources
/w/{workspace}/workspaces/prune_versions:
post:
summary: prune old versions of scripts, flows, or apps
operationId: pruneVersions
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
resource_type:
type: string
enum:
- scripts
- flows
- apps
required:
- resource_type
responses:
"200":
description: number of pruned versions
content:
application/json:
schema:
type: object
properties:
pruned:
type: integer
required:
- pruned
/w/{workspace}/workspaces/public_app_rate_limit:
post:
summary: Set public app rate limit for this workspace
@@ -20413,6 +20483,9 @@ components:
email:
type: string
description: Email of the user who owns this schedule, used for permissioned_as
permissioned_as:
type: string
description: The user or group this schedule runs as (e.g., 'u/admin' or 'g/mygroup')
error:
type: string
nullable: true
@@ -20495,6 +20568,7 @@ components:
- is_flow
- enabled
- email
- permissioned_as
ScheduleWJobs:
allOf:
@@ -20616,12 +20690,12 @@ components:
type: string
nullable: true
description: Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean to skip (true) or run (false)
email:
permissioned_as:
type: string
description: Email of the user who the scheduled jobs run as. Used during deployment to preserve the original schedule owner.
preserve_email:
description: The user or group this schedule runs as. Used during deployment to preserve the original schedule owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- schedule
@@ -20709,12 +20783,14 @@ components:
type: string
nullable: true
description: Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean to skip (true) or run (false)
email:
permissioned_as:
type: string
description: Email of the user who the scheduled jobs run as. Used during deployment to preserve the original schedule owner.
preserve_email:
nullable: true
description: "The user or group this schedule runs as (e.g., 'u/admin' or 'g/mygroup'). Only admins and wm_deployers can set this via preserve_permissioned_as."
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
nullable: true
description: "If true and user is admin/wm_deployers, preserve the provided permissioned_as instead of using the deploying user's identity"
required:
- schedule
- timezone
@@ -20755,9 +20831,9 @@ components:
script_path:
type: string
description: Path to the script or flow to execute when triggered
email:
permissioned_as:
type: string
description: Email of the user who owns this trigger, used for permissioned_as
description: The user or group this trigger runs as (permissioned_as)
extra_perms:
type: object
description: Additional permissions for this trigger
@@ -20782,7 +20858,7 @@ components:
required:
- path
- script_path
- email
- permissioned_as
- extra_perms
- workspace_id
- edited_by
@@ -21071,12 +21147,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
@@ -21163,12 +21239,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- script_path
@@ -21331,12 +21407,12 @@ components:
retry:
description: Retry configuration for failed executions
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
@@ -21399,12 +21475,12 @@ components:
retry:
description: Retry configuration for failed executions
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
@@ -21574,12 +21650,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- script_path
@@ -21634,12 +21710,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- script_path
@@ -21776,12 +21852,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: "Retry configuration for failed executions."
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- script_path
@@ -21944,12 +22020,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- queue_url
- aws_resource_path
@@ -21996,12 +22072,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- queue_url
- aws_resource_path
@@ -22160,12 +22236,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- script_path
@@ -22208,12 +22284,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- script_path
@@ -22343,12 +22419,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
@@ -22413,12 +22489,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
@@ -22520,12 +22596,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
@@ -22575,12 +22651,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- script_path
@@ -22629,12 +22705,12 @@ components:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
mode:
$ref: "#/components/schemas/TriggerMode"
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
@@ -22661,12 +22737,12 @@ components:
$ref: "#/components/schemas/ScriptArgs"
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- script_path
@@ -24092,7 +24168,7 @@ components:
kind:
type: string
enum:
["script", "flow", "app", "resource", "variable", "resource_type"]
["script", "flow", "app", "raw_app", "resource", "variable", "resource_type"]
description: Type of the item
path:
type: string
@@ -24394,6 +24470,19 @@ components:
description: Users that can bypass this ruleset
items:
type: string
QuotaInfo:
type: object
properties:
used:
type: integer
limit:
type: integer
prunable:
type: integer
required:
- used
- limit
- prunable
NativeServiceName:
type: string
enum:

View File

@@ -1179,7 +1179,7 @@ async fn create_app_internal<'a>(
.await?;
if nb_apps.unwrap_or(0) >= 1000 {
return Err(Error::BadRequest(
"You have reached the maximum number of apps (1000) on cloud. Contact support@windmill.dev to increase the limit"
"You have reached the maximum number of apps (1000) on cloud. Check your usage in Workspace Settings > General > Cloud Quotas. Contact support@windmill.dev to increase the limit"
.to_string(),
));
}

View File

@@ -21,6 +21,9 @@ use tokio::sync::RwLock;
use crate::db::{ApiAuthed, DB};
use windmill_common::min_version::MIN_KEEP_ALIVE_VERSION;
use windmill_common::otel_oss::{
otel_set_health_db_latency, otel_set_health_db_unresponsive, otel_set_health_status_phase,
};
use windmill_common::utils::GIT_VERSION;
use windmill_common::IS_READY;
@@ -448,6 +451,9 @@ async fn perform_health_check(db: &DB) -> HealthCheckResult {
let checked_at = Utc::now();
let db_check = check_database_with_latency(db).await;
otel_set_health_db_latency(db_check.latency_ms as f64);
otel_set_health_db_unresponsive(!db_check.healthy);
let workers_alive = if db_check.healthy {
check_worker_count(db).await
} else {
@@ -462,6 +468,12 @@ async fn perform_health_check(db: &DB) -> HealthCheckResult {
HealthStatus::Healthy
};
otel_set_health_status_phase(match status {
HealthStatus::Healthy => "healthy",
HealthStatus::Degraded => "degraded",
HealthStatus::Unhealthy => "unhealthy",
});
let response = HealthStatusResponse {
status,
checked_at,

View File

@@ -64,7 +64,6 @@ use crate::scim_oss::has_scim_token;
use windmill_common::error::AppError;
mod ai;
mod google;
mod apps;
pub mod args;
mod audit;
@@ -77,6 +76,7 @@ mod bedrock;
mod capture;
mod concurrency_groups;
mod db;
mod google;
mod drafts;
#[cfg(feature = "private")]
@@ -376,7 +376,7 @@ pub async fn run_server(
));
let cors = CorsLayer::new()
.allow_methods([http::Method::GET, http::Method::POST])
.allow_methods([http::Method::GET, http::Method::POST, http::Method::DELETE])
.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION])
.allow_origin(Any);
@@ -461,24 +461,39 @@ pub async fn run_server(
// Setup MCP server
#[allow(unused_variables)]
let (mcp_router, mcp_cancellation_token) = {
let (mcp_router, gateway_mcp_router, mcp_cancellation_token) = {
#[cfg(feature = "mcp")]
if server_mode || mcp_mode {
use mcp::add_www_authenticate_header;
use mcp::{
add_www_authenticate_header, add_www_authenticate_header_gateway,
extract_workspace_from_token,
};
let (mcp_router, mcp_cancellation_token) =
setup_mcp_server(db.clone(), user_db, _base_internal_url.clone()).await?;
// Apply middleware: auth check inside WWW-Authenticate wrapper so 401s get the header
let mcp_router = mcp_router
// Workspace-scoped MCP router
let workspaced_mcp_router = mcp_router
.clone()
.route_layer(from_extractor::<ApiAuthed>())
.layer(axum::middleware::from_fn(add_www_authenticate_header))
.layer(axum::middleware::from_fn(extract_and_store_workspace_id));
(mcp_router, Some(mcp_cancellation_token))
// Gateway MCP router — resolves workspace from token
let gateway_mcp_router = mcp_router
.route_layer(from_extractor::<ApiAuthed>())
.layer(axum::middleware::from_fn(
add_www_authenticate_header_gateway,
))
.layer(axum::middleware::from_fn(extract_workspace_from_token));
(
workspaced_mcp_router,
gateway_mcp_router,
Some(mcp_cancellation_token),
)
} else {
(Router::new(), None)
(Router::new(), Router::new(), None)
}
#[cfg(not(feature = "mcp"))]
(Router::new(), Option::<()>::None)
(Router::new(), Router::new(), Option::<()>::None)
};
let mcp_list_tools_service = {
@@ -623,6 +638,19 @@ pub async fn run_server(
.nest("/indexer", indexer_oss::management_service())
.nest("/mcp/w/:workspace_id/list_tools", mcp_list_tools_service)
.nest("/health/detailed", health::detailed_service())
.nest(
"/saml",
saml_oss::authed_service().layer(Extension(Arc::clone(&sp_extension))),
)
.nest("/mcp/gateway/oauth/server", {
#[cfg(feature = "mcp")]
{
mcp::oauth_server::gateway_authed_service()
}
#[cfg(not(feature = "mcp"))]
Router::new()
})
.route_layer(from_extractor::<ApiAuthed>())
.route_layer(from_extractor::<users::Tokened>())
// Workspace-scoped OAuth endpoints that don't require authentication
@@ -636,6 +664,16 @@ pub async fn run_server(
#[cfg(not(feature = "mcp"))]
Router::new()
})
// Gateway OAuth endpoints (authorize + token) — no auth required
.nest("/mcp/gateway/oauth/server", {
#[cfg(feature = "mcp")]
{
mcp::oauth_server::gateway_unauthed_service().layer(cors.clone())
}
#[cfg(not(feature = "mcp"))]
Router::new()
})
.nest("/jobs", jobs::global_root_service())
.nest(
"/srch/w/:workspace_id/index",
@@ -675,8 +713,15 @@ pub async fn run_server(
)
.layer(from_extractor::<OptAuthed>())
// Deprecated, here for backwards compatibility: user should use /mcp/w/:workspace_id/mcp instead
.nest("/mcp/w/:workspace_id/sse", mcp_router.clone())
.nest("/mcp/w/:workspace_id/mcp", mcp_router)
.nest(
"/mcp/w/:workspace_id/sse",
mcp_router.clone().layer(cors.clone()),
)
.nest(
"/mcp/w/:workspace_id/mcp",
mcp_router.clone().layer(cors.clone()),
)
.nest("/mcp/gateway", gateway_mcp_router.layer(cors.clone()))
.nest("/agent_workers", {
#[cfg(feature = "agent_worker_server")]
{
@@ -863,6 +908,30 @@ pub async fn run_server(
}
},
)
// Gateway OAuth well-known endpoints
.route(
"/.well-known/oauth-authorization-server/api/mcp/gateway/oauth/server",
{
#[cfg(feature = "mcp")]
{
get(mcp::oauth_server::gateway_oauth_metadata)
}
#[cfg(not(feature = "mcp"))]
{
get(|| async { axum::http::StatusCode::NOT_FOUND })
}
},
)
.route("/.well-known/oauth-protected-resource/api/mcp/gateway", {
#[cfg(feature = "mcp")]
{
get(mcp::oauth_server::gateway_protected_resource_metadata)
}
#[cfg(not(feature = "mcp"))]
{
get(|| async { axum::http::StatusCode::NOT_FOUND })
}
})
// JWKS endpoint for HashiCorp Vault JWT authentication (must be outside /api prefix)
.route(
"/.well-known/jwks.json",

View File

@@ -35,9 +35,14 @@ use windmill_mcp::server::{
use windmill_mcp::WorkspaceId;
use axum::{
extract::Path, http::Request, middleware::Next, response::Response, routing::get, Json, Router,
extract::{Extension, Path},
http::Request,
middleware::Next,
response::Response,
routing::get,
Json, Router,
};
use windmill_common::error::JsonResult;
use windmill_common::{auth::hash_token, db::GatewayWorkspaceId, error::JsonResult};
// McpAuth impl for ApiAuthed is in windmill-api-auth (same crate as the type)
@@ -67,9 +72,16 @@ impl McpBackend for WindmillBackend {
path_prefix: Option<&str>,
) -> BackendResult<Vec<ScriptInfo>> {
let scope_type = if favorites_only { "favorites" } else { "all" };
get_items::<ScriptInfo>(&self.user_db, auth, workspace_id, scope_type, "script", path_prefix)
.await
.map_err(|e| ErrorData::internal_error(e.message, None))
get_items::<ScriptInfo>(
&self.user_db,
auth,
workspace_id,
scope_type,
"script",
path_prefix,
)
.await
.map_err(|e| ErrorData::internal_error(e.message, None))
}
async fn list_flows(
@@ -80,9 +92,16 @@ impl McpBackend for WindmillBackend {
path_prefix: Option<&str>,
) -> BackendResult<Vec<FlowInfo>> {
let scope_type = if favorites_only { "favorites" } else { "all" };
get_items::<FlowInfo>(&self.user_db, auth, workspace_id, scope_type, "flow", path_prefix)
.await
.map_err(|e| ErrorData::internal_error(e.message, None))
get_items::<FlowInfo>(
&self.user_db,
auth,
workspace_id,
scope_type,
"flow",
path_prefix,
)
.await
.map_err(|e| ErrorData::internal_error(e.message, None))
}
async fn list_resource_types(
@@ -439,6 +458,72 @@ pub async fn add_www_authenticate_header(
}
}
/// Middleware for gateway: extract workspace_id from the Bearer token in the DB
/// and inject it as WorkspaceId extension so the MCP runner can use it.
pub async fn extract_workspace_from_token(
Extension(db): Extension<DB>,
mut request: Request<axum::body::Body>,
next: Next,
) -> Response {
if let Some(auth_header) = request
.headers()
.get(axum::http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
{
if let Some(token) = auth_header.strip_prefix("Bearer ") {
let t_hash = hash_token(token);
match sqlx::query_scalar!(
"SELECT workspace_id FROM token WHERE token_hash = $1 AND workspace_id IS NOT NULL AND (expiration > NOW() OR expiration IS NULL)",
t_hash
)
.fetch_optional(&db)
.await
{
Ok(Some(Some(workspace_id))) => {
request
.extensions_mut()
.insert(GatewayWorkspaceId(workspace_id.clone()));
request.extensions_mut().insert(WorkspaceId(workspace_id));
}
Ok(_) => {}
Err(e) => {
tracing::error!("Gateway token workspace lookup failed: {}", e);
}
}
}
}
next.run(request).await
}
/// Middleware that adds WWW-Authenticate header for gateway 401 responses
pub async fn add_www_authenticate_header_gateway(
request: Request<axum::body::Body>,
next: Next,
) -> Response {
use axum::http::StatusCode;
use windmill_common::BASE_URL;
let response = next.run(request).await;
if response.status() == StatusCode::UNAUTHORIZED {
let base_url = BASE_URL.read().await;
let resource_url = format!("{}/api/mcp/gateway", base_url);
let www_authenticate = format!("Bearer resource=\"{}\"", resource_url);
let (mut parts, body) = response.into_parts();
parts.headers.insert(
axum::http::header::WWW_AUTHENTICATE,
www_authenticate
.parse()
.unwrap_or_else(|_| "Bearer".parse().unwrap()),
);
Response::from_parts(parts, body)
} else {
response
}
}
/// Setup the MCP server with HTTP transport
pub async fn setup_mcp_server(
db: DB,

View File

@@ -10,6 +10,7 @@ mod utils;
// Re-export only what's needed externally
pub mod oauth_server;
pub use core::{
add_www_authenticate_header, extract_and_store_workspace_id, list_tools_service,
add_www_authenticate_header, add_www_authenticate_header_gateway,
extract_and_store_workspace_id, extract_workspace_from_token, list_tools_service,
setup_mcp_server,
};

View File

@@ -234,19 +234,11 @@ fn supported_scopes() -> Vec<String> {
}
/// GET /.well-known/oauth-authorization-server/api/w/:workspace_id/mcp/oauth/server
pub async fn workspaced_oauth_metadata(
Path(workspace_id): Path<String>,
) -> Json<AuthorizationMetadata> {
let base_url = BASE_URL.read().await;
let issuer = format!("{}/api/w/{}/mcp/oauth/server", base_url, workspace_id);
Json(AuthorizationMetadata {
issuer,
authorization_endpoint: format!(
"{}/api/w/{}/mcp/oauth/server/authorize",
base_url, workspace_id
),
token_endpoint: format!("{}/api/w/{}/mcp/oauth/server/token", base_url, workspace_id),
fn build_oauth_metadata(oauth_prefix: &str, base_url: &str) -> AuthorizationMetadata {
AuthorizationMetadata {
issuer: format!("{}{}", base_url, oauth_prefix),
authorization_endpoint: format!("{}{}/authorize", base_url, oauth_prefix),
token_endpoint: format!("{}{}/token", base_url, oauth_prefix),
registration_endpoint: Some(format!("{}/api/mcp/oauth/server/register", base_url)),
scopes_supported: Some(supported_scopes()),
response_types_supported: Some(vec!["code".to_string()]),
@@ -255,7 +247,28 @@ pub async fn workspaced_oauth_metadata(
"refresh_token".to_string(),
]),
code_challenge_methods_supported: Some(vec!["S256".to_string()]),
})
}
}
fn build_protected_resource_metadata(
resource_path: &str,
oauth_prefix: &str,
base_url: &str,
) -> ProtectedResourceMetadata {
ProtectedResourceMetadata {
resource: format!("{}{}", base_url, resource_path),
authorization_servers: vec![format!("{}{}", base_url, oauth_prefix)],
scopes_supported: Some(supported_scopes()),
bearer_methods_supported: Some(vec!["header".to_string()]),
}
}
pub async fn workspaced_oauth_metadata(
Path(workspace_id): Path<String>,
) -> Json<AuthorizationMetadata> {
let base_url = BASE_URL.read().await;
let oauth_prefix = format!("/api/w/{}/mcp/oauth/server", workspace_id);
Json(build_oauth_metadata(&oauth_prefix, &base_url))
}
/// GET /.well-known/oauth-protected-resource/api/mcp/w/:workspace_id/mcp
@@ -263,14 +276,13 @@ pub async fn protected_resource_metadata_by_path(
Path(workspace_id): Path<String>,
) -> Json<ProtectedResourceMetadata> {
let base_url = BASE_URL.read().await;
let resource_url = format!("{}/api/mcp/w/{}/mcp", base_url, workspace_id);
let auth_server_url = format!("{}/api/w/{}/mcp/oauth/server", base_url, workspace_id);
Json(ProtectedResourceMetadata {
resource: resource_url,
authorization_servers: vec![auth_server_url],
scopes_supported: Some(supported_scopes()),
bearer_methods_supported: Some(vec!["header".to_string()]),
})
let resource_path = format!("/api/mcp/w/{}/mcp", workspace_id);
let oauth_prefix = format!("/api/w/{}/mcp/oauth/server", workspace_id);
Json(build_protected_resource_metadata(
&resource_path,
&oauth_prefix,
&base_url,
))
}
/// POST /api/mcp/oauth/server/register - dynamic client registration
@@ -386,7 +398,9 @@ async fn handle_authorization_code_grant(
let access_token = rd_string(32);
let access_token_hash = hash_token(&access_token);
let access_token_prefix = access_token.get(..TOKEN_PREFIX_LEN).unwrap_or(&access_token);
let access_token_prefix = access_token
.get(..TOKEN_PREFIX_LEN)
.unwrap_or(&access_token);
let plaintext: Option<&str> = if MIN_VERSION_SUPPORTS_TOKEN_HASH.met().await {
None
} else {
@@ -530,7 +544,9 @@ async fn handle_refresh_token_grant(
// Generate new tokens
let new_access_token = rd_string(32);
let new_access_token_hash = hash_token(&new_access_token);
let new_access_token_prefix = new_access_token.get(..TOKEN_PREFIX_LEN).unwrap_or(&new_access_token);
let new_access_token_prefix = new_access_token
.get(..TOKEN_PREFIX_LEN)
.unwrap_or(&new_access_token);
let new_plaintext: Option<&str> = if MIN_VERSION_SUPPORTS_TOKEN_HASH.met().await {
None
} else {
@@ -595,18 +611,19 @@ async fn handle_refresh_token_grant(
}))
}
/// GET /api/w/:workspace_id/mcp/oauth/server/authorize - redirects to consent page
pub async fn workspaced_oauth_authorize(
Extension(db): Extension<DB>,
Path(workspace_id): Path<String>,
Query(params): Query<AuthorizeQuery>,
) -> impl IntoResponse {
/// Shared authorize logic — validates params, looks up client, redirects to consent page.
/// When `workspace_id` is Some, passes it to frontend; when None, passes `gateway=true`.
async fn oauth_authorize_inner(
db: &DB,
params: AuthorizeQuery,
workspace_id: Option<String>,
) -> axum::response::Response {
let client = match sqlx::query_as!(
OAuthClient,
"SELECT client_id, client_name, redirect_uris FROM mcp_oauth_server_client WHERE client_id = $1",
params.client_id
)
.fetch_optional(&db)
.fetch_optional(db)
.await
{
Ok(Some(client)) => client,
@@ -677,12 +694,18 @@ pub async fn workspaced_oauth_authorize(
}
};
// Build frontend redirect params — gateway mode vs workspace mode
let (mode_key, mode_value) = match &workspace_id {
Some(w_id) => ("workspace_id", w_id.as_str()),
None => ("gateway", "true"),
};
let base_url = BASE_URL.read().await;
let frontend_url = format!(
"{}/oauth/mcp_authorize?{}",
base_url,
serde_urlencoded::to_string(&[
("workspace_id", workspace_id.as_str()),
(mode_key, mode_value),
("client_id", params.client_id.as_str()),
("client_name", client.client_name.as_str()),
("redirect_uri", params.redirect_uri.as_str()),
@@ -698,12 +721,12 @@ pub async fn workspaced_oauth_authorize(
Redirect::temporary(&frontend_url).into_response()
}
/// POST /api/w/:workspace_id/mcp/oauth/server/approve - user approval (frontend)
pub async fn workspaced_oauth_approve(
Extension(db): Extension<DB>,
Path(workspace_id): Path<String>,
authed: ApiAuthed,
Json(form): Json<ApprovalForm>,
/// Shared approve logic — validates form, checks membership, stores auth code.
async fn oauth_approve_inner(
db: &DB,
authed: &ApiAuthed,
workspace_id: &str,
form: ApprovalForm,
) -> Result<Json<ApprovalResponse>> {
// Verify user is a member of the workspace
let is_member = sqlx::query_scalar!(
@@ -711,7 +734,7 @@ pub async fn workspaced_oauth_approve(
workspace_id,
authed.email
)
.fetch_one(&db)
.fetch_one(db)
.await
.map_err(|e| Error::InternalErr(format!("Database error: {}", e)))?
.unwrap_or(false);
@@ -728,7 +751,7 @@ pub async fn workspaced_oauth_approve(
"SELECT client_id, client_name, redirect_uris FROM mcp_oauth_server_client WHERE client_id = $1",
form.client_id
)
.fetch_optional(&db)
.fetch_optional(db)
.await
.map_err(|e| Error::InternalErr(format!("Database error: {}", e)))?
.ok_or_else(|| Error::BadRequest("Unknown client_id".to_string()))?;
@@ -778,7 +801,7 @@ pub async fn workspaced_oauth_approve(
&form.code_challenge,
&form.code_challenge_method,
)
.execute(&db)
.execute(db)
.await
.map_err(|e| Error::InternalErr(format!("Failed to store authorization code: {}", e)))?;
@@ -792,6 +815,25 @@ pub async fn workspaced_oauth_approve(
}))
}
/// GET /api/w/:workspace_id/mcp/oauth/server/authorize
pub async fn workspaced_oauth_authorize(
Extension(db): Extension<DB>,
Path(workspace_id): Path<String>,
Query(params): Query<AuthorizeQuery>,
) -> impl IntoResponse {
oauth_authorize_inner(&db, params, Some(workspace_id)).await
}
/// POST /api/w/:workspace_id/mcp/oauth/server/approve
pub async fn workspaced_oauth_approve(
Extension(db): Extension<DB>,
Path(workspace_id): Path<String>,
authed: ApiAuthed,
Json(form): Json<ApprovalForm>,
) -> Result<Json<ApprovalResponse>> {
oauth_approve_inner(&db, &authed, &workspace_id, form).await
}
/// PKCE validation (S256 only)
fn validate_pkce_s256(verifier: &str, challenge: &str) -> bool {
let mut hasher = Sha256::new();
@@ -851,6 +893,73 @@ impl IntoResponse for OAuthErrorRedirect {
}
}
// ── Gateway (workspace-agnostic) OAuth handlers ──
// Thin wrappers that delegate to the shared inner functions above.
pub async fn gateway_oauth_metadata() -> Json<AuthorizationMetadata> {
let base_url = BASE_URL.read().await;
Json(build_oauth_metadata(
"/api/mcp/gateway/oauth/server",
&base_url,
))
}
pub async fn gateway_protected_resource_metadata() -> Json<ProtectedResourceMetadata> {
let base_url = BASE_URL.read().await;
Json(build_protected_resource_metadata(
"/api/mcp/gateway",
"/api/mcp/gateway/oauth/server",
&base_url,
))
}
/// GET /api/mcp/gateway/oauth/server/authorize — gateway mode (no workspace in path)
pub async fn gateway_oauth_authorize(
Extension(db): Extension<DB>,
Query(params): Query<AuthorizeQuery>,
) -> impl IntoResponse {
oauth_authorize_inner(&db, params, None).await
}
/// POST /api/mcp/gateway/oauth/server/approve — workspace_id comes from JSON body
pub async fn gateway_oauth_approve(
Extension(db): Extension<DB>,
authed: ApiAuthed,
Json(form): Json<GatewayApprovalForm>,
) -> Result<Json<ApprovalResponse>> {
if form.workspace_id.is_empty() {
return Err(Error::BadRequest("workspace_id is required".to_string()));
}
oauth_approve_inner(
&db,
&authed,
&form.workspace_id,
ApprovalForm {
client_id: form.client_id,
redirect_uri: form.redirect_uri,
scope: form.scope,
state: form.state,
code_challenge: form.code_challenge,
code_challenge_method: form.code_challenge_method,
},
)
.await
}
/// Gateway approval form — same as ApprovalForm but includes workspace_id
#[derive(Debug, Deserialize)]
pub struct GatewayApprovalForm {
pub client_id: String,
pub redirect_uri: String,
pub scope: String,
pub state: String,
pub code_challenge: String,
pub code_challenge_method: String,
pub workspace_id: String,
}
// ── Router constructors ──
/// Mounted at /api/mcp/oauth/server
pub fn global_service() -> Router {
Router::new().route("/register", post(oauth_register))
@@ -869,3 +978,17 @@ pub fn workspaced_unauthed_service() -> Router {
pub fn workspaced_authed_service() -> Router {
Router::new().route("/approve", post(workspaced_oauth_approve))
}
/// Gateway OAuth endpoints that don't require authentication
/// Mounted at /api/mcp/gateway/oauth/server (outside authenticated section)
pub fn gateway_unauthed_service() -> Router {
Router::new()
.route("/authorize", get(gateway_oauth_authorize))
.route("/token", post(oauth_token))
}
/// Gateway OAuth endpoints that require authentication
/// Mounted at /api/mcp/gateway/oauth/server (inside authenticated section)
pub fn gateway_authed_service() -> Router {
Router::new().route("/approve", post(gateway_oauth_approve))
}

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