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:
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user