fix(Postgres Node): Convert js arrays to postgres type, if column type is ARRAY (#9160)

This commit is contained in:
Michael Kret 2024-04-18 12:58:04 +03:00 committed by GitHub
parent d756609826
commit 08e35027f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 160 additions and 7 deletions

View file

@ -11,7 +11,7 @@ export class Postgres extends VersionedNodeType {
name: 'postgres', name: 'postgres',
icon: 'file:postgres.svg', icon: 'file:postgres.svg',
group: ['input'], group: ['input'],
defaultVersion: 2.3, defaultVersion: 2.4,
description: 'Get, add and update data in Postgres', description: 'Get, add and update data in Postgres',
parameterPane: 'wide', parameterPane: 'wide',
}; };
@ -22,6 +22,7 @@ export class Postgres extends VersionedNodeType {
2.1: new PostgresV2(baseDescription), 2.1: new PostgresV2(baseDescription),
2.2: new PostgresV2(baseDescription), 2.2: new PostgresV2(baseDescription),
2.3: new PostgresV2(baseDescription), 2.3: new PostgresV2(baseDescription),
2.4: new PostgresV2(baseDescription),
}; };
super(nodeVersions, baseDescription); super(nodeVersions, baseDescription);

View file

@ -10,7 +10,9 @@ import {
prepareItem, prepareItem,
replaceEmptyStringsByNulls, replaceEmptyStringsByNulls,
wrapData, wrapData,
convertArraysToPostgresFormat,
} from '../../v2/helpers/utils'; } from '../../v2/helpers/utils';
import type { ColumnInfo } from '../../v2/helpers/interfaces';
const node: INode = { const node: INode = {
id: '1', id: '1',
@ -373,3 +375,78 @@ describe('Test PostgresV2, checkItemAgainstSchema', () => {
} }
}); });
}); });
describe('Test PostgresV2, convertArraysToPostgresFormat', () => {
it('should convert js arrays to postgres format', () => {
const item = {
jsonb_array: [
{
key: 'value44',
},
],
json_array: [
{
key: 'value54',
},
],
int_array: [1, 2, 5],
text_array: ['one', 't"w"o'],
bool_array: [true, false],
};
const schema: ColumnInfo[] = [
{
column_name: 'id',
data_type: 'integer',
is_nullable: 'NO',
udt_name: 'int4',
column_default: "nextval('test_data_array_id_seq'::regclass)",
},
{
column_name: 'jsonb_array',
data_type: 'ARRAY',
is_nullable: 'YES',
udt_name: '_jsonb',
column_default: null,
},
{
column_name: 'json_array',
data_type: 'ARRAY',
is_nullable: 'YES',
udt_name: '_json',
column_default: null,
},
{
column_name: 'int_array',
data_type: 'ARRAY',
is_nullable: 'YES',
udt_name: '_int4',
column_default: null,
},
{
column_name: 'bool_array',
data_type: 'ARRAY',
is_nullable: 'YES',
udt_name: '_bool',
column_default: null,
},
{
column_name: 'text_array',
data_type: 'ARRAY',
is_nullable: 'YES',
udt_name: '_text',
column_default: null,
},
];
convertArraysToPostgresFormat(item, schema, node, 0);
expect(item).toEqual({
jsonb_array: '{"{\\"key\\":\\"value44\\"}"}',
json_array: '{"{\\"key\\":\\"value54\\"}"}',
int_array: '{1,2,5}',
text_array: '{"one","t\\"w\\"o"}',
bool_array: '{"true","false"}',
});
});
});

View file

