n8n-nodes-base
Version:
Base nodes of n8n
443 lines • 18.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getWhereClauses = exports.isWhereClause = exports.splitQueryToStatements = exports.prepareQueryLegacy = exports.prepareQueryAndReplacements = void 0;
exports.escapeSqlIdentifier = escapeSqlIdentifier;
exports.prepareErrorItem = prepareErrorItem;
exports.parseMySqlError = parseMySqlError;
exports.wrapData = wrapData;
exports.prepareOutput = prepareOutput;
exports.configureQueryRunner = configureQueryRunner;
exports.addWhereClauses = addWhereClauses;
exports.addSortRules = addSortRules;
exports.replaceEmptyStringsByNulls = replaceEmptyStringsByNulls;
const n8n_workflow_1 = require("n8n-workflow");
const interfaces_1 = require("./interfaces");
const utilities_1 = require("../../../../utils/utilities");
const common_descriptions_1 = require("../actions/common.descriptions");
function escapeSqlIdentifier(identifier) {
const parts = identifier.match(/(`[^`]*`|[^.`]+)/g) ?? [];
return parts
.map((part) => {
const trimmedPart = part.trim();
if (trimmedPart.startsWith('`') && trimmedPart.endsWith('`')) {
return trimmedPart;
}
return `\`${trimmedPart}\``;
})
.join('.');
}
function findParameterMatches(rawQuery, regex) {
const matches = [];
let match;
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, index) {
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, rawQuery) {
return matches.filter(({ index }) => !isInsideQuotes(rawQuery, index));
}
function processParameterReplacements(query, validMatches, replacements) {
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, replacements) {
const nonNameMatches = validMatches.filter((match) => !match.isName);
nonNameMatches.sort((a, b) => Number(a.paramNumber) - Number(b.paramNumber));
const values = [];
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, replacements) {
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}`);
}
}
}
const prepareQueryAndReplacements = (rawQuery, nodeVersion, replacements) => {
if (replacements === undefined) {
return { query: rawQuery, values: [] };
}
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 (0, exports.prepareQueryLegacy)(rawQuery, replacements);
};
exports.prepareQueryAndReplacements = prepareQueryAndReplacements;
const prepareQueryLegacy = (rawQuery, replacements) => {
let query = rawQuery;
const values = [];
const regex = /\$(\d+)(?::name)?/g;
const matches = rawQuery.match(regex) || [];
for (const match of matches) {
if (match.includes(':name')) {
const matchIndex = Number(match.replace('$', '').replace(':name', '')) - 1;
query = query.replace(match, escapeSqlIdentifier(replacements[matchIndex].toString()));
}
else {
const matchIndex = Number(match.replace('$', '')) - 1;
query = query.replace(match, '?');
values.push(replacements[matchIndex]);
}
}
return { query, values };
};
exports.prepareQueryLegacy = prepareQueryLegacy;
function prepareErrorItem(item, error, index) {
return {
json: { message: error.message, item: { ...item }, itemIndex: index, error: { ...error } },
pairedItem: { item: index },
};
}
function parseMySqlError(error, itemIndex = 0, queries) {
let message = error.message;
const description = `sql: ${error.sql}, code: ${error.code}`;
if (queries?.length &&
(message || '').toLowerCase().includes('you have an error in your sql syntax')) {
let queryIndex = itemIndex;
const failedStatement = ((message.split("near '")[1] || '').split("' at")[0] || '').split(';')[0];
if (failedStatement) {
if (queryIndex === 0 && queries.length > 1) {
const failedQueryIndex = queries.findIndex((query) => query.includes(failedStatement));
if (failedQueryIndex !== -1) {
queryIndex = failedQueryIndex;
}
}
const lines = queries[queryIndex].split('\n');
const failedLine = lines.findIndex((line) => line.includes(failedStatement));
if (failedLine !== -1) {
message = `You have an error in your SQL syntax on line ${failedLine + 1} near '${failedStatement}'`;
}
}
}
if ((error?.message).includes('ECONNREFUSED')) {
message = 'Connection refused';
}
return new n8n_workflow_1.NodeOperationError(this.getNode(), error, {
message,
description,
itemIndex,
});
}
function wrapData(data) {
if (!Array.isArray(data)) {
return [{ json: data }];
}
return data.map((item) => ({
json: item,
}));
}
function prepareOutput(response, options, statements, constructExecutionHelper, itemData) {
let returnData = [];
if (options.detailedOutput) {
response.forEach((entry, index) => {
const item = {
sql: statements[index],
data: entry,
};
const executionData = constructExecutionHelper(wrapData(item), {
itemData,
});
returnData = returnData.concat(executionData);
});
}
else {
response
.filter((entry) => Array.isArray(entry))
.forEach((entry, index) => {
const executionData = constructExecutionHelper(wrapData(entry), {
itemData: Array.isArray(itemData) ? itemData[index] : itemData,
});
returnData = returnData.concat(executionData);
});
}
if (!returnData.length) {
if (options?.nodeVersion < 2.2) {
returnData.push({ json: { success: true }, pairedItem: itemData });
}
else {
const isSelectQuery = statements
.filter((statement) => !statement.startsWith('--'))
.every((statement) => statement
.replace(/\/\*.*?\*\//g, '') // remove multiline comments
.replace(/\n/g, '')
.toLowerCase()
.startsWith('select'));
if (!isSelectQuery) {
returnData.push({ json: { success: true }, pairedItem: itemData });
}
}
}
return returnData;
}
const END_OF_STATEMENT = /;(?=(?:[^'\\]|'[^']*?'|\\[\s\S])*?$)/g;
const splitQueryToStatements = (query, filterOutEmpty = true) => {
const statements = query
.replace(/\n/g, '')
.split(END_OF_STATEMENT)
.map((statement) => statement.trim());
return filterOutEmpty ? statements.filter((statement) => statement !== '') : statements;
};
exports.splitQueryToStatements = splitQueryToStatements;
function configureQueryRunner(options, pool) {
return async (queries) => {
if (queries.length === 0) {
return [];
}
let returnData = [];
const mode = options.queryBatching || interfaces_1.BATCH_MODE.SINGLE;
let connection;
try {
connection = await pool.getConnection();
}
catch (e) {
const error = parseMySqlError.call(this, e);
if (!this.continueOnFail()) {
throw error;
}
return [{ json: { message: error.message, error: { ...error } } }];
}
if (mode === interfaces_1.BATCH_MODE.SINGLE) {
const formattedQueries = queries.map(({ query, values }) => connection.format(query, values));
try {
//releasing connection after formatting queries, otherwise pool.query() will fail with timeout
connection.release();
let singleQuery = '';
if (formattedQueries.length > 1) {
singleQuery = formattedQueries.map((query) => query.trim().replace(/;$/, '')).join(';');
}
else {
singleQuery = formattedQueries[0];
}
let response = (await pool.query(singleQuery))[0];
if (!response)
return [];
let statements;
if (options?.nodeVersion <= 2.3) {
statements = singleQuery
.replace(/\n/g, '')
.split(';')
.filter((statement) => statement !== '');
}
else {
statements = (0, exports.splitQueryToStatements)(singleQuery);
}
if (Array.isArray(response)) {
if (statements.length === 1)
response = [response];
}
else {
response = [response];
}
//because single query is used in this mode mapping itemIndex not possible, setting all items as paired
const pairedItem = (0, utilities_1.generatePairedItemData)(queries.length);
returnData = returnData.concat(prepareOutput(response, options, statements, this.helpers.constructExecutionMetaData, pairedItem));
}
catch (err) {
const error = parseMySqlError.call(this, err, 0, formattedQueries);
if (!this.continueOnFail())
throw error;
returnData.push({ json: { message: error.message, error: { ...error } } });
}
}
else {
if (mode === interfaces_1.BATCH_MODE.INDEPENDENTLY) {
let formattedQuery = '';
for (const [index, queryWithValues] of queries.entries()) {
try {
const { query, values } = queryWithValues;
formattedQuery = connection.format(query, values);
let statements;
if (options?.nodeVersion <= 2.3) {
statements = formattedQuery.split(';').map((q) => q.trim());
}
else {
statements = (0, exports.splitQueryToStatements)(formattedQuery, false);
}
const responses = [];
for (const statement of statements) {
if (statement === '')
continue;
const response = (await connection.query(statement))[0];
responses.push(response);
}
returnData = returnData.concat(prepareOutput(responses, options, statements, this.helpers.constructExecutionMetaData, { item: index }));
}
catch (err) {
const error = parseMySqlError.call(this, err, index, [formattedQuery]);
if (!this.continueOnFail()) {
connection.release();
throw error;
}
returnData.push(prepareErrorItem(queries[index], error, index));
}
}
}
if (mode === interfaces_1.BATCH_MODE.TRANSACTION) {
await connection.beginTransaction();
let formattedQuery = '';
for (const [index, queryWithValues] of queries.entries()) {
try {
const { query, values } = queryWithValues;
formattedQuery = connection.format(query, values);
let statements;
if (options?.nodeVersion <= 2.3) {
statements = formattedQuery.split(';').map((q) => q.trim());
}
else {
statements = (0, exports.splitQueryToStatements)(formattedQuery, false);
}
const responses = [];
for (const statement of statements) {
if (statement === '')
continue;
const response = (await connection.query(statement))[0];
responses.push(response);
}
returnData = returnData.concat(prepareOutput(responses, options, statements, this.helpers.constructExecutionMetaData, { item: index }));
}
catch (err) {
const error = parseMySqlError.call(this, err, index, [formattedQuery]);
if (connection) {
await connection.rollback();
connection.release();
}
if (!this.continueOnFail())
throw error;
returnData.push(prepareErrorItem(queries[index], error, index));
// Return here because we already rolled back the transaction
return returnData;
}
}
await connection.commit();
}
connection.release();
}
return returnData;
};
}
function addWhereClauses(node, itemIndex, query, clauses, replacements, combineConditions) {
if (clauses.length === 0)
return [query, replacements];
let combineWith = 'AND';
if (combineConditions === 'OR') {
combineWith = 'OR';
}
let whereQuery = ' WHERE';
const values = [];
clauses.forEach((clause, index) => {
if (clause.condition === 'equal') {
clause.condition = '=';
}
if (['>', '<', '>=', '<='].includes(clause.condition)) {
const value = Number(clause.value);
if (Number.isNaN(value)) {
throw new n8n_workflow_1.NodeOperationError(node, `Operator in entry ${index + 1} of 'Select Rows' works with numbers, but value ${clause.value} is not a number`, {
itemIndex,
});
}
clause.value = value;
}
let valueReplacement = ' ';
if (clause.condition !== 'IS NULL' && clause.condition !== 'IS NOT NULL') {
valueReplacement = ' ?';
values.push(clause.value);
}
const operator = index === clauses.length - 1 ? '' : ` ${combineWith}`;
whereQuery += ` ${escapeSqlIdentifier(clause.column)} ${clause.condition}${valueReplacement}${operator}`;
});
return [`${query}${whereQuery}`, replacements.concat.apply(replacements, values)];
}
function addSortRules(query, rules, replacements) {
if (rules.length === 0)
return [query, replacements];
let orderByQuery = ' ORDER BY';
const values = [];
rules.forEach((rule, index) => {
const endWith = index === rules.length - 1 ? '' : ',';
const direction = rule.direction === 'ASC' ? 'ASC' : 'DESC';
orderByQuery += ` ${escapeSqlIdentifier(rule.column)} ${direction}${endWith}`;
});
return [`${query}${orderByQuery}`, replacements.concat.apply(replacements, values)];
}
function replaceEmptyStringsByNulls(items, replace) {
if (!replace)
return [...items];
const returnData = items.map((item) => {
const newItem = { ...item };
const keys = Object.keys(newItem.json);
for (const key of keys) {
if (newItem.json[key] === '') {
newItem.json[key] = null;
}
}
return newItem;
});
return returnData;
}
// operations use 'equal' instead of '=' because of the way expressions are handled
// manually add '=' to allow entering it instead of 'equal'
const conditionSet = new Set(common_descriptions_1.operatorOptions.map((option) => option.value)).add('=');
const isWhereClause = (clause) => {
if (typeof clause !== 'object' || clause === null)
return false;
if (!('column' in clause))
return false;
if (!('condition' in clause) ||
typeof clause.condition !== 'string' ||
!conditionSet.has(clause.condition))
return false;
return true;
};
exports.isWhereClause = isWhereClause;
const getWhereClauses = (ctx, itemIndex) => {
const whereClauses = ctx.getNodeParameter('where', itemIndex, []);
const whereClausesValues = whereClauses.values;
if (!Array.isArray(whereClausesValues)) {
return [];
}
const someInvalid = whereClausesValues.some((clause) => !(0, exports.isWhereClause)(clause));
if (someInvalid) {
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Invalid where clause', {
itemIndex,
});
}
return whereClausesValues;
};
exports.getWhereClauses = getWhereClauses;
//# sourceMappingURL=utils.js.map