fix(workflow): stop relative date filter from crashing on empty/invalid date values (#22384)

## Problem

A workflow **Filter** step on a `DATE`/`DATE_TIME` field using
`IS_RELATIVE` crashes with a `RangeError` when the referenced step
output is empty or invalid. The empty value is coerced into an `Invalid
Date` (`new Date("undefined")`),
whose `.getTime()` is `NaN`, and
`Temporal.Instant.fromEpochMilliseconds(NaN)` throws, failing the
affected workflow runs.

This is a latent regression from the Date → Temporal migration (#16544):
the previous `date-fns` implementation silently returned `false` on an
invalid date, but Temporal is strict and throws. The guard was never
carried over.

## Fix

Validate the coerced date once at the boundary in `evaluateDateFilter` —
the single place arbitrary/empty step output is turned into a `Date`. An
unparseable date now resolves to "does not match" for every comparison
operand (`IS`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`,
`IS_AFTER`, `IS_RELATIVE`), restoring the pre-migration contract.
`IS_EMPTY` / `IS_NOT_EMPTY` are intentionally excluded so emptiness is
still evaluated on the raw operand.

## Tests
Added a parameterized regression test covering every date comparison
operand with empty and missing step output, asserting no throw and a
`false` result.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22384?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Abdul Rahman
2026-07-01 13:01:25 +05:30
committed by GitHub
parent 18c0d117a3
commit 59d708e324
2 changed files with 51 additions and 6 deletions
@@ -1042,6 +1042,45 @@ describe('evaluateFilterConditions', () => {
true,
);
});
it.each([
ViewFilterOperand.IS,
ViewFilterOperand.IS_IN_PAST,
ViewFilterOperand.IS_IN_FUTURE,
ViewFilterOperand.IS_TODAY,
ViewFilterOperand.IS_BEFORE,
ViewFilterOperand.IS_AFTER,
ViewFilterOperand.IS_RELATIVE,
])(
'should not match and not throw when the left operand is an empty/invalid date (%s)',
(operand) => {
const relativeValue = JSON.stringify({
direction: 'PAST',
amount: 1,
unit: 'DAY',
});
const emptyStepOutputFilter = createFilter(
operand,
'',
relativeValue,
'DATE',
);
const missingStepOutputFilter = createFilter(
operand,
undefined,
relativeValue,
'DATE_TIME',
);
expect(
evaluateFilterConditions({ filters: [emptyStepOutputFilter] }),
).toBe(false);
expect(
evaluateFilterConditions({ filters: [missingStepOutputFilter] }),
).toBe(false);
},
);
});
describe('currency operands', () => {
@@ -216,8 +216,20 @@ function evaluateBooleanFilter(filter: ResolvedFilter): boolean {
function evaluateDateFilter(filter: ResolvedFilter): boolean {
// TODO: refactor this with Temporal
if (filter.operand === ViewFilterOperand.IS_EMPTY) {
return !isDefined(filter.leftOperand) || filter.leftOperand === '';
}
if (filter.operand === ViewFilterOperand.IS_NOT_EMPTY) {
return isDefined(filter.leftOperand) && filter.leftOperand !== '';
}
const dateLeftValue = new Date(String(filter.leftOperand));
if (Number.isNaN(dateLeftValue.getTime())) {
return false;
}
switch (filter.operand) {
case ViewFilterOperand.IS:
return (
@@ -245,12 +257,6 @@ function evaluateDateFilter(filter: ResolvedFilter): boolean {
new Date(String(filter.rightOperand)).getTime()
);
case ViewFilterOperand.IS_EMPTY:
return !isDefined(filter.leftOperand) || filter.leftOperand === '';
case ViewFilterOperand.IS_NOT_EMPTY:
return isDefined(filter.leftOperand) && filter.leftOperand !== '';
case ViewFilterOperand.IS_RELATIVE:
return parseAndEvaluateRelativeDateFilter({
dateToCheck: dateLeftValue,