Skip to content

Commit ecae4f6

Browse files
committed
feat: adding cookie (exist , not exist) conditions
1 parent 934d12c commit ecae4f6

4 files changed

Lines changed: 185 additions & 6 deletions

File tree

packages/rules/src/rule-manager.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {RuleManagerInterface} from './interfaces/rule-manager';
1515

1616
import {
1717
Config,
18+
CookieMatchingOptions,
1819
RuleElement,
1920
RuleAnd,
2021
RuleOrWhen,
@@ -165,15 +166,19 @@ export class RuleManager implements RuleManagerInterface {
165166
rule: rule
166167
})
167168
);
168-
return (
169+
const hasMatching =
169170
Object.prototype.hasOwnProperty.call(rule, 'matching') &&
170171
typeof rule.matching === 'object' &&
171172
Object.prototype.hasOwnProperty.call(rule.matching, 'match_type') &&
172173
typeof rule.matching.match_type === 'string' &&
173174
Object.prototype.hasOwnProperty.call(rule.matching, 'negated') &&
174-
typeof rule.matching.negated === 'boolean' &&
175-
Object.prototype.hasOwnProperty.call(rule, 'value')
176-
);
175+
typeof rule.matching.negated === 'boolean';
176+
if (!hasMatching) return false;
177+
const matchType = rule.matching.match_type as string;
178+
if (matchType === CookieMatchingOptions.EXISTS || matchType === CookieMatchingOptions.DOES_NOT_EXIST || matchType === 'not_exists') {
179+
return true;
180+
}
181+
return Object.prototype.hasOwnProperty.call(rule, 'value');
177182
}
178183

