diff --git a/packages/server/src/api/workflows.js b/packages/server/src/api/workflows.js index ff1dadd..5adc062 100644 --- a/packages/server/src/api/workflows.js +++ b/packages/server/src/api/workflows.js @@ -706,15 +706,14 @@ export default function workflowsPluginFactory(registry) { return reply.code(err.statusCode ?? 400).send({ error: err.message }); } const raw = fsStore.readWorkflowYaml(owner, file); - if (raw == null) { - return reply.code(404).send({ error: "workflow not found" }); - } let name = null; - try { - const parsed = yaml.parse(raw); - name = parsed?.name ?? null; - } catch { - // ignore + if (raw != null) { + try { + const parsed = yaml.parse(raw); + name = parsed?.name ?? null; + } catch { + // ignore + } } try { const item = await moveWorkflowToTrash({ diff --git a/packages/server/test/workflow-history-smoke.js b/packages/server/test/workflow-history-smoke.js index 3694988..353ccee 100644 --- a/packages/server/test/workflow-history-smoke.js +++ b/packages/server/test/workflow-history-smoke.js @@ -136,6 +136,18 @@ const warnings = collectWorkflowWarnings( ); 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({ workflowId, owner, diff --git a/packages/server/workflow-trash.js b/packages/server/workflow-trash.js index f0cb464..6984275 100644 --- a/packages/server/workflow-trash.js +++ b/packages/server/workflow-trash.js @@ -62,29 +62,49 @@ export async function isInTrash(owner, file) { 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 {{ * workflowId: string, * owner: string, * file: string, * name?: string | null, * }} opts + * @returns {Promise | null>} trash item, or null if there was no file to keep */ export async function moveWorkflowToTrash(opts) { + unregisterWorkflow(opts.owner, opts.file); + const sourcePath = path.join(WORKFLOWS_DIR, opts.owner, opts.file); if (!fs.existsSync(sourcePath)) { - const err = new Error("workflow not found"); - err.statusCode = 404; - throw err; + const existing = await db("workflow_trash") + .where({ owner: opts.owner, file: opts.file }) + .first(); + return existing ? rowToItem(existing) : null; } const trashPath = trashFilePath(opts.owner, opts.file); fs.mkdirSync(path.dirname(trashPath), { recursive: true }); fs.renameSync(sourcePath, trashPath); - const registered = fsStore.readRegisters(opts.owner).filter((f) => f !== opts.file); - fsStore.writeRegisters(opts.owner, registered); + const existing = await db("workflow_trash") + .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 deleted_at = nowIso();