feat(Merge Node): Better pairedItem mapping in combineBySql operation if SELECT query (#13849)

Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com>
This commit is contained in:
Michael Kret
2025-03-13 12:20:44 +02:00
committed by GitHub
parent d2e4706b97
commit 881d3f8771
6 changed files with 368 additions and 24 deletions

View File

@@ -1,8 +1,11 @@
import { Container } from '@n8n/di';
import alasql from 'alasql';
import type { Database } from 'alasql';
import { ErrorReporter } from 'n8n-core';
import type {
IDataObject,
IExecuteFunctions,
INode,
INodeExecutionData,
INodeProperties,
IPairedItemData,
@@ -12,6 +15,7 @@ import { NodeOperationError } from 'n8n-workflow';
import { getResolvables, updateDisplayOptions } from '@utils/utilities';
import { numberInputsProperty } from '../../helpers/descriptions';
import { modifySelectQuery, rowToExecutionData } from '../../helpers/utils';
export const properties: INodeProperties[] = [
numberInputsProperty,
@@ -39,15 +43,102 @@ const displayOptions = {
export const description = updateDisplayOptions(displayOptions, properties);
const prepareError = (node: INode, error: Error) => {
let message = '';
if (typeof error === 'string') {
message = error;
} else {
message = error.message;
}
throw new NodeOperationError(node, error, {
message: 'Issue while executing query',
description: message,
itemIndex: 0,
});
};
async function executeSelectWithMappedPairedItems(
node: INode,
inputsData: INodeExecutionData[][],
query: string,
): Promise<INodeExecutionData[][]> {
const returnData: INodeExecutionData[] = [];
const db: typeof Database = new (alasql as any).Database(node.id);
try {
for (let i = 0; i < inputsData.length; i++) {
const inputData = inputsData[i];
db.exec(`CREATE TABLE input${i + 1}`);
db.tables[`input${i + 1}`].data = inputData.map((entry) => ({
...entry.json,
pairedItem: entry.pairedItem,
}));
}
} catch (error) {
throw new NodeOperationError(node, error, {
message: 'Issue while creating table from',
description: error.message,
itemIndex: 0,
});
}
try {
const result: IDataObject[] = db.exec(modifySelectQuery(query, inputsData.length));
for (const item of result) {
if (Array.isArray(item)) {
returnData.push(...item.map((entry) => rowToExecutionData(entry)));
} else if (typeof item === 'object') {
returnData.push(rowToExecutionData(item));
}
}
if (!returnData.length) {
returnData.push({ json: { success: true } });
}
} catch (error) {
prepareError(node, error as Error);
} finally {
delete alasql.databases[node.id];
}
return [returnData];
}
export async function execute(
this: IExecuteFunctions,
inputsData: INodeExecutionData[][],
): Promise<INodeExecutionData[][]> {
const nodeId = this.getNode().id;
const node = this.getNode();
const returnData: INodeExecutionData[] = [];
const pairedItem: IPairedItemData[] = [];
const db: typeof Database = new (alasql as any).Database(nodeId);
let query = this.getNodeParameter('query', 0) as string;
for (const resolvable of getResolvables(query)) {
query = query.replace(resolvable, this.evaluateExpression(resolvable, 0) as string);
}
const isSelectQuery = node.typeVersion >= 3.1 ? query.toLowerCase().startsWith('select') : false;
if (isSelectQuery) {
try {
return await executeSelectWithMappedPairedItems(node, inputsData, query);
} catch (error) {
Container.get(ErrorReporter).error(error, {
extra: {
nodeName: node.name,
nodeType: node.type,
nodeVersion: node.typeVersion,
workflowId: this.getWorkflow().id,
},
});
}
}
const db: typeof Database = new (alasql as any).Database(node.id);
try {
for (let i = 0; i < inputsData.length; i++) {
@@ -90,7 +181,7 @@ export async function execute(
db.tables[`input${i + 1}`].data = inputData.map((entry) => entry.json);
}
} catch (error) {
throw new NodeOperationError(this.getNode(), error, {
throw new NodeOperationError(node, error, {
message: 'Issue while creating table from',
description: error.message,
itemIndex: 0,
@@ -98,12 +189,6 @@ export async function execute(
}
try {
let query = this.getNodeParameter('query', 0) as string;
for (const resolvable of getResolvables(query)) {
query = query.replace(resolvable, this.evaluateExpression(resolvable, 0) as string);
}
const result: IDataObject[] = db.exec(query);
for (const item of result) {
@@ -118,20 +203,10 @@ export async function execute(
returnData.push({ json: { success: true }, pairedItem });
}
} catch (error) {
let message = '';
if (typeof error === 'string') {
message = error;
} else {
message = error.message;
}
throw new NodeOperationError(this.getNode(), error, {
message: 'Issue while executing query',
description: message,
itemIndex: 0,
});
prepareError(node, error as Error);
} finally {
delete alasql.databases[node.id];
}
delete alasql.databases[nodeId];
return [returnData];
}

View File

@@ -9,7 +9,7 @@ export const versionDescription: INodeTypeDescription = {
name: 'merge',
group: ['transform'],
description: 'Merges data of multiple streams once data from both is available',
version: [3],
version: [3, 3.1],
defaults: {
name: 'Merge',
},

View File

@@ -386,3 +386,43 @@ export function getNodeInputsData(this: IExecuteFunctions) {
return returnData;
}
export const rowToExecutionData = (data: IDataObject): INodeExecutionData => {
const keys = Object.keys(data);
const pairedItem: IPairedItemData[] = [];
const json: IDataObject = {};
for (const key of keys) {
if (key.startsWith('pairedItem')) {
if (data[key] === undefined) continue;
pairedItem.push(data[key] as IPairedItemData);
} else {
json[key] = data[key];
}
}
return { json, pairedItem };
};
export function modifySelectQuery(userQuery: string, inputLength: number): string {
const selectMatch = userQuery.match(/SELECT\s+(.+?)\s+FROM/i);
if (!selectMatch) return userQuery;
let selectedColumns = selectMatch[1].trim();
if (selectedColumns === '*') {
return userQuery;
}
const pairedItemColumns = [];
for (let i = 1; i <= inputLength; i++) {
if (userQuery.includes(`input${i}`)) {
pairedItemColumns.push(`input${i}.pairedItem AS pairedItem${i}`);
}
}
selectedColumns += pairedItemColumns.length ? ', ' + pairedItemColumns.join(', ') : '';
return userQuery.replace(selectMatch[0], `SELECT ${selectedColumns} FROM`);
}