first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,190 @@
import type { IExecuteFunctions, INode } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { mock } from 'jest-mock-extended';
import { resolveDataTableId } from '../../common/utils';
const mockNode: INode = {
id: 'test-node',
name: 'Test Node',
type: 'n8n-nodes-base.dataTable',
typeVersion: 1,
position: [0, 0],
parameters: {},
};
describe('resolveDataTableId', () => {
describe('list mode', () => {
it('should return the value directly when mode is list', async () => {
const ctx = mock<IExecuteFunctions>();
ctx.getNode.mockReturnValue(mockNode);
const resourceLocator = {
mode: 'list' as const,
value: 'table-id-123',
};
const result = await resolveDataTableId(ctx, resourceLocator);
expect(result).toBe('table-id-123');
});
it('should handle UUIDs in list mode', async () => {
const ctx = mock<IExecuteFunctions>();
ctx.getNode.mockReturnValue(mockNode);
const resourceLocator = {
mode: 'list' as const,
value: '550e8400-e29b-41d4-a716-446655440000',
};
const result = await resolveDataTableId(ctx, resourceLocator);
expect(result).toBe('550e8400-e29b-41d4-a716-446655440000');
});
});
describe('id mode', () => {
it('should return the value directly when mode is id', async () => {
const ctx = mock<IExecuteFunctions>();
ctx.getNode.mockReturnValue(mockNode);
const resourceLocator = {
mode: 'id' as const,
value: 'custom-table-id',
};
const result = await resolveDataTableId(ctx, resourceLocator);
expect(result).toBe('custom-table-id');
});
it('should handle numeric IDs in id mode', async () => {
const ctx = mock<IExecuteFunctions>();
ctx.getNode.mockReturnValue(mockNode);
const resourceLocator = {
mode: 'id' as const,
value: '12345',
};
const result = await resolveDataTableId(ctx, resourceLocator);
expect(result).toBe('12345');
});
});
describe('name mode', () => {
it('should look up table by name and return its ID', async () => {
const ctx = mock<IExecuteFunctions>();
ctx.getNode.mockReturnValue(mockNode);
const mockAggregateProxy = {
getManyAndCount: jest.fn().mockResolvedValue({
data: [{ id: 'resolved-table-id', name: 'my table' }],
count: 1,
}),
};
ctx.helpers = {
getDataTableAggregateProxy: jest.fn().mockResolvedValue(mockAggregateProxy),
} as any;
const resourceLocator = {
mode: 'name' as const,
value: 'My Table',
};
const result = await resolveDataTableId(ctx, resourceLocator);
expect(result).toBe('resolved-table-id');
expect(mockAggregateProxy.getManyAndCount).toHaveBeenCalledWith({
filter: { name: 'my table' },
take: 1,
});
});
it('should convert table name to lowercase for lookup', async () => {
const ctx = mock<IExecuteFunctions>();
ctx.getNode.mockReturnValue(mockNode);
const mockAggregateProxy = {
getManyAndCount: jest.fn().mockResolvedValue({
data: [{ id: 'table-id', name: 'customers' }],
count: 1,
}),
};
ctx.helpers = {
getDataTableAggregateProxy: jest.fn().mockResolvedValue(mockAggregateProxy),
} as any;
const resourceLocator = {
mode: 'name' as const,
value: 'CUSTOMERS',
};
await resolveDataTableId(ctx, resourceLocator);
expect(mockAggregateProxy.getManyAndCount).toHaveBeenCalledWith({
filter: { name: 'customers' },
take: 1,
});
});
it('should throw error when table name is not found', async () => {
const ctx = mock<IExecuteFunctions>();
ctx.getNode.mockReturnValue(mockNode);
const mockAggregateProxy = {
getManyAndCount: jest.fn().mockResolvedValue({
data: [],
count: 0,
}),
};
ctx.helpers = {
getDataTableAggregateProxy: jest.fn().mockResolvedValue(mockAggregateProxy),
} as any;
const resourceLocator = {
mode: 'name' as const,
value: 'NonExistentTable',
};
await expect(resolveDataTableId(ctx, resourceLocator)).rejects.toThrow(NodeOperationError);
await expect(resolveDataTableId(ctx, resourceLocator)).rejects.toThrow(
'Data table with name "NonExistentTable" not found',
);
});
it('should handle special characters in table names', async () => {
const ctx = mock<IExecuteFunctions>();
ctx.getNode.mockReturnValue(mockNode);
const mockAggregateProxy = {
getManyAndCount: jest.fn().mockResolvedValue({
data: [{ id: 'table-id', name: 'users & customers' }],
count: 1,
}),
};
ctx.helpers = {
getDataTableAggregateProxy: jest.fn().mockResolvedValue(mockAggregateProxy),
} as any;
const resourceLocator = {
mode: 'name' as const,
value: 'Users & Customers',
};
const result = await resolveDataTableId(ctx, resourceLocator);
expect(result).toBe('table-id');
expect(mockAggregateProxy.getManyAndCount).toHaveBeenCalledWith({
filter: { name: 'users & customers' },
take: 1,
});
});
});
});
@@ -0,0 +1,660 @@
import {
type INode,
NodeOperationError,
type IDataTableProjectService,
type IExecuteFunctions,
} from 'n8n-workflow';
import type { FieldEntry } from '../../common/constants';
import { ANY_CONDITION, ALL_CONDITIONS } from '../../common/constants';
import { DATA_TABLE_ID_FIELD } from '../../common/fields';
import { executeSelectMany, getSelectFilter } from '../../common/selectMany';
describe('selectMany utils', () => {
let mockExecuteFunctions: IExecuteFunctions;
const getManyRowsAndCount = jest.fn();
const dataTableProxy = jest.mocked<IDataTableProjectService>({
getManyRowsAndCount,
} as unknown as IDataTableProjectService);
const dataTableId = 2345;
let filters: FieldEntry[];
const node = { id: 1 } as unknown as INode;
beforeEach(() => {
filters = [
{
condition: 'eq',
keyName: 'id',
keyValue: 1,
},
];
const mockDataTableProxy = {
getColumns: jest.fn().mockResolvedValue([
{ name: 'name', type: 'string' },
{ name: 'age', type: 'number' },
{ name: 'status', type: 'string' },
]),
};
mockExecuteFunctions = {
getNode: jest.fn().mockReturnValue(node),
getNodeParameter: jest.fn().mockImplementation((field) => {
switch (field) {
case DATA_TABLE_ID_FIELD:
return dataTableId;
case 'filters.conditions':
return filters;
case 'matchType':
return ANY_CONDITION;
}
}),
helpers: {
getDataTableProxy: jest.fn().mockResolvedValue(mockDataTableProxy),
},
} as unknown as IExecuteFunctions;
jest.clearAllMocks();
});
describe('executeSelectMany', () => {
it('should get a few rows', async () => {
// ARRANGE
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1 }], count: 1 });
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
expect(result).toEqual([{ json: { id: 1 } }]);
});
it('should get a paginated amount of rows', async () => {
// ARRANGE
getManyRowsAndCount.mockReturnValueOnce({
data: Array.from({ length: 1000 }, (_, k) => ({ id: k })),
count: 2345,
});
getManyRowsAndCount.mockReturnValueOnce({
data: Array.from({ length: 1000 }, (_, k) => ({ id: k + 1000 })),
count: 2345,
});
getManyRowsAndCount.mockReturnValueOnce({
data: Array.from({ length: 345 }, (_, k) => ({ id: k + 2000 })),
count: 2345,
});
filters = [];
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
expect(result.length).toBe(2345);
expect(result[0]).toEqual({ json: { id: 0 } });
expect(result[2344]).toEqual({ json: { id: 2344 } });
});
it('should pass null through correctly', async () => {
// ARRANGE
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1, colA: null }], count: 1 });
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
expect(result).toEqual([{ json: { id: 1, colA: null } }]);
});
it('should panic if pagination gets out of sync', async () => {
// ARRANGE
getManyRowsAndCount.mockReturnValueOnce({
data: Array.from({ length: 1000 }, (_, k) => ({ id: k })),
count: 2345,
});
getManyRowsAndCount.mockReturnValueOnce({
data: Array.from({ length: 1000 }, (_, k) => ({ id: k + 1000 })),
count: 2344,
});
filters = [];
// ACT ASSERT
await expect(executeSelectMany(mockExecuteFunctions, 0, dataTableProxy)).rejects.toEqual(
new NodeOperationError(
node,
'synchronization error: result count changed during pagination',
),
);
});
describe('filter conditions', () => {
it('should handle "eq" condition', async () => {
// ARRANGE
filters = [{ condition: 'eq', keyName: 'name', keyValue: 'John' }];
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1, name: 'John' }], count: 1 });
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
expect(result).toEqual([{ json: { id: 1, name: 'John' } }]);
});
it('should handle "neq" condition', async () => {
// ARRANGE
filters = [{ condition: 'neq', keyName: 'name', keyValue: 'John' }];
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1, name: 'Jane' }], count: 1 });
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
expect(result).toEqual([{ json: { id: 1, name: 'Jane' } }]);
});
it('should handle "gt" condition with numbers', async () => {
// ARRANGE
filters = [{ condition: 'gt', keyName: 'age', keyValue: 25 }];
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1, age: 30 }], count: 1 });
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
expect(result).toEqual([{ json: { id: 1, age: 30 } }]);
});
it('should handle "gte" condition with numbers', async () => {
// ARRANGE
filters = [{ condition: 'gte', keyName: 'age', keyValue: 25 }];
getManyRowsAndCount.mockReturnValue({
data: [
{ id: 1, age: 25 },
{ id: 2, age: 30 },
],
count: 2,
});
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
expect(result).toEqual([{ json: { id: 1, age: 25 } }, { json: { id: 2, age: 30 } }]);
});
it('should handle "lt" condition with numbers', async () => {
// ARRANGE
filters = [{ condition: 'lt', keyName: 'age', keyValue: 30 }];
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1, age: 25 }], count: 1 });
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
expect(result).toEqual([{ json: { id: 1, age: 25 } }]);
});
it('should handle "lte" condition with numbers', async () => {
// ARRANGE
filters = [{ condition: 'lte', keyName: 'age', keyValue: 30 }];
getManyRowsAndCount.mockReturnValue({
data: [
{ id: 1, age: 25 },
{ id: 2, age: 30 },
],
count: 2,
});
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
expect(result).toEqual([{ json: { id: 1, age: 25 } }, { json: { id: 2, age: 30 } }]);
});
it('should handle "like" condition with pattern matching', async () => {
// ARRANGE
filters = [{ condition: 'like', keyName: 'name', keyValue: '%Mar%' }];
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1, name: 'Anne-Marie' }], count: 1 });
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
expect(result).toEqual([{ json: { id: 1, name: 'Anne-Marie' } }]);
});
it('should handle "ilike" condition with case-insensitive pattern matching', async () => {
// ARRANGE
filters = [{ condition: 'ilike', keyName: 'name', keyValue: '%mar%' }];
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1, name: 'Anne-Marie' }], count: 1 });
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
expect(result).toEqual([{ json: { id: 1, name: 'Anne-Marie' } }]);
});
it('should handle multiple conditions with ANY_CONDITION (OR logic - matches records satisfying either condition)', async () => {
// ARRANGE
filters = [
{ condition: 'eq', keyName: 'status', keyValue: 'active' },
{ condition: 'gt', keyName: 'age', keyValue: 50 },
];
getManyRowsAndCount.mockReturnValue({
data: [{ id: 1, status: 'active', age: 25 }],
count: 1,
});
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
expect(result).toEqual([{ json: { id: 1, status: 'active', age: 25 } }]);
});
it('should handle multiple conditions with ALL_CONDITIONS (AND logic - matches records satisfying all conditions)', async () => {
// ARRANGE
filters = [
{ condition: 'eq', keyName: 'status', keyValue: 'active' },
{ condition: 'gte', keyName: 'age', keyValue: 21 },
];
mockExecuteFunctions.getNodeParameter = jest.fn().mockImplementation((field) => {
switch (field) {
case DATA_TABLE_ID_FIELD:
return dataTableId;
case 'filters.conditions':
return filters;
case 'matchType':
return ALL_CONDITIONS;
}
});
getManyRowsAndCount.mockReturnValue({
data: [{ id: 1, status: 'active', age: 25 }],
count: 1,
});
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
expect(result).toEqual([{ json: { id: 1, status: 'active', age: 25 } }]);
});
it('should handle ALL_CONDITIONS excluding records that match only one condition (proves AND logic)', async () => {
// ARRANGE
filters = [
{ condition: 'eq', keyName: 'status', keyValue: 'inactive' },
{ condition: 'gte', keyName: 'age', keyValue: 21 },
];
mockExecuteFunctions.getNodeParameter = jest.fn().mockImplementation((field) => {
switch (field) {
case DATA_TABLE_ID_FIELD:
return dataTableId;
case 'filters.conditions':
return filters;
case 'matchType':
return ALL_CONDITIONS;
}
});
getManyRowsAndCount.mockReturnValue({
data: [],
count: 0,
});
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
expect(result).toEqual([]);
});
it('should handle ANY_CONDITION including records that match only one condition (proves OR logic)', async () => {
// ARRANGE
filters = [
{ condition: 'eq', keyName: 'status', keyValue: 'inactive' },
{ condition: 'gte', keyName: 'age', keyValue: 21 },
];
mockExecuteFunctions.getNodeParameter = jest.fn().mockImplementation((field) => {
switch (field) {
case DATA_TABLE_ID_FIELD:
return dataTableId;
case 'filters.conditions':
return filters;
case 'matchType':
return ANY_CONDITION;
}
});
getManyRowsAndCount.mockReturnValue({
data: [{ id: 1, status: 'active', age: 25 }],
count: 1,
});
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
expect(result).toEqual([{ json: { id: 1, status: 'active', age: 25 } }]);
});
it('should convert Date objects to ISO strings in output (v1.1+)', async () => {
// ARRANGE
const testDate = new Date('2025-12-11T10:30:59.000Z');
const testUpdatedDate = new Date('2025-12-12T11:16:53.385Z');
filters = [];
mockExecuteFunctions.getNodeParameter = jest.fn().mockImplementation((field) => {
switch (field) {
case DATA_TABLE_ID_FIELD:
return dataTableId;
case 'filters.conditions':
return filters;
case 'matchType':
return ANY_CONDITION;
case 'returnAll':
return true;
}
});
mockExecuteFunctions.getNode = jest.fn().mockReturnValue({ ...node, typeVersion: 1.1 });
getManyRowsAndCount.mockReturnValue({
data: [
{
id: 1,
completedDate: testDate,
createdAt: testDate,
updatedAt: testUpdatedDate,
status: 'active',
},
],
count: 1,
});
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
// Dates should be converted to ISO strings, not Date objects
expect(result).toEqual([
{
json: {
id: 1,
completedDate: '2025-12-11T10:30:59.000Z',
createdAt: '2025-12-11T10:30:59.000Z',
updatedAt: '2025-12-12T11:16:53.385Z',
status: 'active',
},
},
]);
});
it('should keep Date objects in output (v1.0 - legacy behavior)', async () => {
// ARRANGE
const testDate = new Date('2025-12-11T10:30:59.000Z');
const testUpdatedDate = new Date('2025-12-12T11:16:53.385Z');
filters = [];
mockExecuteFunctions.getNodeParameter = jest.fn().mockImplementation((field) => {
switch (field) {
case DATA_TABLE_ID_FIELD:
return dataTableId;
case 'filters.conditions':
return filters;
case 'matchType':
return ANY_CONDITION;
case 'returnAll':
return true;
}
});
mockExecuteFunctions.getNode = jest.fn().mockReturnValue({ ...node, typeVersion: 1 });
getManyRowsAndCount.mockReturnValue({
data: [
{
id: 1,
completedDate: testDate,
createdAt: testDate,
updatedAt: testUpdatedDate,
status: 'active',
},
],
count: 1,
});
// ACT
const result = await executeSelectMany(mockExecuteFunctions, 0, dataTableProxy);
// ASSERT
expect(result).toEqual([
{
json: {
id: 1,
completedDate: testDate,
createdAt: testDate,
updatedAt: testUpdatedDate,
status: 'active',
},
},
]);
});
});
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', () => {
it('should validate filter conditions against table schema', async () => {
// ARRANGE
filters = [
{ condition: 'eq', keyName: 'name', keyValue: 'John' }, // Valid column
{ condition: 'eq', keyName: 'invalid_column', keyValue: 'test' }, // Invalid column
];
// ACT & ASSERT
await expect(getSelectFilter(mockExecuteFunctions, 0)).rejects.toEqual(
new NodeOperationError(
node,
'Filter validation failed: Column(s) "invalid_column" do not exist in the selected table. ' +
'This often happens when switching between tables with different schemas. ' +
'Please update your filter conditions.',
),
);
});
it('should allow system columns in filter conditions', async () => {
// ARRANGE
filters = [
{ condition: 'eq', keyName: 'id', keyValue: 1 }, // System column
{ condition: 'neq', keyName: 'createdAt', keyValue: null }, // System column
];
// ACT
const result = await getSelectFilter(mockExecuteFunctions, 0);
// ASSERT
expect(result).toBeDefined();
expect(result.filters).toHaveLength(2);
});
it('should allow combination of system and custom columns', async () => {
// ARRANGE
filters = [
{ condition: 'eq', keyName: 'id', keyValue: 1 }, // System column
{ condition: 'eq', keyName: 'name', keyValue: 'John' }, // Custom column
];
// ACT
const result = await getSelectFilter(mockExecuteFunctions, 0);
// ASSERT
expect(result).toBeDefined();
expect(result.filters).toHaveLength(2);
});
it('should pass validation when no filters are provided', async () => {
// ARRANGE
filters = [];
// ACT
const result = await getSelectFilter(mockExecuteFunctions, 0);
// ASSERT
expect(result).toBeDefined();
expect(result.filters).toHaveLength(0);
});
it('should report multiple invalid columns in error message', async () => {
// ARRANGE
filters = [
{ condition: 'eq', keyName: 'invalid1', keyValue: 'test1' },
{ condition: 'eq', keyName: 'invalid2', keyValue: 'test2' },
];
// ACT & ASSERT
await expect(getSelectFilter(mockExecuteFunctions, 0)).rejects.toEqual(
new NodeOperationError(
node,
'Filter validation failed: Column(s) "invalid1, invalid2" do not exist in the selected table. ' +
'This often happens when switching between tables with different schemas. ' +
'Please update your filter conditions.',
),
);
});
});
});
@@ -0,0 +1,464 @@
import { DateTime } from 'luxon';
import type { INode } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { ANY_CONDITION, ALL_CONDITIONS, type FieldEntry } from '../../common/constants';
import { dataObjectToApiInput, buildGetManyFilter } from '../../common/utils';
const mockNode: INode = {
id: 'test-node',
name: 'Test Node',
type: 'test',
typeVersion: 1,
position: [0, 0],
parameters: {},
};
describe('dataObjectToApiInput', () => {
describe('primitive types', () => {
it('should handle string values', () => {
const input = { name: 'John', email: 'john@example.com' };
const result = dataObjectToApiInput(input, mockNode, 0);
expect(result).toEqual({
name: 'John',
email: 'john@example.com',
});
});
it('should handle number values', () => {
const input = { age: 25, price: 99.99, count: 0 };
const result = dataObjectToApiInput(input, mockNode, 0);
expect(result).toEqual({
age: 25,
price: 99.99,
count: 0,
});
});
it('should handle boolean values', () => {
const input = { isActive: true, isDeleted: false };
const result = dataObjectToApiInput(input, mockNode, 0);
expect(result).toEqual({
isActive: true,
isDeleted: false,
});
});
});
describe('null and undefined values', () => {
it('should convert null values to null', () => {
const input = { field1: null, field2: 'value' };
const result = dataObjectToApiInput(input, mockNode, 0);
expect(result).toEqual({
field1: null,
field2: 'value',
});
});
it('should convert undefined values to null', () => {
const input = { field1: undefined, field2: 'value' };
const result = dataObjectToApiInput(input, mockNode, 0);
expect(result).toEqual({
field1: null,
field2: 'value',
});
});
});
describe('Date objects', () => {
it('should handle JavaScript Date objects', () => {
const testDate = new Date('2025-09-01T12:00:00.000Z');
const input = { createdAt: testDate, name: 'test' };
const result = dataObjectToApiInput(input, mockNode, 0);
expect(result).toEqual({
createdAt: testDate,
name: 'test',
});
});
});
describe('Luxon DateTime objects', () => {
it('should convert Luxon DateTime objects to JavaScript Date', () => {
const luxonDateTime = DateTime.fromISO('2025-09-01T12:00:00.000Z');
const input = { createdAt: luxonDateTime, name: 'test' };
const result = dataObjectToApiInput(input, mockNode, 0);
expect(result.name).toBe('test');
expect(result.createdAt).toBeInstanceOf(Date);
expect((result.createdAt as Date).toISOString()).toBe('2025-09-01T12:00:00.000Z');
});
});
describe('date-like objects', () => {
it('should convert objects with toISOString method to Date', () => {
const dateLikeObject = {
toISOString: () => '2025-09-01T12:00:00.000Z',
};
const input = { createdAt: dateLikeObject, name: 'test' };
const result = dataObjectToApiInput(input, mockNode, 0);
expect(result.name).toBe('test');
expect(result.createdAt).toBeInstanceOf(Date);
expect((result.createdAt as Date).toISOString()).toBe('2025-09-01T12:00:00.000Z');
});
it('should handle date-like objects where toISOString throws', () => {
const dateLikeObject = {
toISOString: () => {
throw new Error('toISOString failed');
},
};
const input = { createdAt: dateLikeObject, name: 'test' };
expect(() => dataObjectToApiInput(input, mockNode, 0)).toThrow(NodeOperationError);
expect(() => dataObjectToApiInput(input, mockNode, 0)).toThrow('unexpected object input');
});
});
describe('error cases', () => {
it('should throw error for array inputs', () => {
const input = { items: ['item1', 'item2'] };
expect(() => dataObjectToApiInput(input, mockNode, 0)).toThrow(NodeOperationError);
expect(() => dataObjectToApiInput(input, mockNode, 0)).toThrow(
'unexpected array input \'["item1","item2"]\' in row 0',
);
});
it('should throw error for plain objects', () => {
const input = { metadata: { key: 'value' } };
expect(() => dataObjectToApiInput(input, mockNode, 0)).toThrow(NodeOperationError);
expect(() => dataObjectToApiInput(input, mockNode, 0)).toThrow(
'unexpected object input \'{"key":"value"}\' in row 0',
);
});
it('should throw error for objects without toISOString method', () => {
const input = { config: { setting1: true, setting2: 'value' } };
expect(() => dataObjectToApiInput(input, mockNode, 0)).toThrow(NodeOperationError);
expect(() => dataObjectToApiInput(input, mockNode, 0)).toThrow('unexpected object input');
});
test('dataObjectToApiInput throws on invalid date-like object', () => {
const dateLikeObject = {
toISOString: () => 'not-a-date',
};
const input = { createdAt: dateLikeObject, name: 'test' };
expect(() => dataObjectToApiInput(input, mockNode, 0)).toThrow(NodeOperationError);
expect(() => dataObjectToApiInput(input, mockNode, 0)).toThrow(
"unexpected object input '{}' in row 0",
);
});
it('should include correct row number in error message', () => {
const input = { items: ['item1'] };
expect(() => dataObjectToApiInput(input, mockNode, 5)).toThrow(
'unexpected array input \'["item1"]\' in row 5',
);
});
});
describe('mixed data types', () => {
it('should handle mixed valid data types', () => {
const testDate = new Date('2025-09-01T12:00:00.000Z');
const luxonDateTime = DateTime.fromISO('2025-09-02T10:30:00.000Z');
const dateLikeObject = {
toISOString: () => '2025-09-03T08:15:00.000Z',
};
const input = {
name: 'John Doe',
age: 30,
isActive: true,
createdAt: testDate,
updatedAt: luxonDateTime,
scheduledAt: dateLikeObject,
deletedAt: null,
description: undefined,
};
const result = dataObjectToApiInput(input, mockNode, 0);
expect(result.name).toBe('John Doe');
expect(result.age).toBe(30);
expect(result.isActive).toBe(true);
expect(result.createdAt).toBe(testDate);
expect(result.updatedAt).toBeInstanceOf(Date);
expect((result.updatedAt as Date).toISOString()).toBe('2025-09-02T10:30:00.000Z');
expect(result.scheduledAt).toBeInstanceOf(Date);
expect((result.scheduledAt as Date).toISOString()).toBe('2025-09-03T08:15:00.000Z');
expect(result.deletedAt).toBe(null);
expect(result.description).toBe(null);
});
});
});
describe('buildGetManyFilter', () => {
describe('isEmpty/isNotEmpty translation', () => {
it('should translate isEmpty to eq with null value', () => {
const fieldEntries = [
{ keyName: 'name', condition: 'isEmpty' as const, keyValue: 'ignored' },
];
const result = buildGetManyFilter(fieldEntries, ALL_CONDITIONS, { name: 'string' }, mockNode);
expect(result).toEqual({
type: 'and',
filters: [
{
columnName: 'name',
condition: 'eq',
value: null,
},
],
});
});
it('should translate isNotEmpty to neq with null value', () => {
const fieldEntries = [
{ keyName: 'email', condition: 'isNotEmpty' as const, keyValue: 'ignored' },
];
const result = buildGetManyFilter(fieldEntries, ANY_CONDITION, { email: 'string' }, mockNode);
expect(result).toEqual({
type: 'or',
filters: [
{
columnName: 'email',
condition: 'neq',
value: null,
},
],
});
});
it('should handle mixed conditions including isEmpty/isNotEmpty', () => {
const fieldEntries = [
{ keyName: 'name', condition: 'eq' as const, keyValue: 'John' },
{ keyName: 'email', condition: 'isEmpty' as const, keyValue: 'ignored' },
{ keyName: 'phone', condition: 'isNotEmpty' as const, keyValue: 'ignored' },
];
const result = buildGetManyFilter(
fieldEntries,
ALL_CONDITIONS,
{
name: 'string',
email: 'string',
phone: 'string',
},
mockNode,
);
expect(result).toEqual({
type: 'and',
filters: [
{
columnName: 'name',
condition: 'eq',
value: 'John',
},
{
columnName: 'email',
condition: 'eq',
value: null,
},
{
columnName: 'phone',
condition: 'neq',
value: null,
},
],
});
});
});
describe('isTrue/isFalse translation', () => {
it('should translate isTrue to eq with true value', () => {
const fieldEntries = [
{ keyName: 'isActive', condition: 'isTrue' as const, keyValue: 'ignored' },
];
const result = buildGetManyFilter(
fieldEntries,
ALL_CONDITIONS,
{ isActive: 'boolean' },
mockNode,
);
expect(result).toEqual({
type: 'and',
filters: [
{
columnName: 'isActive',
condition: 'eq',
value: true,
},
],
});
});
it('should translate isFalse to eq with false value', () => {
const fieldEntries = [
{ keyName: 'email', condition: 'isFalse' as const, keyValue: 'ignored' },
];
const result = buildGetManyFilter(
fieldEntries,
ANY_CONDITION,
{ email: 'boolean' },
mockNode,
);
expect(result).toEqual({
type: 'or',
filters: [
{
columnName: 'email',
condition: 'eq',
value: false,
},
],
});
});
it('should handle mixed conditions including isTrue/isFalse', () => {
const fieldEntries = [
{ keyName: 'name', condition: 'eq' as const, keyValue: 'John' },
{ keyName: 'isActive', condition: 'isTrue' as const, keyValue: 'ignored' },
{ keyName: 'isDeleted', condition: 'isFalse' as const, keyValue: 'ignored' },
];
const result = buildGetManyFilter(
fieldEntries,
ALL_CONDITIONS,
{
name: 'string',
isActive: 'boolean',
isDeleted: 'boolean',
},
mockNode,
);
expect(result).toEqual({
type: 'and',
filters: [
{
columnName: 'name',
condition: 'eq',
value: 'John',
},
{
columnName: 'isActive',
condition: 'eq',
value: true,
},
{
columnName: 'isDeleted',
condition: 'eq',
value: false,
},
],
});
});
});
it('should handle other conditions', () => {
const fieldEntries = [
{ keyName: 'age', condition: 'gt' as const, keyValue: 18 },
{ keyName: 'name', condition: 'like' as const, keyValue: '%john%' },
];
const result = buildGetManyFilter(
fieldEntries,
ANY_CONDITION,
{
age: 'number',
name: 'string',
},
mockNode,
);
expect(result).toEqual({
type: 'or',
filters: [
{
columnName: 'age',
condition: 'gt',
value: 18,
},
{
columnName: 'name',
condition: 'like',
value: '%john%',
},
],
});
});
describe('date handling in filters', () => {
it('should pass Date objects through unchanged', () => {
const testDate = new Date('2025-10-06T08:14:42.274Z');
const fieldEntries: FieldEntry[] = [
{ keyName: 'createdAt', condition: 'lte', keyValue: testDate },
];
const result = buildGetManyFilter(
fieldEntries,
ALL_CONDITIONS,
{ createdAt: 'date' },
mockNode,
);
expect(result).toEqual({
type: 'and',
filters: [
{
columnName: 'createdAt',
condition: 'lte',
value: testDate,
},
],
});
});
it('should convert ISO date strings to Date objects', () => {
const dateString = '2025-10-06T08:14:42.274Z';
const fieldEntries: FieldEntry[] = [
{ keyName: 'createdAt', condition: 'lte', keyValue: dateString },
];
const result = buildGetManyFilter(
fieldEntries,
ALL_CONDITIONS,
{ createdAt: 'date' },
mockNode,
);
expect(result.filters[0].value).toBeInstanceOf(Date);
expect((result.filters[0].value as Date).toISOString()).toBe(dateString);
});
it('should throw an Error for invalid date strings', () => {
const invalidDateString = 'invalid-date';
const fieldEntries: FieldEntry[] = [
{ keyName: 'createdAt', condition: 'lte', keyValue: invalidDateString },
];
expect(() =>
buildGetManyFilter(fieldEntries, ALL_CONDITIONS, { createdAt: 'date' }, mockNode),
).toThrowError(`Invalid date string '${invalidDateString}' for column 'createdAt'`);
});
});
});
@@ -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,
}),
);
});
});
});
@@ -0,0 +1,566 @@
import { mock } from 'jest-mock-extended';
import type {
IExecuteFunctions,
IDataTableProjectAggregateService,
IDataTableProjectService,
INode,
} from 'n8n-workflow';
import * as createOperation from '../../actions/table/create.operation';
import * as deleteOperation from '../../actions/table/delete.operation';
import * as listOperation from '../../actions/table/list.operation';
import * as updateOperation from '../../actions/table/update.operation';
const mockNode: INode = {
id: 'test-node',
name: 'Test Node',
type: 'n8n-nodes-base.dataTable',
typeVersion: 1,
position: [0, 0],
parameters: {},
};
describe('Table Operations', () => {
describe('Create Operation', () => {
it('should create a new data table with columns', async () => {
const mockExecuteFunctions = mock<IExecuteFunctions>();
const mockAggregateProxy = mock<IDataTableProjectAggregateService>();
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'tableName') return 'My Test Table';
if (paramName === 'columns.column') {
return [
{ name: 'name', type: 'string' },
{ name: 'age', type: 'number' },
{ name: 'isActive', type: 'boolean' },
];
}
if (paramName === 'options') return {};
return undefined;
});
mockExecuteFunctions.helpers = {
getDataTableAggregateProxy: jest.fn().mockResolvedValue(mockAggregateProxy),
} as any;
const mockResult = {
id: 'table-123',
name: 'My Test Table',
columns: [
{ name: 'name', type: 'string', index: 0 },
{ name: 'age', type: 'number', index: 1 },
{ name: 'isActive', type: 'boolean', index: 2 },
],
};
mockAggregateProxy.createDataTable.mockResolvedValue(mockResult as any);
const result = await createOperation.execute.call(mockExecuteFunctions, 0);
expect(mockAggregateProxy.createDataTable).toHaveBeenCalledWith({
name: 'My Test Table',
columns: [
{ name: 'name', type: 'string', index: 0 },
{ name: 'age', type: 'number', index: 1 },
{ name: 'isActive', type: 'boolean', index: 2 },
],
});
expect(result).toEqual([{ json: mockResult }]);
});
it('should create a table with empty columns array', async () => {
const mockExecuteFunctions = mock<IExecuteFunctions>();
const mockAggregateProxy = mock<IDataTableProjectAggregateService>();
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'tableName') return 'Empty Table';
if (paramName === 'columns.column') return [];
if (paramName === 'options') return {};
return undefined;
});
mockExecuteFunctions.helpers = {
getDataTableAggregateProxy: jest.fn().mockResolvedValue(mockAggregateProxy),
} as any;
const mockResult = {
id: 'table-456',
name: 'Empty Table',
columns: [],
};
mockAggregateProxy.createDataTable.mockResolvedValue(mockResult as any);
const result = await createOperation.execute.call(mockExecuteFunctions, 0);
expect(mockAggregateProxy.createDataTable).toHaveBeenCalledWith({
name: 'Empty Table',
columns: [],
});
expect(result).toEqual([{ json: mockResult }]);
});
it('should return existing table when createIfNotExists is enabled and table exists', async () => {
const mockExecuteFunctions = mock<IExecuteFunctions>();
const mockAggregateProxy = mock<IDataTableProjectAggregateService>();
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'tableName') return 'Existing Table';
if (paramName === 'columns.column') {
return [{ name: 'col1', type: 'string' }];
}
if (paramName === 'options') return { createIfNotExists: true };
return undefined;
});
mockExecuteFunctions.helpers = {
getDataTableAggregateProxy: jest.fn().mockResolvedValue(mockAggregateProxy),
} as any;
const existingTable = {
id: 'existing-123',
name: 'Existing Table',
columns: [{ name: 'oldCol', type: 'string' }],
};
mockAggregateProxy.getManyAndCount.mockResolvedValue({
data: [existingTable],
count: 1,
} as any);
const result = await createOperation.execute.call(mockExecuteFunctions, 0);
expect(mockAggregateProxy.getManyAndCount).toHaveBeenCalledWith({
filter: { name: 'Existing Table' },
take: 1,
});
expect(mockAggregateProxy.createDataTable).not.toHaveBeenCalled();
expect(result).toEqual([{ json: existingTable }]);
});
it('should create new table when createIfNotExists is enabled but table does not exist', async () => {
const mockExecuteFunctions = mock<IExecuteFunctions>();
const mockAggregateProxy = mock<IDataTableProjectAggregateService>();
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'tableName') return 'New Table';
if (paramName === 'columns.column') {
return [{ name: 'col1', type: 'string' }];
}
if (paramName === 'options') return { createIfNotExists: true };
return undefined;
});
mockExecuteFunctions.helpers = {
getDataTableAggregateProxy: jest.fn().mockResolvedValue(mockAggregateProxy),
} as any;
mockAggregateProxy.getManyAndCount.mockResolvedValue({
data: [],
count: 0,
} as any);
const newTable = {
id: 'new-123',
name: 'New Table',
columns: [{ name: 'col1', type: 'string', index: 0 }],
};
mockAggregateProxy.createDataTable.mockResolvedValue(newTable as any);
const result = await createOperation.execute.call(mockExecuteFunctions, 0);
expect(mockAggregateProxy.getManyAndCount).toHaveBeenCalledWith({
filter: { name: 'New Table' },
take: 1,
});
expect(mockAggregateProxy.createDataTable).toHaveBeenCalledWith({
name: 'New Table',
columns: [{ name: 'col1', type: 'string', index: 0 }],
});
expect(result).toEqual([{ json: newTable }]);
});
it('should support all column types', async () => {
const mockExecuteFunctions = mock<IExecuteFunctions>();
const mockAggregateProxy = mock<IDataTableProjectAggregateService>();
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'tableName') return 'Multi Type Table';
if (paramName === 'columns.column') {
return [
{ name: 'stringCol', type: 'string' },
{ name: 'numberCol', type: 'number' },
{ name: 'booleanCol', type: 'boolean' },
{ name: 'dateCol', type: 'date' },
];
}
if (paramName === 'options') return {};
return undefined;
});
mockExecuteFunctions.helpers = {
getDataTableAggregateProxy: jest.fn().mockResolvedValue(mockAggregateProxy),
} as any;
const mockResult = {
id: 'table-789',
name: 'Multi Type Table',
columns: [
{ name: 'stringCol', type: 'string', index: 0 },
{ name: 'numberCol', type: 'number', index: 1 },
{ name: 'booleanCol', type: 'boolean', index: 2 },
{ name: 'dateCol', type: 'date', index: 3 },
],
};
mockAggregateProxy.createDataTable.mockResolvedValue(mockResult as any);
const result = await createOperation.execute.call(mockExecuteFunctions, 0);
expect(mockAggregateProxy.createDataTable).toHaveBeenCalledWith({
name: 'Multi Type Table',
columns: [
{ name: 'stringCol', type: 'string', index: 0 },
{ name: 'numberCol', type: 'number', index: 1 },
{ name: 'booleanCol', type: 'boolean', index: 2 },
{ name: 'dateCol', type: 'date', index: 3 },
],
});
expect(result).toEqual([{ json: mockResult }]);
});
});
describe('Delete Operation', () => {
it('should delete a data table successfully', async () => {
const mockExecuteFunctions = mock<IExecuteFunctions>();
const mockDataTableProxy = mock<IDataTableProjectService>();
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'dataTableId') return 'table-123';
return undefined;
});
mockExecuteFunctions.helpers = {
getDataTableProxy: jest.fn().mockResolvedValue(mockDataTableProxy),
} as any;
mockDataTableProxy.deleteDataTable.mockResolvedValue(true);
const result = await deleteOperation.execute.call(mockExecuteFunctions, 0);
expect(mockDataTableProxy.deleteDataTable).toHaveBeenCalled();
expect(result).toEqual([{ json: { success: true, deletedTableId: 'table-123' } }]);
});
it('should return success false when deletion fails', async () => {
const mockExecuteFunctions = mock<IExecuteFunctions>();
const mockDataTableProxy = mock<IDataTableProjectService>();
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'dataTableId') return 'table-456';
return undefined;
});
mockExecuteFunctions.helpers = {
getDataTableProxy: jest.fn().mockResolvedValue(mockDataTableProxy),
} as any;
mockDataTableProxy.deleteDataTable.mockResolvedValue(false);
const result = await deleteOperation.execute.call(mockExecuteFunctions, 0);
expect(mockDataTableProxy.deleteDataTable).toHaveBeenCalled();
expect(result).toEqual([{ json: { success: false, deletedTableId: 'table-456' } }]);
});
});
describe('List Operation', () => {
it('should list all data tables without filters', async () => {
const mockExecuteFunctions = mock<IExecuteFunctions>();
const mockAggregateProxy = mock<IDataTableProjectAggregateService>();
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'returnAll') return true;
if (paramName === 'limit') return 50;
if (paramName === 'options') return {};
return undefined;
});
mockExecuteFunctions.helpers = {
getDataTableAggregateProxy: jest.fn().mockResolvedValue(mockAggregateProxy),
} as any;
const mockTables = [
{ id: 'table-1', name: 'Table 1', columns: [] },
{ id: 'table-2', name: 'Table 2', columns: [] },
];
mockAggregateProxy.getManyAndCount.mockResolvedValue({
data: mockTables,
count: 2,
} as any);
const result = await listOperation.execute.call(mockExecuteFunctions, 0);
expect(mockAggregateProxy.getManyAndCount).toHaveBeenCalledWith({
skip: 0,
take: 100,
});
expect(result).toEqual([{ json: mockTables[0] }, { json: mockTables[1] }]);
});
it('should list data tables with limit', async () => {
const mockExecuteFunctions = mock<IExecuteFunctions>();
const mockAggregateProxy = mock<IDataTableProjectAggregateService>();
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'returnAll') return false;
if (paramName === 'limit') return 2;
if (paramName === 'options') return {};
return undefined;
});
mockExecuteFunctions.helpers = {
getDataTableAggregateProxy: jest.fn().mockResolvedValue(mockAggregateProxy),
} as any;
const mockTables = [
{ id: 'table-1', name: 'Table 1', columns: [] },
{ id: 'table-2', name: 'Table 2', columns: [] },
];
mockAggregateProxy.getManyAndCount.mockResolvedValue({
data: mockTables,
count: 10,
} as any);
const result = await listOperation.execute.call(mockExecuteFunctions, 0);
expect(mockAggregateProxy.getManyAndCount).toHaveBeenCalledWith({
skip: 0,
take: 2,
});
expect(result).toEqual([{ json: mockTables[0] }, { json: mockTables[1] }]);
});
it('should filter data tables by name', async () => {
const mockExecuteFunctions = mock<IExecuteFunctions>();
const mockAggregateProxy = mock<IDataTableProjectAggregateService>();
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'returnAll') return false;
if (paramName === 'limit') return 50;
if (paramName === 'options') return { filterName: 'Test' };
return undefined;
});
mockExecuteFunctions.helpers = {
getDataTableAggregateProxy: jest.fn().mockResolvedValue(mockAggregateProxy),
} as any;
const mockTables = [{ id: 'table-1', name: 'Test Table', columns: [] }];
mockAggregateProxy.getManyAndCount.mockResolvedValue({
data: mockTables,
count: 1,
} as any);
const result = await listOperation.execute.call(mockExecuteFunctions, 0);
expect(mockAggregateProxy.getManyAndCount).toHaveBeenCalledWith({
filter: { name: 'test' },
skip: 0,
take: 50,
});
expect(result).toEqual([{ json: mockTables[0] }]);
});
it('should sort data tables by name ascending', async () => {
const mockExecuteFunctions = mock<IExecuteFunctions>();
const mockAggregateProxy = mock<IDataTableProjectAggregateService>();
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'returnAll') return false;
if (paramName === 'limit') return 50;
if (paramName === 'options') return { sortField: 'name', sortDirection: 'asc' };
return undefined;
});
mockExecuteFunctions.helpers = {
getDataTableAggregateProxy: jest.fn().mockResolvedValue(mockAggregateProxy),
} as any;
const mockTables = [
{ id: 'table-1', name: 'A Table', columns: [] },
{ id: 'table-2', name: 'B Table', columns: [] },
];
mockAggregateProxy.getManyAndCount.mockResolvedValue({
data: mockTables,
count: 2,
} as any);
const result = await listOperation.execute.call(mockExecuteFunctions, 0);
expect(mockAggregateProxy.getManyAndCount).toHaveBeenCalledWith({
sortBy: 'name:asc',
skip: 0,
take: 50,
});
expect(result).toEqual([{ json: mockTables[0] }, { json: mockTables[1] }]);
});
it('should handle pagination for returnAll option', async () => {
const mockExecuteFunctions = mock<IExecuteFunctions>();
const mockAggregateProxy = mock<IDataTableProjectAggregateService>();
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'returnAll') return true;
if (paramName === 'limit') return 50;
if (paramName === 'options') return {};
return undefined;
});
mockExecuteFunctions.helpers = {
getDataTableAggregateProxy: jest.fn().mockResolvedValue(mockAggregateProxy),
} as any;
// First page
const firstPageTables = Array.from({ length: 100 }, (_, i) => ({
id: `table-${i}`,
name: `Table ${i}`,
columns: [],
}));
// Second page (partial)
const secondPageTables = Array.from({ length: 50 }, (_, i) => ({
id: `table-${i + 100}`,
name: `Table ${i + 100}`,
columns: [],
}));
mockAggregateProxy.getManyAndCount
.mockResolvedValueOnce({
data: firstPageTables,
count: 150,
} as any)
.mockResolvedValueOnce({
data: secondPageTables,
count: 150,
} as any);
const result = await listOperation.execute.call(mockExecuteFunctions, 0);
expect(mockAggregateProxy.getManyAndCount).toHaveBeenCalledTimes(2);
expect(mockAggregateProxy.getManyAndCount).toHaveBeenNthCalledWith(1, {
skip: 0,
take: 100,
});
expect(mockAggregateProxy.getManyAndCount).toHaveBeenNthCalledWith(2, {
skip: 100,
take: 100,
});
expect(result.length).toBe(150);
});
it('should return empty array when no tables exist', async () => {
const mockExecuteFunctions = mock<IExecuteFunctions>();
const mockAggregateProxy = mock<IDataTableProjectAggregateService>();
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'returnAll') return true;
if (paramName === 'limit') return 50;
if (paramName === 'options') return {};
return undefined;
});
mockExecuteFunctions.helpers = {
getDataTableAggregateProxy: jest.fn().mockResolvedValue(mockAggregateProxy),
} as any;
mockAggregateProxy.getManyAndCount.mockResolvedValue({
data: [],
count: 0,
} as any);
const result = await listOperation.execute.call(mockExecuteFunctions, 0);
expect(result).toEqual([]);
});
});
describe('Update Operation', () => {
it('should update a data table name successfully', async () => {
const mockExecuteFunctions = mock<IExecuteFunctions>();
const mockDataTableProxy = mock<IDataTableProjectService>();
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'dataTableId') return 'table-123';
if (paramName === 'newName') return 'Updated Table Name';
return undefined;
});
mockExecuteFunctions.helpers = {
getDataTableProxy: jest.fn().mockResolvedValue(mockDataTableProxy),
} as any;
mockDataTableProxy.updateDataTable.mockResolvedValue(true);
const result = await updateOperation.execute.call(mockExecuteFunctions, 0);
expect(mockDataTableProxy.updateDataTable).toHaveBeenCalledWith({
name: 'Updated Table Name',
});
expect(result).toEqual([{ json: { success: true, name: 'Updated Table Name' } }]);
});
it('should return success false when update fails', async () => {
const mockExecuteFunctions = mock<IExecuteFunctions>();
const mockDataTableProxy = mock<IDataTableProjectService>();
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'dataTableId') return 'table-456';
if (paramName === 'newName') return 'Failed Update';
return undefined;
});
mockExecuteFunctions.helpers = {
getDataTableProxy: jest.fn().mockResolvedValue(mockDataTableProxy),
} as any;
mockDataTableProxy.updateDataTable.mockResolvedValue(false);
const result = await updateOperation.execute.call(mockExecuteFunctions, 0);
expect(mockDataTableProxy.updateDataTable).toHaveBeenCalledWith({ name: 'Failed Update' });
expect(result).toEqual([{ json: { success: false, name: 'Failed Update' } }]);
});
});
});