Skip to content

Commit

Permalink
feat(dt-functions): introduce date expression extensions (#4045)
Browse files Browse the repository at this point in the history
* 🎉 Add Date Extensions into the mix

* ✨ Introduce additional date extension methods

* ✅ Add Date Expression Extension tests

* 🔧 Add ability to debug tests
  • Loading branch information
valya committed Nov 9, 2022
1 parent ae507ec commit d65b6c6
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 44 deletions.
11 changes: 11 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Jest: current file",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["${fileBasenameNoExtension}"],
"console": "integratedTerminal",
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
}
},
{
"name": "Attach to running n8n",
"processId": "${command:PickProcess}",
"request": "attach",
Expand Down
184 changes: 147 additions & 37 deletions packages/workflow/src/Extensions/DateExtensions.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,184 @@
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
import { DateTime, DurationObjectUnits } from 'luxon';
import {
DateTime,
DateTimeFormatOptions,
Duration,
DurationObjectUnits,
LocaleOptions,
} from 'luxon';
import { BaseExtension, ExtensionMethodHandler } from './Extensions';

type DurationUnit = 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';
type DatePart =
| 'day'
| 'month'
| 'year'
| 'hour'
| 'minute'
| 'second'
| 'weekNumber'
| 'yearDayNumber'
| 'weekday';

export class DateExtensions extends BaseExtension<string> {
methodMapping = new Map<string, ExtensionMethodHandler<string>>();
export class DateExtensions extends BaseExtension<Date> {
methodMapping = new Map<string, ExtensionMethodHandler<Date>>();

constructor() {
super();
this.initializeMethodMap();
}

bind(mainArg: string, extraArgs?: number[] | string[] | boolean[] | undefined) {
bind(mainArg: Date, extraArgs?: Date | Date[] | number[] | string[] | boolean[] | undefined) {
return Array.from(this.methodMapping).reduce((p, c) => {
const [key, method] = c;
Object.assign(p, {
[key]: () => {
return method.call('', mainArg, extraArgs);
return method.call(Date, mainArg, extraArgs);
},
});
return p;
}, {} as object);
}

private initializeMethodMap(): void {
this.methodMapping = new Map<string, (value: string) => string | Date>([]);
}

private generateDurationObject(
value: number,
unit: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year',
) {
const durationObject = {} as DurationObjectUnits;

if (unit === 'minute') {
durationObject.minutes = value;
} else if (unit === 'hour') {
durationObject.hours = value;
} else if (unit === 'day') {
durationObject.days = value;
} else if (unit === 'week') {
durationObject.weeks = value;
} else if (unit === 'month') {
durationObject.months = value;
} else if (unit === 'year') {
durationObject.years = value;
this.methodMapping = new Map<
string,
(
value: Date,
extraArgs?: Date | Date[] | string | number[] | string[] | boolean[] | undefined,
) => string | Date | boolean | number | Duration
>([
['begginingOf', this.begginingOf],
['endOfMonth', this.endOfMonth],
['extract', this.extract],
['format', this.format],
['isBetween', this.isBetween],
['isDst', this.isDst],
['isInLast', this.isInLast],
['isWeekend', this.isWeekend],
['minus', this.minus],
['plus', this.plus],
['toLocaleString', this.toLocaleString],
['toTimeFromNow', this.toTimeFromNow],
['timeTo', this.timeTo],
]);
}

private generateDurationObject(durationValue: number, unit: DurationUnit) {
return { [`${unit}s`]: durationValue } as DurationObjectUnits;
}

begginingOf(date: Date, extraArgs?: any): Date {
const [unit = 'week'] = extraArgs as DurationUnit[];
return DateTime.fromJSDate(date).startOf(unit).toJSDate();
}

endOfMonth(date: Date): Date {
return DateTime.fromJSDate(date).endOf('month').toJSDate();
}

extract(date: Date, extraArgs?: any): number | Date {
const [part] = extraArgs as DatePart[];
if (part === 'yearDayNumber') {
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));
}
return durationObject;

return DateTime.fromJSDate(date).get(part);
}

isDst(value: string): boolean {
return DateTime.fromJSDate(new Date(value)).isInDST;
format(date: Date, extraArgs: any): string {
const [format, localeOpts] = extraArgs as [string, LocaleOptions];
return DateTime.fromJSDate(date).toFormat(format, { ...localeOpts });
}

isInLast(value: string, extraArgs = [0, 'minute']): boolean {
const durationValue = extraArgs[0] as number;
const unit = extraArgs[1] as DurationUnit;
isBetween(date: Date, extraArgs?: any): boolean {
const [first, second] = extraArgs as string[];
const firstDate = new Date(first);
const secondDate = new Date(second);

if (firstDate > secondDate) {
return secondDate < date && date < firstDate;
}
return secondDate > date && date > firstDate;
}

isDst(date: Date): boolean {
return DateTime.fromJSDate(date).isInDST;
}

isInLast(date: Date, extraArgs?: any): boolean {
const [durationValue = 0, unit = 'minute'] = extraArgs as [number, DurationUnit];

const dateInThePast = DateTime.now().minus(this.generateDurationObject(durationValue, unit));
const thisDate = DateTime.fromJSDate(new Date(value));
const thisDate = DateTime.fromJSDate(date);
return dateInThePast <= thisDate && thisDate <= DateTime.now();
}

plus(value: string, extraArgs = [0, 'minute']): Date {
const durationValue: number = extraArgs[0] as number;
const unit = extraArgs[1] as DurationUnit;
return DateTime.fromJSDate(new Date(value))
isWeekend(date: Date): boolean {
enum DAYS {
saturday = 6,
sunday = 7,
}
return [DAYS.saturday, DAYS.sunday].includes(DateTime.fromJSDate(date).weekday);
}

minus(date: Date, extraArgs?: any): Date {
const [durationValue = 0, unit = 'minute'] = extraArgs as [number, DurationUnit];

return DateTime.fromJSDate(date)
.minus(this.generateDurationObject(durationValue, unit))
.toJSDate();
}

plus(date: Date, extraArgs?: any): Date {
const [durationValue = 0, unit = 'minute'] = extraArgs as [number, DurationUnit];

return DateTime.fromJSDate(date)
.plus(this.generateDurationObject(durationValue, unit))
.toJSDate();
}

toLocaleString(date: Date, extraArgs?: any): string {
const [format, localeOpts] = extraArgs as [DateTimeFormatOptions, LocaleOptions];
return DateTime.fromJSDate(date).toLocaleString(format, localeOpts);
}

toTimeFromNow(date: Date): string {
const diffObj = DateTime.fromJSDate(date).diffNow().toObject();

if (diffObj.years) {
return `${diffObj.years} years ago`;
}
if (diffObj.months) {
return `${diffObj.months} months ago`;
}
if (diffObj.weeks) {
return `${diffObj.weeks} weeks ago`;
}
if (diffObj.days) {
return `${diffObj.days} days ago`;
}
if (diffObj.hours) {
return `${diffObj.hours} hours ago`;
}
if (diffObj.minutes) {
return `${diffObj.minutes} minutes ago`;
}
if (diffObj.seconds && diffObj.seconds > 10) {
return `${diffObj.seconds} seconds ago`;
}
return 'just now';
}

timeTo(date: Date, extraArgs?: any): Duration {
const [diff = new Date().toISOString(), unit = 'seconds'] = extraArgs as [string, DurationUnit];
const diffDate = new Date(diff);
return DateTime.fromJSDate(date).diff(DateTime.fromJSDate(diffDate), unit);
}
}
19 changes: 12 additions & 7 deletions packages/workflow/src/Extensions/ExpressionExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,23 @@ import * as BabelCore from '@babel/core';
import * as BabelTypes from '@babel/types';
import { DateTime, Interval, Duration } from 'luxon';
import { ExpressionExtensionError } from '../ExpressionError';
// import { DateExtensions } from './DateExtensions';

import { DateExtensions } from './DateExtensions';
import { StringExtensions } from './StringExtensions';

const EXPRESSION_EXTENDER = 'extend';

const stringExtensions = new StringExtensions();
// const dateExtensions = new DateExtensions();
const dateExtensions = new DateExtensions();

const EXPRESSION_EXTENSION_METHODS = Array.from(
new Set([
...stringExtensions.listMethods(),
// ...dateExtensions.listMethods(),
...dateExtensions.listMethods(),
'sayHi',
'toDecimal',
'isBlank',
// 'toLocaleString',
'toLocaleString',
]),
);

Expand Down Expand Up @@ -149,10 +150,14 @@ export function extend(mainArg: unknown, ...extraArgs: unknown[]): ExtMethods {

return true;
},
// toLocaleString(): string {
// // return dateExtensions.toLocaleString(mainArg as string, extraArgs);
// },
toLocaleString(): string {
return dateExtensions.toLocaleString(new Date(mainArg as string), extraArgs);
},
...stringExtensions.bind(mainArg as string, extraArgs as string[] | undefined),
...dateExtensions.bind(
new Date(mainArg as string),
extraArgs as number[] | string[] | boolean[] | undefined,
),
};

return extensions;
Expand Down
69 changes: 69 additions & 0 deletions packages/workflow/test/ExpressionExtensions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @jest-environment jsdom
*/

import { Expression, Workflow } from '../src';
import * as Helpers from './Helpers';
import { DateTime } from 'luxon';
import { extend } from '../src/Extensions';
import { DateExtensions } from '../src/Extensions/DateExtensions';

describe('Expression Extensions', () => {
describe('extend()', () => {
const nodeTypes = Helpers.NodeTypes();
const workflow = new Workflow({
nodes: [
{
name: 'node',
typeVersion: 1,
type: 'test.set',
id: 'uuid-1234',
position: [0, 0],
parameters: {},
},
],
connections: {},
active: false,
nodeTypes,
});
const expression = new Expression(workflow);

const evaluate = (value: string) =>
expression.getParameterValue(value, null, 0, 0, 'node', [], 'manual', 'America/New_York', {});

it('should be able to utilize date expression extension methods', () => {
const dateExtensions = (...args: any[]) => {
return extend(DateTime.now(), ...args) as unknown as DateExtensions;
};
const JUST_NOW_STRING_RESULT = 'just now';
// Date sensitive test case here so testing it to not be undefined should be enough
expect(evaluate('={{DateTime.now().isWeekend()}}')).not.toEqual(undefined);

expect(evaluate('={{DateTime.now().toTimeFromNow()}}')).toEqual(JUST_NOW_STRING_RESULT);

expect(evaluate('={{DateTime.now().begginingOf("week")}}')).toEqual(
dateExtensions('week').begginingOf.call({}, new Date(), 'week'),
);

expect(evaluate('={{ DateTime.now().endOfMonth() }}')).toEqual(
dateExtensions().endOfMonth.call({}, new Date()),
);

expect(evaluate('={{ DateTime.now().extract("day") }}')).toEqual(
dateExtensions('day').extract.call({}, new Date(), 'day'),
);

expect(evaluate('={{ DateTime.now().format("yyyy LLL dd") }}')).toEqual(
dateExtensions('yyyy LLL dd').format.call({}, new Date(), 'yyyy LLL dd'),
);

expect(evaluate('={{ DateTime.now().format("yyyy LLL dd") }}')).not.toEqual(
dateExtensions("HH 'hours and' mm 'minutes'").format.call(
{},
new Date(),
"HH 'hours and' mm 'minutes'",
),
);
});
});
});

0 comments on commit d65b6c6

Please sign in to comment.