mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
fix(Data Table Node): Fix date handling on the node (no-changelog) (#19030)
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { DateTime } from 'luxon';
|
||||||
import type {
|
import type {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
INode,
|
INode,
|
||||||
@@ -6,6 +7,7 @@ import type {
|
|||||||
IDataStoreProjectService,
|
IDataStoreProjectService,
|
||||||
IExecuteFunctions,
|
IExecuteFunctions,
|
||||||
ILoadOptionsFunctions,
|
ILoadOptionsFunctions,
|
||||||
|
DataStoreColumnJsType,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeOperationError } from 'n8n-workflow';
|
import { NodeOperationError } from 'n8n-workflow';
|
||||||
|
|
||||||
@@ -13,6 +15,14 @@ import type { FieldEntry, FilterType } from './constants';
|
|||||||
import { ALL_FILTERS, ANY_FILTER } from './constants';
|
import { ALL_FILTERS, ANY_FILTER } from './constants';
|
||||||
import { DATA_TABLE_ID_FIELD } from './fields';
|
import { DATA_TABLE_ID_FIELD } from './fields';
|
||||||
|
|
||||||
|
type DateLike = { toISOString: () => string };
|
||||||
|
|
||||||
|
function isDateLike(v: unknown): v is DateLike {
|
||||||
|
return (
|
||||||
|
v !== null && typeof v === 'object' && 'toISOString' in v && typeof v.toISOString === 'function'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// We need two functions here since the available getNodeParameter
|
// We need two functions here since the available getNodeParameter
|
||||||
// overloads vary with the index
|
// overloads vary with the index
|
||||||
export async function getDataTableProxyExecute(
|
export async function getDataTableProxyExecute(
|
||||||
@@ -87,9 +97,13 @@ export function isFieldArray(value: unknown): value is FieldEntry[] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dataObjectToApiInput(data: IDataObject, node: INode, row: number) {
|
export function dataObjectToApiInput(
|
||||||
|
data: IDataObject,
|
||||||
|
node: INode,
|
||||||
|
row: number,
|
||||||
|
): Record<string, DataStoreColumnJsType> {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(data).map(([k, v]) => {
|
Object.entries(data).map(([k, v]): [string, DataStoreColumnJsType] => {
|
||||||
if (v === undefined || v === null) return [k, null];
|
if (v === undefined || v === null) return [k, null];
|
||||||
|
|
||||||
if (Array.isArray(v)) {
|
if (Array.isArray(v)) {
|
||||||
@@ -99,7 +113,28 @@ export function dataObjectToApiInput(data: IDataObject, node: INode, row: number
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(v instanceof Date) && typeof v === 'object') {
|
if (v instanceof Date) {
|
||||||
|
return [k, v];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
// Luxon DateTime
|
||||||
|
if (DateTime.isDateTime(v)) {
|
||||||
|
return [k, v.toJSDate()];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDateLike(v)) {
|
||||||
|
try {
|
||||||
|
const dateObj = new Date(v.toISOString());
|
||||||
|
if (isNaN(dateObj.getTime())) {
|
||||||
|
throw new Error('Invalid date');
|
||||||
|
}
|
||||||
|
return [k, dateObj];
|
||||||
|
} catch {
|
||||||
|
// Fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw new NodeOperationError(
|
throw new NodeOperationError(
|
||||||
node,
|
node,
|
||||||
`unexpected object input '${JSON.stringify(v)}' in row ${row}`,
|
`unexpected object input '${JSON.stringify(v)}' in row ${row}`,
|
||||||
|
|||||||
203
packages/nodes-base/nodes/DataTable/test/common/utils.test.ts
Normal file
203
packages/nodes-base/nodes/DataTable/test/common/utils.test.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import type { INode } from 'n8n-workflow';
|
||||||
|
import { NodeOperationError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { dataObjectToApiInput } from '../../common/utils';
|
||||||
|
|
||||||
|
describe('dataObjectToApiInput', () => {
|
||||||
|
const mockNode: INode = {
|
||||||
|
id: 'test-node',
|
||||||
|
name: 'Test Node',
|
||||||
|
type: 'test',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [0, 0],
|
||||||
|
parameters: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user