fix(Postgres Node): Account for JSON expressions (#12012)

Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com>
This commit is contained in:
Dana
2024-12-20 18:35:23 +01:00
committed by GitHub
parent d4116630a6
commit 06b86af735
4 changed files with 140 additions and 23 deletions

View File

@@ -14,6 +14,7 @@ import * as select from '../../v2/actions/database/select.operation';
import * as update from '../../v2/actions/database/update.operation'; import * as update from '../../v2/actions/database/update.operation';
import * as upsert from '../../v2/actions/database/upsert.operation'; import * as upsert from '../../v2/actions/database/upsert.operation';
import type { ColumnInfo, PgpDatabase, QueriesRunner } from '../../v2/helpers/interfaces'; import type { ColumnInfo, PgpDatabase, QueriesRunner } from '../../v2/helpers/interfaces';
import * as utils from '../../v2/helpers/utils';
const runQueries: QueriesRunner = jest.fn(); const runQueries: QueriesRunner = jest.fn();
@@ -360,6 +361,99 @@ describe('Test PostgresV2, executeQuery operation', () => {
nodeOptions, 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', () => { describe('Test PostgresV2, insert operation', () => {

View File

@@ -13,6 +13,7 @@ import {
replaceEmptyStringsByNulls, replaceEmptyStringsByNulls,
wrapData, wrapData,
convertArraysToPostgresFormat, convertArraysToPostgresFormat,
isJSON,
} from '../../v2/helpers/utils'; } from '../../v2/helpers/utils';
const node: INode = { 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', () => { describe('Test PostgresV2, wrapData', () => {
it('should wrap object in json', () => { it('should wrap object in json', () => {
const data = { const data = {

View File

@@ -3,7 +3,6 @@ import type {
IExecuteFunctions, IExecuteFunctions,
INodeExecutionData, INodeExecutionData,
INodeProperties, INodeProperties,
NodeParameterValueType,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow';
@@ -15,7 +14,7 @@ import type {
QueriesRunner, QueriesRunner,
QueryWithValues, QueryWithValues,
} from '../../helpers/interfaces'; } from '../../helpers/interfaces';
import { replaceEmptyStringsByNulls } from '../../helpers/utils'; import { isJSON, replaceEmptyStringsByNulls, stringToArray } from '../../helpers/utils';
import { optionsCollection } from '../common.descriptions'; import { optionsCollection } from '../common.descriptions';
const properties: INodeProperties[] = [ const properties: INodeProperties[] = [
@@ -54,20 +53,19 @@ export async function execute(
nodeOptions: PostgresNodeOptions, nodeOptions: PostgresNodeOptions,
_db?: PgpDatabase, _db?: PgpDatabase,
): Promise<INodeExecutionData[]> { ): Promise<INodeExecutionData[]> {
items = replaceEmptyStringsByNulls(items, nodeOptions.replaceEmptyStrings as boolean); const queries: QueryWithValues[] = replaceEmptyStringsByNulls(
items,
const queries: QueryWithValues[] = []; nodeOptions.replaceEmptyStrings as boolean,
).map((_, index) => {
for (let i = 0; i < items.length; i++) { let query = this.getNodeParameter('query', index) as string;
let query = this.getNodeParameter('query', i) as string;
for (const resolvable of getResolvables(query)) { 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<IDataObject | string> = []; let values: Array<IDataObject | string> = [];
let queryReplacement = this.getNodeParameter('options.queryReplacement', i, ''); let queryReplacement = this.getNodeParameter('options.queryReplacement', index, '');
if (typeof queryReplacement === 'number') { if (typeof queryReplacement === 'number') {
queryReplacement = String(queryReplacement); queryReplacement = String(queryReplacement);
@@ -78,14 +76,6 @@ export async function execute(
const rawReplacements = (node.parameters.options as IDataObject)?.queryReplacement as string; 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) { if (rawReplacements) {
const nodeVersion = nodeOptions.nodeVersion as number; const nodeVersion = nodeOptions.nodeVersion as number;
@@ -94,7 +84,12 @@ export async function execute(
const resolvables = getResolvables(rawValues); const resolvables = getResolvables(rawValues);
if (resolvables.length) { if (resolvables.length) {
for (const resolvable of resolvables) { 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); if (evaluatedValues.length) values.push(...evaluatedValues);
} }
} else { } else {
@@ -112,7 +107,7 @@ export async function execute(
if (resolvables.length) { if (resolvables.length) {
for (const resolvable of resolvables) { for (const resolvable of resolvables) {
values.push(this.evaluateExpression(`${resolvable}`, i) as IDataObject); values.push(this.evaluateExpression(`${resolvable}`, index) as IDataObject);
} }
} else { } else {
values.push(rawValue); values.push(rawValue);
@@ -127,7 +122,7 @@ export async function execute(
throw new NodeOperationError( throw new NodeOperationError(
this.getNode(), this.getNode(),
'Query Parameters must be a string of comma-separated values or an array of values', '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); return await runQueries(queries, items, nodeOptions);
} }

View File

@@ -4,6 +4,7 @@ import type {
INode, INode,
INodeExecutionData, INodeExecutionData,
INodePropertyOptions, INodePropertyOptions,
NodeParameterValueType,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeOperationError, jsonParse } from 'n8n-workflow'; import { NodeOperationError, jsonParse } from 'n8n-workflow';
@@ -20,6 +21,23 @@ import type {
} from './interfaces'; } from './interfaces';
import { generatePairedItemData } from '../../../../utils/utilities'; 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[] { export function wrapData(data: IDataObject | IDataObject[]): INodeExecutionData[] {
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
return [{ json: data }]; return [{ json: data }];