diff --git a/packages/nodes-base/nodes/DataTable/actions/row/get.operation.ts b/packages/nodes-base/nodes/DataTable/actions/row/get.operation.ts
index 633bd84df2e..ff34e0b1bb4 100644
--- a/packages/nodes-base/nodes/DataTable/actions/row/get.operation.ts
+++ b/packages/nodes-base/nodes/DataTable/actions/row/get.operation.ts
@@ -45,6 +45,59 @@ export const description: INodeProperties[] = [
default: ROWS_LIMIT_DEFAULT,
description: 'Max number of results to return',
},
+ {
+ displayName: 'Order By',
+ name: 'orderBy',
+ type: 'boolean',
+ displayOptions,
+ default: false,
+ description: 'Whether to sort the results by a column',
+ },
+ {
+ // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
+ displayName: 'Order By Column',
+ name: 'orderByColumn',
+ type: 'options',
+ // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
+ description:
+ 'Choose from the list, or specify using an expression',
+ typeOptions: {
+ loadOptionsDependsOn: ['dataTableId.value'],
+ loadOptionsMethod: 'getDataTableColumns',
+ },
+ displayOptions: {
+ ...displayOptions,
+ show: {
+ ...displayOptions.show,
+ orderBy: [true],
+ },
+ },
+ default: 'createdAt',
+ },
+ {
+ displayName: 'Order By Direction',
+ name: 'orderByDirection',
+ type: 'options',
+ options: [
+ {
+ name: 'Ascending',
+ value: 'ASC',
+ },
+ {
+ name: 'Descending',
+ value: 'DESC',
+ },
+ ],
+ displayOptions: {
+ ...displayOptions,
+ show: {
+ ...displayOptions.show,
+ orderBy: [true],
+ },
+ },
+ default: 'DESC',
+ description: 'Sort direction for the column',
+ },
];
export async function execute(
@@ -53,5 +106,17 @@ export async function execute(
): Promise {
const dataTableProxy = await getDataTableProxyExecute(this, index);
- return await executeSelectMany(this, index, dataTableProxy);
+ // Extract sort parameters
+ let sortBy: [string, 'ASC' | 'DESC'] | undefined;
+ const orderBy = this.getNodeParameter('orderBy', index, false) as boolean;
+
+ if (orderBy) {
+ const column = this.getNodeParameter('orderByColumn', index, '') as string;
+ const direction = this.getNodeParameter('orderByDirection', index, 'ASC') as 'ASC' | 'DESC';
+ if (column) {
+ sortBy = [column, direction];
+ }
+ }
+
+ return await executeSelectMany(this, index, dataTableProxy, false, undefined, sortBy);
}
diff --git a/packages/nodes-base/nodes/DataTable/common/selectMany.ts b/packages/nodes-base/nodes/DataTable/common/selectMany.ts
index 78a92a151ad..f88e89ec27d 100644
--- a/packages/nodes-base/nodes/DataTable/common/selectMany.ts
+++ b/packages/nodes-base/nodes/DataTable/common/selectMany.ts
@@ -180,6 +180,7 @@ export async function executeSelectMany(
dataTableProxy: IDataTableProjectService,
rejectEmpty = false,
limit?: number,
+ sortBy?: [string, 'ASC' | 'DESC'],
): Promise> {
const filter = await getSelectFilter(ctx, index);
@@ -205,6 +206,7 @@ export async function executeSelectMany(
skip,
take: limit ? Math.min(take, limit - result.length) : take,
filter,
+ sortBy,
});
const wrapped = data.map((json) => ({
json: shouldConvertDates ? convertDatesToIsoStrings(json) : json,
diff --git a/packages/nodes-base/nodes/DataTable/test/common/selectMany.test.ts b/packages/nodes-base/nodes/DataTable/test/common/selectMany.test.ts
index 74f2e0ddaaa..ce45e2a56fa 100644
--- a/packages/nodes-base/nodes/DataTable/test/common/selectMany.test.ts
+++ b/packages/nodes-base/nodes/DataTable/test/common/selectMany.test.ts
@@ -437,6 +437,145 @@ describe('selectMany utils', () => {
]);
});
});
+
+ describe('sorting', () => {
+ it('should pass sortBy parameter to getManyRowsAndCount with ASC direction', async () => {
+ // ARRANGE
+ filters = [];
+ const sortBy: [string, 'ASC' | 'DESC'] = ['name', 'ASC'];
+ getManyRowsAndCount.mockReturnValue({
+ data: [
+ { id: 1, name: 'Alice' },
+ { id: 2, name: 'Bob' },
+ ],
+ count: 2,
+ });
+
+ // ACT
+ await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy, false, undefined, sortBy);
+
+ // ASSERT
+ expect(getManyRowsAndCount).toHaveBeenCalledWith(
+ expect.objectContaining({
+ sortBy: ['name', 'ASC'],
+ }),
+ );
+ });
+
+ it('should pass sortBy parameter to getManyRowsAndCount with DESC direction', async () => {
+ // ARRANGE
+ filters = [];
+ const sortBy: [string, 'ASC' | 'DESC'] = ['id', 'DESC'];
+ getManyRowsAndCount.mockReturnValue({
+ data: [
+ { id: 3, name: 'Charlie' },
+ { id: 2, name: 'Bob' },
+ { id: 1, name: 'Alice' },
+ ],
+ count: 3,
+ });
+
+ // ACT
+ await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy, false, undefined, sortBy);
+
+ // ASSERT
+ expect(getManyRowsAndCount).toHaveBeenCalledWith(
+ expect.objectContaining({
+ sortBy: ['id', 'DESC'],
+ }),
+ );
+ });
+
+ it('should work with undefined sortBy', async () => {
+ // ARRANGE
+ filters = [];
+ getManyRowsAndCount.mockReturnValue({
+ data: [{ id: 1, name: 'Alice' }],
+ count: 1,
+ });
+
+ // ACT
+ await executeSelectMany(
+ mockExecuteFunctions,
+ 0,
+ dataTableProxy,
+ false,
+ undefined,
+ undefined,
+ );
+
+ // ASSERT
+ expect(getManyRowsAndCount).toHaveBeenCalledWith(
+ expect.objectContaining({
+ sortBy: undefined,
+ }),
+ );
+ });
+
+ it('should combine sortBy with filter conditions', async () => {
+ // ARRANGE
+ filters = [{ condition: 'eq', keyName: 'status', keyValue: 'active' }];
+ const sortBy: [string, 'ASC' | 'DESC'] = ['name', 'ASC'];
+ getManyRowsAndCount.mockReturnValue({
+ data: [
+ { id: 1, name: 'Alice', status: 'active' },
+ { id: 2, name: 'Bob', status: 'active' },
+ ],
+ count: 2,
+ });
+
+ // ACT
+ await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy, false, undefined, sortBy);
+
+ // ASSERT
+ expect(getManyRowsAndCount).toHaveBeenCalledWith(
+ expect.objectContaining({
+ sortBy: ['name', 'ASC'],
+ filter: expect.objectContaining({
+ type: 'or',
+ filters: [
+ {
+ columnName: 'status',
+ condition: 'eq',
+ value: 'active',
+ },
+ ],
+ }),
+ }),
+ );
+ });
+
+ it('should maintain sortBy across paginated requests', async () => {
+ // ARRANGE
+ filters = [];
+ const sortBy: [string, 'ASC' | 'DESC'] = ['id', 'ASC'];
+ getManyRowsAndCount.mockReturnValueOnce({
+ data: Array.from({ length: 1000 }, (_, k) => ({ id: k })),
+ count: 1500,
+ });
+ getManyRowsAndCount.mockReturnValueOnce({
+ data: Array.from({ length: 500 }, (_, k) => ({ id: k + 1000 })),
+ count: 1500,
+ });
+
+ // ACT
+ await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy, false, undefined, sortBy);
+
+ // ASSERT
+ expect(getManyRowsAndCount).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({
+ sortBy: ['id', 'ASC'],
+ }),
+ );
+ expect(getManyRowsAndCount).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ sortBy: ['id', 'ASC'],
+ }),
+ );
+ });
+ });
});
describe('getSelectFilter', () => {
diff --git a/packages/nodes-base/nodes/DataTable/test/row/get.operation.test.ts b/packages/nodes-base/nodes/DataTable/test/row/get.operation.test.ts
new file mode 100644
index 00000000000..1ee66a77035
--- /dev/null
+++ b/packages/nodes-base/nodes/DataTable/test/row/get.operation.test.ts
@@ -0,0 +1,315 @@
+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('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('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,
+ }),
+ );
+ });
+ });
+});