mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-02 17:57:06 +02:00
Co-authored-by: Matsu <matias.huhta@n8n.io> Co-authored-by: Dimitri Lavrenük <20122620+dlavrenuek@users.noreply.github.com> Co-authored-by: Charlie Kolb <charlie@n8n.io> Co-authored-by: RomanDavydchuk <roman.davydchuk@n8n.io> Co-authored-by: Jaakko Husso <jaakko@n8n.io> Co-authored-by: Dawid Myslak <dawid.myslak@gmail.com> Co-authored-by: Svetoslav Dekov <svetoslav.dekov@n8n.io> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Guillaume Jacquart <jacquart.guillaume@gmail.com> Co-authored-by: Sandra Zollner <sandra.zollner@n8n.io> Co-authored-by: Milorad FIlipović <milorad@n8n.io> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: Ricardo Espinoza <ricardo@n8n.io>
389 lines
12 KiB
TypeScript
389 lines
12 KiB
TypeScript
import { NodeOperationError } from 'n8n-workflow';
|
|
import type { IDataTableProjectService, IExecuteFunctions, INode } from 'n8n-workflow';
|
|
|
|
import { ANY_CONDITION } from '../../common/constants';
|
|
import { DATA_TABLE_ID_FIELD } from '../../common/fields';
|
|
import * as getOperation from '../../actions/row/get.operation';
|
|
|
|
describe('DataTable Get Operation - Sort Feature', () => {
|
|
let mockExecuteFunctions: IExecuteFunctions;
|
|
let mockDataTableProxy: IDataTableProjectService;
|
|
const node = { id: 'test', typeVersion: 1.1 } as INode;
|
|
|
|
beforeEach(() => {
|
|
const getManyRowsAndCount = jest.fn();
|
|
const getColumns = jest.fn();
|
|
|
|
mockDataTableProxy = {
|
|
getManyRowsAndCount,
|
|
getColumns,
|
|
} as unknown as IDataTableProjectService;
|
|
|
|
getColumns.mockResolvedValue([
|
|
{ name: 'id', type: 'number' },
|
|
{ name: 'name', type: 'string' },
|
|
{ name: 'age', type: 'number' },
|
|
{ name: 'status', type: 'string' },
|
|
]);
|
|
|
|
mockExecuteFunctions = {
|
|
getNode: jest.fn().mockReturnValue(node),
|
|
getNodeParameter: jest.fn(),
|
|
helpers: {
|
|
getDataTableProxy: jest.fn().mockResolvedValue(mockDataTableProxy),
|
|
},
|
|
} as unknown as IExecuteFunctions;
|
|
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('Single Column Sort', () => {
|
|
it('should sort by column ascending', async () => {
|
|
// ARRANGE
|
|
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation((param) => {
|
|
if (param === DATA_TABLE_ID_FIELD) return { mode: 'id', value: 'table123' };
|
|
if (param === 'orderBy') return true;
|
|
if (param === 'orderByColumn') return 'name';
|
|
if (param === 'orderByDirection') return 'ASC';
|
|
if (param === 'returnAll') return false;
|
|
if (param === 'limit') return 10;
|
|
if (param === 'filters.conditions') return [];
|
|
if (param === 'matchType') return ANY_CONDITION;
|
|
return undefined;
|
|
});
|
|
|
|
(mockDataTableProxy.getManyRowsAndCount as jest.Mock).mockResolvedValue({
|
|
data: [
|
|
{ id: 1, name: 'Alice' },
|
|
{ id: 2, name: 'Bob' },
|
|
],
|
|
count: 2,
|
|
});
|
|
|
|
// ACT
|
|
await getOperation.execute.call(mockExecuteFunctions, 0);
|
|
|
|
// ASSERT
|
|
expect(mockDataTableProxy.getManyRowsAndCount).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
sortBy: ['name', 'ASC'],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should sort by column descending', async () => {
|
|
// ARRANGE
|
|
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation((param) => {
|
|
if (param === DATA_TABLE_ID_FIELD) return { mode: 'id', value: 'table123' };
|
|
if (param === 'orderBy') return true;
|
|
if (param === 'orderByColumn') return 'age';
|
|
if (param === 'orderByDirection') return 'DESC';
|
|
if (param === 'returnAll') return false;
|
|
if (param === 'limit') return 10;
|
|
if (param === 'filters.conditions') return [];
|
|
if (param === 'matchType') return ANY_CONDITION;
|
|
return undefined;
|
|
});
|
|
|
|
(mockDataTableProxy.getManyRowsAndCount as jest.Mock).mockResolvedValue({
|
|
data: [
|
|
{ id: 2, age: 30 },
|
|
{ id: 1, age: 25 },
|
|
],
|
|
count: 2,
|
|
});
|
|
|
|
// ACT
|
|
await getOperation.execute.call(mockExecuteFunctions, 0);
|
|
|
|
// ASSERT
|
|
expect(mockDataTableProxy.getManyRowsAndCount).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
sortBy: ['age', 'DESC'],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it.each(['createdAt', 'updatedAt'])(
|
|
'should allow sorting by system column %s even though getColumns does not include it',
|
|
async (column: string) => {
|
|
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation((param) => {
|
|
if (param === DATA_TABLE_ID_FIELD) return { mode: 'id', value: 'table123' };
|
|
if (param === 'orderBy') return true;
|
|
if (param === 'orderByColumn') return column;
|
|
if (param === 'orderByDirection') return 'DESC';
|
|
if (param === 'returnAll') return false;
|
|
if (param === 'limit') return 10;
|
|
if (param === 'filters.conditions') return [];
|
|
if (param === 'matchType') return ANY_CONDITION;
|
|
return undefined;
|
|
});
|
|
|
|
(mockDataTableProxy.getManyRowsAndCount as jest.Mock).mockResolvedValue({
|
|
data: [{ id: 1 }],
|
|
count: 1,
|
|
});
|
|
|
|
await getOperation.execute.call(mockExecuteFunctions, 0);
|
|
|
|
expect(mockDataTableProxy.getManyRowsAndCount).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
sortBy: [column, 'DESC'],
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
|
|
it('should sort by id column', async () => {
|
|
// ARRANGE
|
|
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation((param) => {
|
|
if (param === DATA_TABLE_ID_FIELD) return { mode: 'id', value: 'table123' };
|
|
if (param === 'orderBy') return true;
|
|
if (param === 'orderByColumn') return 'id';
|
|
if (param === 'orderByDirection') return 'ASC';
|
|
if (param === 'returnAll') return true;
|
|
if (param === 'filters.conditions') return [];
|
|
if (param === 'matchType') return ANY_CONDITION;
|
|
return undefined;
|
|
});
|
|
|
|
(mockDataTableProxy.getManyRowsAndCount as jest.Mock).mockResolvedValue({
|
|
data: [
|
|
{ id: 1, name: 'Alice' },
|
|
{ id: 2, name: 'Bob' },
|
|
{ id: 3, name: 'Charlie' },
|
|
],
|
|
count: 3,
|
|
});
|
|
|
|
// ACT
|
|
await getOperation.execute.call(mockExecuteFunctions, 0);
|
|
|
|
// ASSERT
|
|
expect(mockDataTableProxy.getManyRowsAndCount).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
sortBy: ['id', 'ASC'],
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('No Sort Rule', () => {
|
|
it('should work without sort rule (orderBy false)', async () => {
|
|
// ARRANGE
|
|
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation((param) => {
|
|
if (param === DATA_TABLE_ID_FIELD) return { mode: 'id', value: 'table123' };
|
|
if (param === 'orderBy') return false;
|
|
if (param === 'returnAll') return false;
|
|
if (param === 'limit') return 10;
|
|
if (param === 'filters.conditions') return [];
|
|
if (param === 'matchType') return ANY_CONDITION;
|
|
return undefined;
|
|
});
|
|
|
|
(mockDataTableProxy.getManyRowsAndCount as jest.Mock).mockResolvedValue({
|
|
data: [{ id: 1 }],
|
|
count: 1,
|
|
});
|
|
|
|
// ACT
|
|
await getOperation.execute.call(mockExecuteFunctions, 0);
|
|
|
|
// ASSERT
|
|
expect(mockDataTableProxy.getManyRowsAndCount).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
sortBy: undefined,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should work with v1.0 (legacy version)', async () => {
|
|
// ARRANGE
|
|
const v10Node = { id: 'test', typeVersion: 1.0 } as INode;
|
|
(mockExecuteFunctions.getNode as jest.Mock).mockReturnValue(v10Node);
|
|
|
|
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation((param) => {
|
|
if (param === DATA_TABLE_ID_FIELD) return { mode: 'id', value: 'table123' };
|
|
if (param === 'orderBy') return false;
|
|
if (param === 'returnAll') return false;
|
|
if (param === 'limit') return 10;
|
|
if (param === 'filters.conditions') return [];
|
|
if (param === 'matchType') return ANY_CONDITION;
|
|
return undefined;
|
|
});
|
|
|
|
(mockDataTableProxy.getManyRowsAndCount as jest.Mock).mockResolvedValue({
|
|
data: [{ id: 1 }],
|
|
count: 1,
|
|
});
|
|
|
|
// ACT
|
|
await getOperation.execute.call(mockExecuteFunctions, 0);
|
|
|
|
// ASSERT
|
|
expect(mockDataTableProxy.getManyRowsAndCount).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
sortBy: undefined,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Sort with Filters', () => {
|
|
it('should combine sort and filters correctly', async () => {
|
|
// ARRANGE
|
|
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation((param) => {
|
|
if (param === DATA_TABLE_ID_FIELD) return { mode: 'id', value: 'table123' };
|
|
if (param === 'orderBy') return true;
|
|
if (param === 'orderByColumn') return 'name';
|
|
if (param === 'orderByDirection') return 'ASC';
|
|
if (param === 'returnAll') return false;
|
|
if (param === 'limit') return 10;
|
|
if (param === 'filters.conditions') {
|
|
return [{ keyName: 'status', condition: 'eq', keyValue: 'active' }];
|
|
}
|
|
if (param === 'matchType') return ANY_CONDITION;
|
|
return undefined;
|
|
});
|
|
|
|
(mockDataTableProxy.getManyRowsAndCount as jest.Mock).mockResolvedValue({
|
|
data: [
|
|
{ id: 1, name: 'Alice', status: 'active' },
|
|
{ id: 2, name: 'Bob', status: 'active' },
|
|
],
|
|
count: 2,
|
|
});
|
|
|
|
// ACT
|
|
await getOperation.execute.call(mockExecuteFunctions, 0);
|
|
|
|
// ASSERT
|
|
expect(mockDataTableProxy.getManyRowsAndCount).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
sortBy: ['name', 'ASC'],
|
|
filter: expect.objectContaining({
|
|
type: 'or',
|
|
filters: [
|
|
{
|
|
columnName: 'status',
|
|
condition: 'eq',
|
|
value: 'active',
|
|
},
|
|
],
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Column Validation', () => {
|
|
it('should throw when orderBy column does not exist in the table', async () => {
|
|
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation((param) => {
|
|
if (param === DATA_TABLE_ID_FIELD) return { mode: 'id', value: 'table123' };
|
|
if (param === 'orderBy') return true;
|
|
if (param === 'orderByColumn') return 'nonExistentColumn';
|
|
if (param === 'returnAll') return false;
|
|
if (param === 'limit') return 10;
|
|
if (param === 'filters.conditions') return [];
|
|
if (param === 'matchType') return ANY_CONDITION;
|
|
return undefined;
|
|
});
|
|
|
|
await expect(getOperation.execute.call(mockExecuteFunctions, 0)).rejects.toThrow(
|
|
NodeOperationError,
|
|
);
|
|
expect(mockDataTableProxy.getManyRowsAndCount).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not throw when orderBy column exists in the table', async () => {
|
|
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation((param) => {
|
|
if (param === DATA_TABLE_ID_FIELD) return { mode: 'id', value: 'table123' };
|
|
if (param === 'orderBy') return true;
|
|
if (param === 'orderByColumn') return 'name';
|
|
if (param === 'orderByDirection') return 'ASC';
|
|
if (param === 'returnAll') return false;
|
|
if (param === 'limit') return 10;
|
|
if (param === 'filters.conditions') return [];
|
|
if (param === 'matchType') return ANY_CONDITION;
|
|
return undefined;
|
|
});
|
|
|
|
(mockDataTableProxy.getManyRowsAndCount as jest.Mock).mockResolvedValue({
|
|
data: [{ id: 1, name: 'Alice' }],
|
|
count: 1,
|
|
});
|
|
|
|
await expect(getOperation.execute.call(mockExecuteFunctions, 0)).resolves.not.toThrow();
|
|
expect(mockDataTableProxy.getManyRowsAndCount).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Sort with Pagination', () => {
|
|
it('should maintain sort order with returnAll=true', async () => {
|
|
// ARRANGE
|
|
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation((param) => {
|
|
if (param === DATA_TABLE_ID_FIELD) return { mode: 'id', value: 'table123' };
|
|
if (param === 'orderBy') return true;
|
|
if (param === 'orderByColumn') return 'id';
|
|
if (param === 'orderByDirection') return 'DESC';
|
|
if (param === 'returnAll') return true;
|
|
if (param === 'filters.conditions') return [];
|
|
if (param === 'matchType') return ANY_CONDITION;
|
|
return undefined;
|
|
});
|
|
|
|
(mockDataTableProxy.getManyRowsAndCount as jest.Mock).mockResolvedValue({
|
|
data: [{ id: 5 }, { id: 4 }, { id: 3 }],
|
|
count: 3,
|
|
});
|
|
|
|
// ACT
|
|
await getOperation.execute.call(mockExecuteFunctions, 0);
|
|
|
|
// ASSERT
|
|
expect(mockDataTableProxy.getManyRowsAndCount).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
sortBy: ['id', 'DESC'],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should maintain sort order with limit', async () => {
|
|
// ARRANGE
|
|
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation((param) => {
|
|
if (param === DATA_TABLE_ID_FIELD) return { mode: 'id', value: 'table123' };
|
|
if (param === 'orderBy') return true;
|
|
if (param === 'orderByColumn') return 'name';
|
|
if (param === 'orderByDirection') return 'ASC';
|
|
if (param === 'returnAll') return false;
|
|
if (param === 'limit') return 5;
|
|
if (param === 'filters.conditions') return [];
|
|
if (param === 'matchType') return ANY_CONDITION;
|
|
return undefined;
|
|
});
|
|
|
|
(mockDataTableProxy.getManyRowsAndCount as jest.Mock).mockResolvedValue({
|
|
data: [
|
|
{ id: 1, name: 'Alice' },
|
|
{ id: 2, name: 'Bob' },
|
|
{ id: 3, name: 'Charlie' },
|
|
{ id: 4, name: 'David' },
|
|
{ id: 5, name: 'Eve' },
|
|
],
|
|
count: 10,
|
|
});
|
|
|
|
// ACT
|
|
await getOperation.execute.call(mockExecuteFunctions, 0);
|
|
|
|
// ASSERT
|
|
expect(mockDataTableProxy.getManyRowsAndCount).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
sortBy: ['name', 'ASC'],
|
|
take: 5,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
});
|