forked from n8n-io/n8n
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(dt-functions): introduce date expression extensions (n8n-io#4045)
* 🎉 Add Date Extensions into the mix * ✨ Introduce additional date extension methods * ✅ Add Date Expression Extension tests * 🔧 Add ability to debug tests
- Loading branch information
Showing
4 changed files
with
239 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'", | ||
), | ||
); | ||
}); | ||
}); | ||
}); |