From ccbd91459425713d870bfe03919defd98b366215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dimitri=20Lavren=C3=BCk?= <20122620+dlavrenuek@users.noreply.github.com> Date: Fri, 24 Apr 2026 09:44:08 +0200 Subject: [PATCH] fix(MySql Node): Fix table name filtering (#28911) --- .../nodes/MySql/test/v2/listSearch.test.ts | 108 ++++++++++++++++++ .../nodes/MySql/v2/methods/listSearch.ts | 14 ++- 2 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 packages/nodes-base/nodes/MySql/test/v2/listSearch.test.ts diff --git a/packages/nodes-base/nodes/MySql/test/v2/listSearch.test.ts b/packages/nodes-base/nodes/MySql/test/v2/listSearch.test.ts new file mode 100644 index 00000000000..3d89fe8074a --- /dev/null +++ b/packages/nodes-base/nodes/MySql/test/v2/listSearch.test.ts @@ -0,0 +1,108 @@ +/* eslint-disable n8n-nodes-base/node-param-display-name-miscased */ +import type { ILoadOptionsFunctions } from 'n8n-workflow'; + +import { searchTables } from '../../v2/methods/listSearch'; + +const mockRelease = jest.fn(); +const mockEnd = jest.fn().mockResolvedValue(undefined); + +const createMockPool = (rows: object[]) => { + const mockQuery = jest.fn().mockResolvedValue([rows]); + const mockFormat = jest + .fn() + .mockImplementation((query: string, values: unknown[]) => + values.reduce((q, v) => q.replace('?', String(v)), query), + ); + const mockConnection = { + format: mockFormat, + query: mockQuery, + release: mockRelease, + }; + return { + pool: { getConnection: jest.fn().mockResolvedValue(mockConnection), end: mockEnd }, + mockFormat, + mockQuery, + }; +}; + +const mockLoadOptionsFunctions = (database = 'test_db'): ILoadOptionsFunctions => + ({ + getCredentials: jest.fn().mockResolvedValue({ database }), + getNodeParameter: jest.fn().mockReturnValue({}), + }) as unknown as ILoadOptionsFunctions; + +jest.mock('../../v2/transport', () => ({ createPool: jest.fn() })); + +import { createPool } from '../../v2/transport'; + +describe('MySQL v2 / listSearch / searchTables', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockEnd.mockResolvedValue(undefined); + }); + + it('should return all tables when no filter is provided', async () => { + const rows = [{ TABLE_NAME: 'users' }, { TABLE_NAME: 'orders' }]; + const { pool, mockFormat } = createMockPool(rows); + (createPool as jest.Mock).mockResolvedValue(pool); + + const result = await searchTables.call(mockLoadOptionsFunctions()); + + expect(result).toEqual({ + results: [ + { name: 'users', value: 'users' }, + { name: 'orders', value: 'orders' }, + ], + }); + + const [sql] = mockFormat.mock.calls[0]; + expect(sql).not.toContain('LIKE'); + }); + + it('should filter tables by name when filter is provided', async () => { + const rows = [{ TABLE_NAME: 'users' }]; + const { pool, mockFormat } = createMockPool(rows); + (createPool as jest.Mock).mockResolvedValue(pool); + + const result = await searchTables.call(mockLoadOptionsFunctions(), 'user'); + + expect(result).toEqual({ + results: [{ name: 'users', value: 'users' }], + }); + + const [sql, values] = mockFormat.mock.calls[0]; + expect(sql).toContain('LIKE ?'); + expect(values).toContain('%user%'); + }); + + it('should return empty results when no tables match filter', async () => { + const { pool } = createMockPool([]); + (createPool as jest.Mock).mockResolvedValue(pool); + + const result = await searchTables.call(mockLoadOptionsFunctions(), 'nonexistent'); + + expect(result).toEqual({ results: [] }); + }); + + it('should always close the pool', async () => { + const { pool } = createMockPool([]); + (createPool as jest.Mock).mockResolvedValue(pool); + + await searchTables.call(mockLoadOptionsFunctions()); + + expect(mockEnd).toHaveBeenCalled(); + }); + + it('should close the pool even when query throws', async () => { + const mockConnection = { + format: (_q: string, v: unknown[]) => v, + query: jest.fn().mockRejectedValue(new Error('DB error')), + release: mockRelease, + }; + const pool = { getConnection: jest.fn().mockResolvedValue(mockConnection), end: mockEnd }; + (createPool as jest.Mock).mockResolvedValue(pool); + + await expect(searchTables.call(mockLoadOptionsFunctions())).rejects.toThrow('DB error'); + expect(mockEnd).toHaveBeenCalled(); + }); +}); diff --git a/packages/nodes-base/nodes/MySql/v2/methods/listSearch.ts b/packages/nodes-base/nodes/MySql/v2/methods/listSearch.ts index 65ec2f53067..314a86d9e3c 100644 --- a/packages/nodes-base/nodes/MySql/v2/methods/listSearch.ts +++ b/packages/nodes-base/nodes/MySql/v2/methods/listSearch.ts @@ -3,7 +3,10 @@ import type { IDataObject, ILoadOptionsFunctions, INodeListSearchResult } from ' import type { MysqlNodeCredentials } from '../helpers/interfaces'; import { createPool } from '../transport'; -export async function searchTables(this: ILoadOptionsFunctions): Promise { +export async function searchTables( + this: ILoadOptionsFunctions, + filter?: string, +): Promise { const credentials = await this.getCredentials('mySql'); const nodeOptions = this.getNodeParameter('options', 0) as IDataObject; @@ -13,8 +16,13 @@ export async function searchTables(this: ILoadOptionsFunctions): Promise