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:
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user