UNPKG

n8n-nodes-base

Version:

Base nodes of n8n

527 lines 22 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.runQueriesAndHandleErrors = exports.getWhereClauses = exports.isWhereClause = exports.convertArraysToPostgresFormat = exports.configureTableSchemaUpdater = void 0; exports.isJSON = isJSON; exports.evaluateExpression = evaluateExpression; exports.stringToArray = stringToArray; exports.wrapData = wrapData; exports.prepareErrorItem = prepareErrorItem; exports.parsePostgresError = parsePostgresError; exports.addWhereClauses = addWhereClauses; exports.addSortRules = addSortRules; exports.addReturning = addReturning; exports.configureQueryRunner = configureQueryRunner; exports.replaceEmptyStringsByNulls = replaceEmptyStringsByNulls; exports.prepareItem = prepareItem; exports.hasJsonDataTypeInSchema = hasJsonDataTypeInSchema; exports.convertValuesToJsonWithPgp = convertValuesToJsonWithPgp; exports.columnFeatureSupport = columnFeatureSupport; exports.getTableSchema = getTableSchema; exports.uniqueColumns = uniqueColumns; exports.getEnums = getEnums; exports.getEnumValues = getEnumValues; exports.doesRowExist = doesRowExist; exports.checkItemAgainstSchema = checkItemAgainstSchema; const n8n_workflow_1 = require("n8n-workflow"); const utilities_1 = require("../../../../utils/utilities"); const common_descriptions_1 = require("../actions/common.descriptions"); function isJSON(str) { try { JSON.parse(str.trim()); return true; } catch { return false; } } function evaluateExpression(expression) { if (expression === undefined) { return ''; } else if (expression === null) { return 'null'; } else { return typeof expression === 'object' ? JSON.stringify(expression) : expression.toString(); } } function stringToArray(str) { if (str === undefined) return []; return String(str) .split(',') .filter((entry) => entry) .map((entry) => entry.trim()); } function wrapData(data) { if (!Array.isArray(data)) { return [{ json: data }]; } return data.map((item) => ({ json: item, })); } function prepareErrorItem(error, index) { return { json: { error: { ...error } }, pairedItem: { item: index }, }; } function parsePostgresError(node, error, queries, itemIndex) { if (error.message.includes('syntax error at or near') && queries.length) { try { const snippet = error.message.match(/syntax error at or near "(.*)"/)[1]; const failedQureryIndex = queries.findIndex((query) => query.query.includes(snippet)); if (failedQureryIndex !== -1) { if (!itemIndex) { itemIndex = failedQureryIndex; } const failedQuery = queries[failedQureryIndex].query; const lines = failedQuery.split('\n'); const lineIndex = lines.findIndex((line) => line.includes(snippet)); const errorMessage = `Syntax error at line ${lineIndex + 1} near "${snippet}"`; error.message = errorMessage; } } catch { } } let message = error.message; const errorDescription = error.description ? error.description : error.detail || error.hint; let description = errorDescription; if (!description && queries[itemIndex || 0]?.query) { description = `Failed query: ${queries[itemIndex || 0].query}`; } if (error.message.includes('ECONNREFUSED')) { message = 'Connection refused'; try { description = error.message.split('ECONNREFUSED ')[1].trim(); } catch (e) { } } if (error.message.includes('ENOTFOUND')) { message = 'Host not found'; try { description = error.message.split('ENOTFOUND ')[1].trim(); } catch (e) { } } if (error.message.includes('ETIMEDOUT')) { message = 'Connection timed out'; try { description = error.message.split('ETIMEDOUT ')[1].trim(); } catch (e) { } } return new n8n_workflow_1.NodeOperationError(node, error, { message, description, itemIndex, }); } function addWhereClauses(_node, _itemIndex, query, clauses, replacements, combineConditions) { if (clauses.length === 0) return [query, replacements]; let combineWith = 'AND'; if (combineConditions === 'OR') { combineWith = 'OR'; } let replacementIndex = replacements.length + 1; let whereQuery = ' WHERE'; const values = []; clauses.forEach((clause, index) => { if (clause.condition === 'equal') { clause.condition = '='; } if (['>', '<', '>=', '<='].includes(clause.condition)) { const numericValue = Number(clause.value); if (String(clause.value).trim() !== '' && !Number.isNaN(numericValue)) { clause.value = numericValue; } } const columnReplacement = `$${replacementIndex}:name`; values.push(clause.column); replacementIndex = replacementIndex + 1; let valueReplacement = ''; if (clause.condition !== 'IS NULL' && clause.condition !== 'IS NOT NULL') { valueReplacement = ` $${replacementIndex}`; values.push(clause.value); replacementIndex = replacementIndex + 1; } const operator = index === clauses.length - 1 ? '' : ` ${combineWith}`; whereQuery += ` ${columnReplacement} ${clause.condition}${valueReplacement}${operator}`; }); return [`${query}${whereQuery}`, replacements.concat(...values)]; } function addSortRules(query, rules, replacements) { if (rules.length === 0) return [query, replacements]; let replacementIndex = replacements.length + 1; let orderByQuery = ' ORDER BY'; const values = []; rules.forEach((rule, index) => { const columnReplacement = `$${replacementIndex}:name`; values.push(rule.column); replacementIndex = replacementIndex + 1; const endWith = index === rules.length - 1 ? '' : ','; const sortDirection = rule.direction === 'DESC' ? 'DESC' : 'ASC'; orderByQuery += ` ${columnReplacement} ${sortDirection}${endWith}`; }); return [`${query}${orderByQuery}`, replacements.concat(...values)]; } function addReturning(query, outputColumns, replacements) { if (outputColumns.includes('*')) return [`${query} RETURNING *`, replacements]; const replacementIndex = replacements.length + 1; return [`${query} RETURNING $${replacementIndex}:name`, [...replacements, outputColumns]]; } const isSelectQuery = (query) => { return query .replace(/\/\*.*?\*\//g, '') // remove multiline comments .replace(/\n/g, '') .split(';') .filter((statement) => statement && !statement.startsWith('--')) // remove comments and empty statements .every((statement) => statement.trim().toLowerCase().startsWith('select')); }; function configureQueryRunner(node, continueOnFail, pgp, db) { return async (queries, options) => { let returnData = []; const emptyReturnData = options.operation === 'select' ? [] : [{ json: { success: true } }]; const queryBatching = options.queryBatching || 'single'; if (queryBatching === 'single') { try { returnData = (await db.multi(pgp.helpers.concat(queries))) .map((result, i) => { return this.helpers.constructExecutionMetaData(wrapData(result), { itemData: { item: i }, }); }) .flat(); if (!returnData.length) { const pairedItem = (0, utilities_1.generatePairedItemData)(queries.length); if (options?.nodeVersion < 2.3) { if (emptyReturnData.length) { emptyReturnData[0].pairedItem = pairedItem; } returnData = emptyReturnData; } else { returnData = queries.every((query) => isSelectQuery(query.query)) ? [] : [{ json: { success: true }, pairedItem }]; } } } catch (err) { const error = parsePostgresError(node, err, queries); if (!continueOnFail) throw error; return [ { json: { message: error.message, error: { ...error }, }, }, ]; } } if (queryBatching === 'transaction') { returnData = await db.tx(async (transaction) => { const result = []; for (let i = 0; i < queries.length; i++) { try { const query = queries[i].query; const values = queries[i].values; let transactionResults; if (options?.nodeVersion < 2.3) { transactionResults = await transaction.any(query, values); } else { transactionResults = (await transaction.multi(query, values)).flat(); } if (!transactionResults.length) { if (options?.nodeVersion < 2.3) { transactionResults = emptyReturnData; } else { transactionResults = isSelectQuery(query) ? [] : [{ success: true }]; } } const executionData = this.helpers.constructExecutionMetaData(wrapData(transactionResults), { itemData: { item: i } }); result.push(...executionData); } catch (err) { const error = parsePostgresError(node, err, queries, i); if (!continueOnFail) throw error; result.push(prepareErrorItem(error, i)); return result; } } return result; }); } if (queryBatching === 'independently') { returnData = await db.task(async (task) => { const result = []; for (let i = 0; i < queries.length; i++) { try { const query = queries[i].query; const values = queries[i].values; let transactionResults; if (options?.nodeVersion < 2.3) { transactionResults = await task.any(query, values); } else { transactionResults = (await task.multi(query, values)).flat(); } if (!transactionResults.length) { if (options?.nodeVersion < 2.3) { transactionResults = emptyReturnData; } else { transactionResults = isSelectQuery(query) ? [] : [{ success: true }]; } } const executionData = this.helpers.constructExecutionMetaData(wrapData(transactionResults), { itemData: { item: i } }); result.push(...executionData); } catch (err) { const error = parsePostgresError(node, err, queries, i); if (!continueOnFail) throw error; result.push(prepareErrorItem(error, i)); } } return result; }); } return returnData; }; } 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; } function prepareItem(values) { const item = values.reduce((acc, { column, value }) => { acc[column] = value; return acc; }, {}); return item; } function hasJsonDataTypeInSchema(schema) { return schema.some(({ data_type }) => data_type === 'json'); } function convertValuesToJsonWithPgp(pgp, schema, values) { schema .filter(({ data_type, column_name }) => data_type === 'json' && values[column_name] !== null && values[column_name] !== undefined) .forEach(({ column_name }) => { values[column_name] = pgp.as.json(values[column_name], true); }); return values; } async function columnFeatureSupport(db) { const result = await db.any(`SELECT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'columns' AND table_schema = 'information_schema' AND column_name = 'is_generated' ) as is_generated, EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'columns' AND table_schema = 'information_schema' AND column_name = 'identity_generation' ) as identity_generation;`); return result[0]; } async function getTableSchema(db, schema, table, options) { const select = ['column_name', 'data_type', 'is_nullable', 'udt_name', 'column_default']; if (options?.getColumnsForResourceMapper) { // Check if columns exist before querying (identity_generation was added in v10, is_generated in v12) const supported = await columnFeatureSupport(db); if (supported.identity_generation) { select.push('identity_generation'); } if (supported.is_generated) { select.push('is_generated'); } } const selectString = select.join(', '); const columns = await db.any(`SELECT ${selectString} FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2`, [schema, table]); return columns; } async function uniqueColumns(db, table, schema = 'public') { // Using the modified query from https://wiki.postgresql.org/wiki/Retrieve_primary_key_columns // `quote_ident` - properly quote and escape an identifier // `::regclass` - cast a string to a regclass (internal type for object names) const unique = await db.any(` SELECT DISTINCT a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = (quote_ident($1) || '.' || quote_ident($2))::regclass AND (i.indisprimary OR i.indisunique); `, [schema, table]); return unique; } async function getEnums(db) { const enums = await db.any('SELECT pg_type.typname, pg_enum.enumlabel FROM pg_type JOIN pg_enum ON pg_enum.enumtypid = pg_type.oid;'); return enums.reduce((map, { typname, enumlabel }) => { const existingValues = map.get(typname) ?? []; map.set(typname, [...existingValues, enumlabel]); return map; }, new Map()); } function getEnumValues(enumInfo, enumName) { const values = enumInfo.get(enumName) ?? []; return values.map((value) => ({ name: value, value })); } async function doesRowExist(db, schema, table, values) { const where = []; for (let i = 3; i < 3 + values.length; i += 2) { where.push(`$${i}:name=$${i + 1}`); } const exists = await db.any(`SELECT EXISTS(SELECT 1 FROM $1:name.$2:name WHERE ${where.join(' AND ')})`, [schema, table, ...values]); return exists[0].exists; } function checkItemAgainstSchema(node, item, columnsInfo, index) { if (columnsInfo.length === 0) return item; const schema = columnsInfo.reduce((acc, { column_name, data_type, is_nullable }) => { acc[column_name] = { type: data_type.toUpperCase(), nullable: is_nullable === 'YES' }; return acc; }, {}); for (const key of Object.keys(item)) { if (schema[key] === undefined) { throw new n8n_workflow_1.NodeOperationError(node, `Column '${key}' does not exist in selected table`, { itemIndex: index, }); } if (item[key] === null && !schema[key]?.nullable) { throw new n8n_workflow_1.NodeOperationError(node, `Column '${key}' is not nullable`, { itemIndex: index, }); } } return item; } const configureTableSchemaUpdater = (initialSchema, initialTable) => { let currentSchema = initialSchema; let currentTable = initialTable; return async (db, tableSchema, schema, table) => { if (currentSchema !== schema || currentTable !== table) { currentSchema = schema; currentTable = table; tableSchema = await getTableSchema(db, schema, table); } return tableSchema; }; }; exports.configureTableSchemaUpdater = configureTableSchemaUpdater; /** * If postgress column type is array we need to convert it to fornmat that postgres understands, original object data would be modified * @param data the object with keys representing column names and values * @param schema table schema * @param node INode * @param itemIndex the index of the current item * @returns a new data object with the arrays converted to postgres format */ const convertArraysToPostgresFormat = (data, schema, node, itemIndex = 0) => { const newData = (0, n8n_workflow_1.deepCopy)(data); for (const columnInfo of schema) { // in case column type is array we need to convert it to fornmat that postgres understands if (columnInfo.data_type.toUpperCase() === 'ARRAY') { let columnValue = newData[columnInfo.column_name]; if (typeof columnValue === 'string') { columnValue = (0, n8n_workflow_1.jsonParse)(columnValue); } if (Array.isArray(columnValue)) { const arrayEntries = columnValue.map((entry) => { if (typeof entry === 'number') { return entry; } if (typeof entry === 'boolean') { entry = String(entry); } if (typeof entry === 'object') { entry = JSON.stringify(entry); } if (typeof entry === 'string') { return `"${entry.replace(/"/g, '\\"')}"`; //escape double quotes } return entry; }); // wrap in {} instead of [] as postgres does and join with , newData[columnInfo.column_name] = `{${arrayEntries.join(',')}}`; } else { if (columnInfo.is_nullable === 'NO') { throw new n8n_workflow_1.NodeOperationError(node, `Column '${columnInfo.column_name}' has to be an array`, { itemIndex, }); } } } } return newData; }; exports.convertArraysToPostgresFormat = convertArraysToPostgresFormat; // 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; const runQueriesAndHandleErrors = async (runQueries, queries, nodeOptions, errorItemsMap) => { // if we have any errors and we are not running the queries independently // (i.e. `transaction` or `single` mode), we don't want to execute any // queries that didn't error, since the operation should be atomic if (errorItemsMap.size > 0 && nodeOptions.queryBatching !== 'independently') { return Array.from(errorItemsMap.values()); } const returnData = await runQueries(queries, nodeOptions); const total = returnData.length + errorItemsMap.size; const result = new Array(total); let returnDataIndex = 0; for (let i = 0; i < total; i++) { const errorItem = errorItemsMap.get(i); if (errorItem) { result[i] = errorItem; } else if (returnDataIndex < returnData.length) { result[i] = returnData[returnDataIndex++]; } } return result; }; exports.runQueriesAndHandleErrors = runQueriesAndHandleErrors; //# sourceMappingURL=utils.js.map