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
@@ -17,6 +17,7 @@
"test:profiles": "node test/profiles-smoke.js"
},
"dependencies": {
"@jerapah-flow/shared": "workspace:*",
"@aws-sdk/client-s3": "^3.1111.0",
"@aws-sdk/s3-request-presigner": "^3.1111.0",
"@fastify/cookie": "^11.0.2",
+1 -31
View File
@@ -1,31 +1 @@
/**
* Shallow merge: step overlay keys replace profile defaults (including "").
* Nested objects/arrays are replaced, not deep-merged.
*
* @param {unknown} profileConfig
* @param {unknown} stepConfig
* @returns {Record<string, unknown>}
*/
export function mergeProfileConfig(profileConfig, stepConfig) {
const base =
profileConfig != null && typeof profileConfig === "object" && !Array.isArray(profileConfig)
? { ...profileConfig }
: {};
if (stepConfig == null || typeof stepConfig !== "object" || Array.isArray(stepConfig)) {
return base;
}
return { ...base, ...stepConfig };
}
/**
* @param {unknown} config
* @returns {boolean}
*/
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";
+1 -12
View File
@@ -31,7 +31,7 @@ import {
sendSuccessPage,
} from "./http-trigger-auth.js";
import { resolveConfigRefs } from "./config-refs.js";
import { mergeProfileConfig } from "./profile-config.js";
import { HTTP_METHODS, hasWorkflowTrigger, mergeProfileConfig } from "@jerapah-flow/shared";
import { getProfilePlain } from "./profiles-store.js";
import {
buildFailureAlertData,
@@ -46,17 +46,6 @@ import { ensureInitialRevision } from "./workflow-history.js";
*/
const MAX_WORKFLOW_TRIGGER_DEPTH = 8;
const HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"];
/**
* @param {unknown} workflow
*/
function hasWorkflowTrigger(workflow) {
if (!workflow || typeof workflow !== "object") return false;
const triggers = /** @type {{ triggers?: Array<{ type?: string }> }} */ (workflow)
.triggers;
return (triggers ?? []).some((t) => t?.type === "workflow");
}
/**
* @param {import("fastify").FastifyInstance} server
+3 -6
View File
@@ -2,12 +2,9 @@
* Step return contract: `{ output, context?, skipRemaining? }`.
*/
/**
* @param {unknown} value
*/
export function isPlainObject(value) {
return value != null && typeof value === "object" && !Array.isArray(value);
}
import { isPlainObject } from "@jerapah-flow/shared";
export { isPlainObject };
/**
* @param {unknown} value
+5 -27
View File
@@ -1,36 +1,14 @@
import yaml from "yaml";
import {
ensureWorkflowFilename,
suggestCopyFilename,
} from "@jerapah-flow/shared";
import {
newWorkflowFilename,
workflowFileStem,
} from "./workflow-normalize.js";
export function ensureWorkflowFilename(file) {
const trimmed = String(file ?? "").trim();
if (!trimmed) return "";
return /\.ya?ml$/i.test(trimmed) ? trimmed : `${trimmed}.yaml`;
}
/**
* Legacy human-readable copy name (kept for UI hints).
* @param {string} file
* @param {string[]} existingFiles
*/
export function suggestCopyFilename(file, existingFiles = []) {
const name = ensureWorkflowFilename(file) || "workflow.yaml";
const match = name.match(/^(.*?)(\.ya?ml)$/i);
const base = match ? match[1] : name;
const ext = match ? match[2] : ".yaml";
const existing = new Set(existingFiles);
const copyMatch = base.match(/^(.*)-copy(?:-(\d+))?$/);
const root = copyMatch ? copyMatch[1] : base;
const candidate = (i) =>
i <= 1 ? `${root}-copy${ext}` : `${root}-copy-${i}${ext}`;
let n = copyMatch ? Number(copyMatch[2] || 1) + 1 : 1;
while (existing.has(candidate(n))) n += 1;
return candidate(n);
}
export { ensureWorkflowFilename, suggestCopyFilename };
/**
* UUID-based duplicate filename (default for new duplicates).
+1 -10
View File
@@ -1,4 +1,5 @@
import jsonata from "jsonata";
export { namespacedPath } from "@jerapah-flow/shared";
export const SET_STEP_SCRIPT = "set";
@@ -68,16 +69,6 @@ export async function evaluateJsonata(source, ctx) {
return await result;
}
/**
* Resolve an HTTP path under the owner namespace: /notify -> /u/alice/notify
* @param {string} owner
* @param {string} triggerPath
*/
export function namespacedPath(owner, triggerPath) {
const cleaned = String(triggerPath).replace(/^\/+/, "");
return `/u/${owner}/${cleaned}`;
}
/**
* @param {unknown} step
* @returns {ParsedStep}