Fix delete FK exhaustively; unset-scope surfaces as drift
deleteScript: previous fix missed beta_servers.prior_version_id, beta_server_scripts.prior_version_id, and deployments.beta_id. Cascade from betas.script_id -> beta_servers/beta_server_scripts left dangling prior_version_id refs, and deletions of cascaded betas dangled deployments.beta_id. Now nulls every NO ACTION reference before the DELETE inside a single transaction. Scope unset no longer touches disk from the API. Instead it detaches inventory rows for scripts/plugins so drift Pass 1 flags the on-disk copies as untracked; configs rely on drift Pass 2's compose-diff, which already handles scope=null naturally. The operator reconciles from /deployments using the existing rescan + reconcile buttons — same visible flow as any other drift, no more silent synchronous file removal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1020,26 +1020,36 @@ export function latestVersionForScript(script: ScriptRow): ScriptVersionRow | nu
|
||||
}
|
||||
|
||||
/**
|
||||
* Un-set a script's scope. When scope is being removed and matching servers
|
||||
* currently have the script, remove it from them for consistency. Returns the
|
||||
* server names that had files deleted.
|
||||
* Un-set a script's scope. Files are left on disk so drift picks up the
|
||||
* change on the next /deployments rescan and the operator reconciles from
|
||||
* there like any other drift — the same visible flow as manual drift.
|
||||
*
|
||||
* For scripts/plugins, we detach the inventory rows for this artifact's
|
||||
* versions so drift Pass 1 flags the on-disk copies as untracked. For
|
||||
* configs/baselines, no detach is needed: drift Pass 2 recomposes the
|
||||
* expected dedicated_sp.cfg against the current promoted overlays, and
|
||||
* scope=null naturally drops the overlay from the compose, surfacing the
|
||||
* disparity. Returns the servers whose inventory was detached (empty for
|
||||
* configs/baselines).
|
||||
*/
|
||||
export async function unsetScopeAndCleanup(scriptId: number): Promise<string[]> {
|
||||
export function unsetScopeAndDetach(scriptId: number): string[] {
|
||||
const script = getScript(scriptId);
|
||||
if (!script) throw new BetaError(`no such script (id=${scriptId})`, 'no-such-script');
|
||||
if (!script.scope_type || !script.scope_value) return [];
|
||||
|
||||
const matched = serversMatchingScope(script.scope_type, script.scope_value);
|
||||
const cleaned: string[] = [];
|
||||
for (const name of matched) {
|
||||
const dest = destOfKind(name, script.kind);
|
||||
const targetPath = join(dest, script.target_rel_path);
|
||||
if (existsSync(targetPath)) {
|
||||
await rm(targetPath, { force: true });
|
||||
removeScript(name, script.target_rel_path);
|
||||
cleaned.push(name);
|
||||
const detached: string[] = [];
|
||||
if (script.kind === 'script' || script.kind === 'plugin') {
|
||||
const rows = db()
|
||||
.prepare(
|
||||
`SELECT server FROM server_scripts
|
||||
WHERE rel_path = ?
|
||||
AND script_version_id IN (SELECT id FROM script_versions WHERE script_id = ?)`
|
||||
)
|
||||
.all(script.target_rel_path, scriptId) as Array<{ server: string }>;
|
||||
for (const { server } of rows) {
|
||||
removeScript(server, script.target_rel_path);
|
||||
detached.push(server);
|
||||
}
|
||||
}
|
||||
db().prepare('UPDATE scripts SET scope_type = NULL, scope_value = NULL WHERE id = ?').run(scriptId);
|
||||
return cleaned;
|
||||
return detached;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user