mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 00:05:27 +00:00
test: pin the two-key move, and stop the down migration breaking instances
Revoking the sequence grant would strip a privilege a healthy instance had before this migration ran — the grant it adds is indistinguishable in the catalog from the one ALTER DEFAULT PRIVILEGES gives at creation time — so the down is a comment, matching the other grant-only migrations. The mirror rewrite is spread over three sites that have to agree and fails silently when they don't, so it gets a test: a draft carrying both path keys has both moved, and one carrying neither mirror does not gain one. It reads the value back over HTTP rather than with `sqlx::query!`, which would need an offline cache entry of its own. Also drops twelve `.sqlx` entries this branch added and then superseded, and corrects the doc and openapi text that still described only the typed path being rewritten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0406c3625d
commit
ad97bcf110
-59
@@ -1,59 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT 1 FROM draft WHERE workspace_id = $1 AND path = $2 AND typ = $3 AND email = $4",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "draft_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"flow",
|
||||
"app",
|
||||
"raw_app",
|
||||
"resource",
|
||||
"variable",
|
||||
"trigger_schedule",
|
||||
"trigger_webhook",
|
||||
"trigger_default_email",
|
||||
"trigger_email",
|
||||
"trigger_http",
|
||||
"trigger_websocket",
|
||||
"trigger_postgres",
|
||||
"trigger_kafka",
|
||||
"trigger_nats",
|
||||
"trigger_mqtt",
|
||||
"trigger_sqs",
|
||||
"trigger_gcp",
|
||||
"trigger_azure",
|
||||
"trigger_poll",
|
||||
"trigger_cli",
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github",
|
||||
"data_pipeline",
|
||||
"trigger_amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "10f8e68e31df7b6e0bcd10091a46e8f2a00674eb87a5d025484751674656ca25"
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE draft AS d\n SET path = $3,\n value = to_json(\n jsonb_set(to_jsonb(d.value), ARRAY[$4::text], to_jsonb($3::text), false)\n )\n WHERE d.workspace_id = $1\n AND d.path = $2\n AND d.typ::text = ANY($5::text[])\n AND NOT EXISTS (\n SELECT 1 FROM draft o\n WHERE o.workspace_id = d.workspace_id\n AND o.path = $3\n AND o.typ = d.typ\n AND o.email IS NOT DISTINCT FROM d.email\n )\n RETURNING d.id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "30958446d29c3fa521bda176c23994caad0c7a1c05374f872f87ea838a412801"
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE draft AS d\n SET path = $3,\n value = to_json(\n CASE WHEN to_jsonb(d.value) -> $4::text = to_jsonb($2::text)\n THEN jsonb_set(to_jsonb(d.value), ARRAY[$4::text], to_jsonb($3::text), false)\n ELSE to_jsonb(d.value)\n END\n )\n WHERE d.workspace_id = $1\n AND d.path = $2\n AND d.typ::text = ANY($5::text[])\n AND NOT EXISTS (\n SELECT 1 FROM draft o\n WHERE o.workspace_id = d.workspace_id\n AND o.path = $3\n AND o.typ = d.typ\n AND o.email IS NOT DISTINCT FROM d.email\n )\n RETURNING d.id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "381d6ead880f0f0c32e6d87d053bf16fad254b0b070317d9bed4fd97541a0272"
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n EXISTS(SELECT 1 FROM flow WHERE workspace_id = $1 AND path = $2) as \"still_here!\",\n (SELECT fv.path FROM flow_version fv\n JOIN flow f ON f.workspace_id = fv.workspace_id AND f.path = fv.path\n WHERE fv.id = $3 AND fv.workspace_id = $1) as new_path,\n (SELECT f.edited_by FROM flow_version fv\n JOIN flow f ON f.workspace_id = fv.workspace_id AND f.path = fv.path\n WHERE fv.id = $3 AND fv.workspace_id = $1) as new_by",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "still_here!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "new_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "new_by",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "56c66db90fc43db69357c41d55d685fc9f4c2947ab6aef60b36d09c0edd1ce89"
|
||||
}
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE draft\n SET path = $3,\n value = to_json(\n jsonb_set(\n CASE WHEN $7::text IS NULL THEN to_jsonb(value)\n ELSE jsonb_set(to_jsonb(value), ARRAY['summary'], to_jsonb($7::text))\n END,\n ARRAY[$5::text], to_jsonb($3::text), false\n )\n )\n WHERE workspace_id = $1\n AND path = $2\n AND typ = $4\n AND email = $6\n AND NOT EXISTS (\n SELECT 1 FROM draft o\n WHERE o.workspace_id = $1 AND o.path = $3 AND o.typ = $4 AND o.email = $6\n )\n RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "draft_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"flow",
|
||||
"app",
|
||||
"raw_app",
|
||||
"resource",
|
||||
"variable",
|
||||
"trigger_schedule",
|
||||
"trigger_webhook",
|
||||
"trigger_default_email",
|
||||
"trigger_email",
|
||||
"trigger_http",
|
||||
"trigger_websocket",
|
||||
"trigger_postgres",
|
||||
"trigger_kafka",
|
||||
"trigger_nats",
|
||||
"trigger_mqtt",
|
||||
"trigger_sqs",
|
||||
"trigger_gcp",
|
||||
"trigger_azure",
|
||||
"trigger_poll",
|
||||
"trigger_cli",
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github",
|
||||
"data_pipeline",
|
||||
"trigger_amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5980a2c9348982eab53e092496dc2d2e2319d9e87a9deb05238f311928201695"
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n EXISTS(SELECT 1 FROM app WHERE workspace_id = $1 AND path = $2) as \"still_here!\",\n (SELECT a.path FROM app_version av JOIN app a ON a.id = av.app_id\n WHERE av.id = $3 AND a.workspace_id = $1) as new_path,\n (SELECT av.created_by FROM app_version av JOIN app a ON a.id = av.app_id\n WHERE av.id = $3 AND a.workspace_id = $1) as new_by",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "still_here!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "new_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "new_by",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5aafeaefd99e25c66c705e1cbc8681ccec81817226a5d31927b10bf97152c8d9"
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE draft AS d\n SET path = $3,\n value = CASE\n WHEN position(chr(92) || 'u0000' in d.value::text) > 0 THEN d.value\n WHEN to_jsonb(d.value) -> $4::text = to_jsonb($2::text)\n THEN to_json(jsonb_set(to_jsonb(d.value), ARRAY[$4::text], to_jsonb($3::text), false))\n ELSE d.value\n END\n WHERE d.workspace_id = $1\n AND d.path = $2\n AND d.typ::text = ANY($5::text[])\n AND NOT EXISTS (\n SELECT 1 FROM draft o\n WHERE o.workspace_id = d.workspace_id\n AND o.path = $3\n AND o.typ = d.typ\n AND o.email IS NOT DISTINCT FROM d.email\n )\n RETURNING d.id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "666eea2c3d584885923d86b296e197c9249a79402742e0345a7fdf5f43843917"
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE draft\n SET value = to_json(\n CASE WHEN to_jsonb(value) -> 'path' = to_jsonb($2::text)\n THEN jsonb_set(to_jsonb(value), ARRAY['path'], to_jsonb($1::text), false)\n ELSE to_jsonb(value)\n END\n )\n WHERE id = ANY($3)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6e4f241221ff60557d989890b06c9ef7af9869ceeee8030b6bd17d9103931e2f"
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n EXISTS(SELECT 1 FROM script WHERE workspace_id = $1 AND path = $2\n AND NOT archived AND NOT deleted) as \"still_here!\",\n (SELECT path FROM script WHERE workspace_id = $1 AND $3 = ANY(parent_hashes)\n AND NOT archived AND NOT deleted ORDER BY created_at DESC LIMIT 1) as new_path,\n (SELECT created_by FROM script WHERE workspace_id = $1 AND $3 = ANY(parent_hashes)\n AND NOT archived AND NOT deleted ORDER BY created_at DESC LIMIT 1) as new_by",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "still_here!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "new_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "new_by",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "9031e85807fb009d212ab5e9a158f2d467a10ae71ec0aa3f4aae603be1453798"
|
||||
}
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT a.path, av.created_by,\n (SELECT id FROM app_version WHERE app_id = a.id\n ORDER BY created_at DESC LIMIT 1) as \"head!\"\n FROM app_version av JOIN app a ON a.id = av.app_id\n WHERE av.id = $2 AND a.workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "created_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "head!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "ab4f1762b923774f43d5ed088d43828956701d9f9a08f8c202446ea06f292d46"
|
||||
}
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE draft\n SET path = $3,\n value = to_json(\n jsonb_set(\n CASE WHEN $7::text IS NULL THEN to_jsonb(value)\n ELSE jsonb_set(to_jsonb(value), ARRAY['summary'], to_jsonb($7::text))\n END,\n ARRAY[$5::text], to_jsonb($3::text), false\n )\n )\n WHERE workspace_id = $1\n AND path = $2\n AND typ = $4\n AND email = $6\n -- Skipped on a summary-only edit, where the \"target\" row is this\n -- row and the guard would refuse the update against itself.\n AND ($2 = $3 OR NOT EXISTS (\n SELECT 1 FROM draft o\n WHERE o.workspace_id = $1 AND o.path = $3 AND o.typ = $4 AND o.email = $6\n ))\n RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "draft_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"flow",
|
||||
"app",
|
||||
"raw_app",
|
||||
"resource",
|
||||
"variable",
|
||||
"trigger_schedule",
|
||||
"trigger_webhook",
|
||||
"trigger_default_email",
|
||||
"trigger_email",
|
||||
"trigger_http",
|
||||
"trigger_websocket",
|
||||
"trigger_postgres",
|
||||
"trigger_kafka",
|
||||
"trigger_nats",
|
||||
"trigger_mqtt",
|
||||
"trigger_sqs",
|
||||
"trigger_gcp",
|
||||
"trigger_azure",
|
||||
"trigger_poll",
|
||||
"trigger_cli",
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github",
|
||||
"data_pipeline",
|
||||
"trigger_amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "af8981997f299b5c943bbdffb4d52dbff1d6ef4c7c0118574b98e80a7b179e94"
|
||||
}
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE draft\n SET path = $3,\n value = to_json(\n jsonb_set(\n CASE WHEN $7::text IS NULL THEN to_jsonb(value)\n ELSE jsonb_set(to_jsonb(value), ARRAY['summary'], to_jsonb($7::text))\n END,\n ARRAY[$5::text], to_jsonb($3::text), false\n )\n )\n WHERE workspace_id = $1\n AND path = $2\n AND typ = $4\n AND email = $6\n -- A pre-sanitizer NUL escape makes `to_jsonb` raise 22P05. Excluded\n -- here so the statement can't 500; reported below instead. Unlike the\n -- passive carry, rewriting the value IS this operation, so skipping it\n -- silently would move the row and leave its typed path stale.\n AND position(chr(92) || 'u0000' in replace(value::text, chr(92) || chr(92), '')) = 0\n -- Skipped on a summary-only edit, where the \"target\" row is this\n -- row and the guard would refuse the update against itself.\n AND ($2 = $3 OR NOT EXISTS (\n SELECT 1 FROM draft o\n WHERE o.workspace_id = $1 AND o.path = $3 AND o.typ = $4 AND o.email = $6\n ))\n RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "draft_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"flow",
|
||||
"app",
|
||||
"raw_app",
|
||||
"resource",
|
||||
"variable",
|
||||
"trigger_schedule",
|
||||
"trigger_webhook",
|
||||
"trigger_default_email",
|
||||
"trigger_email",
|
||||
"trigger_http",
|
||||
"trigger_websocket",
|
||||
"trigger_postgres",
|
||||
"trigger_kafka",
|
||||
"trigger_nats",
|
||||
"trigger_mqtt",
|
||||
"trigger_sqs",
|
||||
"trigger_gcp",
|
||||
"trigger_azure",
|
||||
"trigger_poll",
|
||||
"trigger_cli",
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github",
|
||||
"data_pipeline",
|
||||
"trigger_amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c902cf38cea14284032312f833ed84637926113f1f6aef6b58e24c88930f1432"
|
||||
}
|
||||
@@ -1,2 +1,5 @@
|
||||
REVOKE ALL ON SEQUENCE draft_id_seq FROM windmill_user;
|
||||
REVOKE ALL ON SEQUENCE draft_id_seq FROM windmill_admin;
|
||||
-- Deliberately empty. On an instance where 20250205131523 succeeded, this grant
|
||||
-- came from its ALTER DEFAULT PRIVILEGES when the sequence was created, and the
|
||||
-- catalog keeps no record distinguishing that from the one above — so a REVOKE
|
||||
-- here would strip a privilege the instance had before this migration ran, and
|
||||
-- leave every autosave failing with `permission denied for sequence`.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
//! Moving a draft must carry both of its path keys.
|
||||
//!
|
||||
//! A draft value holds a typed path and a mirror the editors keep beside it while
|
||||
//! it differs from the row's path (`path`/`draft_path`; which is which depends on
|
||||
//! the kind). The loaders prefer the mirror, so a move that rewrote only the typed
|
||||
//! key left the mirror naming the old location: reopening the item restored the
|
||||
//! old path, and the next autosave wrote it back — undoing the move silently.
|
||||
//!
|
||||
//! The rule is spread over three sites that have to agree (`move_draft`, the
|
||||
//! passive carry in `move_drafts_for_path`, and `resolve_moved_to_in`'s patch);
|
||||
//! this pins the endpoint, including that a draft with no mirror never gains one.
|
||||
|
||||
use serde_json::Value;
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
use windmill_test_utils::*;
|
||||
|
||||
async fn move_to(port: u16, from: &str, to: &str) -> anyhow::Result<String> {
|
||||
Ok(reqwest::Client::new()
|
||||
.post(format!(
|
||||
"http://localhost:{port}/api/w/test-workspace/drafts/move/script/{from}"
|
||||
))
|
||||
.header("Authorization", "Bearer SECRET_TOKEN")
|
||||
.json(&serde_json::json!({ "new_path": to }))
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// The stored draft value at `path`, read back through the API so this test needs
|
||||
/// no `sqlx::query!` (which would want an offline cache entry of its own).
|
||||
async fn value_at(port: u16, path: &str) -> anyhow::Result<Value> {
|
||||
let body: Value = reqwest::Client::new()
|
||||
.get(format!(
|
||||
"http://localhost:{port}/api/w/test-workspace/drafts/get_own/script/{path}"
|
||||
))
|
||||
.header("Authorization", "Bearer SECRET_TOKEN")
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
Ok(body
|
||||
.get("value")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| panic!("no draft at {path}: {body}")))
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base", "drafts_move_mirror"))]
|
||||
async fn test_move_carries_both_path_keys(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
move_to(port, "u/test-user/draft_mirror", "u/test-user/renamed").await?;
|
||||
move_to(port, "u/test-user/draft_plain", "u/test-user/plain2").await?;
|
||||
|
||||
// The mirror follows: left at `u/test-user/friendly` it would win at load and
|
||||
// walk the item back there.
|
||||
let moved = value_at(port, "u/test-user/renamed").await?;
|
||||
assert_eq!(moved["path"], "u/test-user/renamed");
|
||||
assert_eq!(moved["draft_path"], "u/test-user/renamed");
|
||||
|
||||
// A draft that never had a mirror must not be given one.
|
||||
let plain = value_at(port, "u/test-user/plain2").await?;
|
||||
assert_eq!(plain["path"], "u/test-user/plain2");
|
||||
assert_eq!(plain.get("draft_path"), None, "mirror injected: {plain}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
-- Fixture for the two-path-key move test.
|
||||
--
|
||||
-- Two draft-only scripts owned by test-user, both parked at a generated storage
|
||||
-- key the way a session-created draft is. One carries the `draft_path` mirror the
|
||||
-- session editor writes while the typed path differs from that key; the other
|
||||
-- carries no mirror at all, which is what a plain draft looks like.
|
||||
|
||||
INSERT INTO draft (workspace_id, path, typ, value, email) VALUES
|
||||
('test-workspace', 'u/test-user/draft_mirror', 'script',
|
||||
'{"path": "u/test-user/friendly", "draft_path": "u/test-user/friendly",
|
||||
"content": "x", "language": "bun", "summary": "S"}',
|
||||
'test@windmill.dev'),
|
||||
('test-workspace', 'u/test-user/draft_plain', 'script',
|
||||
'{"path": "u/test-user/draft_plain",
|
||||
"content": "x", "language": "bun", "summary": "S"}',
|
||||
'test@windmill.dev');
|
||||
@@ -1393,7 +1393,7 @@ async fn update_flow(
|
||||
flow_path,
|
||||
&nf.path,
|
||||
UserDraftItemKind::Flow.typed_path_field(),
|
||||
UserDraftItemKind::Flow.mirror_path_field(),
|
||||
UserDraftItemKind::Flow.mirror_path_field(),
|
||||
UserDraftItemKind::Flow
|
||||
.base_version_field()
|
||||
.map(|f| (f, version.to_string())),
|
||||
|
||||
@@ -9457,7 +9457,7 @@ paths:
|
||||
/w/{workspace}/drafts/move/{kind}/{path}:
|
||||
post:
|
||||
summary: move the current user's draft-only item to another path
|
||||
description: Relocates the authed user's own draft row (and the typed path inside its value). Only for draft-only items — a deployed item must be moved through its own deploy endpoint, which carries every draft with it. Restricted to script, flow, app and raw_app; any other kind is rejected with 400, because only these keep their deploy target where this endpoint rewrites it.
|
||||
description: Relocates the authed user's own draft row, along with both path keys inside its value (the typed path and the mirror the editors keep beside it). Only for draft-only items — a deployed item must be moved through its own deploy endpoint, which carries every draft with it. Restricted to script, flow, app and raw_app; any other kind is rejected with 400, because only these keep their deploy target where this endpoint rewrites it.
|
||||
operationId: moveDraft
|
||||
tags:
|
||||
- draft
|
||||
|
||||
@@ -3458,7 +3458,7 @@ async fn update_app_internal<'a>(
|
||||
path,
|
||||
&npath,
|
||||
UserDraftItemKind::App.typed_path_field(),
|
||||
UserDraftItemKind::App.mirror_path_field(),
|
||||
UserDraftItemKind::App.mirror_path_field(),
|
||||
UserDraftItemKind::App
|
||||
.base_version_field()
|
||||
.map(|f| (f, v_id.to_string())),
|
||||
|
||||
@@ -909,8 +909,8 @@ pub struct MoveDraftRequest {
|
||||
}
|
||||
|
||||
/// Relocate the authed user's own DRAFT-ONLY item. Such an item is nothing but
|
||||
/// its draft row, so moving it is a rewrite of that row's path plus the typed
|
||||
/// path inside its value — there is no deployed row, schedule or trigger to
|
||||
/// its draft row, so moving it is a rewrite of that row's path plus both path
|
||||
/// keys inside its value — there is no deployed row, schedule or trigger to
|
||||
/// cascade to, and no second party to notify (a draft is private to its owner).
|
||||
///
|
||||
/// Scoped to the caller's own row on purpose: two users can each have a draft
|
||||
|
||||
Reference in New Issue
Block a user