mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-17 13:23:35 +00:00
Fix: automation (#1943)
* Fix(automation): Handles undefined values in conditions (#1933) * Fix(automation): Handles undefined values in conditions Addresses an issue where conditions with the 'not_equals' operator incorrectly evaluated undefined values. This change ensures that empty string comparisons correctly identify missing values in automation rules. * Fixes automation "not equals" logic Simplifies the 'not_equals' condition evaluation in automations by reusing the 'equals' condition, improving code readability and consistency. Fixes #1932 * Test(automation): Expanding filter condition testing Expanding test cases to cover various data types and edge case for each operators. Unexpected behavior have been mark with a TO_DO. * Fix(automation): Handles default filter case - Ensures that their is a default scenario. - Fixing Deepsource issue "No default cases in switch statements JS-0047" * Test(automation): Remove abstraction in test * move to test.each * extract a isEquivalent function * lowercase contains test --------- Co-authored-by: Philippe Allard-Rousse <philrousse@gmail.com>
This commit is contained in:
committed by
GitHub
parent
904db95f59
commit
efede83271
@@ -9,6 +9,7 @@ import * as oscClient from '../clients/osc.client.js';
|
|||||||
import * as httpClient from '../clients/http.client.js';
|
import * as httpClient from '../clients/http.client.js';
|
||||||
|
|
||||||
import { makeOSCAction, makeHTTPAction } from './testUtils.js';
|
import { makeOSCAction, makeHTTPAction } from './testUtils.js';
|
||||||
|
import { RuntimeState } from '../../../stores/runtimeState.js';
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
vi.mock('../../../classes/data-provider/DataProvider.js', () => {
|
vi.mock('../../../classes/data-provider/DataProvider.js', () => {
|
||||||
@@ -100,117 +101,440 @@ describe('testConditions()', () => {
|
|||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('equals operator', () => {
|
describe('equals', () => {
|
||||||
it('should compare two equal values', () => {
|
test.each([
|
||||||
const mockStore = makeRuntimeStateData({ clock: 10 });
|
{
|
||||||
const result = testConditions([{ field: 'clock', operator: 'equals', value: '10' }], 'all', mockStore);
|
description: 'number and string number',
|
||||||
expect(result).toBe(true);
|
value: '10',
|
||||||
});
|
state: 10,
|
||||||
|
},
|
||||||
it('string comparisons should be case insensitive', () => {
|
{
|
||||||
const mockStore = makeRuntimeStateData({ eventNow: { title: 'Title' } as PlayableEvent });
|
description: 'string and string',
|
||||||
const result = testConditions(
|
value: 'Title',
|
||||||
[{ field: 'eventNow.title', operator: 'equals', value: 'title' }],
|
state: 'Title',
|
||||||
'all',
|
},
|
||||||
mockStore,
|
{
|
||||||
);
|
description: 'null and empty string',
|
||||||
expect(result).toBe(true);
|
value: '',
|
||||||
});
|
state: null,
|
||||||
|
},
|
||||||
it('should check if a value does not exist', () => {
|
{
|
||||||
const mockStore = makeRuntimeStateData({ eventNow: null });
|
description: 'undefined and empty string',
|
||||||
const result = testConditions([{ field: 'eventNow.title', operator: 'equals', value: '' }], 'all', mockStore);
|
value: '',
|
||||||
expect(result).toBe(true);
|
state: undefined,
|
||||||
});
|
},
|
||||||
|
{
|
||||||
it('should handle trueness boolean comparisons', () => {
|
description: 'boolean true and string true',
|
||||||
const mockStore = makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent });
|
value: 'true',
|
||||||
|
state: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'boolean false equals string false',
|
||||||
|
value: 'false',
|
||||||
|
state: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'not case sensitive for strings',
|
||||||
|
value: 'title',
|
||||||
|
state: 'Title',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'not case sensitive for booleans',
|
||||||
|
value: 'TRUE',
|
||||||
|
state: true,
|
||||||
|
},
|
||||||
|
])('$description', ({ value, state }) => {
|
||||||
expect(
|
expect(
|
||||||
testConditions([{ field: 'eventNow.countToEnd', operator: 'equals', value: 'true' }], 'all', mockStore),
|
testConditions([{ field: 'test.path', operator: 'equals', value }], 'all', {
|
||||||
|
test: { path: state },
|
||||||
|
} as unknown as RuntimeState),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
testConditions([{ field: 'eventNow.countToEnd', operator: 'equals', value: 'false' }], 'all', mockStore),
|
testConditions([{ field: 'test.path', operator: 'not_equals', value }], 'all', {
|
||||||
).toBe(false);
|
test: { path: state },
|
||||||
});
|
} as unknown as RuntimeState),
|
||||||
|
|
||||||
it('should handle falseness boolean comparisons', () => {
|
|
||||||
const mockStore = makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent });
|
|
||||||
expect(
|
|
||||||
testConditions([{ field: 'eventNow.countToEnd', operator: 'equals', value: 'false' }], 'all', mockStore),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
testConditions([{ field: 'eventNow.countToEnd', operator: 'equals', value: 'true' }], 'all', mockStore),
|
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('not_equals operator', () => {
|
describe('not_equals', () => {
|
||||||
it('should check if two values are different', () => {
|
test.each([
|
||||||
const mockStore = makeRuntimeStateData({ clock: 10 });
|
{
|
||||||
const result = testConditions([{ field: 'clock', operator: 'not_equals', value: '11' }], 'all', mockStore);
|
description: 'boolean true and string false',
|
||||||
expect(result).toBe(true);
|
value: 'false',
|
||||||
|
state: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'boolean false and string true',
|
||||||
|
value: 'true',
|
||||||
|
state: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'number and non-numeric string',
|
||||||
|
value: 'lighting',
|
||||||
|
state: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'number and different number',
|
||||||
|
value: '100',
|
||||||
|
state: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: '10 and 11',
|
||||||
|
value: '11',
|
||||||
|
state: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: '10 and 5',
|
||||||
|
value: '5',
|
||||||
|
state: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: '10 and 1',
|
||||||
|
value: '1',
|
||||||
|
state: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'string and substring',
|
||||||
|
value: 'lighting',
|
||||||
|
state: 'testing-lighting-10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'string and different string',
|
||||||
|
value: 'sound',
|
||||||
|
state: 'testing-lighting-10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'null and non-empty string',
|
||||||
|
value: 'not-empty',
|
||||||
|
state: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'undefined and non-empty string',
|
||||||
|
value: 'not-empty',
|
||||||
|
state: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'undefined and " " string',
|
||||||
|
value: ' ',
|
||||||
|
state: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'boolean false and empty string',
|
||||||
|
value: '',
|
||||||
|
state: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: '0 and empty string',
|
||||||
|
value: '',
|
||||||
|
state: 0,
|
||||||
|
},
|
||||||
|
])('$description', ({ value, state }) => {
|
||||||
|
expect(
|
||||||
|
testConditions([{ field: 'test.path', operator: 'not_equals', value }], 'all', {
|
||||||
|
test: { path: state },
|
||||||
|
} as unknown as RuntimeState),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
testConditions([{ field: 'test.path', operator: 'equals', value }], 'all', {
|
||||||
|
test: { path: state },
|
||||||
|
} as unknown as RuntimeState),
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('greater_than operator', () => {
|
describe('greater_than', () => {
|
||||||
it('should check if the given value is smaller', () => {
|
test.each([
|
||||||
const mockStore = makeRuntimeStateData({ clock: 10 });
|
{
|
||||||
const result = testConditions([{ field: 'clock', operator: 'greater_than', value: '9' }], 'all', mockStore);
|
description: '10 > 5',
|
||||||
expect(result).toBe(true);
|
value: '5',
|
||||||
});
|
state: 10,
|
||||||
it('should handle values which are not numbers', () => {
|
},
|
||||||
const mockStore = makeRuntimeStateData({ clock: 10 });
|
{
|
||||||
const result = testConditions([{ field: 'clock', operator: 'greater_than', value: 'testing' }], 'all', mockStore);
|
description: '10 > 1',
|
||||||
expect(result).toBe(false);
|
value: '1',
|
||||||
|
state: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: '100 > 5',
|
||||||
|
value: '5',
|
||||||
|
state: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: '11 > 10',
|
||||||
|
value: '10',
|
||||||
|
state: 11,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: '0 > -1',
|
||||||
|
value: '-1',
|
||||||
|
state: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: '-100 > -150',
|
||||||
|
value: '-150',
|
||||||
|
state: -100,
|
||||||
|
},
|
||||||
|
])('$description', ({ value, state }) => {
|
||||||
|
expect(
|
||||||
|
testConditions([{ field: 'test.path', operator: 'greater_than', value }], 'all', {
|
||||||
|
test: { path: state },
|
||||||
|
} as unknown as RuntimeState),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
testConditions([{ field: 'test.path', operator: 'less_than', value }], 'all', {
|
||||||
|
test: { path: state },
|
||||||
|
} as unknown as RuntimeState),
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('less_than operator', () => {
|
describe('less_than', () => {
|
||||||
it('should check if the given value is larger', () => {
|
test.each([
|
||||||
const mockStore = makeRuntimeStateData({ clock: 10 });
|
{
|
||||||
const result = testConditions([{ field: 'clock', operator: 'less_than', value: '11' }], 'all', mockStore);
|
description: '10 > 5',
|
||||||
expect(result).toBe(true);
|
value: '10',
|
||||||
});
|
state: 5,
|
||||||
it('should handle values which are not numbers', () => {
|
},
|
||||||
const mockStore = makeRuntimeStateData({ clock: 10 });
|
{
|
||||||
const result = testConditions([{ field: 'clock', operator: 'less_than', value: 'testing' }], 'all', mockStore);
|
description: '10 > 1',
|
||||||
expect(result).toBe(false);
|
value: '10',
|
||||||
|
state: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: '100 > 5',
|
||||||
|
value: '100',
|
||||||
|
state: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: '11 > 10',
|
||||||
|
value: '11',
|
||||||
|
state: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: '0 > -1',
|
||||||
|
value: '0',
|
||||||
|
state: -1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: '-100 > -150',
|
||||||
|
value: '-100',
|
||||||
|
state: -150,
|
||||||
|
},
|
||||||
|
])('$description', ({ value, state }) => {
|
||||||
|
expect(
|
||||||
|
testConditions([{ field: 'test.path', operator: 'less_than', value }], 'all', {
|
||||||
|
test: { path: state },
|
||||||
|
} as unknown as RuntimeState),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
testConditions([{ field: 'test.path', operator: 'greater_than', value }], 'all', {
|
||||||
|
test: { path: state },
|
||||||
|
} as unknown as RuntimeState),
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('contains operator', () => {
|
describe('invalid and edge case for greater/less_than', () => {
|
||||||
it('should check if value contains given string', () => {
|
test.each([
|
||||||
const result = testConditions(
|
{
|
||||||
[{ field: 'eventNow.title', operator: 'contains', value: 'lighting' }],
|
description: 'number and non-numeric string',
|
||||||
'all',
|
value: 'lighting',
|
||||||
makeRuntimeStateData({ eventNow: makeOntimeEvent({ title: 'test-lighting' }) as PlayableEvent }),
|
state: 10,
|
||||||
);
|
},
|
||||||
expect(result).toBe(true);
|
{
|
||||||
|
description: '0 and empty string',
|
||||||
const result2 = testConditions(
|
value: '',
|
||||||
[{ field: 'eventNow.title', operator: 'contains', value: 'sound' }],
|
state: 0,
|
||||||
'all',
|
},
|
||||||
makeRuntimeStateData({ eventNow: makeOntimeEvent({ title: 'test-lighting' }) as PlayableEvent }),
|
{
|
||||||
);
|
description: 'equal values',
|
||||||
expect(result2).toBe(false);
|
value: '10',
|
||||||
|
state: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'boolean false and empty string',
|
||||||
|
value: '',
|
||||||
|
state: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'undefined and non-empty string',
|
||||||
|
value: 'not-empty',
|
||||||
|
state: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'null and non-empty string',
|
||||||
|
value: 'not-empty',
|
||||||
|
state: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'string and different string',
|
||||||
|
value: 'sound',
|
||||||
|
state: 'testing-lighting-10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'string and substring',
|
||||||
|
value: 'lighting',
|
||||||
|
state: 'testing-lighting-10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'boolean true and uppercase TRUE',
|
||||||
|
value: 'TRUE',
|
||||||
|
state: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'string and lowercase string',
|
||||||
|
value: 'title',
|
||||||
|
state: 'Title',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'boolean false and string true',
|
||||||
|
value: 'true',
|
||||||
|
state: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'boolean false and string false',
|
||||||
|
value: 'false',
|
||||||
|
state: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'boolean true and string false',
|
||||||
|
value: 'false',
|
||||||
|
state: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'string and string',
|
||||||
|
value: 'Title',
|
||||||
|
state: 'Title',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'null and empty string',
|
||||||
|
value: '',
|
||||||
|
state: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'undefined and empty string',
|
||||||
|
value: '',
|
||||||
|
state: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'boolean true and string true',
|
||||||
|
value: 'true',
|
||||||
|
state: true,
|
||||||
|
},
|
||||||
|
])('$description', ({ value, state }) => {
|
||||||
|
expect(
|
||||||
|
testConditions([{ field: 'test.path', operator: 'less_than', value }], 'all', {
|
||||||
|
test: { path: state },
|
||||||
|
} as unknown as RuntimeState),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
testConditions([{ field: 'test.path', operator: 'greater_than', value }], 'all', {
|
||||||
|
test: { path: state },
|
||||||
|
} as unknown as RuntimeState),
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('not_contains operator', () => {
|
describe('contains', () => {
|
||||||
it('should check if value does not contain given string', () => {
|
test.each([
|
||||||
const result = testConditions(
|
{
|
||||||
[{ field: 'eventNow.title', operator: 'not_contains', value: 'lighting' }],
|
description: 'substring in string',
|
||||||
'all',
|
value: 'lighting',
|
||||||
makeRuntimeStateData({ eventNow: makeOntimeEvent({ title: 'test-lighting' }) as PlayableEvent }),
|
state: 'testing-lighting-10',
|
||||||
);
|
},
|
||||||
expect(result).toBe(false);
|
{
|
||||||
|
description: 'number in string',
|
||||||
|
value: '10',
|
||||||
|
state: 'testing-lighting-10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'digit in string',
|
||||||
|
value: '1',
|
||||||
|
state: 'testing-lighting-10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'empty in empty',
|
||||||
|
value: '',
|
||||||
|
state: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'empty in not-empty',
|
||||||
|
value: '',
|
||||||
|
state: '12345', // TODO: is this intended
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'string equals string',
|
||||||
|
value: 'Title',
|
||||||
|
state: 'Title',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'case sensitive',
|
||||||
|
value: 'title',
|
||||||
|
state: 'Title',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'number does contain string',
|
||||||
|
value: '10',
|
||||||
|
state: 2105,
|
||||||
|
},
|
||||||
|
])('$description', ({ value, state }) => {
|
||||||
|
expect(
|
||||||
|
testConditions([{ field: 'test.path', operator: 'contains', value }], 'all', {
|
||||||
|
test: { path: state },
|
||||||
|
} as unknown as RuntimeState),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
testConditions([{ field: 'test.path', operator: 'not_contains', value }], 'all', {
|
||||||
|
test: { path: state },
|
||||||
|
} as unknown as RuntimeState),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const result2 = testConditions(
|
describe('not_contains', () => {
|
||||||
[{ field: 'eventNow.title', operator: 'not_contains', value: 'sound' }],
|
test.each([
|
||||||
'all',
|
{
|
||||||
makeRuntimeStateData({ eventNow: makeOntimeEvent({ title: 'test-lighting' }) as PlayableEvent }),
|
description: 'number does not contain substring',
|
||||||
);
|
value: '456',
|
||||||
expect(result2).toBe(true);
|
state: 12345,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'substring not in string',
|
||||||
|
value: 'sound',
|
||||||
|
state: 'testing-lighting-10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'string and undefined',
|
||||||
|
value: 'sound',
|
||||||
|
state: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'string and null',
|
||||||
|
value: 'sound',
|
||||||
|
state: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'boolean true and string true',
|
||||||
|
value: 'true',
|
||||||
|
state: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'boolean false and string false',
|
||||||
|
value: 'false',
|
||||||
|
state: false,
|
||||||
|
},
|
||||||
|
])('$description', ({ value, state }) => {
|
||||||
|
expect(
|
||||||
|
testConditions([{ field: 'test.path', operator: 'not_contains', value }], 'all', {
|
||||||
|
test: { path: state },
|
||||||
|
} as unknown as RuntimeState),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
testConditions([{ field: 'test.path', operator: 'contains', value }], 'all', {
|
||||||
|
test: { path: state },
|
||||||
|
} as unknown as RuntimeState),
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { isOntimeCloud } from '../../setup/environment.js';
|
|||||||
import { emitOSC } from './clients/osc.client.js';
|
import { emitOSC } from './clients/osc.client.js';
|
||||||
import { emitHTTP } from './clients/http.client.js';
|
import { emitHTTP } from './clients/http.client.js';
|
||||||
import { getAutomationsEnabled, getAutomations, getAutomationTriggers } from './automation.dao.js';
|
import { getAutomationsEnabled, getAutomations, getAutomationTriggers } from './automation.dao.js';
|
||||||
import { isBooleanEquals, isGreaterThan, isLessThan } from './automation.utils.js';
|
import { isContained, isEquivalent, isGreaterThan, isLessThan } from './automation.utils.js';
|
||||||
import { toOntimeAction } from './clients/ontime.client.js';
|
import { toOntimeAction } from './clients/ontime.client.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,29 +92,17 @@ export function testConditions(
|
|||||||
// we use loose equality to be able to check for converted values (eg '10' == 10)
|
// we use loose equality to be able to check for converted values (eg '10' == 10)
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case 'equals':
|
case 'equals':
|
||||||
// handle the case where we are comparing boolean strings
|
return isEquivalent(fieldValue, lowerCasedValue);
|
||||||
if (typeof fieldValue === 'boolean') {
|
|
||||||
return isBooleanEquals(fieldValue, lowerCasedValue);
|
|
||||||
}
|
|
||||||
// make string comparisons case insensitive
|
|
||||||
if (typeof fieldValue === 'string') {
|
|
||||||
return fieldValue.toLowerCase() === lowerCasedValue;
|
|
||||||
}
|
|
||||||
// overload the edge case where we use empty string to check if a value does not exist
|
|
||||||
if (value === '' && fieldValue === undefined) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return fieldValue == value;
|
|
||||||
case 'not_equals':
|
case 'not_equals':
|
||||||
return fieldValue != value;
|
return !isEquivalent(fieldValue, lowerCasedValue);
|
||||||
case 'greater_than':
|
case 'greater_than':
|
||||||
return isGreaterThan(fieldValue, value);
|
return isGreaterThan(fieldValue, value);
|
||||||
case 'less_than':
|
case 'less_than':
|
||||||
return isLessThan(fieldValue, value);
|
return isLessThan(fieldValue, value);
|
||||||
case 'contains':
|
case 'contains':
|
||||||
return typeof fieldValue === 'string' && fieldValue.includes(value);
|
return isContained(fieldValue, lowerCasedValue);
|
||||||
case 'not_contains':
|
case 'not_contains':
|
||||||
return typeof fieldValue === 'string' && !fieldValue.includes(value);
|
return !isContained(fieldValue, lowerCasedValue);
|
||||||
default: {
|
default: {
|
||||||
operator satisfies never;
|
operator satisfies never;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -148,6 +148,38 @@ const quickAliases: AliasesDefinition = {
|
|||||||
startedAt: { key: 'timer.startedAt', cb: (value: string) => formatDisplayFromString(value) },
|
startedAt: { key: 'timer.startedAt', cb: (value: string) => formatDisplayFromString(value) },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function isEquivalent(a: unknown, b: string): boolean {
|
||||||
|
// handle the case where we are comparing boolean strings
|
||||||
|
if (typeof a === 'boolean') {
|
||||||
|
return isBooleanEquals(a, b);
|
||||||
|
}
|
||||||
|
// make string comparisons case insensitive
|
||||||
|
if (typeof a === 'string') {
|
||||||
|
return a.toLowerCase() === b;
|
||||||
|
}
|
||||||
|
// overload the edge case where we use empty string to check if a value does not exist
|
||||||
|
// this also avoids the case where 0 == ''
|
||||||
|
if (b === '') {
|
||||||
|
return a === null || a === undefined;
|
||||||
|
}
|
||||||
|
return a == b;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isContained(a: unknown, b: string): boolean {
|
||||||
|
// handle the case where we are comparing boolean strings
|
||||||
|
if (typeof a === 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// make string comparisons case insensitive
|
||||||
|
if (typeof a === 'string') {
|
||||||
|
return a.toLowerCase().includes(b);
|
||||||
|
}
|
||||||
|
if (typeof a === 'number') {
|
||||||
|
return a.toString().toLowerCase().includes(b);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility encapsulates logic for comparing two strings which may encode numbers
|
* Utility encapsulates logic for comparing two strings which may encode numbers
|
||||||
* @example isGreaterThan('10', '5') // true
|
* @example isGreaterThan('10', '5') // true
|
||||||
|
|||||||
Reference in New Issue
Block a user