mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(Wait Node): Validate datetime for specific time mode (#14701)
Co-authored-by: Danny Martini <danny@n8n.io> Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
committed by
GitHub
parent
3feab31792
commit
3641c1fb87
@@ -1,4 +1,3 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
@@ -8,10 +7,11 @@ import type {
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
NodeOperationError,
|
||||
NodeConnectionTypes,
|
||||
WAIT_INDEFINITELY,
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
tryToParseDateTime,
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '../../utils/utilities';
|
||||
@@ -506,20 +506,17 @@ export class Wait extends Webhook {
|
||||
// a number of seconds added to the current timestamp
|
||||
waitTill = new Date(new Date().getTime() + waitAmount);
|
||||
} else {
|
||||
const dateTimeStr = context.getNodeParameter('dateTime', 0) as string;
|
||||
try {
|
||||
const dateTimeStrRaw = context.getNodeParameter('dateTime', 0);
|
||||
const parsedDateTime = tryToParseDateTime(dateTimeStrRaw, context.getTimezone());
|
||||
|
||||
if (isNaN(Date.parse(dateTimeStr))) {
|
||||
waitTill = parsedDateTime.toUTC().toJSDate();
|
||||
} catch (e) {
|
||||
throw new NodeOperationError(
|
||||
context.getNode(),
|
||||
'[Wait node] Cannot put execution to wait because `dateTime` parameter is not a valid date. Please pick a specific date and time to wait until.',
|
||||
);
|
||||
}
|
||||
|
||||
waitTill = DateTime.fromFormat(dateTimeStr, "yyyy-MM-dd'T'HH:mm:ss", {
|
||||
zone: context.getTimezone(),
|
||||
})
|
||||
.toUTC()
|
||||
.toJSDate();
|
||||
}
|
||||
|
||||
const waitValue = Math.max(waitTill.getTime() - new Date().getTime(), 0);
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { DateTime } from 'luxon';
|
||||
import { NodeOperationError, type IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { getWorkflowFilenames, testWorkflows } from '@test/nodes/Helpers';
|
||||
|
||||
import { Wait } from '../Wait.node';
|
||||
|
||||
const workflows = getWorkflowFilenames(__dirname);
|
||||
|
||||
describe('Execute Wait Node', () => {
|
||||
@@ -15,5 +22,52 @@ describe('Execute Wait Node', () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ value: 'invalid_date', isValid: false },
|
||||
{
|
||||
value: '2025-04-18T10:50:47.560',
|
||||
isValid: true,
|
||||
expectedWaitTill: new Date('2025-04-18T10:50:47.560Z'),
|
||||
},
|
||||
{
|
||||
value: '2025-04-18T10:50:47.560+02:00',
|
||||
isValid: true,
|
||||
expectedWaitTill: new Date('2025-04-18T08:50:47.560Z'),
|
||||
},
|
||||
{
|
||||
value: DateTime.fromISO('2025-04-18T10:50:47.560Z').toJSDate(),
|
||||
isValid: true,
|
||||
expectedWaitTill: new Date('2025-04-18T10:50:47.560Z'),
|
||||
},
|
||||
{
|
||||
value: DateTime.fromISO('2025-04-18T10:50:47.560Z'),
|
||||
isValid: true,
|
||||
expectedWaitTill: new Date('2025-04-18T10:50:47.560Z'),
|
||||
},
|
||||
])(
|
||||
'Test Wait Node with specificTime $value and isValid $isValid',
|
||||
async ({ value, isValid, expectedWaitTill }) => {
|
||||
const putExecutionToWaitSpy = jest.fn();
|
||||
const waitNode = new Wait();
|
||||
const executeFunctionsMock = mock<IExecuteFunctions>({
|
||||
getNodeParameter: jest.fn().mockImplementation((paramName: string) => {
|
||||
if (paramName === 'resume') return 'specificTime';
|
||||
if (paramName === 'dateTime') return value;
|
||||
}),
|
||||
getTimezone: jest.fn().mockReturnValue('UTC'),
|
||||
putExecutionToWait: putExecutionToWaitSpy,
|
||||
getInputData: jest.fn(),
|
||||
getNode: jest.fn(),
|
||||
});
|
||||
|
||||
if (isValid) {
|
||||
await expect(waitNode.execute(executeFunctionsMock)).resolves.not.toThrow();
|
||||
expect(putExecutionToWaitSpy).toHaveBeenCalledWith(expectedWaitTill);
|
||||
} else {
|
||||
await expect(waitNode.execute(executeFunctionsMock)).rejects.toThrow(NodeOperationError);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
testWorkflows(workflows);
|
||||
});
|
||||
|
||||
@@ -65,13 +65,15 @@ export const tryToParseBoolean = (value: unknown): value is boolean => {
|
||||
});
|
||||
};
|
||||
|
||||
export const tryToParseDateTime = (value: unknown): DateTime => {
|
||||
if (value instanceof DateTime && value.isValid) {
|
||||
export const tryToParseDateTime = (value: unknown, defaultZone?: string): DateTime => {
|
||||
if (DateTime.isDateTime(value) && value.isValid) {
|
||||
// Ignore the defaultZone if the value is already a DateTime
|
||||
// because DateTime objects already contain the zone information
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
const fromJSDate = DateTime.fromJSDate(value);
|
||||
const fromJSDate = DateTime.fromJSDate(value, { zone: defaultZone });
|
||||
if (fromJSDate.isValid) {
|
||||
return fromJSDate;
|
||||
}
|
||||
@@ -80,24 +82,24 @@ export const tryToParseDateTime = (value: unknown): DateTime => {
|
||||
const dateString = String(value).trim();
|
||||
|
||||
// Rely on luxon to parse different date formats
|
||||
const isoDate = DateTime.fromISO(dateString, { setZone: true });
|
||||
const isoDate = DateTime.fromISO(dateString, { zone: defaultZone, setZone: true });
|
||||
if (isoDate.isValid) {
|
||||
return isoDate;
|
||||
}
|
||||
const httpDate = DateTime.fromHTTP(dateString, { setZone: true });
|
||||
const httpDate = DateTime.fromHTTP(dateString, { zone: defaultZone, setZone: true });
|
||||
if (httpDate.isValid) {
|
||||
return httpDate;
|
||||
}
|
||||
const rfc2822Date = DateTime.fromRFC2822(dateString, { setZone: true });
|
||||
const rfc2822Date = DateTime.fromRFC2822(dateString, { zone: defaultZone, setZone: true });
|
||||
if (rfc2822Date.isValid) {
|
||||
return rfc2822Date;
|
||||
}
|
||||
const sqlDate = DateTime.fromSQL(dateString, { setZone: true });
|
||||
const sqlDate = DateTime.fromSQL(dateString, { zone: defaultZone, setZone: true });
|
||||
if (sqlDate.isValid) {
|
||||
return sqlDate;
|
||||
}
|
||||
|
||||
const parsedDateTime = DateTime.fromMillis(Date.parse(dateString));
|
||||
const parsedDateTime = DateTime.fromMillis(Date.parse(dateString), { zone: defaultZone });
|
||||
if (parsedDateTime.isValid) {
|
||||
return parsedDateTime;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { DateTime, Settings } from 'luxon';
|
||||
|
||||
import { getValueDescription, validateFieldType } from '@/TypeValidation';
|
||||
import { getValueDescription, tryToParseDateTime, validateFieldType } from '@/TypeValidation';
|
||||
|
||||
describe('Type Validation', () => {
|
||||
describe('Dates', () => {
|
||||
@@ -292,4 +292,39 @@ describe('Type Validation', () => {
|
||||
expect(getValueDescription({})).toBe('object');
|
||||
});
|
||||
});
|
||||
|
||||
describe('tryToParseDateTime', () => {
|
||||
it('should NOT use defaultZone if set', () => {
|
||||
const result = tryToParseDateTime('2025-04-17T06:22:20-04:00', 'Europe/Brussels');
|
||||
|
||||
expect(result.zoneName).toEqual('UTC-4');
|
||||
expect(result.toISO()).toEqual('2025-04-17T06:22:20.000-04:00');
|
||||
});
|
||||
|
||||
it('should use defaultZone if timezone is not set', () => {
|
||||
const result = tryToParseDateTime('2025-04-17T06:22:20', 'Europe/Brussels');
|
||||
|
||||
expect(result.zoneName).toEqual('Europe/Brussels');
|
||||
expect(result.toISO()).toEqual('2025-04-17T06:22:20.000+02:00');
|
||||
});
|
||||
|
||||
it('should use the system timezone when defaultZone arg is not given', () => {
|
||||
Settings.defaultZone = 'UTC-7';
|
||||
const result = tryToParseDateTime('2025-04-17T06:22:20');
|
||||
|
||||
expect(result.zoneName).toEqual('UTC-7');
|
||||
expect(result.toISO()).toEqual('2025-04-17T06:22:20.000-07:00');
|
||||
});
|
||||
|
||||
it('should not impact DateTime zone', () => {
|
||||
const dateTime = DateTime.fromObject(
|
||||
{ year: 2025, month: 1, day: 1 },
|
||||
{ zone: 'Asia/Tokyo' },
|
||||
);
|
||||
const result = tryToParseDateTime(dateTime, 'Europe/Brussels');
|
||||
|
||||
expect(result.zoneName).toEqual('Asia/Tokyo');
|
||||
expect(result.toISO()).toEqual('2025-01-01T00:00:00.000+09:00');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user