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:
parent
8bc00f707b
commit
27bce50210
@ -47,6 +47,7 @@
|
|||||||
* 3. Nested properties in custom_attributes (conversation_type, etc.)
|
* 3. Nested properties in custom_attributes (conversation_type, etc.)
|
||||||
*/
|
*/
|
||||||
import jsonLogic from 'json-logic-js';
|
import jsonLogic from 'json-logic-js';
|
||||||
|
import { coerceToDate } from '@chatwoot/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a value from a conversation based on the attribute key
|
* Gets a value from a conversation based on the attribute key
|
||||||
@ -157,6 +158,20 @@ const contains = (filterValue, conversationValue) => {
|
|||||||
return false;
|
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
|
* Checks if a value matches a filter condition
|
||||||
* @param {*} conversationValue - The value to check
|
* @param {*} conversationValue - The value to check
|
||||||
@ -195,10 +210,10 @@ const matchesCondition = (conversationValue, filter) => {
|
|||||||
return false; // We already handled null/undefined above
|
return false; // We already handled null/undefined above
|
||||||
|
|
||||||
case 'is_greater_than':
|
case 'is_greater_than':
|
||||||
return new Date(conversationValue) > new Date(filterValue);
|
return compareDates(conversationValue, filterValue, (a, b) => a > b);
|
||||||
|
|
||||||
case 'is_less_than':
|
case 'is_less_than':
|
||||||
return new Date(conversationValue) < new Date(filterValue);
|
return compareDates(conversationValue, filterValue, (a, b) => a < b);
|
||||||
|
|
||||||
case 'days_before': {
|
case 'days_before': {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
@ -347,6 +362,7 @@ export const matchesFilters = (conversation, filters) => {
|
|||||||
conversation,
|
conversation,
|
||||||
filters[0].attribute_key
|
filters[0].attribute_key
|
||||||
);
|
);
|
||||||
|
|
||||||
return matchesCondition(value, filters[0]);
|
return matchesCondition(value, filters[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -463,6 +463,241 @@ describe('filterHelpers', () => {
|
|||||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
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', () => {
|
describe('days_before operator', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Set the date to March 25, 2022
|
// Set the date to March 25, 2022
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
"@breezystack/lamejs": "^1.2.7",
|
"@breezystack/lamejs": "^1.2.7",
|
||||||
"@chatwoot/ninja-keys": "1.2.3",
|
"@chatwoot/ninja-keys": "1.2.3",
|
||||||
"@chatwoot/prosemirror-schema": "1.1.1-next",
|
"@chatwoot/prosemirror-schema": "1.1.1-next",
|
||||||
"@chatwoot/utils": "^0.0.45",
|
"@chatwoot/utils": "^0.0.46",
|
||||||
"@formkit/core": "^1.6.7",
|
"@formkit/core": "^1.6.7",
|
||||||
"@formkit/vue": "^1.6.7",
|
"@formkit/vue": "^1.6.7",
|
||||||
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@ -23,8 +23,8 @@ importers:
|
|||||||
specifier: 1.1.1-next
|
specifier: 1.1.1-next
|
||||||
version: 1.1.1-next
|
version: 1.1.1-next
|
||||||
'@chatwoot/utils':
|
'@chatwoot/utils':
|
||||||
specifier: ^0.0.45
|
specifier: ^0.0.46
|
||||||
version: 0.0.45
|
version: 0.0.46
|
||||||
'@formkit/core':
|
'@formkit/core':
|
||||||
specifier: ^1.6.7
|
specifier: ^1.6.7
|
||||||
version: 1.6.7
|
version: 1.6.7
|
||||||
@ -406,8 +406,8 @@ packages:
|
|||||||
'@chatwoot/prosemirror-schema@1.1.1-next':
|
'@chatwoot/prosemirror-schema@1.1.1-next':
|
||||||
resolution: {integrity: sha512-/M2qZ+ZF7GlQNt1riwVP499fvp3hxSqd5iy8hxyF9pkj9qQ+OKYn5JK+v3qwwqQY3IxhmNOn1Lp6tm7vstrd9Q==}
|
resolution: {integrity: sha512-/M2qZ+ZF7GlQNt1riwVP499fvp3hxSqd5iy8hxyF9pkj9qQ+OKYn5JK+v3qwwqQY3IxhmNOn1Lp6tm7vstrd9Q==}
|
||||||
|
|
||||||
'@chatwoot/utils@0.0.45':
|
'@chatwoot/utils@0.0.46':
|
||||||
resolution: {integrity: sha512-zqmuri6MrEFAY1tLv7Z3HBy4Ig60LhSrLkEiHegVsOVSxPv4Bedq+xmAW7LphvcLNgbkkvu17MU91gvMVlpEHw==}
|
resolution: {integrity: sha512-a68CQ+aPFfyMr7dnXUUSt/kwHEazBd7Y8aidDZeDp5eL7sych7EpmT5XMTmhttlqMiRsmwETblXJJ2fBH6I44A==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
'@codemirror/commands@6.7.0':
|
'@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-utils: 1.2.2(prosemirror-model@1.22.3)(prosemirror-state@1.4.3)
|
||||||
prosemirror-view: 1.34.1
|
prosemirror-view: 1.34.1
|
||||||
|
|
||||||
'@chatwoot/utils@0.0.45':
|
'@chatwoot/utils@0.0.46':
|
||||||
dependencies:
|
dependencies:
|
||||||
date-fns: 2.30.0
|
date-fns: 2.30.0
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user