mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
fix(MySQL Node): Do not replace $ values with null (#17327)
Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
@@ -11,7 +11,7 @@ export class MySql extends VersionedNodeType {
|
|||||||
name: 'mySql',
|
name: 'mySql',
|
||||||
icon: { light: 'file:mysql.svg', dark: 'file:mysql.dark.svg' },
|
icon: { light: 'file:mysql.svg', dark: 'file:mysql.dark.svg' },
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
defaultVersion: 2.4,
|
defaultVersion: 2.5,
|
||||||
description: 'Get, add and update data in MySQL',
|
description: 'Get, add and update data in MySQL',
|
||||||
parameterPane: 'wide',
|
parameterPane: 'wide',
|
||||||
};
|
};
|
||||||
@@ -23,6 +23,7 @@ export class MySql extends VersionedNodeType {
|
|||||||
2.2: new MySqlV2(baseDescription),
|
2.2: new MySqlV2(baseDescription),
|
||||||
2.3: new MySqlV2(baseDescription),
|
2.3: new MySqlV2(baseDescription),
|
||||||
2.4: new MySqlV2(baseDescription),
|
2.4: new MySqlV2(baseDescription),
|
||||||
|
2.5: new MySqlV2(baseDescription),
|
||||||
};
|
};
|
||||||
|
|
||||||
super(nodeVersions, baseDescription);
|
super(nodeVersions, baseDescription);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { INode } from 'n8n-workflow';
|
import type { INode } from 'n8n-workflow';
|
||||||
|
|
||||||
import type { SortRule, WhereClause } from '../../v2/helpers/interfaces';
|
import type { SortRule, WhereClause } from '../../v2/helpers/interfaces';
|
||||||
|
import * as utils from '../../v2/helpers/utils';
|
||||||
import {
|
import {
|
||||||
prepareQueryAndReplacements,
|
prepareQueryAndReplacements,
|
||||||
wrapData,
|
wrapData,
|
||||||
@@ -26,6 +27,7 @@ describe('Test MySql V2, prepareQueryAndReplacements', () => {
|
|||||||
it('should transform query and values', () => {
|
it('should transform query and values', () => {
|
||||||
const preparedQuery = prepareQueryAndReplacements(
|
const preparedQuery = prepareQueryAndReplacements(
|
||||||
'SELECT * FROM $1:name WHERE id = $2 AND name = $4 AND $3:name = 28',
|
'SELECT * FROM $1:name WHERE id = $2 AND name = $4 AND $3:name = 28',
|
||||||
|
2.5,
|
||||||
['table', 15, 'age', 'Name'],
|
['table', 15, 'age', 'Name'],
|
||||||
);
|
);
|
||||||
expect(preparedQuery).toBeDefined();
|
expect(preparedQuery).toBeDefined();
|
||||||
@@ -36,6 +38,136 @@ describe('Test MySql V2, prepareQueryAndReplacements', () => {
|
|||||||
expect(preparedQuery.values[0]).toEqual(15);
|
expect(preparedQuery.values[0]).toEqual(15);
|
||||||
expect(preparedQuery.values[1]).toEqual('Name');
|
expect(preparedQuery.values[1]).toEqual('Name');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not replace dollar amounts inside quoted strings', () => {
|
||||||
|
const preparedQuery = prepareQueryAndReplacements(
|
||||||
|
"INSERT INTO test_table(content) VALUES('This is for testing $60')",
|
||||||
|
2.5,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
expect(preparedQuery).toBeDefined();
|
||||||
|
expect(preparedQuery.query).toEqual(
|
||||||
|
"INSERT INTO test_table(content) VALUES('This is for testing $60')",
|
||||||
|
);
|
||||||
|
expect(preparedQuery.values.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed parameters and dollar amounts in quotes', () => {
|
||||||
|
const preparedQuery = prepareQueryAndReplacements(
|
||||||
|
"INSERT INTO $1:name(content, price) VALUES('Product costs $60', $2)",
|
||||||
|
2.5,
|
||||||
|
['products', 59.99],
|
||||||
|
);
|
||||||
|
expect(preparedQuery).toBeDefined();
|
||||||
|
expect(preparedQuery.query).toEqual(
|
||||||
|
"INSERT INTO `products`(content, price) VALUES('Product costs $60', ?)",
|
||||||
|
);
|
||||||
|
expect(preparedQuery.values.length).toEqual(1);
|
||||||
|
expect(preparedQuery.values[0]).toEqual(59.99);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle parameters in double quotes', () => {
|
||||||
|
const preparedQuery = prepareQueryAndReplacements(
|
||||||
|
'INSERT INTO test_table(content) VALUES("Price is $100 and $200")',
|
||||||
|
2.5,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
expect(preparedQuery).toBeDefined();
|
||||||
|
expect(preparedQuery.query).toEqual(
|
||||||
|
'INSERT INTO test_table(content) VALUES("Price is $100 and $200")',
|
||||||
|
);
|
||||||
|
expect(preparedQuery.values.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process parameters in correct order despite reverse processing', () => {
|
||||||
|
const preparedQuery = prepareQueryAndReplacements(
|
||||||
|
'SELECT * FROM table WHERE col1 = $1 AND col2 = $2 AND col3 = $3 AND col4 = $4 AND col5 = $5',
|
||||||
|
2.5,
|
||||||
|
['value1', 'value2', 'value3', 'value4', 'value5'],
|
||||||
|
);
|
||||||
|
expect(preparedQuery).toBeDefined();
|
||||||
|
expect(preparedQuery.query).toEqual(
|
||||||
|
'SELECT * FROM table WHERE col1 = ? AND col2 = ? AND col3 = ? AND col4 = ? AND col5 = ?',
|
||||||
|
);
|
||||||
|
expect(preparedQuery.values.length).toEqual(5);
|
||||||
|
expect(preparedQuery.values[0]).toEqual('value1');
|
||||||
|
expect(preparedQuery.values[1]).toEqual('value2');
|
||||||
|
expect(preparedQuery.values[2]).toEqual('value3');
|
||||||
|
expect(preparedQuery.values[3]).toEqual('value4');
|
||||||
|
expect(preparedQuery.values[4]).toEqual('value5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle escaped single quotes correctly', () => {
|
||||||
|
const preparedQuery = prepareQueryAndReplacements(
|
||||||
|
"INSERT INTO test_table(content) VALUES('Don''t replace $1 here')",
|
||||||
|
2.5,
|
||||||
|
['should_not_appear', 123],
|
||||||
|
);
|
||||||
|
expect(preparedQuery).toBeDefined();
|
||||||
|
expect(preparedQuery.query).toEqual(
|
||||||
|
"INSERT INTO test_table(content) VALUES('Don''t replace $1 here')",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle escaped double quotes correctly', () => {
|
||||||
|
const preparedQuery = prepareQueryAndReplacements(
|
||||||
|
"INSERT INTO test_table(content) VALUES('Don\"'t replace $1 here')",
|
||||||
|
2.5,
|
||||||
|
['should_not_appear', 123],
|
||||||
|
);
|
||||||
|
expect(preparedQuery).toBeDefined();
|
||||||
|
expect(preparedQuery.query).toEqual(
|
||||||
|
"INSERT INTO test_table(content) VALUES('Don\"'t replace $1 here')",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use legacy processing for versions < 2.5', () => {
|
||||||
|
const legacySpy = jest.spyOn(utils, 'prepareQueryLegacy');
|
||||||
|
|
||||||
|
prepareQueryAndReplacements('SELECT * FROM $1:name WHERE id = $2', 2.4, ['users', 123]);
|
||||||
|
|
||||||
|
expect(legacySpy).toHaveBeenCalledWith('SELECT * FROM $1:name WHERE id = $2', ['users', 123]);
|
||||||
|
|
||||||
|
legacySpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use new processing for versions >= 2.5', () => {
|
||||||
|
const legacySpy = jest.spyOn(utils, 'prepareQueryLegacy');
|
||||||
|
|
||||||
|
prepareQueryAndReplacements('SELECT * FROM $1:name WHERE id = $2', 2.5, ['users', 123]);
|
||||||
|
|
||||||
|
expect(legacySpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when parameter is referenced but no replacement value provided', () => {
|
||||||
|
expect(() => {
|
||||||
|
prepareQueryAndReplacements(
|
||||||
|
'SELECT * FROM users WHERE id = $4',
|
||||||
|
2.5,
|
||||||
|
['value1', 'value2'], // Only 2 values but query references $4
|
||||||
|
);
|
||||||
|
}).toThrow('Parameter $4 referenced in query but no replacement value provided at index 4');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when multiple parameters are missing replacement values', () => {
|
||||||
|
expect(() => {
|
||||||
|
prepareQueryAndReplacements(
|
||||||
|
'SELECT * FROM users WHERE id = $3 AND name = $5',
|
||||||
|
2.5,
|
||||||
|
['value1'], // Only 1 value but query references $3 and $5
|
||||||
|
);
|
||||||
|
}).toThrow('Parameter $3 referenced in query but no replacement value provided at index 3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw error when all referenced parameters have replacement values', () => {
|
||||||
|
expect(() => {
|
||||||
|
prepareQueryAndReplacements(
|
||||||
|
'SELECT * FROM users WHERE id = $1 AND name = $2',
|
||||||
|
2.5,
|
||||||
|
['123', 'John'], // Correct number of values
|
||||||
|
);
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Test MySql V2, wrapData', () => {
|
describe('Test MySql V2, wrapData', () => {
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ export async function execute(
|
|||||||
|
|
||||||
const options = this.getNodeParameter('options', i, {});
|
const options = this.getNodeParameter('options', i, {});
|
||||||
|
|
||||||
|
const nodeVersion = Number(nodeOptions.nodeVersion);
|
||||||
|
|
||||||
let values;
|
let values;
|
||||||
let queryReplacement = options.queryReplacement || [];
|
let queryReplacement = options.queryReplacement || [];
|
||||||
|
|
||||||
@@ -78,7 +80,7 @@ export async function execute(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const preparedQuery = prepareQueryAndReplacements(rawQuery, values);
|
const preparedQuery = prepareQueryAndReplacements(rawQuery, nodeVersion, values);
|
||||||
|
|
||||||
if ((nodeOptions.nodeVersion as number) >= 2.3) {
|
if ((nodeOptions.nodeVersion as number) >= 2.3) {
|
||||||
const parsedNumbers = preparedQuery.values.map((value) => {
|
const parsedNumbers = preparedQuery.values.map((value) => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const versionDescription: INodeTypeDescription = {
|
|||||||
name: 'mySql',
|
name: 'mySql',
|
||||||
icon: { light: 'file:mysql.svg', dark: 'file:mysql.dark.svg' },
|
icon: { light: 'file:mysql.svg', dark: 'file:mysql.dark.svg' },
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: [2, 2.1, 2.2, 2.3, 2.4],
|
version: [2, 2.1, 2.2, 2.3, 2.4, 2.5],
|
||||||
subtitle: '={{ $parameter["operation"] }}',
|
subtitle: '={{ $parameter["operation"] }}',
|
||||||
description: 'Get, add and update data in MySQL',
|
description: 'Get, add and update data in MySQL',
|
||||||
defaults: {
|
defaults: {
|
||||||
|
|||||||
@@ -46,3 +46,10 @@ export type MysqlNodeCredentials = {
|
|||||||
connectTimeout: number;
|
connectTimeout: number;
|
||||||
} & WithSSL &
|
} & WithSSL &
|
||||||
WithSSHTunnel;
|
WithSSHTunnel;
|
||||||
|
|
||||||
|
export type ParameterMatch = {
|
||||||
|
match: string;
|
||||||
|
index: number;
|
||||||
|
paramNumber: string;
|
||||||
|
isName: boolean;
|
||||||
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { NodeOperationError } from 'n8n-workflow';
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
Mysql2Pool,
|
Mysql2Pool,
|
||||||
|
ParameterMatch,
|
||||||
QueryMode,
|
QueryMode,
|
||||||
QueryValues,
|
QueryValues,
|
||||||
QueryWithValues,
|
QueryWithValues,
|
||||||
@@ -35,11 +36,119 @@ export function escapeSqlIdentifier(identifier: string): string {
|
|||||||
.join('.');
|
.join('.');
|
||||||
}
|
}
|
||||||
|
|
||||||
export const prepareQueryAndReplacements = (rawQuery: string, replacements?: QueryValues) => {
|
function findParameterMatches(rawQuery: string, regex: RegExp): ParameterMatch[] {
|
||||||
|
const matches: ParameterMatch[] = [];
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
|
while ((match = regex.exec(rawQuery)) !== null) {
|
||||||
|
matches.push({
|
||||||
|
match: match[0],
|
||||||
|
index: match.index,
|
||||||
|
paramNumber: match[1],
|
||||||
|
isName: match[0].includes(':name'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInsideQuotes(rawQuery: string, index: number): boolean {
|
||||||
|
const beforeMatch = rawQuery.substring(0, index);
|
||||||
|
const singleQuoteCount = (beforeMatch.match(/'/g) || []).length;
|
||||||
|
const doubleQuoteCount = (beforeMatch.match(/"/g) || []).length;
|
||||||
|
return singleQuoteCount % 2 !== 0 || doubleQuoteCount % 2 !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterValidMatches(matches: ParameterMatch[], rawQuery: string): ParameterMatch[] {
|
||||||
|
return matches.filter(({ index }) => !isInsideQuotes(rawQuery, index));
|
||||||
|
}
|
||||||
|
|
||||||
|
function processParameterReplacements(
|
||||||
|
query: string,
|
||||||
|
validMatches: ParameterMatch[],
|
||||||
|
replacements: QueryValues,
|
||||||
|
): string {
|
||||||
|
let processedQuery = query;
|
||||||
|
|
||||||
|
for (const { match: matchStr, paramNumber, isName } of validMatches.reverse()) {
|
||||||
|
const matchIndex = Number(paramNumber) - 1;
|
||||||
|
|
||||||
|
if (matchIndex >= 0 && matchIndex < replacements.length) {
|
||||||
|
if (isName) {
|
||||||
|
processedQuery = processedQuery.replace(
|
||||||
|
matchStr,
|
||||||
|
escapeSqlIdentifier(replacements[matchIndex].toString()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
processedQuery = processedQuery.replace(matchStr, '?');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractValuesFromMatches(
|
||||||
|
validMatches: ParameterMatch[],
|
||||||
|
replacements: QueryValues,
|
||||||
|
): QueryValues {
|
||||||
|
const nonNameMatches = validMatches.filter((match) => !match.isName);
|
||||||
|
nonNameMatches.sort((a, b) => Number(a.paramNumber) - Number(b.paramNumber));
|
||||||
|
|
||||||
|
const values: QueryValues = [];
|
||||||
|
for (const { paramNumber } of nonNameMatches) {
|
||||||
|
const matchIndex = Number(paramNumber) - 1;
|
||||||
|
if (matchIndex >= 0 && matchIndex < replacements.length) {
|
||||||
|
values.push(replacements[matchIndex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateReferencedParameters(
|
||||||
|
validMatches: ParameterMatch[],
|
||||||
|
replacements: QueryValues,
|
||||||
|
): void {
|
||||||
|
for (const match of validMatches) {
|
||||||
|
const paramIndex = Number(match.paramNumber) - 1;
|
||||||
|
if (paramIndex >= replacements.length || paramIndex < 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Parameter $${match.paramNumber} referenced in query but no replacement value provided at index ${paramIndex + 1}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prepareQueryAndReplacements = (
|
||||||
|
rawQuery: string,
|
||||||
|
nodeVersion: number,
|
||||||
|
replacements?: QueryValues,
|
||||||
|
) => {
|
||||||
if (replacements === undefined) {
|
if (replacements === undefined) {
|
||||||
return { query: rawQuery, values: [] };
|
return { query: rawQuery, values: [] };
|
||||||
}
|
}
|
||||||
// in UI for replacements we use syntax identical to Postgres Query Replacement, but we need to convert it to mysql2 replacement syntax
|
|
||||||
|
if (nodeVersion >= 2.5) {
|
||||||
|
const regex = /\$(\d+)(?::name)?/g;
|
||||||
|
const matches = findParameterMatches(rawQuery, regex);
|
||||||
|
const validMatches = filterValidMatches(matches, rawQuery);
|
||||||
|
|
||||||
|
validateReferencedParameters(validMatches, replacements);
|
||||||
|
|
||||||
|
const query = processParameterReplacements(rawQuery, validMatches, replacements);
|
||||||
|
const values = extractValuesFromMatches(validMatches, replacements);
|
||||||
|
|
||||||
|
return { query, values };
|
||||||
|
}
|
||||||
|
|
||||||
|
return prepareQueryLegacy(rawQuery, replacements);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prepareQueryLegacy = (
|
||||||
|
rawQuery: string,
|
||||||
|
replacements: QueryValues,
|
||||||
|
): QueryWithValues => {
|
||||||
let query: string = rawQuery;
|
let query: string = rawQuery;
|
||||||
const values: QueryValues = [];
|
const values: QueryValues = [];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user