mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-28 16:02:47 +00:00
fix: drop script lineage a fork does not have when copying drafts into it
A script draft kept its base and parent_hash on the grounds that a fork shares its parent's script hashes. That holds for what the fork cloned, but not for a version deployed in the parent afterwards, and a deploy in the fork naming it as parent is refused. The copy now keeps the lineage only when the target has that version. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
bc08c7ffb0
commit
a0696922d3
-66
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO draft (workspace_id, path, typ, value, created_at, email, base)\n SELECT $2, path, typ,\n to_json(\n CASE WHEN typ IN ('script', 'flow') THEN clean - 'on_behalf_of' ELSE clean END\n - CASE WHEN typ = 'flow' THEN 'version_id'\n WHEN typ IN ('app', 'raw_app') THEN 'parent_version'\n ELSE '' END\n ),\n created_at, email,\n CASE WHEN typ = 'script' THEN base END\n FROM (\n SELECT d.path, d.typ, d.created_at, d.email, d.base,\n replace(replace(replace(d.value::text, chr(92) || chr(92), chr(1)), chr(92) || 'u0000', ''), chr(1), chr(92) || chr(92))::jsonb AS clean\n FROM draft d\n WHERE d.workspace_id = $1\n AND CASE WHEN $4::text[] IS NULL THEN d.email = $3 OR d.email IS NULL\n ELSE d.email = $3\n AND (d.typ::text, d.path) IN (SELECT * FROM unnest($4::text[], $5::text[]))\n END\n ) s\n RETURNING typ AS \"typ: UserDraftItemKind\", path",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "typ: UserDraftItemKind",
|
||||
"type_info": {
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "16a1515956430730ac432af5d0bbdd4c20cd50574a47f3d76639cecd7aff67c4"
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO draft (workspace_id, path, typ, value, created_at, email, base)\n SELECT $2::text, path, typ,\n to_json(\n CASE WHEN typ IN ('script', 'flow') THEN clean - 'on_behalf_of' ELSE clean END\n - CASE WHEN typ = 'flow' THEN 'version_id'\n WHEN typ IN ('app', 'raw_app') THEN 'parent_version'\n ELSE '' END\n - CASE WHEN typ = 'script' AND NOT EXISTS (\n SELECT 1 FROM script sc WHERE sc.workspace_id::text = $2::text\n AND lpad(to_hex(sc.hash), 16, '0') = clean->>'parent_hash')\n THEN 'parent_hash' ELSE '' END\n ),\n created_at, email,\n CASE WHEN typ = 'script' AND EXISTS (\n SELECT 1 FROM script sc WHERE sc.workspace_id::text = $2::text\n AND lpad(to_hex(sc.hash), 16, '0') = base)\n THEN base END\n FROM (\n SELECT d.path, d.typ, d.created_at, d.email, d.base,\n replace(replace(replace(d.value::text, chr(92) || chr(92), chr(1)), chr(92) || 'u0000', ''), chr(1), chr(92) || chr(92))::jsonb AS clean\n FROM draft d\n WHERE d.workspace_id = $1\n AND CASE WHEN $4::text[] IS NULL THEN d.email = $3 OR d.email IS NULL\n ELSE d.email = $3\n AND (d.typ::text, d.path) IN (SELECT * FROM unnest($4::text[], $5::text[]))\n END\n ) s\n RETURNING typ AS \"typ: UserDraftItemKind\", path",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "typ: UserDraftItemKind",
|
||||
"type_info": {
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "8b62d25c98a6b3db3a0b7039cacf79799e6d7c7c6c9532545844bec9ec314516"
|
||||
}
|
||||
@@ -467,6 +467,32 @@ async fn test_transfer_drafts_to_fork(db: Pool<Postgres>) -> anyhow::Result<()>
|
||||
.await?;
|
||||
assert_eq!(on_behalf_of, "u/test-user");
|
||||
|
||||
// A script version the fork does not have (deployed in the source after the fork was made)
|
||||
// is no lineage there: a deploy naming it as parent would be refused.
|
||||
let r = c
|
||||
.post(save_url(port, "script", "u/test-user/lineage"))
|
||||
.json(&json!({ "value": { "content": "l", "parent_hash": "0123456789abcdef" } }))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(r.status(), 200, "{}", r.text().await?);
|
||||
let r = c
|
||||
.post(&transfer_url)
|
||||
.json(&json!({
|
||||
"target_workspace": fork,
|
||||
"items": [{ "kind": "script", "path": "u/test-user/lineage" }]
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(r.status(), 200, "{}", r.text().await?);
|
||||
let (parent_hash, base): (Option<String>, Option<String>) = sqlx::query_as(
|
||||
"SELECT value->>'parent_hash', base FROM draft
|
||||
WHERE workspace_id = $1 AND path = 'u/test-user/lineage'",
|
||||
)
|
||||
.bind(fork)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert_eq!((parent_hash, base), (None, None));
|
||||
|
||||
// Only a fork of the source is a valid target.
|
||||
let r = c
|
||||
.post(format!(
|
||||
|
||||
@@ -695,10 +695,11 @@ pub async fn copy_drafts_to_workspace(
|
||||
// deploy time, which is the more accurate answer of the two.
|
||||
let rows = sqlx::query!(
|
||||
// A script hash is content-addressed and shared between a workspace and its forks, so
|
||||
// a script draft's base still names a version the target can have. Flow and app ids
|
||||
// are minted per workspace, so those drafts arrive with no base (staleness falls back
|
||||
// to the timestamps), lineage field included, or the next autosave would re-derive the
|
||||
// source id.
|
||||
// a script draft keeps its base (and `parent_hash`) when the target has that version,
|
||||
// which a fork's clone does; one deployed in the source after the fork was made is not
|
||||
// there, and a deploy naming it as parent would be refused. Flow and app ids are minted
|
||||
// per workspace, so those drafts arrive with no base (staleness falls back to the
|
||||
// timestamps), lineage field included, or the next autosave would re-derive the source id.
|
||||
//
|
||||
// `clean` is `strip_json_nul`'s parity rule in SQL, so a pre-sanitizer U+0000 escape
|
||||
// cannot abort the copy on `to_jsonb` or arrive with its principal unstripped: escaped
|
||||
@@ -706,15 +707,22 @@ pub async fn copy_drafts_to_workspace(
|
||||
// byte) so only a real NUL is removed, and chr(92) spells the backslash so no escape
|
||||
// sequence reaches this source file.
|
||||
r#"INSERT INTO draft (workspace_id, path, typ, value, created_at, email, base)
|
||||
SELECT $2, path, typ,
|
||||
SELECT $2::text, path, typ,
|
||||
to_json(
|
||||
CASE WHEN typ IN ('script', 'flow') THEN clean - 'on_behalf_of' ELSE clean END
|
||||
- CASE WHEN typ = 'flow' THEN 'version_id'
|
||||
WHEN typ IN ('app', 'raw_app') THEN 'parent_version'
|
||||
ELSE '' END
|
||||
- CASE WHEN typ = 'script' AND NOT EXISTS (
|
||||
SELECT 1 FROM script sc WHERE sc.workspace_id::text = $2::text
|
||||
AND lpad(to_hex(sc.hash), 16, '0') = clean->>'parent_hash')
|
||||
THEN 'parent_hash' ELSE '' END
|
||||
),
|
||||
created_at, email,
|
||||
CASE WHEN typ = 'script' THEN base END
|
||||
CASE WHEN typ = 'script' AND EXISTS (
|
||||
SELECT 1 FROM script sc WHERE sc.workspace_id::text = $2::text
|
||||
AND lpad(to_hex(sc.hash), 16, '0') = base)
|
||||
THEN base END
|
||||
FROM (
|
||||
SELECT d.path, d.typ, d.created_at, d.email, d.base,
|
||||
replace(replace(replace(d.value::text, chr(92) || chr(92), chr(1)), chr(92) || 'u0000', ''), chr(1), chr(92) || chr(92))::jsonb AS clean
|
||||
|
||||
Reference in New Issue
Block a user