fix(Postgres Node): RMC do not mark collumn as required if identity_generation is BY DEFAULT (#13752)

Co-authored-by: Dana <152518854+dana-gill@users.noreply.github.com>
This commit is contained in:
Michael Kret
2025-03-13 07:35:50 +02:00
committed by GitHub
parent ec8a719efa
commit b5632545c5
7 changed files with 149 additions and 9 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.5, defaultVersion: 2.6,
description: 'Get, add and update data in Postgres', description: 'Get, add and update data in Postgres',
parameterPane: 'wide', parameterPane: 'wide',
}; };
@@ -24,6 +24,7 @@ export class Postgres extends VersionedNodeType {
2.3: new PostgresV2(baseDescription), 2.3: new PostgresV2(baseDescription),
2.4: new PostgresV2(baseDescription), 2.4: new PostgresV2(baseDescription),
2.5: new PostgresV2(baseDescription), 2.5: new PostgresV2(baseDescription),
2.6: new PostgresV2(baseDescription),
}; };
super(nodeVersions, baseDescription); super(nodeVersions, baseDescription);

View File

@@ -713,6 +713,51 @@ describe('Test PostgresV2, insert operation', () => {
expect(hasJsonDataTypeInSchemaSpy).toHaveBeenCalledWith(columnsInfo); expect(hasJsonDataTypeInSchemaSpy).toHaveBeenCalledWith(columnsInfo);
}); });
}); });
it('should insert default values if no values are provided', async () => {
const nodeParameters: IDataObject = {
schema: {
__rl: true,
mode: 'list',
value: 'public',
},
table: {
__rl: true,
value: 'my_table',
mode: 'list',
},
dataMode: 'defineBelow',
valuesToSend: {
values: [],
},
options: { nodeVersion: 2.6 },
};
const columnsInfo: ColumnInfo[] = [
{ column_name: 'id', data_type: 'integer', is_nullable: 'NO', udt_name: '' },
];
const nodeOptions = nodeParameters.options as IDataObject;
await insert.execute.call(
createMockExecuteFunction(nodeParameters),
runQueries,
items,
nodeOptions,
createMockDb(columnsInfo),
pgPromise(),
);
expect(runQueries).toHaveBeenCalledWith(
[
{
query: 'INSERT INTO $1:name.$2:name DEFAULT VALUES RETURNING *',
values: ['public', 'my_table', {}],
},
],
items,
nodeOptions,
);
});
}); });
describe('Test PostgresV2, select operation', () => { describe('Test PostgresV2, select operation', () => {

View File

@@ -0,0 +1,88 @@
import type { MockProxy } from 'jest-mock-extended';
import { mock } from 'jest-mock-extended';
import type { ILoadOptionsFunctions } from 'n8n-workflow';
import { getMappingColumns } from '../../v2/methods/resourceMapping';
jest.mock('../../transport', () => {
const originalModule = jest.requireActual('../../transport');
return {
...originalModule,
configurePostgres: jest.fn(async () => ({ db: {} })),
};
});
jest.mock('../../v2/helpers/utils', () => {
const originalModule = jest.requireActual('../../v2/helpers/utils');
return {
...originalModule,
getEnums: jest.fn(() => []),
getEnumValues: jest.fn(),
getTableSchema: jest.fn(() => [
{
column_name: 'id',
data_type: 'bigint',
is_nullable: 'NO',
udt_name: 'int8',
column_default: null,
identity_generation: 'BY DEFAULT',
is_generated: 'NEVER',
},
{
column_name: 'name',
data_type: 'text',
is_nullable: 'YES',
udt_name: 'text',
column_default: null,
identity_generation: null,
is_generated: 'NEVER',
},
]),
};
});
describe('Postgres, resourceMapping', () => {
let loadOptionsFunctions: MockProxy<ILoadOptionsFunctions>;
beforeEach(() => {
loadOptionsFunctions = mock<ILoadOptionsFunctions>();
});
afterEach(() => {
jest.resetAllMocks();
});
it('should mark id as not required if identity_generation is "BY_DEFAULT"', async () => {
loadOptionsFunctions.getCredentials.mockResolvedValue({});
loadOptionsFunctions.getNodeParameter.mockReturnValueOnce('public');
loadOptionsFunctions.getNodeParameter.mockReturnValueOnce('test_table');
loadOptionsFunctions.getNodeParameter.mockReturnValueOnce('insert');
const fields = await getMappingColumns.call(loadOptionsFunctions);
expect(fields).toEqual({
fields: [
{
canBeUsedToMatch: true,
defaultMatch: true,
display: true,
displayName: 'id',
id: 'id',
options: undefined,
required: false,
type: 'number',
},
{
canBeUsedToMatch: true,
defaultMatch: false,
display: true,
displayName: 'name',
id: 'name',
options: undefined,
required: false,
type: 'string',
},
],
});
});
});

View File

@@ -146,15 +146,15 @@ describe('Test PostgresV2, parsePostgresError', () => {
it('should update message with syntax error', () => { it('should update message with syntax error', () => {
// eslint-disable-next-line n8n-local-rules/no-unneeded-backticks // eslint-disable-next-line n8n-local-rules/no-unneeded-backticks
const errorMessage = String.raw`syntax error at or near "seelect"`; const errorMessage = String.raw`syntax error at or near "select"`;
const error = new Error(); const error = new Error();
error.message = errorMessage; error.message = errorMessage;
const parsedError = parsePostgresError(node, error, [ const parsedError = parsePostgresError(node, error, [
{ query: 'seelect * from my_table', values: [] }, { query: 'select * from my_table', values: [] },
]); ]);
expect(parsedError).toBeDefined(); expect(parsedError).toBeDefined();
expect(parsedError.message).toEqual('Syntax error at line 1 near "seelect"'); expect(parsedError.message).toEqual('Syntax error at line 1 near "select"');
expect(parsedError instanceof NodeOperationError).toEqual(true); expect(parsedError instanceof NodeOperationError).toEqual(true);
}); });
}); });
@@ -201,7 +201,7 @@ describe('Test PostgresV2, addWhereClauses', () => {
expect(updatedValues).toEqual(['public', 'my_table', 'id', '1', 'foo', 'select 2']); expect(updatedValues).toEqual(['public', 'my_table', 'id', '1', 'foo', 'select 2']);
}); });
it('should ignore incorect combine conition ad use AND', () => { it('should ignore incorrect combine condition ad use AND', () => {
const query = 'SELECT * FROM $1:name.$2:name'; const query = 'SELECT * FROM $1:name.$2:name';
const values = ['public', 'my_table']; const values = ['public', 'my_table'];
const whereClauses = [ const whereClauses = [
@@ -246,7 +246,7 @@ describe('Test PostgresV2, addSortRules', () => {
expect(updatedQuery).toEqual('SELECT * FROM $1:name.$2:name ORDER BY $3:name DESC'); expect(updatedQuery).toEqual('SELECT * FROM $1:name.$2:name ORDER BY $3:name DESC');
expect(updatedValues).toEqual(['public', 'my_table', 'id']); expect(updatedValues).toEqual(['public', 'my_table', 'id']);
}); });
it('should ignore incorect direction', () => { it('should ignore incorrect direction', () => {
const query = 'SELECT * FROM $1:name.$2:name'; const query = 'SELECT * FROM $1:name.$2:name';
const values = ['public', 'my_table']; const values = ['public', 'my_table'];
const sortRules = [{ column: 'id', direction: 'SELECT * FROM my_table' }]; const sortRules = [{ column: 'id', direction: 'SELECT * FROM my_table' }];
@@ -340,7 +340,7 @@ describe('Test PostgresV2, replaceEmptyStringsByNulls', () => {
}); });
describe('Test PostgresV2, prepareItem', () => { describe('Test PostgresV2, prepareItem', () => {
it('should convert fixedColections values to object', () => { it('should convert fixedCollection values to object', () => {
const values = [ const values = [
{ {
column: 'id', column: 'id',

View File

@@ -241,6 +241,10 @@ export async function execute(
const outputColumns = this.getNodeParameter('options.outputColumns', i, ['*']) as string[]; const outputColumns = this.getNodeParameter('options.outputColumns', i, ['*']) as string[];
if (nodeVersion >= 2.6 && Object.keys(item).length === 0) {
query = 'INSERT INTO $1:name.$2:name DEFAULT VALUES';
}
[query, values] = addReturning(query, outputColumns, values); [query, values] = addReturning(query, outputColumns, values);
queries.push({ query, values }); queries.push({ query, values });

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, 2.4, 2.5], version: [2, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6],
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

@@ -74,7 +74,9 @@ export async function getMappingColumns(
const options = const options =
type === 'options' ? getEnumValues(enumInfo, col.udt_name as string) : undefined; type === 'options' ? getEnumValues(enumInfo, col.udt_name as string) : undefined;
const hasDefault = Boolean(col.column_default); const hasDefault = Boolean(col.column_default);
const isGenerated = col.is_generated === 'ALWAYS' || col.identity_generation === 'ALWAYS'; const isGenerated =
col.is_generated === 'ALWAYS' ||
['ALWAYS', 'BY DEFAULT'].includes(col.identity_generation ?? '');
const nullable = col.is_nullable === 'YES'; const nullable = col.is_nullable === 'YES';
return { return {
id: col.column_name, id: col.column_name,