diff --git a/api/src/utils/adjust-date.ts b/api/src/utils/adjust-date.ts new file mode 100644 index 0000000000..e83301628a --- /dev/null +++ b/api/src/utils/adjust-date.ts @@ -0,0 +1,95 @@ +import { + addYears, + subWeeks, + subYears, + addWeeks, + subMonths, + addMonths, + subDays, + addDays, + subHours, + addHours, + subMinutes, + addMinutes, + subSeconds, + addSeconds, + addMilliseconds, + subMilliseconds, +} from 'date-fns'; +import { clone } from 'lodash'; + +/** + * Adjust a given date by a given change in duration. The adjustment value uses the exact same syntax + * and logic as Vercel's `ms`. + * + * The conversion is lifted straight from `ms`. + */ +export function adjustDate(date: Date, adjustment: string) { + date = clone(date); + + const subtract = adjustment.startsWith('-'); + + if (subtract || adjustment.startsWith('+')) { + adjustment = adjustment.substring(1); + } + + const match = + /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|months?|mth|mo|years?|yrs?|y)?$/i.exec( + adjustment + ); + + if (!match) { + return; + } + + const amount = parseFloat(match[1]); + const type = (match[2] || 'days').toLowerCase(); + + switch (type) { + case 'years': + case 'year': + case 'yrs': + case 'yr': + case 'y': + return subtract ? subYears(date, amount) : addYears(date, amount); + case 'months': + case 'month': + case 'mth': + case 'mo': + return subtract ? subMonths(date, amount) : addMonths(date, amount); + case 'weeks': + case 'week': + case 'w': + return subtract ? subWeeks(date, amount) : addWeeks(date, amount); + case 'days': + case 'day': + case 'd': + return subtract ? subDays(date, amount) : addDays(date, amount); + case 'hours': + case 'hour': + case 'hrs': + case 'hr': + case 'h': + return subtract ? subHours(date, amount) : addHours(date, amount); + case 'minutes': + case 'minute': + case 'mins': + case 'min': + case 'm': + return subtract ? subMinutes(date, amount) : addMinutes(date, amount); + case 'seconds': + case 'second': + case 'secs': + case 'sec': + case 's': + return subtract ? subSeconds(date, amount) : addSeconds(date, amount); + case 'milliseconds': + case 'millisecond': + case 'msecs': + case 'msec': + case 'ms': + return subtract ? subMilliseconds(date, amount) : addMilliseconds(date, amount); + default: + return undefined; + } +} diff --git a/api/src/utils/parse-filter.ts b/api/src/utils/parse-filter.ts index cf7f311ef9..9ebae88b97 100644 --- a/api/src/utils/parse-filter.ts +++ b/api/src/utils/parse-filter.ts @@ -1,5 +1,6 @@ import { Accountability, Filter } from '../types'; import { toArray } from '../utils/to-array'; +import { adjustDate } from './adjust-date'; import { deepMap } from './deep-map'; export function parseFilter(filter: Filter, accountability: Accountability | null): any { @@ -13,7 +14,14 @@ export function parseFilter(filter: Filter, accountability: Accountability | nul else return toArray(val); } - if (val === '$NOW') return new Date(); + if (val.startsWith('$NOW')) { + if (val.includes('(') && val.includes(')')) { + const adjustment = val.match(/\(([^)]+)\)/)?.[1]; + return adjustDate(new Date(), adjustment); + } + + return new Date(); + } if (val === '$CURRENT_USER') return accountability?.user || null; if (val === '$CURRENT_ROLE') return accountability?.role || null; diff --git a/docs/reference/filter-rules.md b/docs/reference/filter-rules.md index beefce5dc4..3c97868289 100644 --- a/docs/reference/filter-rules.md +++ b/docs/reference/filter-rules.md @@ -138,3 +138,5 @@ In addition to static values, you can also filter against _dynamic_ values using - `$CURRENT_USER` — The primary key of the currently authenticated user - `$CURRENT_ROLE` — The primary key of the role for the currently authenticated user - `$NOW` — The current timestamp +- `$NOW()` - The current timestamp plus/minus a given distance, for example `$NOW(-1 year)`, + `$NOW(+2 hours)`