mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 08:02:47 +00:00
* feat: version resource values with history, diff and restore Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: record resource versions in a trigger so direct writes are covered Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: show the selected version's value and tighten history write access Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * perf: gate resource version recording in trigger WHEN clauses Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: clear a resource's past versions, and address review nits Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: restore the displayed version and keep author attribution on pooled writes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: scope history to the selected workspace and gate clearing on ownership Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: gate restore on write access and clearing on the signed-in workspace Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(frontend): share the version-history row between script and resource drawers Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * perf: trim resource version history in the monitor sweep, not on write Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(frontend): match the script versions drawer shell for resource history Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * perf(frontend): highlight version values instead of mounting monaco Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(frontend): match the script drawer's code preview presentation Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: rank version trim in one windowed pass instead of a correlated delete Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(frontend): treat the newest version as current by position Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * perf: gate the resource version trim to an hourly sweep Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: unnest the version row action and correct the trim cadence docs * perf: cap the history listing and use sets for reference lookup * feat: warn when a resource is written more than 60 times a minute * fix: lower the resource write advisory to 20 per minute * fix: discard stale history loads and never diff against an unread value * fix: correct the write advisory boundary and document the eviction lock * fix: read history and the live value from one snapshot * refactor: read the drawer's diff baseline from versions, not the live resource * fix: open the history drawer with no version selected * fix: disarm the clear confirmation and clear the pane when the selection moves * fix: explain the missing diff and drop a guard that can no longer fire --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
109 lines
5.5 KiB
PL/PgSQL
109 lines
5.5 KiB
PL/PgSQL
-- Add up migration script here
|
|
|
|
-- Append-only value history for resources. Keyed by (workspace_id, path) with a
|
|
-- cascading FK so a rename carries the history along and a delete takes it with it;
|
|
-- without ON UPDATE CASCADE a rename would strand the history at the old path.
|
|
CREATE TABLE resource_version (
|
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
workspace_id VARCHAR(50) NOT NULL,
|
|
path VARCHAR(255) NOT NULL,
|
|
resource_type VARCHAR(50) NOT NULL,
|
|
value JSONB,
|
|
created_by VARCHAR(500),
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
FOREIGN KEY (workspace_id, path) REFERENCES resource (workspace_id, path)
|
|
ON DELETE CASCADE ON UPDATE CASCADE
|
|
);
|
|
|
|
-- Serves both "latest version of this path" and the paginated history listing.
|
|
CREATE INDEX index_resource_version_path ON resource_version (workspace_id, path, id DESC);
|
|
|
|
GRANT ALL ON resource_version TO windmill_admin;
|
|
GRANT ALL ON resource_version TO windmill_user;
|
|
|
|
ALTER TABLE resource_version ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY admin_policy ON resource_version FOR ALL TO windmill_admin USING (true);
|
|
|
|
-- Read visibility follows the parent resource: the subquery is itself subject to resource's
|
|
-- own path/extra_perms policies, so the several rules there stay stated once instead of being
|
|
-- mirrored (and left to drift) here.
|
|
--
|
|
-- SELECT only, deliberately. `FOR ALL ... USING` would be reused as the INSERT/UPDATE/DELETE
|
|
-- check expression, and since the subquery is a SELECT it applies resource's *read* policies —
|
|
-- which would let a user with read-only access to a resource write its history. Nothing but the
|
|
-- trigger below writes this table, and it is SECURITY DEFINER so it does not need a policy.
|
|
CREATE POLICY see_parent_resource ON resource_version FOR SELECT TO windmill_user
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM resource r
|
|
WHERE r.workspace_id = resource_version.workspace_id
|
|
AND r.path = resource_version.path
|
|
)
|
|
);
|
|
|
|
-- Seed the value every existing resource currently holds, so the first edit after
|
|
-- this migration has something to diff against. `state` and `cache` are machine
|
|
-- churn (setState, job result caching) and are excluded here and in the trigger, the
|
|
-- same way workspace export excludes them (INTERNAL_RESOURCE_TYPES in
|
|
-- windmill-store/src/resources.rs — keep the two lists in step).
|
|
INSERT INTO resource_version (workspace_id, path, resource_type, value, created_by, created_at)
|
|
SELECT workspace_id, path, resource_type, value, created_by, COALESCE(edited_at, now())
|
|
FROM resource
|
|
WHERE resource_type NOT IN ('state', 'cache');
|
|
|
|
-- Recording lives in a trigger rather than the API handlers because resource values are
|
|
-- written from well outside them: a variable rename rewrites its linked resource
|
|
-- (windmill-store/src/variables.rs), workspace forks clone resources wholesale
|
|
-- (windmill-api-workspaces/src/workspaces.rs), native integrations upsert them
|
|
-- (windmill-native-triggers/src/workspace_integrations.rs), and trashbin restores
|
|
-- reinsert them (windmill-api/src/trash.rs). Handler-level hooks would miss all of those
|
|
-- and every path added later, leaving a history that silently disagrees with the value it
|
|
-- claims to describe.
|
|
--
|
|
-- Deleting a resource hard-deletes it, so the FK cascade takes its whole history with it: a
|
|
-- trashbin restore lands on an empty history and starts a new one at v1.
|
|
CREATE OR REPLACE FUNCTION record_resource_version() RETURNS trigger AS $$
|
|
BEGIN
|
|
-- `session.user` is set by UserDB::begin for authed requests; worker and system writes fall
|
|
-- back to the row's own author. NULLIF because a transaction-local set_config resets the
|
|
-- placeholder to the empty string rather than unsetting it, so a pooled connection that
|
|
-- previously served an authed request reports '' here, not NULL.
|
|
INSERT INTO resource_version (workspace_id, path, resource_type, value, created_by)
|
|
VALUES (
|
|
NEW.workspace_id, NEW.path, NEW.resource_type, NEW.value,
|
|
COALESCE(NULLIF(current_setting('session.user', true), ''), NEW.created_by)
|
|
);
|
|
|
|
-- The per-path cap is enforced by trim_resource_versions in the monitor, not here: trimming
|
|
-- on every write would tax a path `setResource` can drive in a loop, to keep a bound that
|
|
-- does not need to hold instantaneously.
|
|
|
|
RETURN NEW;
|
|
END;
|
|
-- SECURITY DEFINER so history is written on behalf of every writer, including the read-only
|
|
-- policy above, without granting anyone direct write access to the table. `SET search_path FROM
|
|
-- CURRENT` is the injection hardening that goes with it, captured rather than hardcoded so
|
|
-- installs running a non-public PG_SCHEMA still resolve (see
|
|
-- 20260624103600_repair_folder_labels_search_path.up.sql).
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT;
|
|
|
|
-- Split in two, and gated in WHEN rather than in the function body, so the rows that never
|
|
-- produce a version never enter plpgsql at all: `state`/`cache` writes (one per setState and
|
|
-- per cached job result — by far the hottest writers of this table) and updates that leave the
|
|
-- value alone. Separate triggers because a WHEN on INSERT cannot reference OLD.
|
|
CREATE TRIGGER record_resource_version_insert_trigger
|
|
AFTER INSERT ON resource
|
|
FOR EACH ROW
|
|
WHEN (NEW.resource_type NOT IN ('state', 'cache'))
|
|
EXECUTE FUNCTION record_resource_version();
|
|
|
|
CREATE TRIGGER record_resource_version_update_trigger
|
|
AFTER UPDATE ON resource
|
|
FOR EACH ROW
|
|
WHEN (
|
|
NEW.resource_type NOT IN ('state', 'cache')
|
|
AND NEW.value IS DISTINCT FROM OLD.value
|
|
)
|
|
EXECUTE FUNCTION record_resource_version();
|