179184
/**
@@ -263,7 +268,7 @@ export class RuleManager implements RuleManagerInterface {
263268
if (this.isValidRule(rule)) {
264269
try {
265270
const negation = rule.matching.negated || false;
266-
const matching = rule.matching.match_type;
271+
const matching = rule.matching.match_type as string;
267272
if (this.getComparisonProcessorMethods().indexOf(matching) !== -1) {
268273
if (data && typeof data === 'object') {
269274
// Validate data key-value set.
@@ -314,7 +319,20 @@ export class RuleManager implements RuleManagerInterface {
314319
);
315320
}
316321
}
317-
} else {
322+
}
323+
// Key not found or data empty — for existence operators, evaluate with undefined
324+
if (
325+
matching === CookieMatchingOptions.EXISTS ||
326+
matching === CookieMatchingOptions.DOES_NOT_EXIST ||
327+
matching === 'not_exists'
328+
) {
329+
return this._comparisonProcessor[matching](
330+
undefined,
331+
rule.value,
332+
negation
333+
);
334+
}
335+
if (!objectNotEmpty(data) && !this.isUsingCustomInterface(data)) {
318336
this._loggerManager?.trace?.('RuleManager._processRuleItem()', {
319337
warn: ERROR_MESSAGES.RULE_DATA_NOT_VALID,
320338
data

packages/rules/tests/rule-manager.tests.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,124 @@ describe('RuleManager tests', function () {
534534
expect(ruleManager.isRuleMatched(data32, testRuleSet3)).to.equal(true);
535535
}
536536
);
537+
it('isValidRule should return true for exists rule without value', function () {
538+
expect(
539+
ruleManager.isValidRule({
540+
key: 'myCookie',
541+
matching: {
542+
match_type: 'exists',
543+
negated: false
544+
}
545+
})
546+
).to.equal(true);
547+
});
548+
it('isValidRule should return true for doesNotExist rule without value', function () {
549+
expect(
550+
ruleManager.isValidRule({
551+
key: 'myCookie',
552+
matching: {
553+
match_type: 'doesNotExist',
554+
negated: false
555+
}
556+
})
557+
).to.equal(true);
558+
});
559+
it('isRuleMatched should return true for exists when key is present', function () {
560+
const data = {myCookie: 'someValue'};
561+
const ruleSet = {
562+
OR: [
563+
{
564+
AND: [
565+
{
566+
OR_WHEN: [
567+
{
568+
key: 'myCookie',
569+
matching: {
570+
match_type: 'exists',
571+
negated: false
572+
},
573+
value: ''
574+
}
575+
]
576+
}
577+
]
578+
}
579+
]
580+
};
581+
expect(ruleManager.isRuleMatched(data, ruleSet)).to.equal(true);
582+
});
583+
it('isRuleMatched should return false for exists when key is not present', function () {
584+
const data = {otherCookie: 'someValue'};
585+
const ruleSet = {
586+
OR: [
587+
{
588+
AND: [
589+
{
590+
OR_WHEN: [
591+
{
592+
key: 'myCookie',
593+
matching: {
594+
match_type: 'exists',
595+
negated: false
596+
},
597+
value: ''
598+
}
599+
]
600+
}
601+
]
602+
}
603+
]
604+
};
605+
expect(ruleManager.isRuleMatched(data, ruleSet)).to.equal(false);
606+
});
607+
it('isRuleMatched should return true for doesNotExist when key is not present', function () {
608+
const data = {otherCookie: 'someValue'};
609+
const ruleSet = {
610+
OR: [
611+
{
612+
AND: [
613+
{
614+
OR_WHEN: [
615+
{
616+
key: 'myCookie',
617+
matching: {
618+
match_type: 'doesNotExist',
619+
negated: false
620+
},
621+
value: ''
622+
}
623+
]
624+
}
625+
]
626+
}
627+
]
628+
};
629+
expect(ruleManager.isRuleMatched(data, ruleSet)).to.equal(true);
630+
});
631+
it('isRuleMatched should return false for doesNotExist when key is present', function () {
632+
const data = {myCookie: 'someValue'};
633+
const ruleSet = {
634+
OR: [
635+
{
636+
AND: [
637+
{
638+
OR_WHEN: [
639+
{
640+
key: 'myCookie',
641+
matching: {
642+
match_type: 'doesNotExist',
643+
negated: false
644+
},
645+
value: ''
646+
}
647+
]
648+
}
649+
]
650+
}
651+
]
652+
};
653+
expect(ruleManager.isRuleMatched(data, ruleSet)).to.equal(false);
654+
});
537655
it('Should allow to change comparison processor on fly', function () {
538656
const customComparisonProcessor = {
539657
isTypeOf: function (

packages/utils/src/comparisons.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,26 @@ export class Comparisons {
151151
return this._returnNegationCheck(regExp.test(value), negation);
152152
}
153153

154+
static exists(
155+
value: string | number | undefined | null,
156+
testAgainst?: any,
157+
negation?: boolean
158+
): boolean {
159+
const valueExists = value !== undefined && value !== null && value !== '';
160+
return this._returnNegationCheck(valueExists, negation);
161+
}
162+
163+
static not_exists(
164+
value: string | number | undefined | null,
165+
testAgainst?: any,
166+
negation?: boolean
167+
): boolean {
168+
const valueNotExists = value === undefined || value === null || value === '';
169+
return this._returnNegationCheck(valueNotExists, negation);
170+
}
171+
172+
static doesNotExist = this.not_exists;
173+
154174
private static _returnNegationCheck(
155175
value: boolean,
156176
negation = false

packages/utils/tests/comparisons.tests.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,5 +319,28 @@ describe('Comparison Processor utils tests', function () {
319319
);
320320
expect(result).to.equal(false);
321321
});
322+
// Existence operator tests — 'exists' expects true when value is present,
323+
// 'not_exists' and 'doesNotExist' expect the inverse.
324+
const existenceCases: {input: any; label: string; negation?: boolean}[] = [
325+
{input: 'cookieValue', label: 'a non-empty string value'},
326+
{input: 123, label: 'a numeric value'},
327+
{input: undefined, label: 'undefined'},
328+
{input: null, label: 'null'},
329+
{input: '', label: 'empty string'},
330+
{input: 'cookieValue', label: 'non-empty string with negation', negation: true},
331+
{input: undefined, label: 'undefined with negation', negation: true}
332+
];
333+
for (const method of ['exists', 'not_exists', 'doesNotExist'] as const) {
334+
for (const {input, label, negation} of existenceCases) {
335+
const valuePresent = input !== undefined && input !== null && input !== '';
336+
const isExistsMethod = method === 'exists';
337+
const baseResult = isExistsMethod ? valuePresent : !valuePresent;
338+
const expected = negation ? !baseResult : baseResult;
339+
it(`${method} should return ${expected} for ${label}`, function () {
340+
const result = Comparisons[method](input, null, negation);
341+
expect(result).to.equal(expected);
342+
});
343+
}
344+
}
322345
/* eslint-enable */
323346
});

0 commit comments

Comments
 (0)