feat(server): enhance workflow and script handling with new features

- Added `workflowLastModifiedAt` function to retrieve the last modified timestamp of workflow YAML files.
- Introduced `encodeBinaryForWire` and `reviveBinaryFromWire` functions for better handling of binary data in JSON.
- Implemented `useDeleteKv` hook for deleting key-value pairs in the web API.
- Enhanced `scriptsPluginFactory` to support dry-run evaluations with JSONata expressions.
- Added `set-dry-run-smoke.js` test to validate dry-run functionality.
- Updated profile configuration to include `overlayFromMerged` for better profile management.
- Introduced try-session management in the web components to handle step execution states.
- Improved various components to support new try-session features and maintain UI consistency.

Co-authored-by: Nasyarobby Putra <nasyarobby@gmail.com>
This commit is contained in:
2026-08-22 17:52:17 +07:00
parent 72feb39d56
commit beb7e22652
28 changed files with 1753 additions and 143 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
export { isPlainObject } from "./is-plain-object.js";
export { mergeProfileConfig, configHasOverlay } from "./profile-config.js";
export { mergeProfileConfig, overlayFromMerged, configHasOverlay } from "./profile-config.js";
export { ensureWorkflowFilename, suggestCopyFilename } from "./workflow-filename.js";
export { HTTP_METHODS, namespacedPath, hasWorkflowTrigger } from "./workflow-path.js";
+39
View File
@@ -17,6 +17,45 @@ export function mergeProfileConfig(profileConfig, stepConfig) {
return { ...base, ...stepConfig };
}
/**
* Inverse of merge for Apply-to-card: keep only keys whose values differ from the profile.
* No profile → return the merged object as-is (full step config).
*
* @param {unknown} profileConfig
* @param {unknown} mergedConfig
* @returns {Record<string, unknown>}
*/
export function overlayFromMerged(profileConfig, mergedConfig) {
const merged =
mergedConfig != null && typeof mergedConfig === "object" && !Array.isArray(mergedConfig)
? { ...mergedConfig }
: {};
const base =
profileConfig != null && typeof profileConfig === "object" && !Array.isArray(profileConfig)
? profileConfig
: null;
if (!base) return merged;
const overlay = {};
for (const [key, value] of Object.entries(merged)) {
if (!Object.prototype.hasOwnProperty.call(base, key) || !sameConfigValue(base[key], value)) {
overlay[key] = value;
}
}
return overlay;
}
function sameConfigValue(a, b) {
if (Object.is(a, b)) return true;
if (a == null || b == null) return a === b;
if (typeof a !== "object" || typeof b !== "object") return false;
try {
return JSON.stringify(a) === JSON.stringify(b);
} catch {
return false;
}
}
/**
* @param {unknown} config
* @returns {boolean}
@@ -5,6 +5,7 @@ import {
isPlainObject,
mergeProfileConfig,
namespacedPath,
overlayFromMerged,
} from "./index.js";
describe("isPlainObject", () => {
@@ -32,6 +33,29 @@ describe("mergeProfileConfig", () => {
});
});
describe("overlayFromMerged", () => {
it("returns full merged when there is no profile", () => {
expect(overlayFromMerged(null, { a: 1, b: 2 })).toEqual({ a: 1, b: 2 });
expect(overlayFromMerged(undefined, { x: 1 })).toEqual({ x: 1 });
});
it("keeps only keys that differ from the profile", () => {
expect(overlayFromMerged({ a: 1, b: 2 }, { a: 1, b: 3, c: 4 })).toEqual({
b: 3,
c: 4,
});
expect(overlayFromMerged({ a: 1 }, { a: 1 })).toEqual({});
expect(overlayFromMerged({ nested: { a: 1 } }, { nested: { a: 1 } })).toEqual({});
expect(overlayFromMerged({ nested: { a: 1 } }, { nested: { b: 2 } })).toEqual({
nested: { b: 2 },
});
});
it("treats empty-string override as a real overlay key", () => {
expect(overlayFromMerged({ url: "http://a" }, { url: "" })).toEqual({ url: "" });
});
});
describe("configHasOverlay", () => {
it("is true only for non-empty plain objects", () => {
expect(configHasOverlay({ a: 1 })).toBe(true);