mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(Postgres Node): Overhaul node
This commit is contained in:
817
packages/nodes-base/nodes/Postgres/test/v2/operations.test.ts
Normal file
817
packages/nodes-base/nodes/Postgres/test/v2/operations.test.ts
Normal file
@@ -0,0 +1,817 @@
|
||||
import type { IDataObject, IExecuteFunctions, IGetNodeParameterOptions, INode } from 'n8n-workflow';
|
||||
|
||||
import type { ColumnInfo, PgpDatabase, QueriesRunner } from '../../v2/helpers/interfaces';
|
||||
|
||||
import { get } from 'lodash';
|
||||
|
||||
import * as deleteTable from '../../v2/actions/database/deleteTable.operation';
|
||||
import * as executeQuery from '../../v2/actions/database/executeQuery.operation';
|
||||
import * as insert from '../../v2/actions/database/insert.operation';
|
||||
import * as select from '../../v2/actions/database/select.operation';
|
||||
import * as update from '../../v2/actions/database/update.operation';
|
||||
import * as upsert from '../../v2/actions/database/upsert.operation';
|
||||
|
||||
const runQueries: QueriesRunner = jest.fn();
|
||||
|
||||
const node: INode = {
|
||||
id: '1',
|
||||
name: 'Postgres node',
|
||||
typeVersion: 2,
|
||||
type: 'n8n-nodes-base.postgres',
|
||||
position: [60, 760],
|
||||
parameters: {
|
||||
operation: 'executeQuery',
|
||||
},
|
||||
};
|
||||
|
||||
const items = [{ json: {} }];
|
||||
|
||||
const createMockExecuteFunction = (nodeParameters: IDataObject) => {
|
||||
const fakeExecuteFunction = {
|
||||
getNodeParameter(
|
||||
parameterName: string,
|
||||
_itemIndex: number,
|
||||
fallbackValue?: IDataObject | undefined,
|
||||
options?: IGetNodeParameterOptions | undefined,
|
||||
) {
|
||||
const parameter = options?.extractValue ? `${parameterName}.value` : parameterName;
|
||||
return get(nodeParameters, parameter, fallbackValue);
|
||||
},
|
||||
getNode() {
|
||||
return node;
|
||||
},
|
||||
} as unknown as IExecuteFunctions;
|
||||
return fakeExecuteFunction;
|
||||
};
|
||||
|
||||
const createMockDb = (columnInfo: ColumnInfo[]) => {
|
||||
return {
|
||||
async any() {
|
||||
return columnInfo;
|
||||
},
|
||||
} as unknown as PgpDatabase;
|
||||
};
|
||||
|
||||
// if node parameters copied from canvas all default parameters has to be added manualy as JSON would not have them
|
||||
describe('Test PostgresV2, deleteTable operation', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('deleteCommand: delete, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'deleteTable',
|
||||
schema: {
|
||||
__rl: true,
|
||||
mode: 'list',
|
||||
value: 'public',
|
||||
},
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'my_table',
|
||||
mode: 'list',
|
||||
cachedResultName: 'my_table',
|
||||
},
|
||||
deleteCommand: 'delete',
|
||||
where: {
|
||||
values: [
|
||||
{
|
||||
column: 'id',
|
||||
condition: 'LIKE',
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {},
|
||||
};
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
await deleteTable.execute.call(
|
||||
createMockExecuteFunction(nodeParameters),
|
||||
runQueries,
|
||||
items,
|
||||
nodeOptions,
|
||||
);
|
||||
|
||||
expect(runQueries).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
query: 'DELETE FROM $1:name.$2:name WHERE $3:name LIKE $4',
|
||||
values: ['public', 'my_table', 'id', '1'],
|
||||
},
|
||||
],
|
||||
items,
|
||||
nodeOptions,
|
||||
);
|
||||
});
|
||||
|
||||
it('deleteCommand: truncate, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'deleteTable',
|
||||
schema: {
|
||||
__rl: true,
|
||||
mode: 'list',
|
||||
value: 'public',
|
||||
},
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'my_table',
|
||||
mode: 'list',
|
||||
cachedResultName: 'my_table',
|
||||
},
|
||||
deleteCommand: 'truncate',
|
||||
restartSequences: true,
|
||||
options: {
|
||||
cascade: true,
|
||||
},
|
||||
};
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
await deleteTable.execute.call(
|
||||
createMockExecuteFunction(nodeParameters),
|
||||
runQueries,
|
||||
items,
|
||||
nodeOptions,
|
||||
);
|
||||
|
||||
expect(runQueries).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
query: 'TRUNCATE TABLE $1:name.$2:name RESTART IDENTITY CASCADE',
|
||||
values: ['public', 'my_table'],
|
||||
},
|
||||
],
|
||||
items,
|
||||
nodeOptions,
|
||||
);
|
||||
});
|
||||
|
||||
it('deleteCommand: drop, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'deleteTable',
|
||||
schema: {
|
||||
__rl: true,
|
||||
mode: 'list',
|
||||
value: 'public',
|
||||
},
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'my_table',
|
||||
mode: 'list',
|
||||
cachedResultName: 'my_table',
|
||||
},
|
||||
deleteCommand: 'drop',
|
||||
options: {},
|
||||
};
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
await deleteTable.execute.call(
|
||||
createMockExecuteFunction(nodeParameters),
|
||||
runQueries,
|
||||
items,
|
||||
nodeOptions,
|
||||
);
|
||||
|
||||
expect(runQueries).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
query: 'DROP TABLE IF EXISTS $1:name.$2:name',
|
||||
values: ['public', 'my_table'],
|
||||
},
|
||||
],
|
||||
items,
|
||||
nodeOptions,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test PostgresV2, executeQuery operation', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'executeQuery',
|
||||
query: 'select * from $1:name;',
|
||||
options: {
|
||||
queryReplacement: 'my_table',
|
||||
},
|
||||
};
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
await executeQuery.execute.call(
|
||||
createMockExecuteFunction(nodeParameters),
|
||||
runQueries,
|
||||
items,
|
||||
nodeOptions,
|
||||
);
|
||||
|
||||
expect(runQueries).toHaveBeenCalledWith(
|
||||
[{ query: 'select * from $1:name;', values: ['my_table'] }],
|
||||
items,
|
||||
nodeOptions,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test PostgresV2, insert operation', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('dataMode: define, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
schema: {
|
||||
__rl: true,
|
||||
mode: 'list',
|
||||
value: 'public',
|
||||
},
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'my_table',
|
||||
mode: 'list',
|
||||
},
|
||||
dataMode: 'defineBelow',
|
||||
valuesToSend: {
|
||||
values: [
|
||||
{
|
||||
column: 'json',
|
||||
value: '{"test":15}',
|
||||
},
|
||||
{
|
||||
column: 'foo',
|
||||
value: 'select 5',
|
||||
},
|
||||
{
|
||||
column: 'id',
|
||||
value: '4',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {},
|
||||
};
|
||||
const columnsInfo: ColumnInfo[] = [
|
||||
{ column_name: 'id', data_type: 'integer', is_nullable: 'NO' },
|
||||
{ column_name: 'json', data_type: 'json', is_nullable: 'NO' },
|
||||
{ column_name: 'foo', data_type: 'text', is_nullable: 'NO' },
|
||||
];
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
await insert.execute.call(
|
||||
createMockExecuteFunction(nodeParameters),
|
||||
runQueries,
|
||||
items,
|
||||
nodeOptions,
|
||||
createMockDb(columnsInfo),
|
||||
);
|
||||
|
||||
expect(runQueries).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
query: 'INSERT INTO $1:name.$2:name($3:name) VALUES($3:csv) RETURNING *',
|
||||
values: ['public', 'my_table', { json: '{"test":15}', foo: 'select 5', id: '4' }],
|
||||
},
|
||||
],
|
||||
items,
|
||||
nodeOptions,
|
||||
);
|
||||
});
|
||||
|
||||
it('dataMode: autoMapInputData, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
schema: {
|
||||
__rl: true,
|
||||
mode: 'list',
|
||||
value: 'public',
|
||||
},
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'my_table',
|
||||
mode: 'list',
|
||||
},
|
||||
dataMode: 'autoMapInputData',
|
||||
options: {},
|
||||
};
|
||||
const columnsInfo: ColumnInfo[] = [
|
||||
{ column_name: 'id', data_type: 'integer', is_nullable: 'NO' },
|
||||
{ column_name: 'json', data_type: 'json', is_nullable: 'NO' },
|
||||
{ column_name: 'foo', data_type: 'text', is_nullable: 'NO' },
|
||||
];
|
||||
|
||||
const inputItems = [
|
||||
{
|
||||
json: {
|
||||
id: 1,
|
||||
json: {
|
||||
test: 15,
|
||||
},
|
||||
foo: 'data 1',
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
id: 2,
|
||||
json: {
|
||||
test: 10,
|
||||
},
|
||||
foo: 'data 2',
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
id: 3,
|
||||
json: {
|
||||
test: 5,
|
||||
},
|
||||
foo: 'data 3',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
await insert.execute.call(
|
||||
createMockExecuteFunction(nodeParameters),
|
||||
runQueries,
|
||||
inputItems,
|
||||
nodeOptions,
|
||||
createMockDb(columnsInfo),
|
||||
);
|
||||
|
||||
expect(runQueries).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
query: 'INSERT INTO $1:name.$2:name($3:name) VALUES($3:csv) RETURNING *',
|
||||
values: ['public', 'my_table', { id: 1, json: { test: 15 }, foo: 'data 1' }],
|
||||
},
|
||||
{
|
||||
query: 'INSERT INTO $1:name.$2:name($3:name) VALUES($3:csv) RETURNING *',
|
||||
values: ['public', 'my_table', { id: 2, json: { test: 10 }, foo: 'data 2' }],
|
||||
},
|
||||
{
|
||||
query: 'INSERT INTO $1:name.$2:name($3:name) VALUES($3:csv) RETURNING *',
|
||||
values: ['public', 'my_table', { id: 3, json: { test: 5 }, foo: 'data 3' }],
|
||||
},
|
||||
],
|
||||
inputItems,
|
||||
nodeOptions,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test PostgresV2, select operation', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returnAll, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'select',
|
||||
schema: {
|
||||
__rl: true,
|
||||
mode: 'list',
|
||||
value: 'public',
|
||||
},
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'my_table',
|
||||
mode: 'list',
|
||||
cachedResultName: 'my_table',
|
||||
},
|
||||
returnAll: true,
|
||||
options: {},
|
||||
};
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
await select.execute.call(
|
||||
createMockExecuteFunction(nodeParameters),
|
||||
runQueries,
|
||||
items,
|
||||
nodeOptions,
|
||||
);
|
||||
|
||||
expect(runQueries).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
query: 'SELECT * FROM $1:name.$2:name',
|
||||
values: ['public', 'my_table'],
|
||||
},
|
||||
],
|
||||
items,
|
||||
nodeOptions,
|
||||
);
|
||||
});
|
||||
|
||||
it('limit, whereClauses, sortRules, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'select',
|
||||
schema: {
|
||||
__rl: true,
|
||||
mode: 'list',
|
||||
value: 'public',
|
||||
},
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'my_table',
|
||||
mode: 'list',
|
||||
cachedResultName: 'my_table',
|
||||
},
|
||||
limit: 5,
|
||||
where: {
|
||||
values: [
|
||||
{
|
||||
column: 'id',
|
||||
condition: '>=',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
column: 'foo',
|
||||
condition: 'equal',
|
||||
value: 'data 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
sort: {
|
||||
values: [
|
||||
{
|
||||
column: 'id',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
outputColumns: ['json', 'id'],
|
||||
},
|
||||
};
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
await select.execute.call(
|
||||
createMockExecuteFunction(nodeParameters),
|
||||
runQueries,
|
||||
items,
|
||||
nodeOptions,
|
||||
);
|
||||
|
||||
expect(runQueries).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
query:
|
||||
'SELECT $3:name FROM $1:name.$2:name WHERE $4:name >= $5 AND $6:name = $7 ORDER BY $8:name ASC LIMIT 5',
|
||||
values: ['public', 'my_table', ['json', 'id'], 'id', 2, 'foo', 'data 2', 'id'],
|
||||
},
|
||||
],
|
||||
items,
|
||||
nodeOptions,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test PostgresV2, update operation', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('dataMode: define, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'update',
|
||||
schema: {
|
||||
__rl: true,
|
||||
mode: 'list',
|
||||
value: 'public',
|
||||
},
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'my_table',
|
||||
mode: 'list',
|
||||
},
|
||||
dataMode: 'defineBelow',
|
||||
columnToMatchOn: 'id',
|
||||
valueToMatchOn: '1',
|
||||
valuesToSend: {
|
||||
values: [
|
||||
{
|
||||
column: 'json',
|
||||
value: { text: 'some text' },
|
||||
},
|
||||
{
|
||||
column: 'foo',
|
||||
value: 'updated',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
outputColumns: ['json', 'foo'],
|
||||
},
|
||||
};
|
||||
const columnsInfo: ColumnInfo[] = [
|
||||
{ column_name: 'id', data_type: 'integer', is_nullable: 'NO' },
|
||||
{ column_name: 'json', data_type: 'json', is_nullable: 'NO' },
|
||||
{ column_name: 'foo', data_type: 'text', is_nullable: 'NO' },
|
||||
];
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
await update.execute.call(
|
||||
createMockExecuteFunction(nodeParameters),
|
||||
runQueries,
|
||||
items,
|
||||
nodeOptions,
|
||||
createMockDb(columnsInfo),
|
||||
);
|
||||
|
||||
expect(runQueries).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
query:
|
||||
'UPDATE $1:name.$2:name SET $5:name = $6, $7:name = $8 WHERE $3:name = $4 RETURNING $9:name',
|
||||
values: [
|
||||
'public',
|
||||
'my_table',
|
||||
'id',
|
||||
'1',
|
||||
'json',
|
||||
{ text: 'some text' },
|
||||
'foo',
|
||||
'updated',
|
||||
['json', 'foo'],
|
||||
],
|
||||
},
|
||||
],
|
||||
items,
|
||||
nodeOptions,
|
||||
);
|
||||
});
|
||||
|
||||
it('dataMode: autoMapInputData, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'update',
|
||||
schema: {
|
||||
__rl: true,
|
||||
mode: 'list',
|
||||
value: 'public',
|
||||
},
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'my_table',
|
||||
mode: 'list',
|
||||
},
|
||||
dataMode: 'autoMapInputData',
|
||||
columnToMatchOn: 'id',
|
||||
options: {},
|
||||
};
|
||||
const columnsInfo: ColumnInfo[] = [
|
||||
{ column_name: 'id', data_type: 'integer', is_nullable: 'NO' },
|
||||
{ column_name: 'json', data_type: 'json', is_nullable: 'NO' },
|
||||
{ column_name: 'foo', data_type: 'text', is_nullable: 'NO' },
|
||||
];
|
||||
|
||||
const inputItems = [
|
||||
{
|
||||
json: {
|
||||
id: 1,
|
||||
json: {
|
||||
test: 15,
|
||||
},
|
||||
foo: 'data 1',
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
id: 2,
|
||||
json: {
|
||||
test: 10,
|
||||
},
|
||||
foo: 'data 2',
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
id: 3,
|
||||
json: {
|
||||
test: 5,
|
||||
},
|
||||
foo: 'data 3',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
await update.execute.call(
|
||||
createMockExecuteFunction(nodeParameters),
|
||||
runQueries,
|
||||
inputItems,
|
||||
nodeOptions,
|
||||
createMockDb(columnsInfo),
|
||||
);
|
||||
|
||||
expect(runQueries).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
query:
|
||||
'UPDATE $1:name.$2:name SET $5:name = $6, $7:name = $8 WHERE $3:name = $4 RETURNING *',
|
||||
values: ['public', 'my_table', 'id', 1, 'json', { test: 15 }, 'foo', 'data 1'],
|
||||
},
|
||||
{
|
||||
query:
|
||||
'UPDATE $1:name.$2:name SET $5:name = $6, $7:name = $8 WHERE $3:name = $4 RETURNING *',
|
||||
values: ['public', 'my_table', 'id', 2, 'json', { test: 10 }, 'foo', 'data 2'],
|
||||
},
|
||||
{
|
||||
query:
|
||||
'UPDATE $1:name.$2:name SET $5:name = $6, $7:name = $8 WHERE $3:name = $4 RETURNING *',
|
||||
values: ['public', 'my_table', 'id', 3, 'json', { test: 5 }, 'foo', 'data 3'],
|
||||
},
|
||||
],
|
||||
inputItems,
|
||||
nodeOptions,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test PostgresV2, upsert operation', () => {
|
||||
it('dataMode: define, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'upsert',
|
||||
schema: {
|
||||
__rl: true,
|
||||
mode: 'list',
|
||||
value: 'public',
|
||||
},
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'my_table',
|
||||
mode: 'list',
|
||||
},
|
||||
dataMode: 'defineBelow',
|
||||
columnToMatchOn: 'id',
|
||||
valueToMatchOn: '5',
|
||||
valuesToSend: {
|
||||
values: [
|
||||
{
|
||||
column: 'json',
|
||||
value: '{ "test": 5 }',
|
||||
},
|
||||
{
|
||||
column: 'foo',
|
||||
value: 'data 5',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
outputColumns: ['json'],
|
||||
},
|
||||
};
|
||||
const columnsInfo: ColumnInfo[] = [
|
||||
{ column_name: 'id', data_type: 'integer', is_nullable: 'NO' },
|
||||
{ column_name: 'json', data_type: 'json', is_nullable: 'NO' },
|
||||
{ column_name: 'foo', data_type: 'text', is_nullable: 'NO' },
|
||||
];
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
await upsert.execute.call(
|
||||
createMockExecuteFunction(nodeParameters),
|
||||
runQueries,
|
||||
items,
|
||||
nodeOptions,
|
||||
createMockDb(columnsInfo),
|
||||
);
|
||||
|
||||
expect(runQueries).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
query:
|
||||
'INSERT INTO $1:name.$2:name($4:name) VALUES($4:csv) ON CONFLICT ($3:name) DO UPDATE SET $5:name = $6, $7:name = $8 RETURNING $9:name',
|
||||
values: [
|
||||
'public',
|
||||
'my_table',
|
||||
'id',
|
||||
{ json: '{ "test": 5 }', foo: 'data 5', id: '5' },
|
||||
'json',
|
||||
'{ "test": 5 }',
|
||||
'foo',
|
||||
'data 5',
|
||||
['json'],
|
||||
],
|
||||
},
|
||||
],
|
||||
items,
|
||||
nodeOptions,
|
||||
);
|
||||
});
|
||||
|
||||
it('dataMode: autoMapInputData, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'upsert',
|
||||
schema: {
|
||||
__rl: true,
|
||||
mode: 'list',
|
||||
value: 'public',
|
||||
},
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'my_table',
|
||||
mode: 'list',
|
||||
},
|
||||
dataMode: 'autoMapInputData',
|
||||
columnToMatchOn: 'id',
|
||||
options: {},
|
||||
};
|
||||
const columnsInfo: ColumnInfo[] = [
|
||||
{ column_name: 'id', data_type: 'integer', is_nullable: 'NO' },
|
||||
{ column_name: 'json', data_type: 'json', is_nullable: 'NO' },
|
||||
{ column_name: 'foo', data_type: 'text', is_nullable: 'NO' },
|
||||
];
|
||||
|
||||
const inputItems = [
|
||||
{
|
||||
json: {
|
||||
id: 1,
|
||||
json: {
|
||||
test: 15,
|
||||
},
|
||||
foo: 'data 1',
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
id: 2,
|
||||
json: {
|
||||
test: 10,
|
||||
},
|
||||
foo: 'data 2',
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
id: 3,
|
||||
json: {
|
||||
test: 5,
|
||||
},
|
||||
foo: 'data 3',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
await upsert.execute.call(
|
||||
createMockExecuteFunction(nodeParameters),
|
||||
runQueries,
|
||||
inputItems,
|
||||
nodeOptions,
|
||||
createMockDb(columnsInfo),
|
||||
);
|
||||
|
||||
expect(runQueries).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
query:
|
||||
'INSERT INTO $1:name.$2:name($4:name) VALUES($4:csv) ON CONFLICT ($3:name) DO UPDATE SET $5:name = $6, $7:name = $8 RETURNING *',
|
||||
values: [
|
||||
'public',
|
||||
'my_table',
|
||||
'id',
|
||||
{ id: 1, json: { test: 15 }, foo: 'data 1' },
|
||||
'json',
|
||||
{ test: 15 },
|
||||
'foo',
|
||||
'data 1',
|
||||
],
|
||||
},
|
||||
{
|
||||
query:
|
||||
'INSERT INTO $1:name.$2:name($4:name) VALUES($4:csv) ON CONFLICT ($3:name) DO UPDATE SET $5:name = $6, $7:name = $8 RETURNING *',
|
||||
values: [
|
||||
'public',
|
||||
'my_table',
|
||||
'id',
|
||||
{ id: 2, json: { test: 10 }, foo: 'data 2' },
|
||||
'json',
|
||||
{ test: 10 },
|
||||
'foo',
|
||||
'data 2',
|
||||
],
|
||||
},
|
||||
{
|
||||
query:
|
||||
'INSERT INTO $1:name.$2:name($4:name) VALUES($4:csv) ON CONFLICT ($3:name) DO UPDATE SET $5:name = $6, $7:name = $8 RETURNING *',
|
||||
values: [
|
||||
'public',
|
||||
'my_table',
|
||||
'id',
|
||||
{ id: 3, json: { test: 5 }, foo: 'data 3' },
|
||||
'json',
|
||||
{ test: 5 },
|
||||
'foo',
|
||||
'data 3',
|
||||
],
|
||||
},
|
||||
],
|
||||
inputItems,
|
||||
nodeOptions,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { constructExecutionMetaData } from 'n8n-core';
|
||||
import type { IDataObject, INode } from 'n8n-workflow';
|
||||
|
||||
import type { PgpDatabase } from '../../v2/helpers/interfaces';
|
||||
import { configureQueryRunner } from '../../v2/helpers/utils';
|
||||
|
||||
import pgPromise from 'pg-promise';
|
||||
|
||||
const node: INode = {
|
||||
id: '1',
|
||||
name: 'Postgres node',
|
||||
typeVersion: 2,
|
||||
type: 'n8n-nodes-base.postgres',
|
||||
position: [60, 760],
|
||||
parameters: {
|
||||
operation: 'executeQuery',
|
||||
},
|
||||
};
|
||||
|
||||
const createMockDb = (returnData: IDataObject | IDataObject[]) => {
|
||||
return {
|
||||
async any() {
|
||||
return returnData;
|
||||
},
|
||||
async multi() {
|
||||
return returnData;
|
||||
},
|
||||
async tx() {
|
||||
return returnData;
|
||||
},
|
||||
async task() {
|
||||
return returnData;
|
||||
},
|
||||
} as unknown as PgpDatabase;
|
||||
};
|
||||
|
||||
describe('Test PostgresV2, runQueries', () => {
|
||||
it('should execute, should return success true', async () => {
|
||||
const pgp = pgPromise();
|
||||
const db = createMockDb([]);
|
||||
|
||||
const dbMultiSpy = jest.spyOn(db, 'multi');
|
||||
|
||||
const runQueries = configureQueryRunner(node, constructExecutionMetaData, false, pgp, db);
|
||||
|
||||
const result = await runQueries([{ query: 'SELECT * FROM table', values: [] }], [], {});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toEqual([{ json: { success: true } }]);
|
||||
expect(dbMultiSpy).toHaveBeenCalledWith('SELECT * FROM table');
|
||||
});
|
||||
});
|
||||
375
packages/nodes-base/nodes/Postgres/test/v2/utils.test.ts
Normal file
375
packages/nodes-base/nodes/Postgres/test/v2/utils.test.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
import type { IDataObject, INode } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
import {
|
||||
addSortRules,
|
||||
addReturning,
|
||||
addWhereClauses,
|
||||
checkItemAgainstSchema,
|
||||
parsePostgresError,
|
||||
prepareErrorItem,
|
||||
prepareItem,
|
||||
replaceEmptyStringsByNulls,
|
||||
wrapData,
|
||||
} from '../../v2/helpers/utils';
|
||||
|
||||
const node: INode = {
|
||||
id: '1',
|
||||
name: 'Postgres node',
|
||||
typeVersion: 2,
|
||||
type: 'n8n-nodes-base.postgres',
|
||||
position: [60, 760],
|
||||
parameters: {
|
||||
operation: 'executeQuery',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Test PostgresV2, wrapData', () => {
|
||||
it('should wrap object in json', () => {
|
||||
const data = {
|
||||
id: 1,
|
||||
name: 'Name',
|
||||
};
|
||||
const wrappedData = wrapData(data);
|
||||
expect(wrappedData).toBeDefined();
|
||||
expect(wrappedData).toEqual([{ json: data }]);
|
||||
});
|
||||
it('should wrap each object in array in json', () => {
|
||||
const data = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Name',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Name 2',
|
||||
},
|
||||
];
|
||||
const wrappedData = wrapData(data);
|
||||
expect(wrappedData).toBeDefined();
|
||||
expect(wrappedData).toEqual([{ json: data[0] }, { json: data[1] }]);
|
||||
});
|
||||
it('json key from source should be inside json', () => {
|
||||
const data = {
|
||||
json: {
|
||||
id: 1,
|
||||
name: 'Name',
|
||||
},
|
||||
};
|
||||
const wrappedData = wrapData(data);
|
||||
expect(wrappedData).toBeDefined();
|
||||
expect(wrappedData).toEqual([{ json: data }]);
|
||||
expect(Object.keys(wrappedData[0].json)).toContain('json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test PostgresV2, prepareErrorItem', () => {
|
||||
it('should return error info item', () => {
|
||||
const items = [
|
||||
{
|
||||
json: {
|
||||
id: 1,
|
||||
name: 'Name 1',
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
id: 2,
|
||||
name: 'Name 2',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const error = new Error('Test error');
|
||||
const item = prepareErrorItem(items, error, 1);
|
||||
expect(item).toBeDefined();
|
||||
|
||||
expect((item.json.item as IDataObject)?.id).toEqual(2);
|
||||
expect(item.json.message).toEqual('Test error');
|
||||
expect(item.json.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test PostgresV2, parsePostgresError', () => {
|
||||
it('should return NodeOperationError', () => {
|
||||
const error = new Error('Test error');
|
||||
|
||||
const parsedError = parsePostgresError(node, error, [], 1);
|
||||
expect(parsedError).toBeDefined();
|
||||
expect(parsedError.message).toEqual('Test error');
|
||||
expect(parsedError instanceof NodeOperationError).toEqual(true);
|
||||
});
|
||||
|
||||
it('should update message that includes ECONNREFUSED', () => {
|
||||
const error = new Error('ECONNREFUSED');
|
||||
|
||||
const parsedError = parsePostgresError(node, error, [], 1);
|
||||
expect(parsedError).toBeDefined();
|
||||
expect(parsedError.message).toEqual('Connection refused');
|
||||
expect(parsedError instanceof NodeOperationError).toEqual(true);
|
||||
});
|
||||
|
||||
it('should update message with syntax error', () => {
|
||||
// eslint-disable-next-line n8n-local-rules/no-unneeded-backticks
|
||||
const errorMessage = String.raw`syntax error at or near "seelect"`;
|
||||
const error = new Error();
|
||||
error.message = errorMessage;
|
||||
|
||||
const parsedError = parsePostgresError(node, error, [
|
||||
{ query: 'seelect * from my_table', values: [] },
|
||||
]);
|
||||
expect(parsedError).toBeDefined();
|
||||
expect(parsedError.message).toEqual('Syntax error at line 1 near "seelect"');
|
||||
expect(parsedError instanceof NodeOperationError).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test PostgresV2, addWhereClauses', () => {
|
||||
it('should add where clauses to query', () => {
|
||||
const query = 'SELECT * FROM $1:name.$2:name';
|
||||
const values = ['public', 'my_table'];
|
||||
const whereClauses = [{ column: 'id', condition: 'equal', value: '1' }];
|
||||
|
||||
const [updatedQuery, updatedValues] = addWhereClauses(
|
||||
node,
|
||||
0,
|
||||
query,
|
||||
whereClauses,
|
||||
values,
|
||||
'AND',
|
||||
);
|
||||
|
||||
expect(updatedQuery).toEqual('SELECT * FROM $1:name.$2:name WHERE $3:name = $4');
|
||||
expect(updatedValues).toEqual(['public', 'my_table', 'id', '1']);
|
||||
});
|
||||
|
||||
it('should combine where clauses by OR', () => {
|
||||
const query = 'SELECT * FROM $1:name.$2:name';
|
||||
const values = ['public', 'my_table'];
|
||||
const whereClauses = [
|
||||
{ column: 'id', condition: 'equal', value: '1' },
|
||||
{ column: 'foo', condition: 'equal', value: 'select 2' },
|
||||
];
|
||||
|
||||
const [updatedQuery, updatedValues] = addWhereClauses(
|
||||
node,
|
||||
0,
|
||||
query,
|
||||
whereClauses,
|
||||
values,
|
||||
'OR',
|
||||
);
|
||||
|
||||
expect(updatedQuery).toEqual(
|
||||
'SELECT * FROM $1:name.$2:name WHERE $3:name = $4 OR $5:name = $6',
|
||||
);
|
||||
expect(updatedValues).toEqual(['public', 'my_table', 'id', '1', 'foo', 'select 2']);
|
||||
});
|
||||
|
||||
it('should ignore incorect combine conition ad use AND', () => {
|
||||
const query = 'SELECT * FROM $1:name.$2:name';
|
||||
const values = ['public', 'my_table'];
|
||||
const whereClauses = [
|
||||
{ column: 'id', condition: 'equal', value: '1' },
|
||||
{ column: 'foo', condition: 'equal', value: 'select 2' },
|
||||
];
|
||||
|
||||
const [updatedQuery, updatedValues] = addWhereClauses(
|
||||
node,
|
||||
0,
|
||||
query,
|
||||
whereClauses,
|
||||
values,
|
||||
'SELECT * FROM my_table',
|
||||
);
|
||||
|
||||
expect(updatedQuery).toEqual(
|
||||
'SELECT * FROM $1:name.$2:name WHERE $3:name = $4 AND $5:name = $6',
|
||||
);
|
||||
expect(updatedValues).toEqual(['public', 'my_table', 'id', '1', 'foo', 'select 2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test PostgresV2, addSortRules', () => {
|
||||
it('should ORDER BY ASC', () => {
|
||||
const query = 'SELECT * FROM $1:name.$2:name';
|
||||
const values = ['public', 'my_table'];
|
||||
const sortRules = [{ column: 'id', direction: 'ASC' }];
|
||||
|
||||
const [updatedQuery, updatedValues] = addSortRules(query, sortRules, values);
|
||||
|
||||
expect(updatedQuery).toEqual('SELECT * FROM $1:name.$2:name ORDER BY $3:name ASC');
|
||||
expect(updatedValues).toEqual(['public', 'my_table', 'id']);
|
||||
});
|
||||
it('should ORDER BY DESC', () => {
|
||||
const query = 'SELECT * FROM $1:name.$2:name';
|
||||
const values = ['public', 'my_table'];
|
||||
const sortRules = [{ column: 'id', direction: 'DESC' }];
|
||||
|
||||
const [updatedQuery, updatedValues] = addSortRules(query, sortRules, values);
|
||||
|
||||
expect(updatedQuery).toEqual('SELECT * FROM $1:name.$2:name ORDER BY $3:name DESC');
|
||||
expect(updatedValues).toEqual(['public', 'my_table', 'id']);
|
||||
});
|
||||
it('should ignore incorect direction', () => {
|
||||
const query = 'SELECT * FROM $1:name.$2:name';
|
||||
const values = ['public', 'my_table'];
|
||||
const sortRules = [{ column: 'id', direction: 'SELECT * FROM my_table' }];
|
||||
|
||||
const [updatedQuery, updatedValues] = addSortRules(query, sortRules, values);
|
||||
|
||||
expect(updatedQuery).toEqual('SELECT * FROM $1:name.$2:name ORDER BY $3:name ASC');
|
||||
expect(updatedValues).toEqual(['public', 'my_table', 'id']);
|
||||
});
|
||||
it('should add multiple sort rules', () => {
|
||||
const query = 'SELECT * FROM $1:name.$2:name';
|
||||
const values = ['public', 'my_table'];
|
||||
const sortRules = [
|
||||
{ column: 'id', direction: 'ASC' },
|
||||
{ column: 'foo', direction: 'DESC' },
|
||||
];
|
||||
|
||||
const [updatedQuery, updatedValues] = addSortRules(query, sortRules, values);
|
||||
|
||||
expect(updatedQuery).toEqual(
|
||||
'SELECT * FROM $1:name.$2:name ORDER BY $3:name ASC, $4:name DESC',
|
||||
);
|
||||
expect(updatedValues).toEqual(['public', 'my_table', 'id', 'foo']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test PostgresV2, addReturning', () => {
|
||||
it('should add RETURNING', () => {
|
||||
const query = 'UPDATE $1:name.$2:name SET $5:name = $6 WHERE $3:name = $4';
|
||||
const values = ['public', 'my_table', 'id', '1', 'foo', 'updated'];
|
||||
const outputColumns = ['id', 'foo'];
|
||||
|
||||
const [updatedQuery, updatedValues] = addReturning(query, outputColumns, values);
|
||||
|
||||
expect(updatedQuery).toEqual(
|
||||
'UPDATE $1:name.$2:name SET $5:name = $6 WHERE $3:name = $4 RETURNING $7:name',
|
||||
);
|
||||
expect(updatedValues).toEqual([
|
||||
'public',
|
||||
'my_table',
|
||||
'id',
|
||||
'1',
|
||||
'foo',
|
||||
'updated',
|
||||
['id', 'foo'],
|
||||
]);
|
||||
});
|
||||
it('should add RETURNING *', () => {
|
||||
const query = 'UPDATE $1:name.$2:name SET $5:name = $6 WHERE $3:name = $4';
|
||||
const values = ['public', 'my_table', 'id', '1', 'foo', 'updated'];
|
||||
const outputColumns = ['id', 'foo', '*'];
|
||||
|
||||
const [updatedQuery, updatedValues] = addReturning(query, outputColumns, values);
|
||||
|
||||
expect(updatedQuery).toEqual(
|
||||
'UPDATE $1:name.$2:name SET $5:name = $6 WHERE $3:name = $4 RETURNING *',
|
||||
);
|
||||
expect(updatedValues).toEqual(['public', 'my_table', 'id', '1', 'foo', 'updated']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test PostgresV2, replaceEmptyStringsByNulls', () => {
|
||||
it('should replace empty string by null', () => {
|
||||
const items = [
|
||||
{ json: { foo: 'bar', bar: '', spam: undefined } },
|
||||
{ json: { foo: '', bar: '', spam: '' } },
|
||||
{ json: { foo: 0, bar: NaN, spam: false } },
|
||||
];
|
||||
|
||||
const updatedItems = replaceEmptyStringsByNulls(items, true);
|
||||
|
||||
expect(updatedItems).toBeDefined();
|
||||
expect(updatedItems).toEqual([
|
||||
{ json: { foo: 'bar', bar: null, spam: undefined } },
|
||||
{ json: { foo: null, bar: null, spam: null } },
|
||||
{ json: { foo: 0, bar: NaN, spam: false } },
|
||||
]);
|
||||
});
|
||||
it('should do nothing', () => {
|
||||
const items = [
|
||||
{ json: { foo: 'bar', bar: '', spam: undefined } },
|
||||
{ json: { foo: '', bar: '', spam: '' } },
|
||||
{ json: { foo: 0, bar: NaN, spam: false } },
|
||||
];
|
||||
|
||||
const updatedItems = replaceEmptyStringsByNulls(items);
|
||||
|
||||
expect(updatedItems).toBeDefined();
|
||||
expect(updatedItems).toEqual(items);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test PostgresV2, prepareItem', () => {
|
||||
it('should convert fixedColections values to object', () => {
|
||||
const values = [
|
||||
{
|
||||
column: 'id',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
column: 'foo',
|
||||
value: 'bar',
|
||||
},
|
||||
{
|
||||
column: 'bar',
|
||||
value: 'foo',
|
||||
},
|
||||
];
|
||||
|
||||
const item = prepareItem(values);
|
||||
|
||||
expect(item).toBeDefined();
|
||||
expect(item).toEqual({
|
||||
id: '1',
|
||||
foo: 'bar',
|
||||
bar: 'foo',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test PostgresV2, checkItemAgainstSchema', () => {
|
||||
it('should not throw error', () => {
|
||||
const item = { foo: 'updated', id: 2 };
|
||||
const columnsInfo = [
|
||||
{ column_name: 'id', data_type: 'integer', is_nullable: 'NO' },
|
||||
{ column_name: 'json', data_type: 'json', is_nullable: 'NO' },
|
||||
{ column_name: 'foo', data_type: 'text', is_nullable: 'NO' },
|
||||
];
|
||||
|
||||
const result = checkItemAgainstSchema(node, item, columnsInfo, 0);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toEqual(item);
|
||||
});
|
||||
it('should throw error on not existing column', () => {
|
||||
const item = { foo: 'updated', bar: 'updated' };
|
||||
const columnsInfo = [
|
||||
{ column_name: 'id', data_type: 'integer', is_nullable: 'NO' },
|
||||
{ column_name: 'json', data_type: 'json', is_nullable: 'NO' },
|
||||
{ column_name: 'foo', data_type: 'text', is_nullable: 'NO' },
|
||||
];
|
||||
|
||||
try {
|
||||
checkItemAgainstSchema(node, item, columnsInfo, 0);
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual("Column 'bar' does not exist in selected table");
|
||||
}
|
||||
});
|
||||
it('should throw error on not nullable column', () => {
|
||||
const item = { foo: null };
|
||||
const columnsInfo = [
|
||||
{ column_name: 'id', data_type: 'integer', is_nullable: 'NO' },
|
||||
{ column_name: 'foo', data_type: 'text', is_nullable: 'NO' },
|
||||
];
|
||||
|
||||
try {
|
||||
checkItemAgainstSchema(node, item, columnsInfo, 0);
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual("Column 'foo' is not nullable");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user