mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 16:05:58 +00:00
fix: read the app move's author from the head version, not the draft's base
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6093206ad2
commit
7350705cea
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT a.path, head.id as \"head!\", head.created_by as \"head_by!\"\n FROM app_version av\n JOIN app a ON a.id = av.app_id\n JOIN LATERAL (\n SELECT id, created_by FROM app_version\n WHERE app_id = a.id ORDER BY created_at DESC LIMIT 1\n ) head ON true\n WHERE av.id = $2 AND a.workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "head!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "head_by!",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "323f975428149815f3a9bc5c47396c1ad11eab84d03dd8c435b824e2e1b03ff1"
|
||||
}
|
||||
@@ -501,18 +501,26 @@ async fn resolve_moved_to(
|
||||
if still_here {
|
||||
None
|
||||
} else {
|
||||
// `created_by` must come from the HEAD row, not from `av` — `av`
|
||||
// is the version the draft forked from, whose author is usually
|
||||
// the person now reading this. Naming them would make
|
||||
// `moved_by_me` true for the wrong user and restamp a draft that
|
||||
// has never seen the head's content.
|
||||
sqlx::query!(
|
||||
r#"SELECT a.path, av.created_by,
|
||||
(SELECT id FROM app_version WHERE app_id = a.id
|
||||
ORDER BY created_at DESC LIMIT 1) as "head!"
|
||||
FROM app_version av JOIN app a ON a.id = av.app_id
|
||||
r#"SELECT a.path, head.id as "head!", head.created_by as "head_by!"
|
||||
FROM app_version av
|
||||
JOIN app a ON a.id = av.app_id
|
||||
JOIN LATERAL (
|
||||
SELECT id, created_by FROM app_version
|
||||
WHERE app_id = a.id ORDER BY created_at DESC LIMIT 1
|
||||
) head ON true
|
||||
WHERE av.id = $2 AND a.workspace_id = $1"#,
|
||||
w_id,
|
||||
base.parent_version,
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
.map(|r| (r.path, Some(r.created_by), json!(r.head)))
|
||||
.map(|r| (r.path, Some(r.head_by), json!(r.head)))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
@@ -530,12 +538,19 @@ async fn resolve_moved_to(
|
||||
// naming anywhere else ⇒ omit, so a rename staged in this very editor
|
||||
// survives the relocation. Same rule as `move_drafts_for_path`.
|
||||
//
|
||||
// The version is restamped only for whoever performed the move, matching the
|
||||
// `restamp_email` scoping on the passive carry and for the same reason. The
|
||||
// deploy that moved the item may have edited it in the same breath; handing a
|
||||
// teammate the new head would tell them they are up to date with content they
|
||||
// have never seen, and their next deploy would silently revert it. The mover
|
||||
// knows what they just pushed, so only they are spared the prompt.
|
||||
// The version is restamped only for the LAST DEPLOYER AT THE NEW PATH, which
|
||||
// is the strongest "did I put the content there?" test available without
|
||||
// comparing payloads — nothing records who performed a move as distinct from
|
||||
// who deployed. It is deliberately weaker than "the mover": if Alice moves
|
||||
// A→B and Bob then deploys at B, Bob is spared the prompt for a head that
|
||||
// also carries Alice's move-time edits. That residual is a missing prompt for
|
||||
// someone who did deploy the head, not for a bystander.
|
||||
//
|
||||
// The point of the scoping is the bystander: the deploy that moved the item
|
||||
// may have edited it in the same breath, and handing a teammate the new head
|
||||
// would tell them they are up to date with content they have never seen, so
|
||||
// their next deploy would silently revert it. Mirrors `restamp_email` on the
|
||||
// passive carry in `move_drafts_for_path`.
|
||||
let repoint_path = base.typed_path(kind) == Some(path);
|
||||
Ok(moved.map(|(new_path, new_by, head)| {
|
||||
let moved_by_me = new_by.as_deref() == Some(authed.username.as_str());
|
||||
|
||||
@@ -262,6 +262,17 @@ export async function migrateUserDraftsToDb(): Promise<void> {
|
||||
path,
|
||||
requestBody: { value, last_sync: writtenAt, created_at: writtenAt }
|
||||
})
|
||||
if (res.status === 'moved') {
|
||||
// Nothing was written server-side: the item left this path. Unlike a
|
||||
// conflict, where a fresher server row already holds the work, dropping
|
||||
// the LS entry here would destroy the only copy that exists. Leave it
|
||||
// and let a later mount retry — by then the user has usually followed
|
||||
// the item, and this path resolves normally.
|
||||
console.warn(
|
||||
`UserDraft LS→DB migration: ${path} was moved to ${res.moved_to}, keeping the LS copy`
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (res.status === 'conflict') {
|
||||
console.info(
|
||||
`UserDraft LS→DB migration: server draft for ${path} is fresher, dropping LS copy`
|
||||
|
||||
Reference in New Issue
Block a user