mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
fix(Microsoft SQL Node): Fix execute query to allow for non select query to run (#11335)
This commit is contained in:
parent
901888d5b1
commit
ba158b4f85
|
@ -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 }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue