n8n/packages/workflow/src/Extensions/DateExtensions.ts
Iván Ovejero dff8456382
refactor(core): Reorganize error hierarchy in core and workflow packages (no-changelog) (#7820)
Ensure all errors in `core` and `workflow` inherit from
`ApplicationError` so that we start normalizing all the errors we report
to Sentry

Follow-up to:
https://github.com/n8n-io/n8n/pull/7757#discussion_r1404338844

### `core` package

`ApplicationError`
- `FileSystemError` (abstract)
	- `FileNotFoundError`
	- `DisallowedFilepathError`
- `BinaryDataError` (abstract)
	- `InvalidModeError`
	- `InvalidManagerError`
- `InvalidExecutionMetadataError`

### `workflow` package

`ApplicationError`
- `ExecutionBaseError` (abstract)
	- `WorkflowActivationError`
		- `WorkflowDeactivationError`
		- `WebhookTakenError`
	- `WorkflowOperationError`
		- `SubworkflowOperationError`
			- `CliWorkflowOperationError`
	- `ExpressionError`
		- `ExpressionExtensionError`
	- `NodeError` (abstract)
		- `NodeOperationError`
		- `NodeApiError`
	- `NodeSSLError`

Up next:
- Reorganize errors in `cli`
- Flatten the hierarchy in `workflow` (do we really need
`ExecutionBaseError`?)
- Remove `ExecutionError` type
- Stop throwing plain `Error`s
- Replace `severity` with `level`
- Add node and credential types as `tags`
- Add workflow IDs and execution IDs as `extras`
2023-11-27 15:33:21 +01:00

323 lines
8.2 KiB
TypeScript

import { ExpressionExtensionError } from '../errors/expression-extension.error';
import { DateTime } from 'luxon';
import type {
DateTimeUnit,
DurationLike,
DurationObjectUnits,
LocaleOptions,
WeekdayNumbers,
} from 'luxon';
import type { ExtensionMap } from './Extensions';
import { convertToDateTime } from './utils';
type DurationUnit =
| 'milliseconds'
| 'seconds'
| 'minutes'
| 'hours'
| 'days'
| 'weeks'
| 'months'
| 'quarter'
| 'years';
type DatePart =
| 'day'
| 'week'
| 'month'
| 'year'
| 'hour'
| 'minute'
| 'second'
| 'millisecond'
| 'weekNumber'
| 'yearDayNumber'
| 'weekday';
const DURATION_MAP: Record<string, DurationUnit> = {
day: 'days',
month: 'months',
year: 'years',
week: 'weeks',
hour: 'hours',
minute: 'minutes',
second: 'seconds',
millisecond: 'milliseconds',
ms: 'milliseconds',
sec: 'seconds',
secs: 'seconds',
hr: 'hours',
hrs: 'hours',
min: 'minutes',
mins: 'minutes',
};
const DATETIMEUNIT_MAP: Record<string, DateTimeUnit> = {
days: 'day',
months: 'month',
years: 'year',
hours: 'hour',
minutes: 'minute',
seconds: 'second',
milliseconds: 'millisecond',
hrs: 'hour',
hr: 'hour',
mins: 'minute',
min: 'minute',
secs: 'second',
sec: 'second',
ms: 'millisecond',
};
function isDateTime(date: unknown): date is DateTime {
return date ? DateTime.isDateTime(date) : false;
}
function generateDurationObject(durationValue: number, unit: DurationUnit): DurationObjectUnits {
const convertedUnit = DURATION_MAP[unit] || unit;
return { [`${convertedUnit}`]: durationValue };
}
function beginningOf(date: Date | DateTime, extraArgs: DurationUnit[]): Date | DateTime {
const [rawUnit = 'week'] = extraArgs;
const unit = DATETIMEUNIT_MAP[rawUnit] || rawUnit;
if (isDateTime(date)) return date.startOf(unit);
return DateTime.fromJSDate(date).startOf(unit).toJSDate();
}
function endOfMonth(date: Date | DateTime): Date | DateTime {
if (isDateTime(date)) return date.endOf('month');
return DateTime.fromJSDate(date).endOf('month').toJSDate();
}
function extract(date: Date | DateTime, args: DatePart[]): number {
let [part = 'week'] = args;
if (part === 'yearDayNumber') {
date = isDateTime(date) ? date.toJSDate() : date;
const firstDayOfTheYear = new Date(date.getFullYear(), 0, 0);
const diff =
date.getTime() -
firstDayOfTheYear.getTime() +
(firstDayOfTheYear.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000;
return Math.floor(diff / (1000 * 60 * 60 * 24));
}
if (part === 'week') part = 'weekNumber';
const unit = (DATETIMEUNIT_MAP[part] as keyof DateTime) || part;
if (isDateTime(date)) return date.get(unit);
return DateTime.fromJSDate(date).get(unit);
}
function format(date: Date | DateTime, extraArgs: unknown[]): string {
const [dateFormat, localeOpts = {}] = extraArgs as [string, LocaleOptions];
if (isDateTime(date)) {
return date.toFormat(dateFormat, { ...localeOpts });
}
return DateTime.fromJSDate(date).toFormat(dateFormat, { ...localeOpts });
}
function isBetween(
date: Date | DateTime,
extraArgs: Array<string | Date | DateTime>,
): boolean | undefined {
if (extraArgs.length !== 2) {
throw new ExpressionExtensionError('isBetween(): expected exactly two args');
}
const [first, second] = extraArgs;
const firstDate = convertToDateTime(first);
const secondDate = convertToDateTime(second);
if (!firstDate || !secondDate) {
return;
}
if (firstDate > secondDate) {
return secondDate < date && date < firstDate;
}
return secondDate > date && date > firstDate;
}
function isDst(date: Date | DateTime): boolean {
if (isDateTime(date)) {
return date.isInDST;
}
return DateTime.fromJSDate(date).isInDST;
}
function isInLast(date: Date | DateTime, extraArgs: unknown[]): boolean {
const [durationValue = 0, unit = 'minutes'] = extraArgs as [number, DurationUnit];
const dateInThePast = DateTime.now().minus(generateDurationObject(durationValue, unit));
let thisDate = date;
if (!isDateTime(thisDate)) {
thisDate = DateTime.fromJSDate(thisDate);
}
return dateInThePast <= thisDate && thisDate <= DateTime.now();
}
const WEEKEND_DAYS: WeekdayNumbers[] = [6, 7];
function isWeekend(date: Date | DateTime): boolean {
const { weekday } = isDateTime(date) ? date : DateTime.fromJSDate(date);
return WEEKEND_DAYS.includes(weekday);
}
function minus(
date: Date | DateTime,
args: [DurationLike] | [number, DurationUnit],
): Date | DateTime {
if (args.length === 1) {
const [arg] = args;
if (isDateTime(date)) return date.minus(arg);
return DateTime.fromJSDate(date).minus(arg).toJSDate();
}
const [durationValue = 0, unit = 'minutes'] = args;
const duration = generateDurationObject(durationValue, unit);
if (isDateTime(date)) return date.minus(duration);
return DateTime.fromJSDate(date).minus(duration).toJSDate();
}
function plus(
date: Date | DateTime,
args: [DurationLike] | [number, DurationUnit],
): Date | DateTime {
if (args.length === 1) {
const [arg] = args;
if (isDateTime(date)) return date.plus(arg);
return DateTime.fromJSDate(date).plus(arg).toJSDate();
}
const [durationValue = 0, unit = 'minutes'] = args;
const duration = generateDurationObject(durationValue, unit);
if (isDateTime(date)) return date.plus(duration);
return DateTime.fromJSDate(date).plus(duration).toJSDate();
}
endOfMonth.doc = {
name: 'endOfMonth',
returnType: 'Date',
description: 'Transforms a date to the last possible moment that lies within the month.',
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-endOfMonth',
};
isDst.doc = {
name: 'isDst',
returnType: 'boolean',
description: 'Checks if a Date is within Daylight Savings Time.',
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-isDst',
};
isWeekend.doc = {
name: 'isWeekend',
returnType: 'boolean',
description: 'Checks if the Date falls on a Saturday or Sunday.',
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-isWeekend',
};
beginningOf.doc = {
name: 'beginningOf',
description: 'Transform a Date to the start of the given time period. Default unit is `week`.',
returnType: 'Date',
args: [{ name: 'unit?', type: 'DurationUnit' }],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-beginningOf',
};
extract.doc = {
name: 'extract',
description: 'Extracts the part defined in `datePart` from a Date. Default unit is `week`.',
returnType: 'number',
args: [{ name: 'datePart?', type: 'DurationUnit' }],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-extract',
};
format.doc = {
name: 'format',
description: 'Formats a Date in the given structure.',
returnType: 'string',
args: [{ name: 'fmt', type: 'TimeFormat' }],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-format',
};
isBetween.doc = {
name: 'isBetween',
description: 'Checks if a Date is between two given dates.',
returnType: 'boolean',
args: [
{ name: 'date1', type: 'Date|string' },
{ name: 'date2', type: 'Date|string' },
],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-isBetween',
};
isInLast.doc = {
name: 'isInLast',
description: 'Checks if a Date is within a given time period. Default unit is `minute`.',
returnType: 'boolean',
args: [
{ name: 'n', type: 'number' },
{ name: 'unit?', type: 'DurationUnit' },
],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-isInLast',
};
minus.doc = {
name: 'minus',
description: 'Subtracts a given time period from a Date. Default unit is `minute`.',
returnType: 'Date',
args: [
{ name: 'n', type: 'number' },
{ name: 'unit?', type: 'DurationUnit' },
],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-minus',
};
plus.doc = {
name: 'plus',
description: 'Adds a given time period to a Date. Default unit is `minute`.',
returnType: 'Date',
args: [
{ name: 'n', type: 'number' },
{ name: 'unit?', type: 'DurationUnit' },
],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-plus',
};
export const dateExtensions: ExtensionMap = {
typeName: 'Date',
functions: {
beginningOf,
endOfMonth,
extract,
isBetween,
isDst,
isInLast,
isWeekend,
minus,
plus,
format,
},
};