first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
/* eslint-disable n8n-nodes-base/node-param-display-name-miscased */
|
||||
import mysql2 from 'mysql2/promise';
|
||||
import type { ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow';
|
||||
|
||||
import { searchTables } from '../../v1/GenericFunctions';
|
||||
|
||||
jest.mock('mysql2/promise');
|
||||
|
||||
describe('MySQL / v1 / Generic Functions', () => {
|
||||
let mockLoadOptionsFunctions: ILoadOptionsFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
mockLoadOptionsFunctions = {
|
||||
getCredentials: jest.fn().mockResolvedValue({
|
||||
database: 'test_db',
|
||||
}),
|
||||
} as unknown as ILoadOptionsFunctions;
|
||||
});
|
||||
|
||||
describe('searchTables', () => {
|
||||
it('should return matching tables', async () => {
|
||||
const mockRows = [{ table_name: 'users' }, { table_name: 'products' }];
|
||||
|
||||
const mockQuery = jest.fn().mockResolvedValue([mockRows]);
|
||||
const mockEnd = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
(mysql2.createConnection as jest.Mock).mockResolvedValue({
|
||||
query: mockQuery,
|
||||
end: mockEnd,
|
||||
});
|
||||
|
||||
const result: INodeListSearchResult = await searchTables.call(
|
||||
mockLoadOptionsFunctions,
|
||||
'user',
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
results: [
|
||||
{ name: 'users', value: 'users' },
|
||||
{ name: 'products', value: 'products' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
`SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = ?
|
||||
AND table_name LIKE ?
|
||||
ORDER BY table_name`,
|
||||
['test_db', '%user%'],
|
||||
);
|
||||
|
||||
expect(mockEnd).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle empty search query', async () => {
|
||||
const mockRows: any[] = [];
|
||||
|
||||
const mockQuery = jest.fn().mockResolvedValue([mockRows]);
|
||||
const mockEnd = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
(mysql2.createConnection as jest.Mock).mockResolvedValue({
|
||||
query: mockQuery,
|
||||
end: mockEnd,
|
||||
});
|
||||
|
||||
const result = await searchTables.call(mockLoadOptionsFunctions);
|
||||
|
||||
expect(result).toEqual({ results: [] });
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
`SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = ?
|
||||
AND table_name LIKE ?
|
||||
ORDER BY table_name`,
|
||||
['test_db', '%%'],
|
||||
);
|
||||
|
||||
expect(mockEnd).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle database errors', async () => {
|
||||
const mockError = new Error('Database connection failed');
|
||||
|
||||
(mysql2.createConnection as jest.Mock).mockRejectedValue(mockError);
|
||||
|
||||
await expect(searchTables.call(mockLoadOptionsFunctions)).rejects.toThrow(
|
||||
'Database connection failed',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { Connection, QueryResult } from 'mysql2/promise';
|
||||
|
||||
const mockConnection = mock<Connection>();
|
||||
const createConnection = jest.fn().mockReturnValue(mockConnection);
|
||||
jest.mock('mysql2/promise', () => ({ createConnection }));
|
||||
|
||||
describe('Test MySqlV1, executeQuery', () => {
|
||||
mockConnection.query.mockResolvedValue([{ success: true } as unknown as QueryResult, []]);
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
workflowFiles: ['executeQuery.workflow.json'],
|
||||
customAssertions() {
|
||||
expect(mockConnection.query).toHaveBeenCalledTimes(1);
|
||||
expect(mockConnection.query).toHaveBeenCalledWith(
|
||||
"select * from family_parents where (parent_email = 'parent1@mail.com' or parent_email = 'parent2@mail.com') and parent_email <> '';",
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"name": "mysql v1 resolve expression copy",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "d6d9fbcc-d8bc-4f79-8e00-3acf8ffb12de",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
460,
|
||||
460
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "select * from family_parents where (parent_email = {{ \"'\" + $json['Parent 1 email'] + \"'\" }} or parent_email = {{ \"'\" + $json['Parent 2 email'] + \"'\"}}) and parent_email <> '';\n"
|
||||
},
|
||||
"id": "faefc24c-91b4-4b10-85a6-b3cecbceee08",
|
||||
"name": "Get matching families",
|
||||
"type": "n8n-nodes-base.mySql",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
900,
|
||||
460
|
||||
],
|
||||
"credentials": {
|
||||
"mySql": {
|
||||
"id": "93",
|
||||
"name": "MySQL account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "29c30f6e-9f5f-4b3a-80c4-2da762e96bd9",
|
||||
"name": "No Operation, do nothing1",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1120,
|
||||
460
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "Parent 1 email",
|
||||
"stringValue": "parent1@mail.com"
|
||||
},
|
||||
{
|
||||
"name": "Parent 2 email",
|
||||
"stringValue": "parent2@mail.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": "none",
|
||||
"options": {}
|
||||
},
|
||||
"id": "54c7bbf9-dabc-421b-85b2-7c3006f5ee61",
|
||||
"name": "Edit Fields",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [
|
||||
680,
|
||||
460
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"No Operation, do nothing1": [
|
||||
{
|
||||
"json": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get matching families": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "No Operation, do nothing1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Edit Fields": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get matching families",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "aeb01d24-c117-405a-875f-909ea8ccdc16",
|
||||
"id": "GlTwlHZfQwNjbeqv",
|
||||
"meta": {
|
||||
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { createServiceStack, type N8NStack } from 'n8n-containers';
|
||||
import { constructExecutionMetaData } from 'n8n-core';
|
||||
import type { IDataObject, IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
|
||||
import { router } from '../../v2/actions/router';
|
||||
import type { MysqlNodeCredentials } from '../../v2/helpers/interfaces';
|
||||
|
||||
let stack: N8NStack;
|
||||
let credentials: MysqlNodeCredentials;
|
||||
|
||||
beforeAll(async () => {
|
||||
stack = await createServiceStack({ services: ['mysql'] });
|
||||
const meta = stack.serviceResults.mysql!.meta as {
|
||||
externalHost: string;
|
||||
externalPort: number;
|
||||
database: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
credentials = {
|
||||
host: meta.externalHost,
|
||||
port: meta.externalPort,
|
||||
database: meta.database,
|
||||
user: meta.username,
|
||||
password: meta.password,
|
||||
connectTimeout: 10000,
|
||||
ssl: false,
|
||||
sshTunnel: false,
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await stack?.stop();
|
||||
});
|
||||
|
||||
const node: INode = {
|
||||
id: '1',
|
||||
name: 'MySQL',
|
||||
typeVersion: 2.5,
|
||||
type: 'n8n-nodes-base.mySql',
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
function mockExecuteFunctions(
|
||||
params: IDataObject,
|
||||
creds: MysqlNodeCredentials,
|
||||
continueOnFail = false,
|
||||
): IExecuteFunctions {
|
||||
return {
|
||||
getNodeParameter: (name: string, _i: number, fallback?: unknown) => params[name] ?? fallback,
|
||||
getNode: () => node,
|
||||
getInputData: () => [{ json: {} }],
|
||||
continueOnFail: () => continueOnFail,
|
||||
getCredentials: async () => creds,
|
||||
helpers: {
|
||||
constructExecutionMetaData,
|
||||
getSSHClient: () => {
|
||||
throw new Error('No SSH');
|
||||
},
|
||||
},
|
||||
} as unknown as IExecuteFunctions;
|
||||
}
|
||||
|
||||
const badCreds: MysqlNodeCredentials = {
|
||||
host: 'invalid.host',
|
||||
port: 3306,
|
||||
database: 'x',
|
||||
user: 'x',
|
||||
password: 'x',
|
||||
connectTimeout: 1000,
|
||||
ssl: false,
|
||||
sshTunnel: false,
|
||||
};
|
||||
|
||||
describe('MySQL Integration - NODE-4174', () => {
|
||||
test('happy path: SELECT against real MySQL', async () => {
|
||||
const params = {
|
||||
resource: 'database',
|
||||
operation: 'executeQuery',
|
||||
query: 'SELECT 1 as result',
|
||||
options: { queryBatching: 'single', nodeVersion: 2.5 },
|
||||
};
|
||||
|
||||
const result = await router.call(mockExecuteFunctions(params, credentials));
|
||||
|
||||
expect(result[0][0].json).toEqual({ result: 1 });
|
||||
});
|
||||
|
||||
test('bad path: query error returns error item with continueOnFail', async () => {
|
||||
const params = {
|
||||
resource: 'database',
|
||||
operation: 'executeQuery',
|
||||
query: 'SELECT * FROM table_that_does_not_exist',
|
||||
options: { queryBatching: 'single', nodeVersion: 2.5 },
|
||||
};
|
||||
|
||||
const result = await router.call(mockExecuteFunctions(params, credentials, true));
|
||||
|
||||
expect(result[0][0].json).toHaveProperty('message');
|
||||
});
|
||||
|
||||
test('connection error should return error item with continueOnFail', async () => {
|
||||
const params = {
|
||||
resource: 'database',
|
||||
operation: 'executeQuery',
|
||||
query: 'SELECT 1',
|
||||
options: { queryBatching: 'single', nodeVersion: 2.5, connectTimeout: 1000 },
|
||||
};
|
||||
|
||||
const result = await router.call(mockExecuteFunctions(params, badCreds, true));
|
||||
|
||||
expect(result[0][0].json).toHaveProperty('error');
|
||||
});
|
||||
|
||||
test('connection error throws when continueOnFail is false', async () => {
|
||||
const params = {
|
||||
resource: 'database',
|
||||
operation: 'executeQuery',
|
||||
query: 'SELECT 1',
|
||||
options: { queryBatching: 'single', nodeVersion: 2.5, connectTimeout: 1000 },
|
||||
};
|
||||
|
||||
await expect(router.call(mockExecuteFunctions(params, badCreds, false))).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,714 @@
|
||||
import mysql2 from 'mysql2/promise';
|
||||
import type { IDataObject, INode } from 'n8n-workflow';
|
||||
|
||||
import { createMockExecuteFunction } from '@test/nodes/Helpers';
|
||||
|
||||
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';
|
||||
import type { Mysql2Pool, QueryRunner } from '../../v2/helpers/interfaces';
|
||||
import { configureQueryRunner } from '../../v2/helpers/utils';
|
||||
|
||||
const mySqlMockNode: INode = {
|
||||
id: '1',
|
||||
name: 'MySQL node',
|
||||
typeVersion: 2,
|
||||
type: 'n8n-nodes-base.mySql',
|
||||
position: [60, 760],
|
||||
parameters: {
|
||||
operation: 'select',
|
||||
},
|
||||
};
|
||||
|
||||
const fakeConnection = {
|
||||
format(query: string, values: any[]) {
|
||||
return mysql2.format(query, values);
|
||||
},
|
||||
query: jest.fn(async (_query = '') => [{}]),
|
||||
release: jest.fn(),
|
||||
beginTransaction: jest.fn(),
|
||||
commit: jest.fn(),
|
||||
rollback: jest.fn(),
|
||||
};
|
||||
|
||||
const createFakePool = (connection: IDataObject) => {
|
||||
return {
|
||||
getConnection() {
|
||||
return connection;
|
||||
},
|
||||
query: jest.fn(async () => [{}]),
|
||||
} as unknown as Mysql2Pool;
|
||||
};
|
||||
|
||||
const emptyInputItems = [{ json: {}, pairedItem: { item: 0, input: undefined } }];
|
||||
|
||||
describe('Test MySql V2, operations', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should have all operations', () => {
|
||||
expect(deleteTable.execute).toBeDefined();
|
||||
expect(deleteTable.description).toBeDefined();
|
||||
expect(executeQuery.execute).toBeDefined();
|
||||
expect(executeQuery.description).toBeDefined();
|
||||
expect(insert.execute).toBeDefined();
|
||||
expect(insert.description).toBeDefined();
|
||||
expect(select.execute).toBeDefined();
|
||||
expect(select.description).toBeDefined();
|
||||
expect(update.execute).toBeDefined();
|
||||
expect(update.description).toBeDefined();
|
||||
expect(upsert.execute).toBeDefined();
|
||||
expect(upsert.description).toBeDefined();
|
||||
});
|
||||
|
||||
it('deleteTable: drop, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'deleteTable',
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'test_table',
|
||||
mode: 'list',
|
||||
cachedResultName: 'test_table',
|
||||
},
|
||||
deleteCommand: 'drop',
|
||||
options: {},
|
||||
};
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
const pool = createFakePool(fakeConnection);
|
||||
|
||||
const poolQuerySpy = jest.spyOn(pool, 'query');
|
||||
|
||||
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, mySqlMockNode);
|
||||
|
||||
const runQueries: QueryRunner = configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
nodeOptions,
|
||||
pool,
|
||||
);
|
||||
|
||||
const result = await deleteTable.execute.call(fakeExecuteFunction, emptyInputItems, runQueries);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toEqual([{ json: { success: true }, pairedItem: [{ item: 0 }] }]);
|
||||
|
||||
expect(poolQuerySpy).toBeCalledTimes(1);
|
||||
expect(poolQuerySpy).toBeCalledWith('DROP TABLE IF EXISTS `test_table`');
|
||||
});
|
||||
|
||||
it('deleteTable: truncate, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'deleteTable',
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'test_table',
|
||||
mode: 'list',
|
||||
cachedResultName: 'test_table',
|
||||
},
|
||||
deleteCommand: 'truncate',
|
||||
options: {},
|
||||
};
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
const pool = createFakePool(fakeConnection);
|
||||
|
||||
const poolQuerySpy = jest.spyOn(pool, 'query');
|
||||
|
||||
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, mySqlMockNode);
|
||||
|
||||
const runQueries: QueryRunner = configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
nodeOptions,
|
||||
pool,
|
||||
);
|
||||
|
||||
const result = await deleteTable.execute.call(fakeExecuteFunction, emptyInputItems, runQueries);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toEqual([{ json: { success: true }, pairedItem: [{ item: 0 }] }]);
|
||||
|
||||
expect(poolQuerySpy).toBeCalledTimes(1);
|
||||
expect(poolQuerySpy).toBeCalledWith('TRUNCATE TABLE `test_table`');
|
||||
});
|
||||
|
||||
it('deleteTable: delete, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'deleteTable',
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'test_table',
|
||||
mode: 'list',
|
||||
cachedResultName: 'test_table',
|
||||
},
|
||||
deleteCommand: 'delete',
|
||||
where: {
|
||||
values: [
|
||||
{
|
||||
column: 'id',
|
||||
condition: 'equal',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
column: 'name',
|
||||
condition: 'LIKE',
|
||||
value: 'some%',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {},
|
||||
};
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
const pool = createFakePool(fakeConnection);
|
||||
|
||||
const poolQuerySpy = jest.spyOn(pool, 'query');
|
||||
|
||||
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, mySqlMockNode);
|
||||
|
||||
const runQueries: QueryRunner = configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
nodeOptions,
|
||||
pool,
|
||||
);
|
||||
|
||||
const result = await deleteTable.execute.call(fakeExecuteFunction, emptyInputItems, runQueries);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toEqual([{ json: { success: true }, pairedItem: [{ item: 0 }] }]);
|
||||
|
||||
expect(poolQuerySpy).toBeCalledTimes(1);
|
||||
expect(poolQuerySpy).toBeCalledWith(
|
||||
"DELETE FROM `test_table` WHERE `id` = '1' AND `name` LIKE 'some%'",
|
||||
);
|
||||
});
|
||||
|
||||
it('deleteTable: delete, should throw on invalid where clause', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'deleteTable',
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'test_table',
|
||||
mode: 'list',
|
||||
cachedResultName: 'test_table',
|
||||
},
|
||||
deleteCommand: 'delete',
|
||||
where: {
|
||||
values: [
|
||||
{
|
||||
column: 'id',
|
||||
condition: '=1; select 1,2; -- -',
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {},
|
||||
};
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
const pool = createFakePool(fakeConnection);
|
||||
|
||||
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, mySqlMockNode);
|
||||
|
||||
const runQueries: QueryRunner = configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
nodeOptions,
|
||||
pool,
|
||||
);
|
||||
|
||||
const promise = deleteTable.execute.call(fakeExecuteFunction, emptyInputItems, runQueries);
|
||||
|
||||
await expect(promise).rejects.toThrow('Invalid where clause');
|
||||
});
|
||||
|
||||
it('executeQuery, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'executeQuery',
|
||||
query:
|
||||
"DROP TABLE IF EXISTS $1:name;\ncreate table $1:name (id INT, name TEXT);\ninsert into $1:name (id, name) values (1, 'test 1');\nselect * from $1:name;\n",
|
||||
options: {
|
||||
queryBatching: 'independently',
|
||||
queryReplacement: 'test_table',
|
||||
},
|
||||
};
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
const fakeConnectionCopy = { ...fakeConnection };
|
||||
|
||||
fakeConnectionCopy.query = jest.fn(async (query?: string) => {
|
||||
const result = [];
|
||||
if (query?.toLowerCase().includes('select')) {
|
||||
result.push([{ id: 1, name: 'test 1' }]);
|
||||
} else {
|
||||
result.push({});
|
||||
}
|
||||
return result;
|
||||
});
|
||||
const pool = createFakePool(fakeConnectionCopy);
|
||||
|
||||
const connectionQuerySpy = jest.spyOn(fakeConnectionCopy, 'query');
|
||||
|
||||
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, mySqlMockNode);
|
||||
|
||||
const runQueries: QueryRunner = configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
nodeOptions,
|
||||
pool,
|
||||
);
|
||||
|
||||
const result = await executeQuery.execute.call(
|
||||
fakeExecuteFunction,
|
||||
emptyInputItems,
|
||||
runQueries,
|
||||
nodeOptions,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toEqual([
|
||||
{
|
||||
json: {
|
||||
id: 1,
|
||||
name: 'test 1',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(connectionQuerySpy).toBeCalledTimes(4);
|
||||
expect(connectionQuerySpy).toBeCalledWith('DROP TABLE IF EXISTS `test_table`');
|
||||
expect(connectionQuerySpy).toBeCalledWith('create table `test_table` (id INT, name TEXT)');
|
||||
expect(connectionQuerySpy).toBeCalledWith(
|
||||
"insert into `test_table` (id, name) values (1, 'test 1')",
|
||||
);
|
||||
expect(connectionQuerySpy).toBeCalledWith('select * from `test_table`');
|
||||
});
|
||||
it('executeQuery, should parse numbers', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'executeQuery',
|
||||
query: 'SELECT * FROM users LIMIT $1, $2',
|
||||
options: {
|
||||
queryBatching: 'independently',
|
||||
queryReplacement: '2, 5',
|
||||
nodeVersion: 2.3,
|
||||
},
|
||||
};
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
const fakeConnectionCopy = { ...fakeConnection };
|
||||
|
||||
fakeConnectionCopy.query = jest.fn(async (query?: string) => {
|
||||
return [{ query }];
|
||||
});
|
||||
const pool = createFakePool(fakeConnectionCopy);
|
||||
|
||||
const connectionQuerySpy = jest.spyOn(fakeConnectionCopy, 'query');
|
||||
|
||||
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, mySqlMockNode);
|
||||
|
||||
const runQueries: QueryRunner = configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
nodeOptions,
|
||||
pool,
|
||||
);
|
||||
|
||||
const result = await executeQuery.execute.call(
|
||||
fakeExecuteFunction,
|
||||
emptyInputItems,
|
||||
runQueries,
|
||||
nodeOptions,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
|
||||
expect(connectionQuerySpy).toBeCalledWith('SELECT * FROM users LIMIT 2, 5');
|
||||
});
|
||||
|
||||
it('select, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'select',
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'test_table',
|
||||
mode: 'list',
|
||||
cachedResultName: 'test_table',
|
||||
},
|
||||
limit: 2,
|
||||
where: {
|
||||
values: [
|
||||
{
|
||||
column: 'id',
|
||||
condition: '>',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
column: 'name',
|
||||
condition: '=',
|
||||
value: 'test',
|
||||
},
|
||||
],
|
||||
},
|
||||
combineConditions: 'OR',
|
||||
sort: {
|
||||
values: [
|
||||
{
|
||||
column: 'id',
|
||||
direction: 'DESC',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
queryBatching: 'transaction',
|
||||
detailedOutput: false,
|
||||
},
|
||||
};
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
const pool = createFakePool(fakeConnection);
|
||||
|
||||
const connectionQuerySpy = jest.spyOn(fakeConnection, 'query');
|
||||
|
||||
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, mySqlMockNode);
|
||||
|
||||
const runQueries: QueryRunner = configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
{ ...nodeOptions, nodeVersion: 2 },
|
||||
pool,
|
||||
);
|
||||
|
||||
const result = await select.execute.call(fakeExecuteFunction, emptyInputItems, runQueries);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toEqual([{ json: { success: true }, pairedItem: { item: 0 } }]);
|
||||
|
||||
const connectionBeginTransactionSpy = jest.spyOn(fakeConnection, 'beginTransaction');
|
||||
const connectionCommitSpy = jest.spyOn(fakeConnection, 'commit');
|
||||
|
||||
expect(connectionBeginTransactionSpy).toBeCalledTimes(1);
|
||||
|
||||
expect(connectionQuerySpy).toBeCalledTimes(1);
|
||||
expect(connectionQuerySpy).toBeCalledWith(
|
||||
"SELECT * FROM `test_table` WHERE `id` > 1 OR `name` = 'test' ORDER BY `id` DESC LIMIT 2",
|
||||
);
|
||||
|
||||
expect(connectionCommitSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('select, should throw on invalid where clause', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'select',
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'test_table',
|
||||
mode: 'list',
|
||||
cachedResultName: 'test_table',
|
||||
},
|
||||
limit: 2,
|
||||
where: {
|
||||
values: [
|
||||
{
|
||||
column: 'id',
|
||||
condition: '=1; select 1,2; -- -',
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
combineConditions: 'OR',
|
||||
sort: {
|
||||
values: [
|
||||
{
|
||||
column: 'id',
|
||||
direction: 'DESC',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
queryBatching: 'transaction',
|
||||
detailedOutput: false,
|
||||
},
|
||||
};
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
const pool = createFakePool(fakeConnection);
|
||||
|
||||
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, mySqlMockNode);
|
||||
|
||||
const runQueries: QueryRunner = configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
{ ...nodeOptions, nodeVersion: 2 },
|
||||
pool,
|
||||
);
|
||||
|
||||
const promise = select.execute.call(fakeExecuteFunction, emptyInputItems, runQueries);
|
||||
|
||||
await expect(promise).rejects.toThrow('Invalid where clause');
|
||||
});
|
||||
|
||||
it('select, should replace direction with ASC or DESC', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'select',
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'test_table',
|
||||
mode: 'list',
|
||||
cachedResultName: 'test_table',
|
||||
},
|
||||
limit: 2,
|
||||
where: {
|
||||
values: [
|
||||
{
|
||||
column: 'id',
|
||||
condition: '>',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
column: 'name',
|
||||
condition: '=',
|
||||
value: 'test',
|
||||
},
|
||||
],
|
||||
},
|
||||
combineConditions: 'OR',
|
||||
sort: {
|
||||
values: [
|
||||
{
|
||||
column: 'id',
|
||||
direction: 'DESC; Select 1,2; -- -',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
queryBatching: 'transaction',
|
||||
detailedOutput: false,
|
||||
},
|
||||
};
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
const pool = createFakePool(fakeConnection);
|
||||
|
||||
const connectionQuerySpy = jest.spyOn(fakeConnection, 'query');
|
||||
|
||||
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, mySqlMockNode);
|
||||
|
||||
const runQueries: QueryRunner = configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
{ ...nodeOptions, nodeVersion: 2 },
|
||||
pool,
|
||||
);
|
||||
|
||||
const result = await select.execute.call(fakeExecuteFunction, emptyInputItems, runQueries);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toEqual([{ json: { success: true }, pairedItem: { item: 0 } }]);
|
||||
|
||||
const connectionBeginTransactionSpy = jest.spyOn(fakeConnection, 'beginTransaction');
|
||||
const connectionCommitSpy = jest.spyOn(fakeConnection, 'commit');
|
||||
|
||||
expect(connectionBeginTransactionSpy).toBeCalledTimes(1);
|
||||
|
||||
expect(connectionQuerySpy).toBeCalledTimes(1);
|
||||
expect(connectionQuerySpy).toBeCalledWith(
|
||||
"SELECT * FROM `test_table` WHERE `id` > 1 OR `name` = 'test' ORDER BY `id` DESC LIMIT 2",
|
||||
);
|
||||
|
||||
expect(connectionCommitSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('insert, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'test_table',
|
||||
mode: 'list',
|
||||
cachedResultName: 'test_table',
|
||||
},
|
||||
dataMode: 'defineBelow',
|
||||
valuesToSend: {
|
||||
values: [
|
||||
{
|
||||
column: 'id',
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
column: 'name',
|
||||
value: 'name 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
queryBatching: 'independently',
|
||||
priority: 'HIGH_PRIORITY',
|
||||
detailedOutput: false,
|
||||
skipOnConflict: true,
|
||||
},
|
||||
};
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
const pool = createFakePool(fakeConnection);
|
||||
|
||||
const connectionQuerySpy = jest.spyOn(fakeConnection, 'query');
|
||||
|
||||
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, mySqlMockNode);
|
||||
|
||||
const runQueries: QueryRunner = configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
nodeOptions,
|
||||
pool,
|
||||
);
|
||||
|
||||
const result = await insert.execute.call(
|
||||
fakeExecuteFunction,
|
||||
emptyInputItems,
|
||||
runQueries,
|
||||
nodeOptions,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toEqual([{ json: { success: true }, pairedItem: { item: 0 } }]);
|
||||
|
||||
expect(connectionQuerySpy).toBeCalledTimes(1);
|
||||
expect(connectionQuerySpy).toBeCalledWith(
|
||||
"INSERT HIGH_PRIORITY IGNORE INTO `test_table` (`id`, `name`) VALUES ('2','name 2')",
|
||||
);
|
||||
});
|
||||
|
||||
it('update, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'update',
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'test_table',
|
||||
mode: 'list',
|
||||
cachedResultName: 'test_table',
|
||||
},
|
||||
dataMode: 'autoMapInputData',
|
||||
columnToMatchOn: 'id',
|
||||
options: {
|
||||
queryBatching: 'independently',
|
||||
},
|
||||
};
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
const pool = createFakePool(fakeConnection);
|
||||
|
||||
const connectionQuerySpy = jest.spyOn(fakeConnection, 'query');
|
||||
|
||||
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, mySqlMockNode);
|
||||
|
||||
const runQueries: QueryRunner = configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
nodeOptions,
|
||||
pool,
|
||||
);
|
||||
|
||||
const inputItems = [
|
||||
{
|
||||
json: {
|
||||
id: 42,
|
||||
name: 'test 4',
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
id: 88,
|
||||
name: 'test 88',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = await update.execute.call(
|
||||
fakeExecuteFunction,
|
||||
inputItems,
|
||||
runQueries,
|
||||
nodeOptions,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toEqual([
|
||||
{ json: { success: true }, pairedItem: { item: 0 } },
|
||||
{ json: { success: true }, pairedItem: { item: 1 } },
|
||||
]);
|
||||
|
||||
expect(connectionQuerySpy).toBeCalledTimes(2);
|
||||
expect(connectionQuerySpy).toBeCalledWith(
|
||||
"UPDATE `test_table` SET `name` = 'test 4' WHERE `id` = 42",
|
||||
);
|
||||
expect(connectionQuerySpy).toBeCalledWith(
|
||||
"UPDATE `test_table` SET `name` = 'test 88' WHERE `id` = 88",
|
||||
);
|
||||
});
|
||||
|
||||
it('upsert, should call runQueries with', async () => {
|
||||
const nodeParameters: IDataObject = {
|
||||
operation: 'upsert',
|
||||
table: {
|
||||
__rl: true,
|
||||
value: 'test_table',
|
||||
mode: 'list',
|
||||
cachedResultName: 'test_table',
|
||||
},
|
||||
columnToMatchOn: 'id',
|
||||
dataMode: 'autoMapInputData',
|
||||
options: {},
|
||||
};
|
||||
|
||||
const nodeOptions = nodeParameters.options as IDataObject;
|
||||
|
||||
const pool = createFakePool(fakeConnection);
|
||||
|
||||
const poolQuerySpy = jest.spyOn(pool, 'query');
|
||||
|
||||
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, mySqlMockNode);
|
||||
|
||||
const runQueries: QueryRunner = configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
nodeOptions,
|
||||
pool,
|
||||
);
|
||||
|
||||
const inputItems = [
|
||||
{
|
||||
json: {
|
||||
id: 42,
|
||||
name: 'test 4',
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
id: 88,
|
||||
name: 'test 88',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = await upsert.execute.call(
|
||||
fakeExecuteFunction,
|
||||
inputItems,
|
||||
runQueries,
|
||||
nodeOptions,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toEqual([{ json: { success: true }, pairedItem: [{ item: 0 }, { item: 1 }] }]);
|
||||
|
||||
expect(poolQuerySpy).toBeCalledTimes(1);
|
||||
expect(poolQuerySpy).toBeCalledWith(
|
||||
"INSERT INTO `test_table`(`id`, `name`) VALUES(42,'test 4') ON DUPLICATE KEY UPDATE `name` = 'test 4';INSERT INTO `test_table`(`id`, `name`) VALUES(88,'test 88') ON DUPLICATE KEY UPDATE `name` = 'test 88'",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,246 @@
|
||||
import mysql2 from 'mysql2/promise';
|
||||
import type { IDataObject, INode } from 'n8n-workflow';
|
||||
|
||||
import { createMockExecuteFunction } from '@test/nodes/Helpers';
|
||||
|
||||
import type { Mysql2Pool, QueryRunner } from '../../v2/helpers/interfaces';
|
||||
import { BATCH_MODE } from '../../v2/helpers/interfaces';
|
||||
import { configureQueryRunner } from '../../v2/helpers/utils';
|
||||
|
||||
const mySqlMockNode: INode = {
|
||||
id: '1',
|
||||
name: 'MySQL node',
|
||||
typeVersion: 2,
|
||||
type: 'n8n-nodes-base.mySql',
|
||||
position: [60, 760],
|
||||
parameters: {
|
||||
operation: 'select',
|
||||
},
|
||||
};
|
||||
|
||||
const fakeConnection = {
|
||||
format(query: string, values: any[]) {
|
||||
return mysql2.format(query, values);
|
||||
},
|
||||
query: jest.fn(async () => [{}]),
|
||||
release: jest.fn(),
|
||||
beginTransaction: jest.fn(),
|
||||
commit: jest.fn(),
|
||||
rollback: jest.fn(),
|
||||
};
|
||||
|
||||
const createFakePool = (connection: IDataObject) => {
|
||||
return {
|
||||
getConnection() {
|
||||
return connection;
|
||||
},
|
||||
query: jest.fn(async () => [{}]),
|
||||
} as unknown as Mysql2Pool;
|
||||
};
|
||||
|
||||
describe('Test MySql V2, runQueries', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('in single query batch mode', () => {
|
||||
it('should set paired items correctly', async () => {
|
||||
const nodeOptions = { queryBatching: BATCH_MODE.SINGLE, nodeVersion: 2 };
|
||||
const pool = createFakePool(fakeConnection);
|
||||
const mockExecuteFns = createMockExecuteFunction({}, mySqlMockNode);
|
||||
|
||||
// @ts-expect-error
|
||||
pool.query = jest.fn(async () => [
|
||||
[[{ finishedAt: '2023-12-30' }], [{ finishedAt: '2023-12-31' }]],
|
||||
]);
|
||||
|
||||
const result = await configureQueryRunner.call(
|
||||
mockExecuteFns,
|
||||
nodeOptions,
|
||||
pool,
|
||||
)([
|
||||
{ query: 'SELECT finishedAt FROM my_table WHERE id = ?', values: [123] },
|
||||
{ query: 'SELECT finishedAt FROM my_table WHERE id = ?', values: [456] },
|
||||
]);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
json: { finishedAt: '2023-12-30' },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
{
|
||||
json: { finishedAt: '2023-12-31' },
|
||||
pairedItem: { item: 1 },
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should execute in "Single" mode, should return success true', async () => {
|
||||
const nodeOptions: IDataObject = { queryBatching: BATCH_MODE.SINGLE, nodeVersion: 2 };
|
||||
|
||||
const pool = createFakePool(fakeConnection);
|
||||
const fakeExecuteFunction = createMockExecuteFunction({}, mySqlMockNode);
|
||||
|
||||
const runQueries: QueryRunner = configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
nodeOptions,
|
||||
pool,
|
||||
);
|
||||
|
||||
const poolGetConnectionSpy = jest.spyOn(pool, 'getConnection');
|
||||
const poolQuerySpy = jest.spyOn(pool, 'query');
|
||||
const connectionReleaseSpy = jest.spyOn(fakeConnection, 'release');
|
||||
const connectionFormatSpy = jest.spyOn(fakeConnection, 'format');
|
||||
|
||||
const result = await runQueries([
|
||||
{ query: 'SELECT * FROM my_table WHERE id = ?', values: [55] },
|
||||
]);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toEqual([{ json: { success: true }, pairedItem: [{ item: 0 }] }]);
|
||||
|
||||
expect(poolGetConnectionSpy).toBeCalledTimes(1);
|
||||
|
||||
expect(connectionReleaseSpy).toBeCalledTimes(1);
|
||||
|
||||
expect(poolQuerySpy).toBeCalledTimes(1);
|
||||
expect(poolQuerySpy).toBeCalledWith('SELECT * FROM my_table WHERE id = 55');
|
||||
|
||||
expect(connectionFormatSpy).toBeCalledTimes(1);
|
||||
expect(connectionFormatSpy).toBeCalledWith('SELECT * FROM my_table WHERE id = ?', [55]);
|
||||
});
|
||||
|
||||
it('should execute in "independently" mode, should return success true', async () => {
|
||||
const nodeOptions: IDataObject = { queryBatching: BATCH_MODE.INDEPENDENTLY, nodeVersion: 2 };
|
||||
|
||||
const pool = createFakePool(fakeConnection);
|
||||
|
||||
const fakeExecuteFunction = createMockExecuteFunction({}, mySqlMockNode);
|
||||
|
||||
const runQueries: QueryRunner = configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
nodeOptions,
|
||||
pool,
|
||||
);
|
||||
|
||||
const poolGetConnectionSpy = jest.spyOn(pool, 'getConnection');
|
||||
|
||||
const connectionReleaseSpy = jest.spyOn(fakeConnection, 'release');
|
||||
const connectionFormatSpy = jest.spyOn(fakeConnection, 'format');
|
||||
const connectionQuerySpy = jest.spyOn(fakeConnection, 'query');
|
||||
|
||||
const result = await runQueries([
|
||||
{
|
||||
query: 'SELECT * FROM my_table WHERE id = ?; SELECT * FROM my_table WHERE id = ?',
|
||||
values: [55, 42],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toEqual([{ json: { success: true }, pairedItem: { item: 0 } }]);
|
||||
|
||||
expect(poolGetConnectionSpy).toBeCalledTimes(1);
|
||||
|
||||
expect(connectionQuerySpy).toBeCalledTimes(2);
|
||||
expect(connectionQuerySpy).toBeCalledWith('SELECT * FROM my_table WHERE id = 55');
|
||||
expect(connectionQuerySpy).toBeCalledWith('SELECT * FROM my_table WHERE id = 42');
|
||||
|
||||
expect(connectionFormatSpy).toBeCalledTimes(1);
|
||||
expect(connectionFormatSpy).toBeCalledWith(
|
||||
'SELECT * FROM my_table WHERE id = ?; SELECT * FROM my_table WHERE id = ?',
|
||||
[55, 42],
|
||||
);
|
||||
|
||||
expect(connectionReleaseSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should execute in "transaction" mode, should return success true', async () => {
|
||||
const nodeOptions: IDataObject = { queryBatching: BATCH_MODE.TRANSACTION, nodeVersion: 2 };
|
||||
|
||||
const pool = createFakePool(fakeConnection);
|
||||
|
||||
const fakeExecuteFunction = createMockExecuteFunction({}, mySqlMockNode);
|
||||
|
||||
const runQueries: QueryRunner = configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
nodeOptions,
|
||||
pool,
|
||||
);
|
||||
|
||||
const poolGetConnectionSpy = jest.spyOn(pool, 'getConnection');
|
||||
|
||||
const connectionReleaseSpy = jest.spyOn(fakeConnection, 'release');
|
||||
const connectionFormatSpy = jest.spyOn(fakeConnection, 'format');
|
||||
const connectionQuerySpy = jest.spyOn(fakeConnection, 'query');
|
||||
const connectionBeginTransactionSpy = jest.spyOn(fakeConnection, 'beginTransaction');
|
||||
const connectionCommitSpy = jest.spyOn(fakeConnection, 'commit');
|
||||
|
||||
const result = await runQueries([
|
||||
{
|
||||
query: 'SELECT * FROM my_table WHERE id = ?; SELECT * FROM my_table WHERE id = ?',
|
||||
values: [55, 42],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toEqual([{ json: { success: true }, pairedItem: { item: 0 } }]);
|
||||
|
||||
expect(poolGetConnectionSpy).toBeCalledTimes(1);
|
||||
|
||||
expect(connectionBeginTransactionSpy).toBeCalledTimes(1);
|
||||
|
||||
expect(connectionQuerySpy).toBeCalledTimes(2);
|
||||
expect(connectionQuerySpy).toBeCalledWith('SELECT * FROM my_table WHERE id = 55');
|
||||
expect(connectionQuerySpy).toBeCalledWith('SELECT * FROM my_table WHERE id = 42');
|
||||
|
||||
expect(connectionFormatSpy).toBeCalledTimes(1);
|
||||
expect(connectionFormatSpy).toBeCalledWith(
|
||||
'SELECT * FROM my_table WHERE id = ?; SELECT * FROM my_table WHERE id = ?',
|
||||
[55, 42],
|
||||
);
|
||||
|
||||
expect(connectionCommitSpy).toBeCalledTimes(1);
|
||||
|
||||
expect(connectionReleaseSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return error item with continueOnFail = true for connection error', async () => {
|
||||
const nodeOptions: IDataObject = { queryBatching: BATCH_MODE.SINGLE, nodeVersion: 2 };
|
||||
const pool = createFakePool(fakeConnection);
|
||||
pool.getConnection = jest.fn(() => {
|
||||
throw new Error('ECONNREFUSED');
|
||||
});
|
||||
const fakeExecuteFunction = createMockExecuteFunction({}, mySqlMockNode);
|
||||
fakeExecuteFunction.continueOnFail = () => true;
|
||||
|
||||
const result = await configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
nodeOptions,
|
||||
pool,
|
||||
)([{ query: 'SELECT * FROM my_table WHERE id = ?', values: [55] }]);
|
||||
|
||||
expect(result).toEqual([{ json: expect.objectContaining({ message: 'Connection refused' }) }]);
|
||||
});
|
||||
|
||||
it('should throw error when continueOnFail = false for connection error', async () => {
|
||||
const nodeOptions: IDataObject = { queryBatching: BATCH_MODE.SINGLE, nodeVersion: 2 };
|
||||
const pool = createFakePool(fakeConnection);
|
||||
pool.getConnection = jest.fn(() => {
|
||||
throw new Error('ECONNREFUSED');
|
||||
});
|
||||
const fakeExecuteFunction = createMockExecuteFunction({}, mySqlMockNode);
|
||||
fakeExecuteFunction.continueOnFail = () => false;
|
||||
|
||||
await expect(
|
||||
configureQueryRunner.call(
|
||||
fakeExecuteFunction,
|
||||
nodeOptions,
|
||||
pool,
|
||||
)([{ query: 'SELECT * FROM my_table WHERE id = ?', values: [55] }]),
|
||||
).rejects.toThrow('Connection refused');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,441 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
|
||||
import type { SortRule, WhereClause } from '../../v2/helpers/interfaces';
|
||||
import * as utils from '../../v2/helpers/utils';
|
||||
import {
|
||||
prepareQueryAndReplacements,
|
||||
wrapData,
|
||||
addWhereClauses,
|
||||
addSortRules,
|
||||
replaceEmptyStringsByNulls,
|
||||
escapeSqlIdentifier,
|
||||
splitQueryToStatements,
|
||||
} from '../../v2/helpers/utils';
|
||||
|
||||
const mySqlMockNode: INode = {
|
||||
id: '1',
|
||||
name: 'MySQL node',
|
||||
typeVersion: 2,
|
||||
type: 'n8n-nodes-base.mySql',
|
||||
position: [60, 760],
|
||||
parameters: {
|
||||
operation: 'select',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Test MySql V2, prepareQueryAndReplacements', () => {
|
||||
it('should transform query and values', () => {
|
||||
const preparedQuery = prepareQueryAndReplacements(
|
||||
'SELECT * FROM $1:name WHERE id = $2 AND name = $4 AND $3:name = 28',
|
||||
2.5,
|
||||
['table', 15, 'age', 'Name'],
|
||||
);
|
||||
expect(preparedQuery).toBeDefined();
|
||||
expect(preparedQuery.query).toEqual(
|
||||
'SELECT * FROM `table` WHERE id = ? AND name = ? AND `age` = 28',
|
||||
);
|
||||
expect(preparedQuery.values.length).toEqual(2);
|
||||
expect(preparedQuery.values[0]).toEqual(15);
|
||||
expect(preparedQuery.values[1]).toEqual('Name');
|
||||
});
|
||||
|
||||
it('should not replace dollar amounts inside quoted strings', () => {
|
||||
const preparedQuery = prepareQueryAndReplacements(
|
||||
"INSERT INTO test_table(content) VALUES('This is for testing $60')",
|
||||
2.5,
|
||||
[],
|
||||
);
|
||||
expect(preparedQuery).toBeDefined();
|
||||
expect(preparedQuery.query).toEqual(
|
||||
"INSERT INTO test_table(content) VALUES('This is for testing $60')",
|
||||
);
|
||||
expect(preparedQuery.values.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should handle mixed parameters and dollar amounts in quotes', () => {
|
||||
const preparedQuery = prepareQueryAndReplacements(
|
||||
"INSERT INTO $1:name(content, price) VALUES('Product costs $60', $2)",
|
||||
2.5,
|
||||
['products', 59.99],
|
||||
);
|
||||
expect(preparedQuery).toBeDefined();
|
||||
expect(preparedQuery.query).toEqual(
|
||||
"INSERT INTO `products`(content, price) VALUES('Product costs $60', ?)",
|
||||
);
|
||||
expect(preparedQuery.values.length).toEqual(1);
|
||||
expect(preparedQuery.values[0]).toEqual(59.99);
|
||||
});
|
||||
|
||||
it('should handle parameters in double quotes', () => {
|
||||
const preparedQuery = prepareQueryAndReplacements(
|
||||
'INSERT INTO test_table(content) VALUES("Price is $100 and $200")',
|
||||
2.5,
|
||||
[],
|
||||
);
|
||||
expect(preparedQuery).toBeDefined();
|
||||
expect(preparedQuery.query).toEqual(
|
||||
'INSERT INTO test_table(content) VALUES("Price is $100 and $200")',
|
||||
);
|
||||
expect(preparedQuery.values.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should process parameters in correct order despite reverse processing', () => {
|
||||
const preparedQuery = prepareQueryAndReplacements(
|
||||
'SELECT * FROM table WHERE col1 = $1 AND col2 = $2 AND col3 = $3 AND col4 = $4 AND col5 = $5',
|
||||
2.5,
|
||||
['value1', 'value2', 'value3', 'value4', 'value5'],
|
||||
);
|
||||
expect(preparedQuery).toBeDefined();
|
||||
expect(preparedQuery.query).toEqual(
|
||||
'SELECT * FROM table WHERE col1 = ? AND col2 = ? AND col3 = ? AND col4 = ? AND col5 = ?',
|
||||
);
|
||||
expect(preparedQuery.values.length).toEqual(5);
|
||||
expect(preparedQuery.values[0]).toEqual('value1');
|
||||
expect(preparedQuery.values[1]).toEqual('value2');
|
||||
expect(preparedQuery.values[2]).toEqual('value3');
|
||||
expect(preparedQuery.values[3]).toEqual('value4');
|
||||
expect(preparedQuery.values[4]).toEqual('value5');
|
||||
});
|
||||
|
||||
it('should handle escaped single quotes correctly', () => {
|
||||
const preparedQuery = prepareQueryAndReplacements(
|
||||
"INSERT INTO test_table(content) VALUES('Don''t replace $1 here')",
|
||||
2.5,
|
||||
['should_not_appear', 123],
|
||||
);
|
||||
expect(preparedQuery).toBeDefined();
|
||||
expect(preparedQuery.query).toEqual(
|
||||
"INSERT INTO test_table(content) VALUES('Don''t replace $1 here')",
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle escaped double quotes correctly', () => {
|
||||
const preparedQuery = prepareQueryAndReplacements(
|
||||
"INSERT INTO test_table(content) VALUES('Don\"'t replace $1 here')",
|
||||
2.5,
|
||||
['should_not_appear', 123],
|
||||
);
|
||||
expect(preparedQuery).toBeDefined();
|
||||
expect(preparedQuery.query).toEqual(
|
||||
"INSERT INTO test_table(content) VALUES('Don\"'t replace $1 here')",
|
||||
);
|
||||
});
|
||||
|
||||
it('should use legacy processing for versions < 2.5', () => {
|
||||
const legacySpy = jest.spyOn(utils, 'prepareQueryLegacy');
|
||||
|
||||
prepareQueryAndReplacements('SELECT * FROM $1:name WHERE id = $2', 2.4, ['users', 123]);
|
||||
|
||||
expect(legacySpy).toHaveBeenCalledWith('SELECT * FROM $1:name WHERE id = $2', ['users', 123]);
|
||||
|
||||
legacySpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should use new processing for versions >= 2.5', () => {
|
||||
const legacySpy = jest.spyOn(utils, 'prepareQueryLegacy');
|
||||
|
||||
prepareQueryAndReplacements('SELECT * FROM $1:name WHERE id = $2', 2.5, ['users', 123]);
|
||||
|
||||
expect(legacySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when parameter is referenced but no replacement value provided', () => {
|
||||
expect(() => {
|
||||
prepareQueryAndReplacements(
|
||||
'SELECT * FROM users WHERE id = $4',
|
||||
2.5,
|
||||
['value1', 'value2'], // Only 2 values but query references $4
|
||||
);
|
||||
}).toThrow('Parameter $4 referenced in query but no replacement value provided at index 4');
|
||||
});
|
||||
|
||||
it('should throw error when multiple parameters are missing replacement values', () => {
|
||||
expect(() => {
|
||||
prepareQueryAndReplacements(
|
||||
'SELECT * FROM users WHERE id = $3 AND name = $5',
|
||||
2.5,
|
||||
['value1'], // Only 1 value but query references $3 and $5
|
||||
);
|
||||
}).toThrow('Parameter $3 referenced in query but no replacement value provided at index 3');
|
||||
});
|
||||
|
||||
it('should not throw error when all referenced parameters have replacement values', () => {
|
||||
expect(() => {
|
||||
prepareQueryAndReplacements(
|
||||
'SELECT * FROM users WHERE id = $1 AND name = $2',
|
||||
2.5,
|
||||
['123', 'John'], // Correct number of values
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test MySql V2, 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 MySql V2, addWhereClauses', () => {
|
||||
it('add where clauses to query', () => {
|
||||
const whereClauses: WhereClause[] = [
|
||||
{ column: 'species', condition: 'equal', value: 'dog' },
|
||||
{ column: 'name', condition: 'equal', value: 'Hunter' },
|
||||
];
|
||||
const [query, values] = addWhereClauses(
|
||||
mySqlMockNode,
|
||||
0,
|
||||
'SELECT * FROM `pet`',
|
||||
whereClauses,
|
||||
[],
|
||||
);
|
||||
expect(query).toEqual('SELECT * FROM `pet` WHERE `species` = ? AND `name` = ?');
|
||||
expect(values.length).toEqual(2);
|
||||
expect(values[0]).toEqual('dog');
|
||||
expect(values[1]).toEqual('Hunter');
|
||||
});
|
||||
it('add where clauses to query combined by OR', () => {
|
||||
const whereClauses: WhereClause[] = [
|
||||
{ column: 'species', condition: 'equal', value: 'dog' },
|
||||
{ column: 'name', condition: 'equal', value: 'Hunter' },
|
||||
];
|
||||
const [query, values] = addWhereClauses(
|
||||
mySqlMockNode,
|
||||
0,
|
||||
'SELECT * FROM `pet`',
|
||||
whereClauses,
|
||||
[],
|
||||
'OR',
|
||||
);
|
||||
expect(query).toEqual('SELECT * FROM `pet` WHERE `species` = ? OR `name` = ?');
|
||||
expect(values.length).toEqual(2);
|
||||
expect(values[0]).toEqual('dog');
|
||||
expect(values[1]).toEqual('Hunter');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test MySql V2, addSortRules', () => {
|
||||
it('should add ORDER by', () => {
|
||||
const sortRules: SortRule[] = [
|
||||
{ column: 'name', direction: 'ASC' },
|
||||
{ column: 'age', direction: 'DESC' },
|
||||
];
|
||||
const [query, values] = addSortRules('SELECT * FROM `pet`', sortRules, []);
|
||||
|
||||
expect(query).toEqual('SELECT * FROM `pet` ORDER BY `name` ASC, `age` DESC');
|
||||
expect(values.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test MySql V2, replaceEmptyStringsByNulls', () => {
|
||||
it('should replace empty strings', () => {
|
||||
const data = [
|
||||
{ json: { id: 1, name: '' } },
|
||||
{ json: { id: '', name: '' } },
|
||||
{ json: { id: null, data: '' } },
|
||||
];
|
||||
const replacedData = replaceEmptyStringsByNulls(data, true);
|
||||
expect(replacedData).toBeDefined();
|
||||
expect(replacedData).toEqual([
|
||||
{ json: { id: 1, name: null } },
|
||||
{ json: { id: null, name: null } },
|
||||
{ json: { id: null, data: null } },
|
||||
]);
|
||||
});
|
||||
it('should not replace empty strings', () => {
|
||||
const data = [{ json: { id: 1, name: '' } }];
|
||||
const replacedData = replaceEmptyStringsByNulls(data);
|
||||
expect(replacedData).toBeDefined();
|
||||
expect(replacedData).toEqual([{ json: { id: 1, name: '' } }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test MySql V2, escapeSqlIdentifier', () => {
|
||||
it('should escape fully qualified identifier', () => {
|
||||
const input = 'db_name.tbl_name.col_name';
|
||||
const escapedIdentifier = escapeSqlIdentifier(input);
|
||||
expect(escapedIdentifier).toEqual('`db_name`.`tbl_name`.`col_name`');
|
||||
});
|
||||
|
||||
it('should escape table name only', () => {
|
||||
const input = 'tbl_name';
|
||||
const escapedIdentifier = escapeSqlIdentifier(input);
|
||||
expect(escapedIdentifier).toEqual('`tbl_name`');
|
||||
});
|
||||
|
||||
it('should escape fully qualified identifier with backticks', () => {
|
||||
const input = '`db_name`.`tbl_name`.`col_name`';
|
||||
const escapedIdentifier = escapeSqlIdentifier(input);
|
||||
expect(escapedIdentifier).toEqual('`db_name`.`tbl_name`.`col_name`');
|
||||
});
|
||||
|
||||
it('should escape identifier with dots', () => {
|
||||
const input = '`db_name`.`some.dotted.tbl_name`';
|
||||
const escapedIdentifier = escapeSqlIdentifier(input);
|
||||
expect(escapedIdentifier).toEqual('`db_name`.`some.dotted.tbl_name`');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test MySql V2, splitQueryToStatements', () => {
|
||||
it('should split query into statements', () => {
|
||||
const query =
|
||||
"insert into models (`created_at`, custom_ship_time, id) values ('2023-09-07 10:26:20', 'some random; data with a semicolon', 1); insert into models (`created_at`, custom_ship_time, id) values ('2023-09-07 10:27:55', 'random data without semicolon\n', 2);";
|
||||
|
||||
const statements = splitQueryToStatements(query);
|
||||
|
||||
expect(statements).toBeDefined();
|
||||
expect(statements).toEqual([
|
||||
"insert into models (`created_at`, custom_ship_time, id) values ('2023-09-07 10:26:20', 'some random; data with a semicolon', 1)",
|
||||
"insert into models (`created_at`, custom_ship_time, id) values ('2023-09-07 10:27:55', 'random data without semicolon', 2)",
|
||||
]);
|
||||
});
|
||||
it('should not split by ; inside string literal', () => {
|
||||
const query =
|
||||
"SELECT custom_ship_time FROM models WHERE models.custom_ship_time LIKE CONCAT('%', ';', '%') LIMIT 10";
|
||||
|
||||
const statements = splitQueryToStatements(query);
|
||||
|
||||
expect(statements).toBeDefined();
|
||||
expect(statements).toEqual([
|
||||
"SELECT custom_ship_time FROM models WHERE models.custom_ship_time LIKE CONCAT('%', ';', '%') LIMIT 10",
|
||||
]);
|
||||
});
|
||||
|
||||
describe('where clause handling', () => {
|
||||
const validOperations = [
|
||||
'equal',
|
||||
'=',
|
||||
'!=',
|
||||
'LIKE',
|
||||
'>',
|
||||
'<',
|
||||
'>=',
|
||||
'<=',
|
||||
'IS NULL',
|
||||
'IS NOT NULL',
|
||||
];
|
||||
const invalidOperations = ['=1 or 1--', '=>', ''];
|
||||
|
||||
test.each(validOperations)('isWhereClause returns true for "%s" operation', (operation) => {
|
||||
expect(
|
||||
utils.isWhereClause({
|
||||
column: 'id',
|
||||
condition: operation,
|
||||
value: '1',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test.each(invalidOperations)('isWhereClause returns false for "%s" operation', (operation) => {
|
||||
expect(
|
||||
utils.isWhereClause({
|
||||
column: 'name',
|
||||
condition: operation,
|
||||
value: 'ok',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('isWhereClause returns false for when column is missing', () => {
|
||||
expect(
|
||||
utils.isWhereClause({
|
||||
condition: 'equal',
|
||||
value: 'ok',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('isWhereClause returns false for when condition is missing', () => {
|
||||
expect(
|
||||
utils.isWhereClause({
|
||||
column: 'id',
|
||||
value: 'ok',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test.each(invalidOperations)(
|
||||
'getWhereClauses throws an exception for "%s" operation',
|
||||
(operation) => {
|
||||
const getNodeParameterMock = jest.fn().mockReturnValue({
|
||||
values: [
|
||||
{
|
||||
column: 'test',
|
||||
condition: '=',
|
||||
value: '3',
|
||||
},
|
||||
{
|
||||
column: 'id',
|
||||
condition: operation,
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
const ctx = mock<IExecuteFunctions>({ getNodeParameter: getNodeParameterMock });
|
||||
expect(() => utils.getWhereClauses(ctx, 0)).toThrow();
|
||||
},
|
||||
);
|
||||
|
||||
test.each(validOperations)(
|
||||
'getWhereClauses returns valid clauses for "%s" operation',
|
||||
(operation) => {
|
||||
const clauses = [
|
||||
{
|
||||
column: 'name',
|
||||
condition: 'LIKE',
|
||||
value: 'Wohn Jick',
|
||||
},
|
||||
{
|
||||
column: 'id',
|
||||
condition: operation,
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
column: 'condition',
|
||||
condition: 'equal',
|
||||
value: 'angry',
|
||||
},
|
||||
];
|
||||
const getNodeParameterMock = jest.fn().mockReturnValue({
|
||||
values: clauses,
|
||||
});
|
||||
const ctx = mock<IExecuteFunctions>({ getNodeParameter: getNodeParameterMock });
|
||||
expect(utils.getWhereClauses(ctx, 0)).toBe(clauses);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user