import type { IDataObject, INode } from 'n8n-workflow'; import mysql2 from 'mysql2/promise'; 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'; import { createMockExecuteFunction } from '@test/nodes/Helpers'; 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('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', 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` undefined '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'", ); }); });