mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(Postgres Node): Allow passing in arrays to JSON columns for insert (#12452)
This commit is contained in:
@@ -6,6 +6,7 @@ import type {
|
|||||||
INode,
|
INode,
|
||||||
INodeParameters,
|
INodeParameters,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
import pgPromise from 'pg-promise';
|
||||||
|
|
||||||
import * as deleteTable from '../../v2/actions/database/deleteTable.operation';
|
import * as deleteTable from '../../v2/actions/database/deleteTable.operation';
|
||||||
import * as executeQuery from '../../v2/actions/database/executeQuery.operation';
|
import * as executeQuery from '../../v2/actions/database/executeQuery.operation';
|
||||||
@@ -506,6 +507,7 @@ describe('Test PostgresV2, insert operation', () => {
|
|||||||
items,
|
items,
|
||||||
nodeOptions,
|
nodeOptions,
|
||||||
createMockDb(columnsInfo),
|
createMockDb(columnsInfo),
|
||||||
|
pgPromise(),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(runQueries).toHaveBeenCalledWith(
|
expect(runQueries).toHaveBeenCalledWith(
|
||||||
@@ -579,6 +581,7 @@ describe('Test PostgresV2, insert operation', () => {
|
|||||||
inputItems,
|
inputItems,
|
||||||
nodeOptions,
|
nodeOptions,
|
||||||
createMockDb(columnsInfo),
|
createMockDb(columnsInfo),
|
||||||
|
pgPromise(),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(runQueries).toHaveBeenCalledWith(
|
expect(runQueries).toHaveBeenCalledWith(
|
||||||
@@ -600,6 +603,96 @@ describe('Test PostgresV2, insert operation', () => {
|
|||||||
nodeOptions,
|
nodeOptions,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('dataMode: define, should accept an array with values if column is of type json', async () => {
|
||||||
|
const convertValuesToJsonWithPgpSpy = jest.spyOn(utils, 'convertValuesToJsonWithPgp');
|
||||||
|
const hasJsonDataTypeInSchemaSpy = jest.spyOn(utils, 'hasJsonDataTypeInSchema');
|
||||||
|
|
||||||
|
const values = [
|
||||||
|
{ value: { id: 1, json: [], foo: 'data 1' }, expected: { id: 1, json: '[]', foo: 'data 1' } },
|
||||||
|
{
|
||||||
|
value: {
|
||||||
|
id: 2,
|
||||||
|
json: [0, 1],
|
||||||
|
foo: 'data 2',
|
||||||
|
},
|
||||||
|
expected: {
|
||||||
|
id: 2,
|
||||||
|
json: '[0,1]',
|
||||||
|
foo: 'data 2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: {
|
||||||
|
id: 2,
|
||||||
|
json: [0],
|
||||||
|
foo: 'data 2',
|
||||||
|
},
|
||||||
|
expected: {
|
||||||
|
id: 2,
|
||||||
|
json: '[0]',
|
||||||
|
foo: 'data 2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
values.forEach(async (value) => {
|
||||||
|
const valuePassedIn = value.value;
|
||||||
|
const nodeParameters: IDataObject = {
|
||||||
|
schema: {
|
||||||
|
__rl: true,
|
||||||
|
mode: 'list',
|
||||||
|
value: 'public',
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
__rl: true,
|
||||||
|
value: 'my_table',
|
||||||
|
mode: 'list',
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
mappingMode: 'defineBelow',
|
||||||
|
value: valuePassedIn,
|
||||||
|
},
|
||||||
|
options: { nodeVersion: 2.5 },
|
||||||
|
};
|
||||||
|
const columnsInfo: ColumnInfo[] = [
|
||||||
|
{ column_name: 'id', data_type: 'integer', is_nullable: 'NO', udt_name: '' },
|
||||||
|
{ column_name: 'json', data_type: 'json', is_nullable: 'NO', udt_name: '' },
|
||||||
|
{ column_name: 'foo', data_type: 'text', is_nullable: 'NO', udt_name: '' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const inputItems = [
|
||||||
|
{
|
||||||
|
json: valuePassedIn,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const nodeOptions = nodeParameters.options as IDataObject;
|
||||||
|
const pg = pgPromise();
|
||||||
|
|
||||||
|
await insert.execute.call(
|
||||||
|
createMockExecuteFunction(nodeParameters),
|
||||||
|
runQueries,
|
||||||
|
inputItems,
|
||||||
|
nodeOptions,
|
||||||
|
createMockDb(columnsInfo),
|
||||||
|
pg,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(runQueries).toHaveBeenCalledWith(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
query: 'INSERT INTO $1:name.$2:name($3:name) VALUES($3:csv) RETURNING *',
|
||||||
|
values: ['public', 'my_table', value.expected],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
inputItems,
|
||||||
|
nodeOptions,
|
||||||
|
);
|
||||||
|
expect(convertValuesToJsonWithPgpSpy).toHaveBeenCalledWith(pg, columnsInfo, valuePassedIn);
|
||||||
|
expect(hasJsonDataTypeInSchemaSpy).toHaveBeenCalledWith(columnsInfo);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Test PostgresV2, select operation', () => {
|
describe('Test PostgresV2, select operation', () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { IDataObject, INode } from 'n8n-workflow';
|
import type { IDataObject, INode } from 'n8n-workflow';
|
||||||
import { NodeOperationError } from 'n8n-workflow';
|
import { NodeOperationError } from 'n8n-workflow';
|
||||||
|
import pgPromise from 'pg-promise';
|
||||||
|
|
||||||
import type { ColumnInfo } from '../../v2/helpers/interfaces';
|
import type { ColumnInfo } from '../../v2/helpers/interfaces';
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +15,8 @@ import {
|
|||||||
wrapData,
|
wrapData,
|
||||||
convertArraysToPostgresFormat,
|
convertArraysToPostgresFormat,
|
||||||
isJSON,
|
isJSON,
|
||||||
|
convertValuesToJsonWithPgp,
|
||||||
|
hasJsonDataTypeInSchema,
|
||||||
} from '../../v2/helpers/utils';
|
} from '../../v2/helpers/utils';
|
||||||
|
|
||||||
const node: INode = {
|
const node: INode = {
|
||||||
@@ -387,6 +390,57 @@ describe('Test PostgresV2, checkItemAgainstSchema', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Test PostgresV2, hasJsonDataType', () => {
|
||||||
|
it('returns true if there are columns which are of type json', () => {
|
||||||
|
const schema: ColumnInfo[] = [
|
||||||
|
{ column_name: 'data', data_type: 'json', is_nullable: 'YES' },
|
||||||
|
{ column_name: 'id', data_type: 'integer', is_nullable: 'NO' },
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(hasJsonDataTypeInSchema(schema)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if there are columns which are of type json', () => {
|
||||||
|
const schema: ColumnInfo[] = [{ column_name: 'id', data_type: 'integer', is_nullable: 'NO' }];
|
||||||
|
|
||||||
|
expect(hasJsonDataTypeInSchema(schema)).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test PostgresV2, convertValuesToJsonWithPgp', () => {
|
||||||
|
it('should use pgp to properly convert values to JSON', () => {
|
||||||
|
const pgp = pgPromise();
|
||||||
|
const pgpJsonSpy = jest.spyOn(pgp.as, 'json');
|
||||||
|
|
||||||
|
const schema: ColumnInfo[] = [
|
||||||
|
{ column_name: 'data', data_type: 'json', is_nullable: 'YES' },
|
||||||
|
{ column_name: 'id', data_type: 'integer', is_nullable: 'NO' },
|
||||||
|
];
|
||||||
|
const values = [
|
||||||
|
{
|
||||||
|
value: { data: [], id: 1 },
|
||||||
|
expected: { data: '[]', id: 1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: { data: [0], id: 1 },
|
||||||
|
expected: { data: '[0]', id: 1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: { data: { key: 2 }, id: 1 },
|
||||||
|
expected: { data: '{"key":2}', id: 1 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
values.forEach((value) => {
|
||||||
|
const data = value.value.data;
|
||||||
|
|
||||||
|
expect(convertValuesToJsonWithPgp(pgp, schema, value.value)).toEqual(value.expected);
|
||||||
|
expect(value.value).toEqual(value.expected);
|
||||||
|
expect(pgpJsonSpy).toHaveBeenCalledWith(data, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Test PostgresV2, convertArraysToPostgresFormat', () => {
|
describe('Test PostgresV2, convertArraysToPostgresFormat', () => {
|
||||||
it('should convert js arrays to postgres format', () => {
|
it('should convert js arrays to postgres format', () => {
|
||||||
const item = {
|
const item = {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import type {
|
import {
|
||||||
IDataObject,
|
type IDataObject,
|
||||||
IExecuteFunctions,
|
type IExecuteFunctions,
|
||||||
INodeExecutionData,
|
type INodeExecutionData,
|
||||||
INodeProperties,
|
type INodeProperties,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { updateDisplayOptions } from '@utils/utilities';
|
import { updateDisplayOptions } from '@utils/utilities';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
PgpClient,
|
||||||
PgpDatabase,
|
PgpDatabase,
|
||||||
PostgresNodeOptions,
|
PostgresNodeOptions,
|
||||||
QueriesRunner,
|
QueriesRunner,
|
||||||
@@ -22,6 +23,8 @@ import {
|
|||||||
prepareItem,
|
prepareItem,
|
||||||
convertArraysToPostgresFormat,
|
convertArraysToPostgresFormat,
|
||||||
replaceEmptyStringsByNulls,
|
replaceEmptyStringsByNulls,
|
||||||
|
hasJsonDataTypeInSchema,
|
||||||
|
convertValuesToJsonWithPgp,
|
||||||
} from '../../helpers/utils';
|
} from '../../helpers/utils';
|
||||||
import { optionsCollection } from '../common.descriptions';
|
import { optionsCollection } from '../common.descriptions';
|
||||||
|
|
||||||
@@ -160,6 +163,7 @@ export async function execute(
|
|||||||
items: INodeExecutionData[],
|
items: INodeExecutionData[],
|
||||||
nodeOptions: PostgresNodeOptions,
|
nodeOptions: PostgresNodeOptions,
|
||||||
db: PgpDatabase,
|
db: PgpDatabase,
|
||||||
|
pgp: PgpClient,
|
||||||
): Promise<INodeExecutionData[]> {
|
): Promise<INodeExecutionData[]> {
|
||||||
items = replaceEmptyStringsByNulls(items, nodeOptions.replaceEmptyStrings as boolean);
|
items = replaceEmptyStringsByNulls(items, nodeOptions.replaceEmptyStrings as boolean);
|
||||||
const nodeVersion = nodeOptions.nodeVersion as number;
|
const nodeVersion = nodeOptions.nodeVersion as number;
|
||||||
@@ -215,11 +219,16 @@ export async function execute(
|
|||||||
: ((this.getNodeParameter('columns.values', i, []) as IDataObject)
|
: ((this.getNodeParameter('columns.values', i, []) as IDataObject)
|
||||||
.values as IDataObject[]);
|
.values as IDataObject[]);
|
||||||
|
|
||||||
if (nodeVersion < 2.2) {
|
item =
|
||||||
item = prepareItem(valuesToSend);
|
nodeVersion < 2.2
|
||||||
} else {
|
? prepareItem(valuesToSend)
|
||||||
item = this.getNodeParameter('columns.value', i) as IDataObject;
|
: hasJsonDataTypeInSchema(tableSchema)
|
||||||
}
|
? convertValuesToJsonWithPgp(
|
||||||
|
pgp,
|
||||||
|
tableSchema,
|
||||||
|
(this.getNodeParameter('columns', i) as IDataObject)?.value as IDataObject,
|
||||||
|
)
|
||||||
|
: (this.getNodeParameter('columns.value', i) as IDataObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
tableSchema = await updateTableSchema(db, tableSchema, schema, table);
|
tableSchema = await updateTableSchema(db, tableSchema, schema, table);
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export async function router(this: IExecuteFunctions): Promise<INodeExecutionDat
|
|||||||
items,
|
items,
|
||||||
options,
|
options,
|
||||||
db,
|
db,
|
||||||
|
pgp,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -394,6 +394,24 @@ export function prepareItem(values: IDataObject[]) {
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasJsonDataTypeInSchema(schema: ColumnInfo[]) {
|
||||||
|
return schema.some(({ data_type }) => data_type === 'json');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertValuesToJsonWithPgp(
|
||||||
|
pgp: PgpClient,
|
||||||
|
schema: ColumnInfo[],
|
||||||
|
values: IDataObject,
|
||||||
|
) {
|
||||||
|
schema
|
||||||
|
.filter(({ data_type }: { data_type: string }) => data_type === 'json')
|
||||||
|
.forEach(({ column_name }) => {
|
||||||
|
values[column_name] = pgp.as.json(values[column_name], true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
export async function columnFeatureSupport(
|
export async function columnFeatureSupport(
|
||||||
db: PgpDatabase,
|
db: PgpDatabase,
|
||||||
): Promise<{ identity_generation: boolean; is_generated: boolean }> {
|
): Promise<{ identity_generation: boolean; is_generated: boolean }> {
|
||||||
|
|||||||
Reference in New Issue
Block a user