617 lines
20 KiB
JavaScript
617 lines
20 KiB
JavaScript
|
import {
|
||
|
parseDateString,
|
||
|
rankRules,
|
||
|
iterateIds,
|
||
|
Condition,
|
||
|
Action,
|
||
|
Rule,
|
||
|
RuleIndexer
|
||
|
} from './rules';
|
||
|
|
||
|
let fieldTypes = new Map(
|
||
|
Object.entries({
|
||
|
id: 'id',
|
||
|
date: 'date',
|
||
|
name: 'string',
|
||
|
category: 'string',
|
||
|
description: 'id',
|
||
|
description2: 'id',
|
||
|
amount: 'number'
|
||
|
})
|
||
|
);
|
||
|
|
||
|
describe('Condition', () => {
|
||
|
test('parses date formats correctly', () => {
|
||
|
expect(parseDateString('2020-08-10')).toEqual({
|
||
|
type: 'date',
|
||
|
date: '2020-08-10'
|
||
|
});
|
||
|
expect(parseDateString('2020-08')).toEqual({
|
||
|
type: 'month',
|
||
|
date: '2020-08'
|
||
|
});
|
||
|
expect(parseDateString('2020')).toEqual({
|
||
|
type: 'year',
|
||
|
date: '2020'
|
||
|
});
|
||
|
|
||
|
// Invalid dates
|
||
|
expect(parseDateString('2020-0')).toBe(null);
|
||
|
expect(parseDateString('2020-14-01')).toBe(null);
|
||
|
expect(parseDateString('2020-05-53')).toBe(null);
|
||
|
});
|
||
|
|
||
|
test('ops handles null fields', () => {
|
||
|
let cond = new Condition('contains', 'name', 'foo', null, fieldTypes);
|
||
|
expect(cond.eval({ name: null })).toBe(false);
|
||
|
|
||
|
cond = new Condition('oneOf', 'name', ['foo'], null, fieldTypes);
|
||
|
expect(cond.eval({ name: null })).toBe(false);
|
||
|
|
||
|
['gt', 'gte', 'lt', 'lte', 'isapprox'].forEach(op => {
|
||
|
let cond = new Condition(op, 'date', '2020-01-01', null, fieldTypes);
|
||
|
expect(cond.eval({ date: null })).toBe(false);
|
||
|
});
|
||
|
|
||
|
cond = new Condition('is', 'id', null, null, fieldTypes);
|
||
|
expect(cond.eval({ id: null })).toBe(true);
|
||
|
});
|
||
|
|
||
|
test('ops handles undefined fields', () => {
|
||
|
const spy = jest.spyOn(console, 'warn').mockImplementation();
|
||
|
|
||
|
let cond = new Condition('is', 'id', null, null, fieldTypes);
|
||
|
// null is strict and won't match undefined
|
||
|
expect(cond.eval({ name: 'James' })).toBe(false);
|
||
|
|
||
|
cond = new Condition('contains', 'name', 'foo', null, fieldTypes);
|
||
|
expect(cond.eval({ date: '2020-01-01' })).toBe(false);
|
||
|
|
||
|
spy.mockRestore();
|
||
|
});
|
||
|
|
||
|
test('date restricts operators for each type', () => {
|
||
|
expect(() => {
|
||
|
let cond = new Condition('isapprox', 'date', '2020-08', null, fieldTypes);
|
||
|
}).toThrow('Invalid date value for');
|
||
|
expect(() => {
|
||
|
let cond = new Condition('gt', 'date', '2020-08', null, fieldTypes);
|
||
|
}).toThrow('Invalid date value for');
|
||
|
expect(() => {
|
||
|
let cond = new Condition('gte', 'date', '2020-08', null, fieldTypes);
|
||
|
}).toThrow('Invalid date value for');
|
||
|
expect(() => {
|
||
|
let cond = new Condition('lt', 'date', '2020-08', null, fieldTypes);
|
||
|
}).toThrow('Invalid date value for');
|
||
|
expect(() => {
|
||
|
let cond = new Condition('lte', 'date', '2020-08', null, fieldTypes);
|
||
|
}).toThrow('Invalid date value for');
|
||
|
});
|
||
|
|
||
|
test('date conditions work with `is` operator', () => {
|
||
|
let cond = new Condition('is', 'date', '2020-08-10', null, fieldTypes);
|
||
|
expect(cond.eval({ date: '2020-08-05' })).toBe(false);
|
||
|
expect(cond.eval({ date: '2020-08-10' })).toBe(true);
|
||
|
|
||
|
cond = new Condition('is', 'date', '2020-08', null, fieldTypes);
|
||
|
expect(cond.eval({ date: '2020-08-05' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-08-10' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-09-10' })).toBe(false);
|
||
|
|
||
|
cond = new Condition('is', 'date', '2020', null, fieldTypes);
|
||
|
expect(cond.eval({ date: '2020-08-05' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-08-10' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-09-10' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2019-09-10' })).toBe(false);
|
||
|
|
||
|
// Approximate dates
|
||
|
cond = new Condition('isapprox', 'date', '2020-08-07', null, fieldTypes);
|
||
|
expect(cond.eval({ date: '2020-08-04' })).toBe(false);
|
||
|
expect(cond.eval({ date: '2020-08-05' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-08-09' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-08-10' })).toBe(false);
|
||
|
});
|
||
|
|
||
|
test('recurring date conditions work with `is` operator', () => {
|
||
|
let cond = new Condition(
|
||
|
'is',
|
||
|
'date',
|
||
|
{
|
||
|
start: '2019-01-01',
|
||
|
frequency: 'monthly',
|
||
|
patterns: [{ type: 'day', value: 15 }]
|
||
|
},
|
||
|
null,
|
||
|
fieldTypes
|
||
|
);
|
||
|
expect(cond.eval({ date: '2018-03-15' })).toBe(false);
|
||
|
expect(cond.eval({ date: '2019-03-15' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-05-15' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-06-15' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-06-10' })).toBe(false);
|
||
|
|
||
|
cond = new Condition(
|
||
|
'is',
|
||
|
'date',
|
||
|
{
|
||
|
start: '2018-01-12',
|
||
|
frequency: 'monthly',
|
||
|
interval: 3
|
||
|
},
|
||
|
null,
|
||
|
fieldTypes
|
||
|
);
|
||
|
expect(cond.eval({ date: '2019-01-12' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2019-04-12' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-07-12' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-06-12' })).toBe(false);
|
||
|
|
||
|
// Approximate dates
|
||
|
cond = new Condition(
|
||
|
'isapprox',
|
||
|
'date',
|
||
|
{
|
||
|
start: '2019-01-01',
|
||
|
frequency: 'monthly',
|
||
|
patterns: [{ type: 'day', value: 15 }]
|
||
|
},
|
||
|
null,
|
||
|
fieldTypes
|
||
|
);
|
||
|
expect(cond.eval({ date: '2019-03-12' })).toBe(false);
|
||
|
expect(cond.eval({ date: '2019-03-13' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2019-03-15' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2019-03-17' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2019-03-18' })).toBe(false);
|
||
|
expect(cond.eval({ date: '2019-04-15' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2019-05-15' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2019-05-17' })).toBe(true);
|
||
|
});
|
||
|
|
||
|
test('date conditions work with comparison operators', () => {
|
||
|
let cond = new Condition('gt', 'date', '2020-08-10', null, fieldTypes);
|
||
|
expect(cond.eval({ date: '2020-08-11' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-08-10' })).toBe(false);
|
||
|
|
||
|
cond = new Condition('gte', 'date', '2020-08-10', null, fieldTypes);
|
||
|
expect(cond.eval({ date: '2020-08-11' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-08-10' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-08-09' })).toBe(false);
|
||
|
|
||
|
cond = new Condition('lt', 'date', '2020-08-10', null, fieldTypes);
|
||
|
expect(cond.eval({ date: '2020-08-09' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-08-10' })).toBe(false);
|
||
|
|
||
|
cond = new Condition('lte', 'date', '2020-08-10', null, fieldTypes);
|
||
|
expect(cond.eval({ date: '2020-08-09' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-08-10' })).toBe(true);
|
||
|
expect(cond.eval({ date: '2020-08-11' })).toBe(false);
|
||
|
});
|
||
|
|
||
|
test('id works with all operators', () => {
|
||
|
let cond = new Condition('is', 'id', 'foo', null, fieldTypes);
|
||
|
expect(cond.eval({ id: 'foo' })).toBe(true);
|
||
|
expect(cond.eval({ id: 'FOO' })).toBe(true);
|
||
|
expect(cond.eval({ id: 'foo2' })).toBe(false);
|
||
|
|
||
|
cond = new Condition('oneOf', 'id', ['foo', 'bar'], null, fieldTypes);
|
||
|
expect(cond.eval({ id: 'foo' })).toBe(true);
|
||
|
expect(cond.eval({ id: 'FOO' })).toBe(true);
|
||
|
expect(cond.eval({ id: 'Bar' })).toBe(true);
|
||
|
expect(cond.eval({ id: 'bar2' })).toBe(false);
|
||
|
});
|
||
|
|
||
|
test('string works with all operators', () => {
|
||
|
let cond = new Condition('is', 'name', 'foo', null, fieldTypes);
|
||
|
expect(cond.eval({ name: 'foo' })).toBe(true);
|
||
|
expect(cond.eval({ name: 'FOO' })).toBe(true);
|
||
|
expect(cond.eval({ name: 'foo2' })).toBe(false);
|
||
|
|
||
|
cond = new Condition('oneOf', 'name', ['foo', 'bar'], null, fieldTypes);
|
||
|
expect(cond.eval({ name: 'foo' })).toBe(true);
|
||
|
expect(cond.eval({ name: 'FOO' })).toBe(true);
|
||
|
expect(cond.eval({ name: 'Bar' })).toBe(true);
|
||
|
expect(cond.eval({ name: 'bar2' })).toBe(false);
|
||
|
|
||
|
cond = new Condition('contains', 'name', 'foo', null, fieldTypes);
|
||
|
expect(cond.eval({ name: 'bar foo baz' })).toBe(true);
|
||
|
expect(cond.eval({ name: 'bar FOOb' })).toBe(true);
|
||
|
expect(cond.eval({ name: 'foo' })).toBe(true);
|
||
|
expect(cond.eval({ name: 'foob' })).toBe(true);
|
||
|
expect(cond.eval({ name: 'bfoo' })).toBe(true);
|
||
|
expect(cond.eval({ name: 'bfo' })).toBe(false);
|
||
|
expect(cond.eval({ name: 'f o o' })).toBe(false);
|
||
|
});
|
||
|
|
||
|
test('number validates value', () => {
|
||
|
let cond = new Condition('isapprox', 'amount', 34, null, fieldTypes);
|
||
|
|
||
|
expect(() => {
|
||
|
cond = new Condition('isapprox', 'amount', 'hello', null, fieldTypes);
|
||
|
}).toThrow('Value must be a number or between amount');
|
||
|
|
||
|
expect(() => {
|
||
|
cond = new Condition(
|
||
|
'is',
|
||
|
'amount',
|
||
|
{ num1: 0, num2: 10 },
|
||
|
null,
|
||
|
fieldTypes
|
||
|
);
|
||
|
}).toThrow('Invalid number value for');
|
||
|
|
||
|
cond = new Condition(
|
||
|
'isbetween',
|
||
|
'amount',
|
||
|
{ num1: 0, num2: 10 },
|
||
|
null,
|
||
|
fieldTypes
|
||
|
);
|
||
|
|
||
|
expect(() => {
|
||
|
cond = new Condition('isbetween', 'amount', 34.22, null, fieldTypes);
|
||
|
}).toThrow('Invalid between value for');
|
||
|
expect(() => {
|
||
|
cond = new Condition(
|
||
|
'isbetween',
|
||
|
'amount',
|
||
|
{ num1: 0 },
|
||
|
null,
|
||
|
fieldTypes
|
||
|
);
|
||
|
}).toThrow('Value must be a number or between amount');
|
||
|
});
|
||
|
|
||
|
test('number works with all operators', () => {
|
||
|
let cond = new Condition('is', 'amount', 155, null, fieldTypes);
|
||
|
expect(cond.eval({ amount: 155 })).toBe(true);
|
||
|
expect(cond.eval({ amount: 167 })).toBe(false);
|
||
|
|
||
|
cond = new Condition('isapprox', 'amount', 1535, null, fieldTypes);
|
||
|
expect(cond.eval({ amount: 1540 })).toBe(true);
|
||
|
expect(cond.eval({ amount: 1300 })).toBe(false);
|
||
|
expect(cond.eval({ amount: 1650 })).toBe(true);
|
||
|
expect(cond.eval({ amount: 1800 })).toBe(false);
|
||
|
|
||
|
cond = new Condition(
|
||
|
'isbetween',
|
||
|
'amount',
|
||
|
{ num1: 32, num2: 86 },
|
||
|
null,
|
||
|
fieldTypes
|
||
|
);
|
||
|
expect(cond.eval({ amount: 30 })).toBe(false);
|
||
|
expect(cond.eval({ amount: 32 })).toBe(true);
|
||
|
expect(cond.eval({ amount: 80 })).toBe(true);
|
||
|
expect(cond.eval({ amount: 86 })).toBe(true);
|
||
|
expect(cond.eval({ amount: 90 })).toBe(false);
|
||
|
|
||
|
cond = new Condition(
|
||
|
'isbetween',
|
||
|
'amount',
|
||
|
{ num1: -16, num2: -20 },
|
||
|
null,
|
||
|
fieldTypes
|
||
|
);
|
||
|
expect(cond.eval({ amount: -18 })).toBe(true);
|
||
|
expect(cond.eval({ amount: -12 })).toBe(false);
|
||
|
|
||
|
cond = new Condition('gt', 'amount', 1.55, null, fieldTypes);
|
||
|
expect(cond.eval({ amount: 1.55 })).toBe(false);
|
||
|
expect(cond.eval({ amount: 1.67 })).toBe(true);
|
||
|
expect(cond.eval({ amount: 1.5 })).toBe(false);
|
||
|
|
||
|
cond = new Condition('gte', 'amount', 1.55, null, fieldTypes);
|
||
|
expect(cond.eval({ amount: 1.55 })).toBe(true);
|
||
|
expect(cond.eval({ amount: 1.67 })).toBe(true);
|
||
|
expect(cond.eval({ amount: 1.5 })).toBe(false);
|
||
|
|
||
|
cond = new Condition('lt', 'amount', 1.55, null, fieldTypes);
|
||
|
expect(cond.eval({ amount: 1.55 })).toBe(false);
|
||
|
expect(cond.eval({ amount: 1.67 })).toBe(false);
|
||
|
expect(cond.eval({ amount: 1.5 })).toBe(true);
|
||
|
|
||
|
cond = new Condition('lte', 'amount', 1.55, null, fieldTypes);
|
||
|
expect(cond.eval({ amount: 1.55 })).toBe(true);
|
||
|
expect(cond.eval({ amount: 1.67 })).toBe(false);
|
||
|
expect(cond.eval({ amount: 1.5 })).toBe(true);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('Action', () => {
|
||
|
test('`set` operator sets a field', () => {
|
||
|
let action = new Action('set', 'name', 'James', null, fieldTypes);
|
||
|
let item = { name: 'Sarah' };
|
||
|
action.exec(item);
|
||
|
expect(item.name).toBe('James');
|
||
|
|
||
|
expect(() => {
|
||
|
let action = new Action('set', 'foo', 'James', null, new Map());
|
||
|
}).toThrow(/invalid field/i);
|
||
|
|
||
|
expect(() => {
|
||
|
let action = new Action('noop', 'name', 'James', null, fieldTypes);
|
||
|
}).toThrow(/invalid action operation/i);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('Rule', () => {
|
||
|
test('executing a rule works', () => {
|
||
|
let rule = new Rule({
|
||
|
conditions: [{ op: 'is', field: 'name', value: 'James' }],
|
||
|
actions: [{ op: 'set', field: 'name', value: 'Sarah' }],
|
||
|
fieldTypes
|
||
|
});
|
||
|
|
||
|
// This matches
|
||
|
expect(rule.exec({ name: 'James' })).toEqual({ name: 'Sarah' });
|
||
|
// It returns updates, not the whole object
|
||
|
expect(rule.exec({ name: 'James', date: '2018-10-01' })).toEqual({
|
||
|
name: 'Sarah'
|
||
|
});
|
||
|
// This does not match
|
||
|
expect(rule.exec({ name: 'James2' })).toEqual(null);
|
||
|
expect(rule.apply({ name: 'James2' })).toEqual({ name: 'James2' });
|
||
|
|
||
|
rule = new Rule({
|
||
|
conditions: [{ op: 'is', field: 'name', value: 'James' }],
|
||
|
actions: [
|
||
|
{ op: 'set', field: 'name', value: 'Sarah' },
|
||
|
{ op: 'set', field: 'category', value: 'Sarah' }
|
||
|
],
|
||
|
fieldTypes
|
||
|
});
|
||
|
|
||
|
expect(rule.exec({ name: 'James' })).toEqual({
|
||
|
name: 'Sarah',
|
||
|
category: 'Sarah'
|
||
|
});
|
||
|
expect(rule.exec({ name: 'James2' })).toEqual(null);
|
||
|
expect(rule.apply({ name: 'James2' })).toEqual({ name: 'James2' });
|
||
|
});
|
||
|
|
||
|
test('rule evaluates conditions as AND', () => {
|
||
|
let rule = new Rule({
|
||
|
conditions: [
|
||
|
{ op: 'is', field: 'name', value: 'James' },
|
||
|
{
|
||
|
op: 'isapprox',
|
||
|
field: 'date',
|
||
|
value: {
|
||
|
start: '2018-01-12',
|
||
|
frequency: 'monthly',
|
||
|
interval: 3
|
||
|
}
|
||
|
}
|
||
|
],
|
||
|
actions: [{ op: 'set', field: 'name', value: 'Sarah' }],
|
||
|
fieldTypes
|
||
|
});
|
||
|
|
||
|
expect(rule.exec({ name: 'James', date: '2018-01-12' })).toEqual({
|
||
|
name: 'Sarah'
|
||
|
});
|
||
|
expect(rule.exec({ name: 'James2', date: '2018-01-12' })).toEqual(null);
|
||
|
expect(rule.exec({ name: 'James', date: '2018-01-10' })).toEqual({
|
||
|
name: 'Sarah'
|
||
|
});
|
||
|
expect(rule.exec({ name: 'James', date: '2018-01-15' })).toEqual(null);
|
||
|
});
|
||
|
|
||
|
test('rules are deterministically ranked', () => {
|
||
|
let rule = (id, conditions) =>
|
||
|
new Rule({ id, conditions, actions: [], fieldTypes });
|
||
|
let expectOrder = (rules, ids) =>
|
||
|
expect(rules.map(r => r.getId())).toEqual(ids);
|
||
|
|
||
|
let rules = [
|
||
|
rule('id1', [{ op: 'contains', field: 'name', value: 'sar' }]),
|
||
|
rule('id2', [{ op: 'contains', field: 'name', value: 'jim' }]),
|
||
|
rule('id3', [{ op: 'is', field: 'name', value: 'James' }])
|
||
|
];
|
||
|
|
||
|
expectOrder(rankRules(rules), ['id1', 'id2', 'id3']);
|
||
|
|
||
|
rules = [
|
||
|
rule('id1', [{ op: 'contains', field: 'name', value: 'sar' }]),
|
||
|
rule('id2', [{ op: 'oneOf', field: 'name', value: ['jim', 'sar'] }]),
|
||
|
rule('id3', [{ op: 'is', field: 'name', value: 'James' }]),
|
||
|
rule('id4', [
|
||
|
{ op: 'is', field: 'name', value: 'James' },
|
||
|
{ op: 'gt', field: 'amount', value: 5 }
|
||
|
]),
|
||
|
rule('id5', [
|
||
|
{ op: 'is', field: 'name', value: 'James' },
|
||
|
{ op: 'gt', field: 'amount', value: 5 },
|
||
|
{ op: 'lt', field: 'amount', value: 10 }
|
||
|
])
|
||
|
];
|
||
|
expectOrder(rankRules(rules), ['id1', 'id4', 'id5', 'id2', 'id3']);
|
||
|
});
|
||
|
|
||
|
test('iterateIds finds all the ids', () => {
|
||
|
let rule = (id, conditions, actions = []) =>
|
||
|
new Rule({ id, conditions, actions, fieldTypes });
|
||
|
|
||
|
let rules = [
|
||
|
rule(
|
||
|
'first',
|
||
|
[{ op: 'is', field: 'description', value: 'id1' }],
|
||
|
[{ op: 'set', field: 'name', value: 'sar' }]
|
||
|
),
|
||
|
rule('second', [
|
||
|
{ op: 'oneOf', field: 'description', value: ['id2', 'id3'] }
|
||
|
]),
|
||
|
rule(
|
||
|
'third',
|
||
|
[{ op: 'is', field: 'name', value: 'James' }],
|
||
|
[{ op: 'set', field: 'description', value: 'id3' }]
|
||
|
),
|
||
|
rule('fourth', [
|
||
|
{ op: 'is', field: 'name', value: 'James' },
|
||
|
{ op: 'gt', field: 'amount', value: 5 }
|
||
|
]),
|
||
|
rule('fifth', [
|
||
|
{ op: 'is', field: 'description2', value: 'id5' },
|
||
|
{ op: 'gt', field: 'amount', value: 5 },
|
||
|
{ op: 'lt', field: 'amount', value: 10 }
|
||
|
])
|
||
|
];
|
||
|
|
||
|
let foundRules = [];
|
||
|
iterateIds(rules, 'description', (rule, value) => {
|
||
|
foundRules.push(rule.getId());
|
||
|
});
|
||
|
expect(foundRules).toEqual(['first', 'second', 'second', 'third']);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('RuleIndexer', () => {
|
||
|
test('indexing a single field works', () => {
|
||
|
let indexer = new RuleIndexer({ field: 'name' });
|
||
|
|
||
|
let rule = new Rule({
|
||
|
conditions: [{ op: 'is', field: 'name', value: 'James' }],
|
||
|
actions: [{ op: 'set', field: 'name', value: 'Sarah' }],
|
||
|
fieldTypes
|
||
|
});
|
||
|
indexer.index(rule);
|
||
|
|
||
|
let rule2 = new Rule({
|
||
|
conditions: [{ op: 'is', field: 'category', value: 'foo' }],
|
||
|
actions: [{ op: 'set', field: 'name', value: 'Sarah' }],
|
||
|
fieldTypes
|
||
|
});
|
||
|
indexer.index(rule2);
|
||
|
|
||
|
// rule2 always gets returned because it's not indexed and always
|
||
|
// needs to be run
|
||
|
expect(indexer.getApplicableRules({ name: 'James' })).toEqual(
|
||
|
new Set([rule, rule2])
|
||
|
);
|
||
|
expect(indexer.getApplicableRules({ name: 'James2' })).toEqual(
|
||
|
new Set([rule2])
|
||
|
);
|
||
|
expect(indexer.getApplicableRules({ amount: 15 })).toEqual(
|
||
|
new Set([rule2])
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('indexing using the firstchar method works', () => {
|
||
|
// A condition that references both of the fields
|
||
|
let indexer = new RuleIndexer({ field: 'category', method: 'firstchar' });
|
||
|
let rule = new Rule({
|
||
|
conditions: [
|
||
|
{ op: 'is', field: 'name', value: 'James' },
|
||
|
{ op: 'is', field: 'category', value: 'food' }
|
||
|
],
|
||
|
actions: [{ op: 'set', field: 'name', value: 'Sarah' }],
|
||
|
fieldTypes
|
||
|
});
|
||
|
indexer.index(rule);
|
||
|
|
||
|
let rule2 = new Rule({
|
||
|
conditions: [{ op: 'is', field: 'category', value: 'bars' }],
|
||
|
actions: [{ op: 'set', field: 'name', value: 'Sarah' }],
|
||
|
fieldTypes
|
||
|
});
|
||
|
indexer.index(rule2);
|
||
|
|
||
|
let rule3 = new Rule({
|
||
|
conditions: [{ op: 'is', field: 'date', value: '2020-01-20' }],
|
||
|
actions: [{ op: 'set', field: 'name', value: 'Sarah' }],
|
||
|
fieldTypes
|
||
|
});
|
||
|
indexer.index(rule3);
|
||
|
|
||
|
expect(indexer.rules.size).toBe(3);
|
||
|
expect(indexer.rules.get('f').size).toBe(1);
|
||
|
expect(indexer.rules.get('b').size).toBe(1);
|
||
|
expect(indexer.rules.get('*').size).toBe(1);
|
||
|
|
||
|
expect(
|
||
|
indexer.getApplicableRules({ name: 'James', category: 'food' })
|
||
|
).toEqual(new Set([rule, rule3]));
|
||
|
expect(
|
||
|
indexer.getApplicableRules({ name: 'James', category: 'f' })
|
||
|
).toEqual(new Set([rule, rule3]));
|
||
|
expect(
|
||
|
indexer.getApplicableRules({ name: 'James', category: 'foo' })
|
||
|
).toEqual(new Set([rule, rule3]));
|
||
|
expect(
|
||
|
indexer.getApplicableRules({ name: 'James', category: 'bars' })
|
||
|
).toEqual(new Set([rule2, rule3]));
|
||
|
expect(indexer.getApplicableRules({ name: 'James' })).toEqual(
|
||
|
new Set([rule3])
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('re-indexing a field works', () => {
|
||
|
let indexer = new RuleIndexer({ field: 'category', method: 'firstchar' });
|
||
|
|
||
|
let rule = new Rule({
|
||
|
id: 'id1',
|
||
|
conditions: [{ op: 'is', field: 'category', value: 'food' }],
|
||
|
actions: [{ op: 'set', field: 'name', value: 'Sarah' }],
|
||
|
fieldTypes
|
||
|
});
|
||
|
indexer.index(rule);
|
||
|
|
||
|
expect(indexer.rules.get('f').size).toBe(1);
|
||
|
expect(indexer.rules.get('*')).toBe(undefined);
|
||
|
expect(indexer.getApplicableRules({ category: 'alco' }).size).toBe(0);
|
||
|
expect(indexer.getApplicableRules({ category: 'food' }).size).toBe(1);
|
||
|
|
||
|
indexer.remove(rule);
|
||
|
|
||
|
expect(indexer.rules.get('f').size).toBe(0);
|
||
|
expect(indexer.getApplicableRules({ category: 'alco' }).size).toBe(0);
|
||
|
expect(indexer.getApplicableRules({ category: 'food' }).size).toBe(0);
|
||
|
|
||
|
rule = new Rule({
|
||
|
conditions: [{ op: 'is', field: 'category', value: 'alcohol' }],
|
||
|
actions: [{ op: 'set', field: 'name', value: 'Sarah' }],
|
||
|
fieldTypes
|
||
|
});
|
||
|
indexer.index(rule);
|
||
|
|
||
|
expect(indexer.rules.get('f').size).toBe(0);
|
||
|
expect(indexer.rules.get('a').size).toBe(1);
|
||
|
expect(indexer.getApplicableRules({ category: 'alco' }).size).toBe(1);
|
||
|
expect(indexer.getApplicableRules({ category: 'food' }).size).toBe(0);
|
||
|
});
|
||
|
|
||
|
test('indexing works with the oneOf operator', () => {
|
||
|
let indexer = new RuleIndexer({ field: 'name', method: 'firstchar' });
|
||
|
|
||
|
let rule = new Rule({
|
||
|
conditions: [
|
||
|
{ op: 'oneOf', field: 'name', value: ['James', 'Sarah', 'Evy'] }
|
||
|
],
|
||
|
actions: [{ op: 'set', field: 'category', value: 'Food' }],
|
||
|
fieldTypes
|
||
|
});
|
||
|
indexer.index(rule);
|
||
|
|
||
|
let rule2 = new Rule({
|
||
|
conditions: [{ op: 'is', field: 'name', value: 'Georgia' }],
|
||
|
actions: [{ op: 'set', field: 'category', value: 'Food' }],
|
||
|
fieldTypes
|
||
|
});
|
||
|
indexer.index(rule2);
|
||
|
|
||
|
expect(indexer.getApplicableRules({ name: 'James' })).toEqual(
|
||
|
new Set([rule])
|
||
|
);
|
||
|
expect(indexer.getApplicableRules({ name: 'Evy' })).toEqual(
|
||
|
new Set([rule])
|
||
|
);
|
||
|
expect(indexer.getApplicableRules({ name: 'Charlotte' })).toEqual(
|
||
|
new Set([])
|
||
|
);
|
||
|
expect(indexer.getApplicableRules({ name: 'Georgia' })).toEqual(
|
||
|
new Set([rule2])
|
||
|
);
|
||
|
});
|
||
|
});
|