@ -19,6 +19,7 @@ import {
configureTableSchemaUpdater, configureTableSchemaUpdater,
getTableSchema, getTableSchema,
prepareItem, prepareItem,
convertArraysToPostgresFormat,
replaceEmptyStringsByNulls, replaceEmptyStringsByNulls,
} from '../../helpers/utils'; } from '../../helpers/utils';
@ -135,7 +136,7 @@ const properties: INodeProperties[] = [
}, },
displayOptions: { displayOptions: {
show: { show: {
'@version': [2.2, 2.3], '@version': [{ _cnd: { gte: 2.2 } }],
}, },
}, },
}, },
@ -224,6 +225,10 @@ export async function execute(
tableSchema = await updateTableSchema(db, tableSchema, schema, table); tableSchema = await updateTableSchema(db, tableSchema, schema, table);
if (nodeVersion >= 2.4) {
convertArraysToPostgresFormat(item, tableSchema, this.getNode(), i);
}
values.push(checkItemAgainstSchema(this.getNode(), item, tableSchema, i)); values.push(checkItemAgainstSchema(this.getNode(), item, tableSchema, i));
const outputColumns = this.getNodeParameter('options.outputColumns', i, ['*']) as string[]; const outputColumns = this.getNodeParameter('options.outputColumns', i, ['*']) as string[];

View file

@ -21,6 +21,7 @@ import {
doesRowExist, doesRowExist,
getTableSchema, getTableSchema,
prepareItem, prepareItem,
convertArraysToPostgresFormat,
replaceEmptyStringsByNulls, replaceEmptyStringsByNulls,
} from '../../helpers/utils'; } from '../../helpers/utils';
@ -172,7 +173,7 @@ const properties: INodeProperties[] = [
}, },
displayOptions: { displayOptions: {
show: { show: {
'@version': [2.2, 2.3], '@version': [{ _cnd: { gte: 2.2 } }],
}, },
}, },
}, },
@ -301,6 +302,10 @@ export async function execute(
tableSchema = await updateTableSchema(db, tableSchema, schema, table); tableSchema = await updateTableSchema(db, tableSchema, schema, table);
if (nodeVersion >= 2.4) {
convertArraysToPostgresFormat(item, tableSchema, this.getNode(), i);
}
item = checkItemAgainstSchema(this.getNode(), item, tableSchema, i); item = checkItemAgainstSchema(this.getNode(), item, tableSchema, i);
let values: QueryValues = [schema, table]; let values: QueryValues = [schema, table];

View file

@ -21,6 +21,7 @@ import {
prepareItem, prepareItem,
replaceEmptyStringsByNulls, replaceEmptyStringsByNulls,
configureTableSchemaUpdater, configureTableSchemaUpdater,
convertArraysToPostgresFormat,
} from '../../helpers/utils'; } from '../../helpers/utils';
import { optionsCollection } from '../common.descriptions'; import { optionsCollection } from '../common.descriptions';
@ -171,7 +172,7 @@ const properties: INodeProperties[] = [
}, },
displayOptions: { displayOptions: {
show: { show: {
'@version': [2.2, 2.3], '@version': [{ _cnd: { gte: 2.2 } }],
}, },
}, },
}, },
@ -270,6 +271,10 @@ export async function execute(
tableSchema = await updateTableSchema(db, tableSchema, schema, table); tableSchema = await updateTableSchema(db, tableSchema, schema, table);
if (nodeVersion >= 2.4) {
convertArraysToPostgresFormat(item, tableSchema, this.getNode(), i);
}
item = checkItemAgainstSchema(this.getNode(), item, tableSchema, i); item = checkItemAgainstSchema(this.getNode(), item, tableSchema, i);
let values: QueryValues = [schema, table]; let values: QueryValues = [schema, table];

View file

@ -8,7 +8,7 @@ export const versionDescription: INodeTypeDescription = {
name: 'postgres', name: 'postgres',
icon: 'file:postgres.svg', icon: 'file:postgres.svg',
group: ['input'], group: ['input'],
version: [2, 2.1, 2.2, 2.3], version: [2, 2.1, 2.2, 2.3, 2.4],
subtitle: '={{ $parameter["operation"] }}', subtitle: '={{ $parameter["operation"] }}',
description: 'Get, add and update data in Postgres', description: 'Get, add and update data in Postgres',
defaults: { defaults: {

View file

@ -16,7 +16,7 @@ export type ColumnInfo = {
data_type: string; data_type: string;
is_nullable: string; is_nullable: string;
udt_name?: string; udt_name?: string;
column_default?: string; column_default?: string | null;
is_generated?: 'ALWAYS' | 'NEVER'; is_generated?: 'ALWAYS' | 'NEVER';
identity_generation?: 'ALWAYS' | 'NEVER'; identity_generation?: 'ALWAYS' | 'NEVER';
}; };

View file

@ -5,7 +5,7 @@ import type {
INodeExecutionData, INodeExecutionData,
INodePropertyOptions, INodePropertyOptions,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow'; import { NodeOperationError, jsonParse } from 'n8n-workflow';
import { generatePairedItemData } from '../../../../utils/utilities'; import { generatePairedItemData } from '../../../../utils/utilities';
import type { import type {
@ -510,3 +510,63 @@ export const configureTableSchemaUpdater = (initialSchema: string, initialTable:
return tableSchema; return tableSchema;
}; };
}; };
/**
* If postgress column type is array we need to convert it to fornmat that postgres understands, original object data would be modified
* @param data the object with keys representing column names and values
* @param schema table schema
* @param node INode
* @param itemIndex the index of the current item
*/
export const convertArraysToPostgresFormat = (
data: IDataObject,
schema: ColumnInfo[],
node: INode,
itemIndex = 0,
) => {
for (const columnInfo of schema) {
//in case column type is array we need to convert it to fornmat that postgres understands
if (columnInfo.data_type.toUpperCase() === 'ARRAY') {
let columnValue = data[columnInfo.column_name];
if (typeof columnValue === 'string') {
columnValue = jsonParse(columnValue);
}
if (Array.isArray(columnValue)) {
const arrayEntries = columnValue.map((entry) => {
if (typeof entry === 'number') {
return entry;
}
if (typeof entry === 'boolean') {
entry = String(entry);
}
if (typeof entry === 'object') {
entry = JSON.stringify(entry);
}
if (typeof entry === 'string') {
return `"${entry.replace(/"/g, '\\"')}"`; //escape double quotes
}
return entry;
});
//wrap in {} instead of [] as postgres does and join with ,
data[columnInfo.column_name] = `{${arrayEntries.join(',')}}`;
} else {
if (columnInfo.is_nullable === 'NO') {
throw new NodeOperationError(
node,
`Column '${columnInfo.column_name}' has to be an array`,
{
itemIndex,
},
);
}
}
}
}
};