Skip to content

Commit

Permalink
fix(Postgres Node): Allow passing in arrays to JSON columns for insert (
Browse files Browse the repository at this point in the history
  • Loading branch information
dana-gill authored Jan 8, 2025
1 parent 2c72047 commit 9dd0686
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 10 deletions.
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

0 comments on commit 9dd0686

Please sign in to comment.