fix(Microsoft SQL Node): Fix execute query to allow for non select query to run (#11335)

This commit is contained in:
Shireen Missi 2024-10-22 10:58:09 +01:00 committed by GitHub
parent 901888d5b1
commit ba158b4f85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 144 additions and 29 deletions

View file

@ -1,9 +1,12 @@
import type { IResult } from 'mssql';
import mssql from 'mssql';
import type { IDataObject, INodeExecutionData } from 'n8n-workflow'; import type { IDataObject, INodeExecutionData } from 'n8n-workflow';
import { deepCopy } from 'n8n-workflow'; import { deepCopy } from 'n8n-workflow';
import mssql from 'mssql';
import type { ITables, OperationInputData } from './interfaces';
import { chunk, flatten } from '@utils/utilities'; import { chunk, flatten } from '@utils/utilities';
import type { ITables, OperationInputData } from './interfaces';
/** /**
* Returns a copy of the item which only contains the json data and * Returns a copy of the item which only contains the json data and
* of that only the defined properties * of that only the defined properties
@ -234,3 +237,36 @@ export async function deleteOperation(tables: ITables, pool: mssql.ConnectionPoo
0, 0,
); );
} }
export async function executeSqlQueryAndPrepareResults(
pool: mssql.ConnectionPool,
rawQuery: string,
itemIndex: number,
): Promise<INodeExecutionData[]> {
const rawResult: IResult<any> = await pool.request().query(rawQuery);
const { recordsets, rowsAffected } = rawResult;
if (Array.isArray(recordsets) && recordsets.length > 0) {
const result: IDataObject[] = recordsets.length > 1 ? flatten(recordsets) : recordsets[0];
return result.map((entry) => ({
json: entry,
pairedItem: [{ item: itemIndex }],
}));
} else if (rowsAffected && rowsAffected.length > 0) {
// Handle non-SELECT queries (e.g., INSERT, UPDATE, DELETE)
return rowsAffected.map((affectedRows, idx) => ({
json: {
message: `Query ${idx + 1} executed successfully`,
rowsAffected: affectedRows,
},
pairedItem: [{ item: itemIndex }],
}));
} else {
return [
{
json: { message: 'Query executed successfully, but no rows were affected' },
pairedItem: [{ item: itemIndex }],
},
];
}
}

View file

@ -12,17 +12,17 @@ import {
NodeConnectionType, NodeConnectionType,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { ITables } from './interfaces'; import { flatten, generatePairedItemData, getResolvables } from '@utils/utilities';
import { import {
configurePool, configurePool,
createTableStruct, createTableStruct,
deleteOperation, deleteOperation,
executeSqlQueryAndPrepareResults,
insertOperation, insertOperation,
updateOperation, updateOperation,
} from './GenericFunctions'; } from './GenericFunctions';
import type { ITables } from './interfaces';
import { flatten, generatePairedItemData, getResolvables } from '@utils/utilities';
export class MicrosoftSql implements INodeType { export class MicrosoftSql implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@ -268,17 +268,8 @@ export class MicrosoftSql implements INodeType {
this.evaluateExpression(resolvable, i) as string, this.evaluateExpression(resolvable, i) as string,
); );
} }
const results = await executeSqlQueryAndPrepareResults(pool, rawQuery, i);
const { recordsets }: IResult<any[]> = await pool.request().query(rawQuery); returnData.push(...results);
const result: IDataObject[] = recordsets.length > 1 ? flatten(recordsets) : recordsets[0];
for (const entry of result) {
returnData.push({
json: entry,
pairedItem: [{ item: i }],
});
}
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
returnData.push({ returnData.push({

View file

@ -1,15 +1,19 @@
import { Request } from 'mssql'; import { Request } from 'mssql';
import type { IResult } from 'mssql';
import type mssql from 'mssql';
import type { IDataObject } from 'n8n-workflow'; import type { IDataObject } from 'n8n-workflow';
import { import {
configurePool, configurePool,
deleteOperation, deleteOperation,
executeSqlQueryAndPrepareResults,
insertOperation, insertOperation,
mssqlChunk, mssqlChunk,
updateOperation, updateOperation,
} from '../GenericFunctions'; } from '../GenericFunctions';
describe('MSSQL tests', () => { describe('MSSQL tests', () => {
let querySpy: jest.SpyInstance<void, Parameters<Request['query']>>; let querySpy: jest.SpyInstance;
let request: Request; let request: Request;
const assertParameters = (parameters: unknown[][] | IDataObject) => { const assertParameters = (parameters: unknown[][] | IDataObject) => {
@ -34,18 +38,12 @@ describe('MSSQL tests', () => {
) { ) {
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
request = this; request = this;
return [ return {
[ recordsets: [],
[ recordset: [],
{ output: {},
recordsets: [], rowsAffected: [0],
recordset: undefined, } as unknown as IResult<unknown>;
output: {},
rowsAffected: [0],
},
],
],
];
}); });
}); });
@ -154,4 +152,94 @@ describe('MSSQL tests', () => {
expect(chunks.map((chunk) => chunk.length)).toEqual([699, 699, 699, 699, 204]); expect(chunks.map((chunk) => chunk.length)).toEqual([699, 699, 699, 699, 204]);
}); });
}); });
describe('executeSqlQueryAndPrepareResults', () => {
it('should handle SELECT query with single record', async () => {
querySpy.mockResolvedValueOnce({
recordsets: [[{ id: 1, name: 'Test' }]] as any,
recordset: [{ id: 1, name: 'Test', columns: [{ name: 'id' }, { name: 'name' }] }],
rowsAffected: [1],
output: {},
} as unknown as IResult<unknown>);
const pool = { request: () => new Request() } as any as mssql.ConnectionPool;
const result = await executeSqlQueryAndPrepareResults(pool, 'SELECT * FROM users', 0);
expect(result).toEqual([
{
json: { id: 1, name: 'Test' },
pairedItem: [{ item: 0 }],
},
]);
expect(querySpy).toHaveBeenCalledWith('SELECT * FROM users');
});
it('should handle SELECT query with multiple records', async () => {
querySpy.mockResolvedValueOnce({
recordsets: [[{ id: 1 }], [{ name: 'Test' }]] as unknown,
rowsAffected: [1, 1],
output: {},
} as unknown as IResult<unknown>);
const pool = { request: () => new Request() } as any as mssql.ConnectionPool;
const result = await executeSqlQueryAndPrepareResults(pool, 'SELECT id; SELECT name', 1);
expect(result).toEqual([
{ json: { id: 1 }, pairedItem: [{ item: 1 }] },
{ json: { name: 'Test' }, pairedItem: [{ item: 1 }] },
]);
});
it('should handle non-SELECT query', async () => {
querySpy.mockResolvedValueOnce({
recordsets: [],
recordset: [],
rowsAffected: [5],
output: {},
} as unknown as IResult<unknown>);
const pool = { request: () => new Request() } as any as mssql.ConnectionPool;
const result = await executeSqlQueryAndPrepareResults(pool, 'UPDATE users SET active = 1', 2);
expect(result).toEqual([
{
json: { message: 'Query 1 executed successfully', rowsAffected: 5 },
pairedItem: [{ item: 2 }],
},
]);
});
it('should handle query with no affected rows', async () => {
querySpy.mockResolvedValueOnce({
recordsets: [],
recordset: [],
rowsAffected: [],
output: {},
} as unknown as IResult<unknown>);
const pool = { request: () => new Request() } as any as mssql.ConnectionPool;
const result = await executeSqlQueryAndPrepareResults(
pool,
'DELETE FROM users WHERE id = 999',
3,
);
expect(result).toEqual([
{
json: { message: 'Query executed successfully, but no rows were affected' },
pairedItem: [{ item: 3 }],
},
]);
});
it('should throw an error when query fails', async () => {
const errorMessage = 'Database error';
querySpy.mockRejectedValueOnce(new Error(errorMessage));
const pool = { request: () => new Request() } as any as mssql.ConnectionPool;
await expect(executeSqlQueryAndPrepareResults(pool, 'INVALID SQL', 4)).rejects.toThrow(
errorMessage,
);
});
});
}); });