feat(shared): extract client/server pure helpers into @jerapah-flow/shared

Share profile merge, workflow filename/path helpers, HTTP_METHODS (with OPTIONS), and isPlainObject so web and server stop drifting. Core script sandboxes keep local isPlainObject copies.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-22 06:01:42 +07:00
co-authored by Cursor
parent 96c4cc7b47
commit 368a2ca365
18 changed files with 263 additions and 225 deletions
+1
View File
@@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"@jerapah-flow/shared": "workspace:*",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
+1 -16
View File
@@ -1,16 +1 @@
export function mergeProfileConfig(base, overlay) {
const a = base && typeof base === "object" && !Array.isArray(base) ? { ...base } : {};
if (overlay == null || typeof overlay !== "object" || Array.isArray(overlay)) {
return a;
}
return { ...a, ...overlay };
}
export function configHasOverlay(config) {
return (
config != null &&
typeof config === "object" &&
!Array.isArray(config) &&
Object.keys(config).length > 0
);
}
export { mergeProfileConfig, configHasOverlay } from "@jerapah-flow/shared";
+2 -4
View File
@@ -1,3 +1,5 @@
import { isPlainObject } from "@jerapah-flow/shared";
export const NEW_SCRIPT_TEMPLATE = `async function main(ctx) {
return { output: ctx.data ?? null, context: ctx.context ?? {} };
}
@@ -72,10 +74,6 @@ export function prettyJson(value) {
}
}
function isPlainObject(value) {
return value != null && typeof value === "object" && !Array.isArray(value);
}
function defaultsFromFields(fields) {
if (!isPlainObject(fields)) return {};
/** @type {Record<string, unknown>} */
+7 -14
View File
@@ -1,7 +1,13 @@
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
import {
HTTP_METHODS,
namespacedPath,
hasWorkflowTrigger,
} from "@jerapah-flow/shared";
const KNOWN_TOP = new Set(["name", "description", "enabled", "scripts", "triggers"]);
const HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
export { HTTP_METHODS, namespacedPath, hasWorkflowTrigger };
export const NEW_WORKFLOW_YAML = `name: new workflow
scripts:
@@ -19,13 +25,6 @@ export function nextUiId(prefix = "ui") {
return `${prefix}-${uidSeq}-${Math.random().toString(36).slice(2, 8)}`;
}
export function namespacedPath(owner, triggerPath) {
const cleaned = String(triggerPath ?? "")
.replace(/^\/+/, "")
.trim();
return `/u/${owner}/${cleaned}`;
}
/**
* @param {string} text
* @returns {{ doc: WorkflowDoc | null, parseError: string | null }}
@@ -162,10 +161,6 @@ export function newWorkflowTrigger() {
};
}
export function hasWorkflowTrigger(workflow) {
return (workflow?.triggers ?? []).some((t) => String(t?.type ?? "").toLowerCase() === "workflow");
}
export function triggerDestinations(workflows, { owner, excludeFile } = {}) {
return (workflows ?? []).filter((w) => {
if (owner && w.owner !== owner) return false;
@@ -174,8 +169,6 @@ export function triggerDestinations(workflows, { owner, excludeFile } = {}) {
});
}
export { HTTP_METHODS };
function readOnFailureWorkflow(raw) {
return typeof raw?.onFailureWorkflow === "string" ? raw.onFailureWorkflow : "";
}
@@ -1,13 +1,11 @@
import { isPlainObject } from "@jerapah-flow/shared";
const PREFIX = "jerapah-flow.workflowTestData:";
export function workflowTestStorageKey(owner, file) {
return `${PREFIX}${owner}/${file}`;
}
function isPlainObject(value) {
return value != null && typeof value === "object" && !Array.isArray(value);
}
/** Overlay saved test JSON on the schema/YAML seed. Stored object keys win. */
export function overlayWorkflowTestData(seed, stored) {
const base = isPlainObject(seed) ? seed : {};