Files
n8n-enterprise-unlocked/packages/nodes-base/nodes/DataTable/test/common/utils.test.ts

299 lines
8.3 KiB
TypeScript

import { DateTime } from 'luxon';
import type { INode } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { ANY_CONDITION, ALL_CONDITIONS } 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 - 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);
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);
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);
expect(result).toEqual({
type: 'and',
filters: [
{
columnName: 'name',
condition: 'eq',
value: 'John',
},
{
columnName: 'email',
condition: 'eq',
value: null,
},
{
columnName: 'phone',
condition: 'neq',
value: null,
},
],
});
});
it('should preserve existing conditions unchanged', () => {
const fieldEntries = [
{ keyName: 'age', condition: 'gt' as const, keyValue: 18 },
{ keyName: 'name', condition: 'like' as const, keyValue: '%john%' },
];
const result = buildGetManyFilter(fieldEntries, ANY_CONDITION);
expect(result).toEqual({
type: 'or',
filters: [
{
columnName: 'age',
condition: 'gt',
value: 18,
},
{
columnName: 'name',
condition: 'like',
value: '%john%',
},
],
});
});
});