import isEqual from 'lodash/isEqual'; import pick from 'lodash/pick'; export var NodeDiffStatus; (function (NodeDiffStatus) { NodeDiffStatus["Eq"] = "equal"; NodeDiffStatus["Modified"] = "modified"; NodeDiffStatus["Added"] = "added"; NodeDiffStatus["Deleted"] = "deleted"; })(NodeDiffStatus || (NodeDiffStatus = {})); export function compareNodes(base, target) { const propsToCompare = ['name', 'type', 'typeVersion', 'webhookId', 'credentials', 'parameters']; const baseNode = pick(base, propsToCompare); const targetNode = pick(target, propsToCompare); return isEqual(baseNode, targetNode); } export function compareWorkflowsNodes(base, target, nodesEqual = compareNodes) { const baseNodes = base.reduce((acc, node) => { acc.set(node.id, node); return acc; }, new Map()); const targetNodes = target.reduce((acc, node) => { acc.set(node.id, node); return acc; }, new Map()); const diff = new Map(); for (const [id, node] of baseNodes.entries()) { if (!targetNodes.has(id)) { diff.set(id, { status: NodeDiffStatus.Deleted, node }); } else if (!nodesEqual(baseNodes.get(id), targetNodes.get(id))) { diff.set(id, { status: NodeDiffStatus.Modified, node }); } else { diff.set(id, { status: NodeDiffStatus.Eq, node }); } } for (const [id, node] of targetNodes.entries()) { if (!baseNodes.has(id)) { diff.set(id, { status: NodeDiffStatus.Added, node }); } } return diff; } function mergeNodeDiff(prev, next) { switch (prev) { case NodeDiffStatus.Added: switch (next) { case NodeDiffStatus.Added: return 'invariant broken'; case NodeDiffStatus.Deleted: return 'undone'; default: return NodeDiffStatus.Added; } case NodeDiffStatus.Deleted: switch (next) { case NodeDiffStatus.Added: return NodeDiffStatus.Modified; default: return 'invariant broken'; } case NodeDiffStatus.Eq: switch (next) { case NodeDiffStatus.Added: return 'invariant broken'; default: return next; } case NodeDiffStatus.Modified: switch (next) { case NodeDiffStatus.Added: return 'invariant broken'; case NodeDiffStatus.Deleted: return NodeDiffStatus.Deleted; default: return NodeDiffStatus.Modified; } } } export class WorkflowChangeSet { nodes; constructor(nodes = new Map()) { this.nodes = nodes; } hasChanges() { for (const nodeDiff of this.nodes.values()) { if (nodeDiff.status !== NodeDiffStatus.Eq) return true; } return false; } mergeNext(wcs) { for (const [key, diff] of wcs.nodes) { const existing = this.nodes.get(key); if (existing) { const diffStatus = mergeNodeDiff(existing.status, diff.status); if (diffStatus === 'invariant broken') { throw new Error('invariant broken'); } if (diffStatus === 'undone') { this.nodes.delete(key); } else { this.nodes.set(key, { ...diff, status: diffStatus }); } } else { this.nodes.set(key, { ...diff, status: NodeDiffStatus.Added }); } } } } // determines whether the second node is a "superset" of the first one, i.e. whether no data // is lost if we were to cleanse the first node function nodeIsAdditive(prevNode, nextNode) { const { parameters: prevParams, ...prev } = prevNode; const { parameters: nextParams, ...next } = nextNode; // abort if the nodes don't match besides parameters if (!compareNodes({ ...prev, parameters: {} }, { ...next, parameters: {} })) return false; const params = Object.keys(prevParams); // abort if prev has some field next does not have if (params.some((x) => !Object.prototype.hasOwnProperty.call(nextParams, x))) return false; for (const key of params) { const left = prevParams[key]; const right = nextParams[key]; // non-strings must be exactly equal to not be lost data if (typeof left === 'string' && typeof right === 'string') { // strings must only be contained in the new string if (!right.includes(left)) return false; } else if (left !== right) return false; } return true; } function mergeAdditiveChanges(_prev, next, diff) { for (const d of diff.values()) { if (d.status === NodeDiffStatus.Deleted) return false; if (d.status === NodeDiffStatus.Added) continue; const nextNode = next.from.nodes.find((x) => x.name === d.node.name); if (!nextNode) throw new Error('invariant broken'); if (d.status === NodeDiffStatus.Modified && !nodeIsAdditive(d.node, nextNode)) return false; } return true; } export const RULES = { mergeAdditiveChanges, }; function compareWorkflows(previous, next) { const nodesDiff = compareWorkflowsNodes(previous.nodes, next.nodes); const workflowChangeSet = new WorkflowChangeSet(nodesDiff); return { workflowChangeSet, groupedWorkflows: [], from: previous, to: next, }; } export function groupWorkflows(workflows, rules) { if (workflows.length === 0) return []; if (workflows.length === 1) { return [ { workflowChangeSet: new WorkflowChangeSet(), groupedWorkflows: [], from: workflows[0], to: workflows[0], }, ]; } const diffs = []; for (let i = 0; i < workflows.length - 1; ++i) { diffs.push(compareWorkflows(workflows[i], workflows[i + 1])); } let prevDiffsLength = diffs.length; do { prevDiffsLength = diffs.length; const n = diffs.length; for (let i = n - 1; i > 0; --i) { const diff = compareWorkflowsNodes(diffs[i - 1].from.nodes, diffs[i].to.nodes); for (const rule of rules) { const shouldMerge = rule(diffs[i - 1], diffs[i], diff); if (shouldMerge) { const right = diffs.pop(); if (!right) throw new Error('invariant broken'); // merge diffs diffs[i - 1].workflowChangeSet.mergeNext(right.workflowChangeSet); diffs[i - 1].groupedWorkflows.push(diffs[i - 1].to); diffs[i - 1].groupedWorkflows.push(...right.groupedWorkflows); diffs[i - 1].to = right.to; break; } } } } while (prevDiffsLength !== diffs.length); return diffs; } //# sourceMappingURL=workflow-diff.js.map