Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Postgres Node): Allow passing in arrays to JSON columns for insert #12452

Merged
merged 3 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions packages/nodes-base/nodes/Postgres/test/v2/operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
INode,
INodeParameters,
} from 'n8n-workflow';
import pgPromise from 'pg-promise';

import * as deleteTable from '../../v2/actions/database/deleteTable.operation';
import * as executeQuery from '../../v2/actions/database/executeQuery.operation';
Expand Down Expand Up @@ -506,6 +507,7 @@ describe('Test PostgresV2, insert operation', () => {
items,
nodeOptions,
createMockDb(columnsInfo),
pgPromise(),
);

expect(runQueries).toHaveBeenCalledWith(
Expand Down Expand Up @@ -579,6 +581,7 @@ describe('Test PostgresV2, insert operation', () => {
inputItems,
nodeOptions,
createMockDb(columnsInfo),
pgPromise(),
);

expect(runQueries).toHaveBeenCalledWith(
Expand All @@ -600,6 +603,96 @@ describe('Test PostgresV2, insert operation', () => {
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', () => {
Expand Down
54 changes: 54 additions & 0 deletions packages/nodes-base/nodes/Postgres/test/v2/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { IDataObject, INode } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import pgPromise from 'pg-promise';

import type { ColumnInfo } from '../../v2/helpers/interfaces';
import {
Expand All @@ -14,6 +15,8 @@ import {
wrapData,
convertArraysToPostgresFormat,
isJSON,
convertValuesToJsonWithPgp,
hasJsonDataTypeInSchema,
} from '../../v2/helpers/utils';

const node: INode = {
Expand Down Expand Up @@ -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', () => {
it('should convert js arrays to postgres format', () => {
const item = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
import {
type IDataObject,
type IExecuteFunctions,
type INodeExecutionData,
type INodeProperties,
} from 'n8n-workflow';

import { updateDisplayOptions } from '@utils/utilities';

import type {
PgpClient,
PgpDatabase,
PostgresNodeOptions,
QueriesRunner,
Expand All @@ -22,6 +23,8 @@ import {
prepareItem,
convertArraysToPostgresFormat,
replaceEmptyStringsByNulls,
hasJsonDataTypeInSchema,
convertValuesToJsonWithPgp,
} from '../../helpers/utils';
import { optionsCollection } from '../common.descriptions';

Expand Down Expand Up @@ -160,6 +163,7 @@ export async function execute(
items: INodeExecutionData[],
nodeOptions: PostgresNodeOptions,
db: PgpDatabase,
pgp: PgpClient,
): Promise<INodeExecutionData[]> {
items = replaceEmptyStringsByNulls(items, nodeOptions.replaceEmptyStrings as boolean);
const nodeVersion = nodeOptions.nodeVersion as number;
Expand Down Expand Up @@ -215,11 +219,16 @@ export async function execute(
: ((this.getNodeParameter('columns.values', i, []) as IDataObject)
.values as IDataObject[]);

if (nodeVersion < 2.2) {
item = prepareItem(valuesToSend);
} else {
item = this.getNodeParameter('columns.value', i) as IDataObject;
}
item =
nodeVersion < 2.2
? prepareItem(valuesToSend)
: 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);
Expand Down
1 change: 1 addition & 0 deletions packages/nodes-base/nodes/Postgres/v2/actions/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export async function router(this: IExecuteFunctions): Promise<INodeExecutionDat
items,
options,
db,
pgp,
);
break;
default:
Expand Down
18 changes: 18 additions & 0 deletions packages/nodes-base/nodes/Postgres/v2/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,24 @@ export function prepareItem(values: IDataObject[]) {
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(
db: PgpDatabase,
): Promise<{ identity_generation: boolean; is_generated: boolean }> {
Expand Down
Loading