diff --git a/packages/nodes-base/nodes/Postgres/test/v2/operations.test.ts b/packages/nodes-base/nodes/Postgres/test/v2/operations.test.ts index 206e6bf74a..a65c2d8de9 100644 --- a/packages/nodes-base/nodes/Postgres/test/v2/operations.test.ts +++ b/packages/nodes-base/nodes/Postgres/test/v2/operations.test.ts @@ -14,6 +14,7 @@ import * as select from '../../v2/actions/database/select.operation'; import * as update from '../../v2/actions/database/update.operation'; import * as upsert from '../../v2/actions/database/upsert.operation'; import type { ColumnInfo, PgpDatabase, QueriesRunner } from '../../v2/helpers/interfaces'; +import * as utils from '../../v2/helpers/utils'; const runQueries: QueriesRunner = jest.fn(); @@ -360,6 +361,99 @@ describe('Test PostgresV2, executeQuery operation', () => { nodeOptions, ); }); + + it('should execute queries with multiple json key/value pairs', async () => { + const nodeParameters: IDataObject = { + operation: 'executeQuery', + query: 'SELECT *\nFROM users\nWHERE username IN ($1, $2, $3)', + options: { + queryReplacement: + '={{ JSON.stringify({id: "7",id2: "848da11d-e72e-44c5-yyyy-c6fb9f17d366"}) }}', + }, + }; + const nodeOptions = nodeParameters.options as IDataObject; + + expect(async () => { + await executeQuery.execute.call( + createMockExecuteFunction(nodeParameters), + runQueries, + items, + nodeOptions, + ); + }).not.toThrow(); + }); + + it('should execute queries with single json key/value pair', async () => { + const nodeParameters: IDataObject = { + operation: 'executeQuery', + query: 'SELECT *\nFROM users\nWHERE username IN ($1, $2, $3)', + options: { + queryReplacement: '={{ {"id": "7"} }}', + }, + }; + const nodeOptions = nodeParameters.options as IDataObject; + + expect(async () => { + await executeQuery.execute.call( + createMockExecuteFunction(nodeParameters), + runQueries, + items, + nodeOptions, + ); + }).not.toThrow(); + }); + + it('should not parse out expressions if there are valid JSON query parameters', async () => { + const query = 'SELECT *\nFROM users\nWHERE username IN ($1, $2, $3)'; + const nodeParameters: IDataObject = { + operation: 'executeQuery', + query, + options: { + queryReplacement: '={{ {"id": "7"} }}', + nodeVersion: 2.6, + }, + }; + const nodeOptions = nodeParameters.options as IDataObject; + + jest.spyOn(utils, 'isJSON'); + jest.spyOn(utils, 'stringToArray'); + + await executeQuery.execute.call( + createMockExecuteFunction(nodeParameters), + runQueries, + items, + nodeOptions, + ); + + expect(utils.isJSON).toHaveBeenCalledTimes(1); + expect(utils.stringToArray).toHaveBeenCalledTimes(0); + }); + + it('should parse out expressions if is invalid JSON in query parameters', async () => { + const query = 'SELECT *\nFROM users\nWHERE username IN ($1, $2, $3)'; + const nodeParameters: IDataObject = { + operation: 'executeQuery', + query, + options: { + queryReplacement: '={{ JSON.stringify({"id": "7"}}) }}', + nodeVersion: 2.6, + }, + }; + const nodeOptions = nodeParameters.options as IDataObject; + + jest.spyOn(utils, 'isJSON'); + jest.spyOn(utils, 'stringToArray'); + + await executeQuery.execute.call( + createMockExecuteFunction(nodeParameters), + runQueries, + items, + nodeOptions, + ); + + expect(utils.isJSON).toHaveBeenCalledTimes(1); + expect(utils.stringToArray).toHaveBeenCalledTimes(1); + }); }); describe('Test PostgresV2, insert operation', () => { diff --git a/packages/nodes-base/nodes/Postgres/test/v2/utils.test.ts b/packages/nodes-base/nodes/Postgres/test/v2/utils.test.ts index b8526c0e6f..25840b33bd 100644 --- a/packages/nodes-base/nodes/Postgres/test/v2/utils.test.ts +++ b/packages/nodes-base/nodes/Postgres/test/v2/utils.test.ts @@ -13,6 +13,7 @@ import { replaceEmptyStringsByNulls, wrapData, convertArraysToPostgresFormat, + isJSON, } from '../../v2/helpers/utils'; const node: INode = { @@ -26,6 +27,15 @@ const node: INode = { }, }; +describe('Test PostgresV2, isJSON', () => { + it('should return true for valid JSON', () => { + expect(isJSON('{"key": "value"}')).toEqual(true); + }); + it('should return false for invalid JSON', () => { + expect(isJSON('{"key": "value"')).toEqual(false); + }); +}); + describe('Test PostgresV2, wrapData', () => { it('should wrap object in json', () => { const data = { diff --git a/packages/nodes-base/nodes/Postgres/v2/actions/database/executeQuery.operation.ts b/packages/nodes-base/nodes/Postgres/v2/actions/database/executeQuery.operation.ts index 06854bf018..75e0c78587 100644 --- a/packages/nodes-base/nodes/Postgres/v2/actions/database/executeQuery.operation.ts +++ b/packages/nodes-base/nodes/Postgres/v2/actions/database/executeQuery.operation.ts @@ -3,7 +3,6 @@ import type { IExecuteFunctions, INodeExecutionData, INodeProperties, - NodeParameterValueType, } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow'; @@ -15,7 +14,7 @@ import type { QueriesRunner, QueryWithValues, } from '../../helpers/interfaces'; -import { replaceEmptyStringsByNulls } from '../../helpers/utils'; +import { isJSON, replaceEmptyStringsByNulls, stringToArray } from '../../helpers/utils'; import { optionsCollection } from '../common.descriptions'; const properties: INodeProperties[] = [ @@ -54,20 +53,19 @@ export async function execute( nodeOptions: PostgresNodeOptions, _db?: PgpDatabase, ): Promise { - items = replaceEmptyStringsByNulls(items, nodeOptions.replaceEmptyStrings as boolean); - - const queries: QueryWithValues[] = []; - - for (let i = 0; i < items.length; i++) { - let query = this.getNodeParameter('query', i) as string; + const queries: QueryWithValues[] = replaceEmptyStringsByNulls( + items, + nodeOptions.replaceEmptyStrings as boolean, + ).map((_, index) => { + let query = this.getNodeParameter('query', index) as string; for (const resolvable of getResolvables(query)) { - query = query.replace(resolvable, this.evaluateExpression(resolvable, i) as string); + query = query.replace(resolvable, this.evaluateExpression(resolvable, index) as string); } let values: Array = []; - let queryReplacement = this.getNodeParameter('options.queryReplacement', i, ''); + let queryReplacement = this.getNodeParameter('options.queryReplacement', index, ''); if (typeof queryReplacement === 'number') { queryReplacement = String(queryReplacement); @@ -78,14 +76,6 @@ export async function execute( const rawReplacements = (node.parameters.options as IDataObject)?.queryReplacement as string; - const stringToArray = (str: NodeParameterValueType | undefined) => { - if (str === undefined) return []; - return String(str) - .split(',') - .filter((entry) => entry) - .map((entry) => entry.trim()); - }; - if (rawReplacements) { const nodeVersion = nodeOptions.nodeVersion as number; @@ -94,7 +84,12 @@ export async function execute( const resolvables = getResolvables(rawValues); if (resolvables.length) { for (const resolvable of resolvables) { - const evaluatedValues = stringToArray(this.evaluateExpression(`${resolvable}`, i)); + const evaluatedExpression = + this.evaluateExpression(`${resolvable}`, index)?.toString() ?? ''; + const evaluatedValues = isJSON(evaluatedExpression) + ? [evaluatedExpression] + : stringToArray(evaluatedExpression); + if (evaluatedValues.length) values.push(...evaluatedValues); } } else { @@ -112,7 +107,7 @@ export async function execute( if (resolvables.length) { for (const resolvable of resolvables) { - values.push(this.evaluateExpression(`${resolvable}`, i) as IDataObject); + values.push(this.evaluateExpression(`${resolvable}`, index) as IDataObject); } } else { values.push(rawValue); @@ -127,7 +122,7 @@ export async function execute( throw new NodeOperationError( this.getNode(), 'Query Parameters must be a string of comma-separated values or an array of values', - { itemIndex: i }, + { itemIndex: index }, ); } } @@ -142,8 +137,8 @@ export async function execute( } } - queries.push({ query, values, options: { partial: true } }); - } + return { query, values, options: { partial: true } }; + }); return await runQueries(queries, items, nodeOptions); } diff --git a/packages/nodes-base/nodes/Postgres/v2/helpers/utils.ts b/packages/nodes-base/nodes/Postgres/v2/helpers/utils.ts index 58f1c8a96f..69e0ff9046 100644 --- a/packages/nodes-base/nodes/Postgres/v2/helpers/utils.ts +++ b/packages/nodes-base/nodes/Postgres/v2/helpers/utils.ts @@ -4,6 +4,7 @@ import type { INode, INodeExecutionData, INodePropertyOptions, + NodeParameterValueType, } from 'n8n-workflow'; import { NodeOperationError, jsonParse } from 'n8n-workflow'; @@ -20,6 +21,23 @@ import type { } from './interfaces'; import { generatePairedItemData } from '../../../../utils/utilities'; +export function isJSON(str: string) { + try { + JSON.parse(str.trim()); + return true; + } catch { + return false; + } +} + +export function stringToArray(str: NodeParameterValueType | undefined) { + if (str === undefined) return []; + return String(str) + .split(',') + .filter((entry) => entry) + .map((entry) => entry.trim()); +} + export function wrapData(data: IDataObject | IDataObject[]): INodeExecutionData[] { if (!Array.isArray(data)) { return [{ json: data }];