Relative date filter JSON format mismatch causes parse failure on /objects/meetings page

https://sonarly.com/issue/24634?type=bug

A relative date filter value stored in JSON format (`{"unit":"DAY","amount":7,"direction":"NEXT"}`) cannot be parsed by the regex-based `relativeDateFilterStringifiedSchema` which expects the format `NEXT_7_DAY;;timezone;;`, causing a crash on the /objects/meetings page when resolving view filters into GraphQL operation filters.

Fix: Three changes fix both the source of bad data and handle existing bad data:

1. **WorkflowStepFilterValueInput.tsx (line 111)**: Changed `JSON.stringify(newRelativeDateFilter)` to `stringifyRelativeDateFilter(newRelativeDateFilter)`. This ensures new workflow step filter values are serialized in the expected regex-stringified format (`NEXT_7_DAY;;timezone;;`) instead of raw JSON. The import was already present on line 12.

2. **resolveRelativeDateTimeFilterStringified.ts**: Added `safeParseRelativeDateFilterJSONStringified` as a fallback when the regex-based `relativeDateFilterStringifiedSchema.safeParse()` fails. This handles existing JSON-format values already persisted in user view filters without breaking the primary parsing path.

3. **resolveRelativeDateFilterStringified.ts**: Same fallback added to the DATE variant (sibling of the DATE_TIME resolver) for consistency, since it has the same vulnerability.

4. **Test file**: Added two test cases verifying that both resolvers correctly handle JSON-stringified filter values as fallback input.

The `safeParseRelativeDateFilterJSONStringified` utility already existed in twenty-shared and uses Zod schema validation, so the fallback is safe and well-validated.
This commit is contained in:
Sonarly Claude Code 2026-04-13 20:03:09 +00:00
parent 455022f652
commit 233f7a7d17
4 changed files with 47 additions and 17 deletions

View file

@ -108,7 +108,7 @@ export const WorkflowStepFilterValueInput = ({
upsertStepFilterSettings({
stepFilterToUpsert: {
...stepFilter,
value: JSON.stringify(newRelativeDateFilter),
value: stringifyRelativeDateFilter(newRelativeDateFilter),
},
});
};

View file

@ -33,6 +33,17 @@ describe('resolveRelativeDateFilterStringified', () => {
expect(result).not.toBeNull();
expect(result?.direction).toBe('NEXT');
});
it('should resolve a JSON-stringified filter as fallback', () => {
const result = resolveRelativeDateFilterStringified(
JSON.stringify({ direction: 'PAST', amount: 7, unit: 'DAY' }),
);
expect(result).not.toBeNull();
expect(result?.direction).toBe('PAST');
expect(result?.start).toBeDefined();
expect(result?.end).toBeDefined();
});
});
describe('resolveRelativeDateTimeFilterStringified', () => {
@ -56,4 +67,15 @@ describe('resolveRelativeDateTimeFilterStringified', () => {
expect(result?.start).toBeDefined();
expect(result?.end).toBeDefined();
});
it('should resolve a JSON-stringified filter as fallback', () => {
const result = resolveRelativeDateTimeFilterStringified(
JSON.stringify({ direction: 'NEXT', amount: 7, unit: 'DAY' }),
);
expect(result).not.toBeNull();
expect(result?.direction).toBe('NEXT');
expect(result?.start).toBeDefined();
expect(result?.end).toBeDefined();
});
});

View file

@ -1,5 +1,6 @@
import { relativeDateFilterStringifiedSchema } from '@/utils/filter/dates/utils/relativeDateFilterStringifiedSchema';
import { resolveRelativeDateFilter } from '@/utils/filter/dates/utils/resolveRelativeDateFilter';
import { safeParseRelativeDateFilterJSONStringified } from '@/utils/safeParseRelativeDateFilterJSONStringified';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'class-validator';
import { Temporal } from 'temporal-polyfill';
@ -16,12 +17,14 @@ export const resolveRelativeDateFilterStringified = (
relativeDateFilterStringified,
);
if (!relativeDateFilterParseResult.success) {
const relativeDateFilter = relativeDateFilterParseResult.success
? relativeDateFilterParseResult.data
: safeParseRelativeDateFilterJSONStringified(relativeDateFilterStringified);
if (!isDefined(relativeDateFilter)) {
return null;
}
const relativeDateFilter = relativeDateFilterParseResult.data;
const referenceTodayZonedDateTime = isDefined(relativeDateFilter.timezone)
? Temporal.Now.zonedDateTimeISO(relativeDateFilter.timezone)
: Temporal.Now.zonedDateTimeISO();

View file

@ -1,5 +1,6 @@
import { relativeDateFilterStringifiedSchema } from '@/utils/filter/dates/utils/relativeDateFilterStringifiedSchema';
import { resolveRelativeDateTimeFilter } from '@/utils/filter/dates/utils/resolveRelativeDateTimeFilter';
import { safeParseRelativeDateFilterJSONStringified } from '@/utils/safeParseRelativeDateFilterJSONStringified';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'class-validator';
import { Temporal } from 'temporal-polyfill';
@ -16,20 +17,24 @@ export const resolveRelativeDateTimeFilterStringified = (
relativeDateTimeFilterStringified,
);
if (relativeDateFilterParseResult.success) {
const relativeDateFilter = relativeDateFilterParseResult.data;
const relativeDateFilter = relativeDateFilterParseResult.success
? relativeDateFilterParseResult.data
: safeParseRelativeDateFilterJSONStringified(
relativeDateTimeFilterStringified,
);
const referenceTodayZonedDateTime = isDefined(relativeDateFilter.timezone)
? Temporal.Now.zonedDateTimeISO(relativeDateFilter.timezone)
: Temporal.Now.zonedDateTimeISO();
const relativeDateFilterWithDateRange = resolveRelativeDateTimeFilter(
relativeDateFilter,
referenceTodayZonedDateTime.round({ smallestUnit: 'second' }),
);
return relativeDateFilterWithDateRange;
} else {
if (!isDefined(relativeDateFilter)) {
return null;
}
const referenceTodayZonedDateTime = isDefined(relativeDateFilter.timezone)
? Temporal.Now.zonedDateTimeISO(relativeDateFilter.timezone)
: Temporal.Now.zonedDateTimeISO();
const relativeDateFilterWithDateRange = resolveRelativeDateTimeFilter(
relativeDateFilter,
referenceTodayZonedDateTime.round({ smallestUnit: 'second' }),
);
return relativeDateFilterWithDateRange;
};