Files
jerapah-flow/packages/server/workflow-normalize.js
T
Cursor Agentandnsrb 7d51ff433f feat(workflows): add revision history, trash, UUID naming, and backup
- Store up to 50 revisions per workflow in SQLite with normalized SHA dedup
- Soft-delete workflows to trash (7-day retention) with restore and permanent purge
- Assign UUID filenames for new and duplicated workflows; show name + file in UI
- Warn on invalid YAML, unknown scripts, and plaintext secrets with save-anyway option
- Add workflow backup zip (workflows + plugins) and merge/replace restore
- Trash page, history panel on editor, and smoke test

Co-authored-by: Nasyarobby Putra <nasyarobby@gmail.com>
2026-08-20 02:14:50 +00:00

63 lines
1.6 KiB
JavaScript

import { createHash, randomUUID } from "node:crypto";
import yaml from "yaml";
/**
* Stable key order for canonical JSON (dedup ignores YAML formatting).
* @param {unknown} value
*/
export function canonicalize(value) {
if (value == null || typeof value !== "object") return value;
if (Array.isArray(value)) return value.map(canonicalize);
const out = {};
for (const key of Object.keys(value).sort()) {
out[key] = canonicalize(value[key]);
}
return out;
}
/**
* Parse YAML to a JS object (null when empty/invalid for callers that handle errors).
* @param {string} content
*/
export function parseWorkflowObject(content) {
if (typeof content !== "string" || !content.trim()) return null;
return yaml.parse(content) ?? null;
}
/**
* SHA256 of normalized workflow content (YAML → object → canonical JSON).
* @param {string} content
*/
export function workflowContentSha(content) {
const parsed = parseWorkflowObject(content);
const canonical = canonicalize(parsed);
const json = JSON.stringify(canonical);
return createHash("sha256").update(json, "utf8").digest("hex");
}
/**
* @param {string} file
*/
export function workflowIdFromFile(file) {
return String(file).replace(/\.ya?ml$/i, "");
}
/**
* New on-disk workflow filename: `{uuid}.yaml`.
* @param {string} [uuid]
*/
export function newWorkflowFilename(uuid) {
const id = uuid ?? randomUUID();
return `${id}.yaml`;
}
const UUID_FILE_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.ya?ml$/i;
/**
* @param {string} file
*/
export function isUuidWorkflowFile(file) {
return UUID_FILE_RE.test(String(file));
}