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>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
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));
|
||||
}
|
||||
Reference in New Issue
Block a user