fix: Incorrect date parsing in matchesFilter (#11679)

The `matchesFilter` is a utility that checks the incoming payload
against a filter and returns `true` or `false`.
For the `greater_than` and `less_than` filter specifically, the date
parsing would fail when the timestamp was a 10 digit number.

This PR solves this by adding a `coerceToDate` method that tries to
parse the given value to a Date object as correctly as possible before
comparing.

Ref: https://github.com/chatwoot/utils/pull/53
This commit is contained in:
Shivam Mishra
2025-06-06 06:08:56 +05:30
committed by GitHub
parent 8bc00f707b
commit 27bce50210
4 changed files with 259 additions and 8 deletions

View File

@@ -47,6 +47,7 @@
* 3. Nested properties in custom_attributes (conversation_type, etc.)
*/
import jsonLogic from 'json-logic-js';
import { coerceToDate } from '@chatwoot/utils';
/**
* Gets a value from a conversation based on the attribute key
@@ -157,6 +158,20 @@ const contains = (filterValue, conversationValue) => {
return false;
};
/**
* Compares two date values using a comparison function
* @param {*} conversationValue - The conversation value to compare
* @param {*} filterValue - The filter value to compare against
* @param {Function} compareFn - The comparison function to apply
* @returns {Boolean} - Returns true if the comparison succeeds, false otherwise
*/
const compareDates = (conversationValue, filterValue, compareFn) => {
const conversationDate = coerceToDate(conversationValue);
const filterDate = coerceToDate(filterValue);
if (conversationDate === null || filterDate === null) return false;
return compareFn(conversationDate, filterDate);
};
/**
* Checks if a value matches a filter condition
* @param {*} conversationValue - The value to check
@@ -195,10 +210,10 @@ const matchesCondition = (conversationValue, filter) => {
return false; // We already handled null/undefined above
case 'is_greater_than':
return new Date(conversationValue) > new Date(filterValue);
return compareDates(conversationValue, filterValue, (a, b) => a > b);
case 'is_less_than':
return new Date(conversationValue) < new Date(filterValue);
return compareDates(conversationValue, filterValue, (a, b) => a < b);
case 'days_before': {
const today = new Date();
@@ -347,6 +362,7 @@ export const matchesFilters = (conversation, filters) => {
conversation,
filters[0].attribute_key
);
return matchesCondition(value, filters[0]);
}

View File

@@ -463,6 +463,241 @@ describe('filterHelpers', () => {
expect(matchesFilters(conversation, filters)).toBe(true);
});
// Test conversation with 10-digit timestamp (seconds) vs standard date filter
it('should match conversation with 10-digit timestamp against date string filter', () => {
const conversation = { created_at: 1647777600 }; // March 20, 2022 in seconds (10 digits)
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: '2022-03-19', // Standard YYYY-MM-DD format
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(true);
});
// Test conversation with 13-digit timestamp (milliseconds) vs standard date filter
it('should match conversation with 13-digit timestamp against date string filter', () => {
const conversation = { created_at: 1647777600000 }; // March 20, 2022 in milliseconds (13 digits)
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: '2022-03-19', // Standard YYYY-MM-DD format
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(true);
});
// Test conversation with string timestamp vs standard date filter
it('should match conversation with string 10-digit timestamp against date string filter', () => {
const conversation = { created_at: '1647777600' }; // March 20, 2022 as string (10 digits)
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: '2022-03-19', // Standard YYYY-MM-DD format
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(true);
});
// Test conversation with string 13-digit timestamp vs standard date filter
it('should match conversation with string 13-digit timestamp against date string filter', () => {
const conversation = { created_at: '1647777600000' }; // March 20, 2022 as string (13 digits)
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: '2022-03-19', // Standard YYYY-MM-DD format
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(true);
});
// Test conversation with mixed format vs standard date filter with time
it('should match conversation with numeric timestamp against ISO date string filter', () => {
const conversation = { created_at: 1647777600000 }; // March 20, 2022 12:00:00 GMT (numeric)
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: '2022-03-19T10:30:00Z', // Standard ISO format from filter
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(true);
});
// Test parseDate with date string without time (should default to 00:00:00)
it('should match conversation with is_greater_than operator using date string without time', () => {
const conversation = { created_at: 1647820800000 }; // March 21, 2022 00:00:00 GMT
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: '2022-03-20', // March 20, 2022 (should become 00:00:00)
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(true);
});
// Test parseDate with ISO date string
it('should match conversation with is_greater_than operator using ISO date string', () => {
const conversation = { created_at: 1647777600000 }; // March 20, 2022
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: '2022-03-19T00:00:00.000Z', // March 19, 2022 ISO format
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(true);
});
// Test parseDate with null/undefined values
it('should handle null filter values in date comparison', () => {
const conversation = { created_at: 1647777600000 }; // March 20, 2022
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: null,
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(false);
});
it('should handle undefined filter values in date comparison', () => {
const conversation = { created_at: 1647777600000 }; // March 20, 2022
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: undefined,
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(false);
});
// Test parseDate with invalid date strings
it('should handle invalid date strings in date comparison', () => {
const conversation = { created_at: 1647777600000 }; // March 20, 2022
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: 'invalid-date-string',
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(false);
});
it('should handle non-date string values in date comparison', () => {
const conversation = { created_at: 1647777600000 }; // March 20, 2022
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: 'not-a-date',
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(false);
});
// Test is_less_than with various date formats
it('should match conversation with is_less_than operator using numeric timestamp', () => {
const conversation = { created_at: 1647691200000 }; // March 19, 2022
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_less_than',
values: 1647777600, // March 20, 2022 as 10-digit timestamp
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(true);
});
it('should not match conversation with is_less_than operator when date is later', () => {
const conversation = { created_at: 1647864000000 }; // March 21, 2022
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_less_than',
values: '2022-03-20T12:00:00Z', // March 20, 2022 with time
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(false);
});
// Edge case: Test with conversation having string timestamp
it('should handle conversation with string timestamp value', () => {
const conversation = { created_at: '1647777600000' }; // March 20, 2022 as string
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: '2022-03-19',
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(true);
});
// Edge case: Test with conversation having 10-digit timestamp
it('should handle conversation with 10-digit timestamp value', () => {
const conversation = { created_at: 1647777600 }; // March 20, 2022 as seconds (10 digits)
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: '2022-03-19',
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(true);
});
// Test date string with different time formats
it('should handle date string with space-separated time', () => {
const conversation = { created_at: 1647777600000 }; // March 20, 2022
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: '2022-03-19 10:30:00', // Date with space-separated time
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(true);
});
// Test parseDate with object input (should return null and fail comparison)
it('should handle non-string, non-number filter values', () => {
const conversation = { created_at: 1647777600000 }; // March 20, 2022
const filters = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: { date: '2022-03-19' }, // Object instead of string/number
query_operator: 'and',
},
];
expect(matchesFilters(conversation, filters)).toBe(false);
});
describe('days_before operator', () => {
beforeEach(() => {
// Set the date to March 25, 2022

View File

@@ -34,7 +34,7 @@
"@breezystack/lamejs": "^1.2.7",
"@chatwoot/ninja-keys": "1.2.3",
"@chatwoot/prosemirror-schema": "1.1.1-next",
"@chatwoot/utils": "^0.0.45",
"@chatwoot/utils": "^0.0.46",
"@formkit/core": "^1.6.7",
"@formkit/vue": "^1.6.7",
"@hcaptcha/vue3-hcaptcha": "^1.3.0",

10
pnpm-lock.yaml generated
View File

@@ -23,8 +23,8 @@ importers:
specifier: 1.1.1-next
version: 1.1.1-next
'@chatwoot/utils':
specifier: ^0.0.45
version: 0.0.45
specifier: ^0.0.46
version: 0.0.46
'@formkit/core':
specifier: ^1.6.7
version: 1.6.7
@@ -406,8 +406,8 @@ packages:
'@chatwoot/prosemirror-schema@1.1.1-next':
resolution: {integrity: sha512-/M2qZ+ZF7GlQNt1riwVP499fvp3hxSqd5iy8hxyF9pkj9qQ+OKYn5JK+v3qwwqQY3IxhmNOn1Lp6tm7vstrd9Q==}
'@chatwoot/utils@0.0.45':
resolution: {integrity: sha512-zqmuri6MrEFAY1tLv7Z3HBy4Ig60LhSrLkEiHegVsOVSxPv4Bedq+xmAW7LphvcLNgbkkvu17MU91gvMVlpEHw==}
'@chatwoot/utils@0.0.46':
resolution: {integrity: sha512-a68CQ+aPFfyMr7dnXUUSt/kwHEazBd7Y8aidDZeDp5eL7sych7EpmT5XMTmhttlqMiRsmwETblXJJ2fBH6I44A==}
engines: {node: '>=10'}
'@codemirror/commands@6.7.0':
@@ -5255,7 +5255,7 @@ snapshots:
prosemirror-utils: 1.2.2(prosemirror-model@1.22.3)(prosemirror-state@1.4.3)
prosemirror-view: 1.34.1
'@chatwoot/utils@0.0.45':
'@chatwoot/utils@0.0.46':
dependencies:
date-fns: 2.30.0