mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-01 15:47:41 +08:00
fix(core): Keep duplicate connections distinct in the workflow diff (#35652)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: James Gee <1285296+geemanjs@users.noreply.github.com>
This commit is contained in:
@@ -12,6 +12,25 @@ export type ConnectionsDiff = {
|
||||
removed: Record<string, INodeConnectionsDiff>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Groups connections by their serialized value, keeping every occurrence.
|
||||
* A bucket may hold the same connection more than once, so comparing by value
|
||||
* alone would collapse the duplicates and hide added or removed ones.
|
||||
*/
|
||||
function groupByValue(connections: IConnection[]) {
|
||||
const byValue = new Map<string, Array<NonNullable<ConnectionEntry['value']>>>();
|
||||
|
||||
connections.forEach((connection, index) => {
|
||||
const key = JSON.stringify(connection);
|
||||
const entries = byValue.get(key);
|
||||
|
||||
if (entries) entries.push({ index, connection });
|
||||
else byValue.set(key, [{ index, connection }]);
|
||||
});
|
||||
|
||||
return byValue;
|
||||
}
|
||||
|
||||
export function compareConnections(prev: IConnections, next: IConnections): ConnectionsDiff {
|
||||
const added: Record<string, INodeConnectionsDiff> = {};
|
||||
const removed: Record<string, INodeConnectionsDiff> = {};
|
||||
@@ -41,22 +60,13 @@ export function compareConnections(prev: IConnections, next: IConnections): Conn
|
||||
const nextConnections = nextInputConnections[sourceIndex] ?? [];
|
||||
|
||||
// Build maps for easier comparison
|
||||
const prevMap = new Map(
|
||||
prevConnections.map((conn, idx) => [
|
||||
JSON.stringify(conn),
|
||||
{ index: idx, connection: conn },
|
||||
]),
|
||||
);
|
||||
const nextMap = new Map(
|
||||
nextConnections.map((conn, idx) => [
|
||||
JSON.stringify(conn),
|
||||
{ index: idx, connection: conn },
|
||||
]),
|
||||
);
|
||||
const prevMap = groupByValue(prevConnections);
|
||||
const nextMap = groupByValue(nextConnections);
|
||||
|
||||
// Find added connections
|
||||
for (const [key, value] of nextMap) {
|
||||
if (!prevMap.has(key)) {
|
||||
for (const [key, entries] of nextMap) {
|
||||
const kept = prevMap.get(key)?.length ?? 0;
|
||||
for (const value of entries.slice(kept)) {
|
||||
if (!added[nodeName]) added[nodeName] = {};
|
||||
if (!added[nodeName][inputName]) added[nodeName][inputName] = [];
|
||||
|
||||
@@ -68,8 +78,9 @@ export function compareConnections(prev: IConnections, next: IConnections): Conn
|
||||
}
|
||||
|
||||
// Find removed connections
|
||||
for (const [key, value] of prevMap) {
|
||||
if (!nextMap.has(key)) {
|
||||
for (const [key, entries] of prevMap) {
|
||||
const kept = nextMap.get(key)?.length ?? 0;
|
||||
for (const value of entries.slice(kept)) {
|
||||
if (!removed[nodeName]) removed[nodeName] = {};
|
||||
if (!removed[nodeName][inputName]) removed[nodeName][inputName] = [];
|
||||
|
||||
|
||||
@@ -546,4 +546,76 @@ describe('compareConnections', () => {
|
||||
expect(result.removed).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('duplicate connections', () => {
|
||||
// Nothing normalizes connections on the way in, so a bucket can hold the same
|
||||
// connection twice. Comparing by value alone would collapse the duplicates and
|
||||
// hide the change.
|
||||
it('should detect removal of one of two identical connections', () => {
|
||||
const prev: IConnections = {
|
||||
node1: {
|
||||
main: [[createConnection('node0', 'main', 0), createConnection('node0', 'main', 0)]],
|
||||
},
|
||||
};
|
||||
const next: IConnections = {
|
||||
node1: {
|
||||
main: [[createConnection('node0', 'main', 0)]],
|
||||
},
|
||||
};
|
||||
|
||||
const result = compareConnections(prev, next);
|
||||
|
||||
expect(result.added).toEqual({});
|
||||
expect(result.removed).toEqual({
|
||||
node1: {
|
||||
main: [
|
||||
{
|
||||
sourceIndex: 0,
|
||||
value: { index: 1, connection: createConnection('node0', 'main', 0) },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect addition of a second identical connection', () => {
|
||||
const prev: IConnections = {
|
||||
node1: {
|
||||
main: [[createConnection('node0', 'main', 0)]],
|
||||
},
|
||||
};
|
||||
const next: IConnections = {
|
||||
node1: {
|
||||
main: [[createConnection('node0', 'main', 0), createConnection('node0', 'main', 0)]],
|
||||
},
|
||||
};
|
||||
|
||||
const result = compareConnections(prev, next);
|
||||
|
||||
expect(result.removed).toEqual({});
|
||||
expect(result.added).toEqual({
|
||||
node1: {
|
||||
main: [
|
||||
{
|
||||
sourceIndex: 0,
|
||||
value: { index: 1, connection: createConnection('node0', 'main', 0) },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should report no change when the same duplicates are on both sides', () => {
|
||||
const connections: IConnections = {
|
||||
node1: {
|
||||
main: [[createConnection('node0', 'main', 0), createConnection('node0', 'main', 0)]],
|
||||
},
|
||||
};
|
||||
|
||||
const result = compareConnections(connections, connections);
|
||||
|
||||
expect(result.added).toEqual({});
|
||||
expect(result.removed).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user