fix(workflows): unregister missing yaml on trash

Ghost register entries returned 404 on delete and stayed in the list. Unregister first so move-to-trash still removes them when the file is gone.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-23 09:17:10 +07:00
co-authored by Cursor
parent dd98421b08
commit e2e70a5aad
3 changed files with 45 additions and 14 deletions
+7 -8
View File
@@ -706,15 +706,14 @@ export default function workflowsPluginFactory(registry) {
return reply.code(err.statusCode ?? 400).send({ error: err.message }); return reply.code(err.statusCode ?? 400).send({ error: err.message });
} }
const raw = fsStore.readWorkflowYaml(owner, file); const raw = fsStore.readWorkflowYaml(owner, file);
if (raw == null) {
return reply.code(404).send({ error: "workflow not found" });
}
let name = null; let name = null;
try { if (raw != null) {
const parsed = yaml.parse(raw); try {
name = parsed?.name ?? null; const parsed = yaml.parse(raw);
} catch { name = parsed?.name ?? null;
// ignore } catch {
// ignore
}
} }
try { try {
const item = await moveWorkflowToTrash({ const item = await moveWorkflowToTrash({
@@ -136,6 +136,18 @@ const warnings = collectWorkflowWarnings(
); );
assert.ok(warnings.warnings.some((w) => w.code === "unknown_script")); assert.ok(warnings.warnings.some((w) => w.code === "unknown_script"));
const ghostFile = newWorkflowFilename();
fsStore.writeRegisters(owner, [file, ghostFile]);
const ghostTrash = await moveWorkflowToTrash({
workflowId: workflowIdFromFile(ghostFile),
owner,
file: ghostFile,
name: null,
});
assert.equal(ghostTrash, null);
assert.ok(!fsStore.readRegisters(owner).includes(ghostFile));
assert.ok(fsStore.readRegisters(owner).includes(file));
const trashed = await moveWorkflowToTrash({ const trashed = await moveWorkflowToTrash({
workflowId, workflowId,
owner, owner,
+26 -6
View File
@@ -62,29 +62,49 @@ export async function isInTrash(owner, file) {
return Boolean(row); return Boolean(row);
} }
function unregisterWorkflow(owner, file) {
const registered = fsStore.readRegisters(owner).filter((f) => f !== file);
fsStore.writeRegisters(owner, registered);
}
/** /**
* Soft-delete: move YAML to trash dir, unregister, keep revision history. * Soft-delete: unregister, move YAML to trash when present, keep revision history.
* Missing YAML (ghost register entries) is still unregistered so it leaves the list.
* @param {{ * @param {{
* workflowId: string, * workflowId: string,
* owner: string, * owner: string,
* file: string, * file: string,
* name?: string | null, * name?: string | null,
* }} opts * }} opts
* @returns {Promise<ReturnType<typeof rowToItem> | null>} trash item, or null if there was no file to keep
*/ */
export async function moveWorkflowToTrash(opts) { export async function moveWorkflowToTrash(opts) {
unregisterWorkflow(opts.owner, opts.file);
const sourcePath = path.join(WORKFLOWS_DIR, opts.owner, opts.file); const sourcePath = path.join(WORKFLOWS_DIR, opts.owner, opts.file);
if (!fs.existsSync(sourcePath)) { if (!fs.existsSync(sourcePath)) {
const err = new Error("workflow not found"); const existing = await db("workflow_trash")
err.statusCode = 404; .where({ owner: opts.owner, file: opts.file })
throw err; .first();
return existing ? rowToItem(existing) : null;
} }
const trashPath = trashFilePath(opts.owner, opts.file); const trashPath = trashFilePath(opts.owner, opts.file);
fs.mkdirSync(path.dirname(trashPath), { recursive: true }); fs.mkdirSync(path.dirname(trashPath), { recursive: true });
fs.renameSync(sourcePath, trashPath); fs.renameSync(sourcePath, trashPath);
const registered = fsStore.readRegisters(opts.owner).filter((f) => f !== opts.file); const existing = await db("workflow_trash")
fsStore.writeRegisters(opts.owner, registered); .where({ owner: opts.owner, file: opts.file })
.first();
if (existing) {
await db("workflow_trash").where({ id: existing.id }).update({
workflow_id: opts.workflowId,
name: opts.name ?? existing.name ?? null,
deleted_at: nowIso(),
trash_path: trashPath,
});
return rowToItem(await db("workflow_trash").where({ id: existing.id }).first());
}
const id = randomUUID(); const id = randomUUID();
const deleted_at = nowIso(); const deleted_at = nowIso();