Skip to content

Commit dcc6dd3

Browse files
authored
feat: add exists operator (#42)
1 parent 6366ef0 commit dcc6dd3

7 files changed

Lines changed: 192 additions & 2 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ There are 4 types of operators you can use (evaluated in that order of precedenc
163163
- `nin: any[]` - True if *not* in an array of values. Comparison is done using the `===` operator
164164
- `inq: any[]` - True if in an array of values. Comparison is done using the `===` operator
165165
- `between: readonly [number, number] (as const)` - True if the value is between the two specified values: greater than or equal to first value and less than or equal to second value.
166+
- `exists: boolean` - True if the value is not null or undefined (when `exists: true`), or True if the value is null or undefined (when `exists: false`). Note: Falsy values like `0`, `false`, and `""` are considered as existing.
166167
- `{property: value}`
167168
- compares the property to that value (shorthand to the `eq` op, without the option to user math or refs to other properties)
168169

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-expression-eval",
3-
"version": "8.0.1",
3+
"version": "8.1.0",
44
"description": "json serializable rule engine / boolean expression evaluator",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/lib/evaluator.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
isNinCompareOp,
1616
isRegexCompareOp,
1717
isRegexiCompareOp,
18+
isExistsCompareOp,
1819
isWithRef, isMathOp,
1920
WithRef
2021
} from './typeGuards';
@@ -108,6 +109,10 @@ async function evaluateCompareOp<C extends Context, Ignore>(expressionValue: Ext
108109
const regexpiValue = computeValue(context, validation, expressionValue.regexpi, expressionKey);
109110
expressionStringAssertion(expressionKey, regexpiValue);
110111
return Boolean(new RegExp(regexpiValue, `i`).exec(contextValue));
112+
} else if (isExistsCompareOp(expressionValue)) {
113+
const shouldExist = expressionValue.exists;
114+
const doesExist = contextValue !== null && contextValue !== undefined;
115+
return shouldExist ? doesExist : !doesExist;
111116
} else if (isGtCompareOp(expressionValue)) {
112117
contextNumberAssertion(expressionKey, contextValue);
113118
const gtValue = computeValue(context, validation, expressionValue.gt, expressionKey);

src/lib/typeGuards.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
LteCompareOp,
1111
RegexCompareOp,
1212
RegexiCompareOp,
13+
ExistsCompareOp,
1314
NotCompareOp,
1415
NotEqualCompareOp,
1516
OrCompareOp, InqCompareOp, NinCompareOp, RuleFunctionsTable, RuleFunctionsParams, MathOp
@@ -90,6 +91,11 @@ export const isRegexiCompareOp = (op: ExtendedCompareOp<any, any, any>)
9091
return (op as RegexiCompareOp<any, any>).regexpi !== undefined;
9192
}
9293

94+
export const isExistsCompareOp = (op: ExtendedCompareOp<any, any, any>)
95+
: op is ExistsCompareOp => {
96+
return (op as ExistsCompareOp).exists !== undefined;
97+
}
98+
9399
export const isEqualCompareOp = (op: ExtendedCompareOp<any, any, any>)
94100
: op is EqualCompareOp<any, any, any> => {
95101
return (op as EqualCompareOp<any, any, any>).eq !== undefined;

src/test/evaluator.spec.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,169 @@ describe('evaluator', () => {
994994

995995
});
996996

997+
describe(`exists`, () => {
998+
it('should evaluate exists op to true when value exists', async () => {
999+
const expression = {
1000+
userId: {exists: true},
1001+
};
1002+
const context = {
1003+
timesCounter: 5,
1004+
userId: 'user@example.com',
1005+
};
1006+
const runOpts: CustomEvaluatorFuncRunOptions = {dryRun: false};
1007+
expect(await validate(expression, context, functionsTable, runOpts)).to.be.an('undefined');
1008+
expect(await evaluate(expression, context, functionsTable, runOpts)).to.eql(true);
1009+
});
1010+
1011+
it('should evaluate exists op to false when value is null', async () => {
1012+
const expression = {
1013+
userId: {exists: true},
1014+
};
1015+
const context = {
1016+
timesCounter: 5,
1017+
userId: null,
1018+
};
1019+
const runOpts: CustomEvaluatorFuncRunOptions = {dryRun: false};
1020+
expect(await evaluate(expression, context, {}, runOpts)).to.eql(false);
1021+
});
1022+
1023+
it('should evaluate exists op to false when value is undefined', async () => {
1024+
const expression = {
1025+
userId: {exists: true},
1026+
};
1027+
const context = {
1028+
timesCounter: 5,
1029+
userId: undefined,
1030+
};
1031+
const runOpts: CustomEvaluatorFuncRunOptions = {dryRun: false};
1032+
expect(await evaluate(expression, context, {}, runOpts)).to.eql(false);
1033+
});
1034+
1035+
it('should evaluate exists op to false for nested undefined property', async () => {
1036+
const expression = {
1037+
'nested.value': {exists: true},
1038+
};
1039+
const context = {
1040+
timesCounter: 5,
1041+
userId: 'user@example.com',
1042+
nested: {
1043+
otherValue: 'exists',
1044+
},
1045+
};
1046+
const runOpts: CustomEvaluatorFuncRunOptions = {dryRun: false};
1047+
expect(await evaluate(expression as any, context, {}, runOpts)).to.eql(false);
1048+
});
1049+
1050+
it('should evaluate exists:false to true when value is null', async () => {
1051+
const expression = {
1052+
userId: {exists: false},
1053+
};
1054+
const context = {
1055+
timesCounter: 5,
1056+
userId: null,
1057+
};
1058+
const runOpts: CustomEvaluatorFuncRunOptions = {dryRun: false};
1059+
expect(await evaluate(expression, context, {}, runOpts)).to.eql(true);
1060+
});
1061+
1062+
it('should evaluate exists:false to true when value is undefined', async () => {
1063+
const expression = {
1064+
userId: {exists: false},
1065+
};
1066+
const context = {
1067+
timesCounter: 5,
1068+
userId: undefined,
1069+
};
1070+
const runOpts: CustomEvaluatorFuncRunOptions = {dryRun: false};
1071+
expect(await evaluate(expression, context, {}, runOpts)).to.eql(true);
1072+
});
1073+
1074+
it('should evaluate exists:false to false when value exists', async () => {
1075+
const expression = {
1076+
userId: {exists: false},
1077+
};
1078+
const context = {
1079+
timesCounter: 5,
1080+
userId: 'user@example.com',
1081+
};
1082+
const runOpts: CustomEvaluatorFuncRunOptions = {dryRun: false};
1083+
expect(await validate(expression, context, functionsTable, runOpts)).to.be.an('undefined');
1084+
expect(await evaluate(expression, context, functionsTable, runOpts)).to.eql(false);
1085+
});
1086+
1087+
it('should consider falsy values as existing (0)', async () => {
1088+
const expression = {
1089+
timesCounter: {exists: true},
1090+
};
1091+
const context = {
1092+
timesCounter: 0,
1093+
userId: 'user@example.com',
1094+
};
1095+
const runOpts: CustomEvaluatorFuncRunOptions = {dryRun: false};
1096+
expect(await validate(expression, context, functionsTable, runOpts)).to.be.an('undefined');
1097+
expect(await evaluate(expression, context, functionsTable, runOpts)).to.eql(true);
1098+
});
1099+
1100+
it('should consider falsy values as existing (false)', async () => {
1101+
const expression = {
1102+
isActive: {exists: true},
1103+
};
1104+
const context = {
1105+
timesCounter: 5,
1106+
userId: 'user@example.com',
1107+
isActive: false,
1108+
};
1109+
const runOpts: CustomEvaluatorFuncRunOptions = {dryRun: false};
1110+
expect(await validate(expression, context, functionsTable, runOpts)).to.be.an('undefined');
1111+
expect(await evaluate(expression, context, functionsTable, runOpts)).to.eql(true);
1112+
});
1113+
1114+
it('should consider falsy values as existing (empty string)', async () => {
1115+
const expression = {
1116+
userId: {exists: true},
1117+
};
1118+
const context = {
1119+
timesCounter: 5,
1120+
userId: '',
1121+
};
1122+
const runOpts: CustomEvaluatorFuncRunOptions = {dryRun: false};
1123+
expect(await validate(expression, context, functionsTable, runOpts)).to.be.an('undefined');
1124+
expect(await evaluate(expression, context, functionsTable, runOpts)).to.eql(true);
1125+
});
1126+
1127+
it('should work with AND logical operator', async () => {
1128+
const expression = {
1129+
and: [
1130+
{userId: {exists: true}},
1131+
{userId: {regexpi: '^GROUP '}},
1132+
],
1133+
};
1134+
const context = {
1135+
timesCounter: 5,
1136+
userId: 'GROUP admin',
1137+
};
1138+
const runOpts: CustomEvaluatorFuncRunOptions = {dryRun: false};
1139+
expect(await validate(expression, context, functionsTable, runOpts)).to.be.an('undefined');
1140+
expect(await evaluate(expression, context, functionsTable, runOpts)).to.eql(true);
1141+
});
1142+
1143+
it('should work with AND logical operator - false case', async () => {
1144+
const expression = {
1145+
and: [
1146+
{userId: {exists: true}},
1147+
{userId: {regexpi: '^GROUP '}},
1148+
],
1149+
};
1150+
const context = {
1151+
timesCounter: 5,
1152+
userId: undefined,
1153+
};
1154+
const runOpts: CustomEvaluatorFuncRunOptions = {dryRun: false};
1155+
expect(await evaluate(expression as any, context, {}, runOpts)).to.eql(false);
1156+
});
1157+
1158+
});
1159+
9971160
describe(`lt`, () => {
9981161
it('should evaluate lt compare op to true', async () => {
9991162
const expression = {

src/test/types/evaluator.test-d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,16 @@ expectType<TestExpressionEval>(new ExpressionHandler<Context, ExpressionFunction
172172
expectError(new ExpressionHandler<Context, ExpressionFunction,
173173
Ignore, CustomEvaluatorFuncRunOptions>({timesCounter: {regexpi: 'sdf'}}, functions));
174174

175+
// exists
176+
expectType<TestExpressionEval>(new ExpressionHandler<Context, ExpressionFunction,
177+
Ignore, CustomEvaluatorFuncRunOptions>
178+
({userId: {exists: true}}, functions));
179+
expectType<TestExpressionEval>(new ExpressionHandler<Context, ExpressionFunction,
180+
Ignore, CustomEvaluatorFuncRunOptions>
181+
({userId: {exists: false}}, functions));
182+
expectError(new ExpressionHandler<Context, ExpressionFunction,
183+
Ignore, CustomEvaluatorFuncRunOptions>({userId: {exists: 'sdf'}}, functions));
184+
175185
// between
176186
expectType<TestExpressionEval>(new ExpressionHandler<Context, ExpressionFunction,
177187
Ignore, CustomEvaluatorFuncRunOptions>

src/types/evaluator.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ export interface RegexiCompareOp<C extends Context, Ignore> {
8282
regexpi: string | PropertyRef<C, Ignore, string>;
8383
}
8484

85+
export interface ExistsCompareOp {
86+
exists: boolean;
87+
}
88+
8589
export type FuncCompares<C extends Context, F extends FunctionsTable<C, CustomEvaluatorFuncRunOptions>,
8690
CustomEvaluatorFuncRunOptions> = {
8791
[K in keyof F]: FuncCompareOp<C, F, K, CustomEvaluatorFuncRunOptions>;
@@ -99,7 +103,8 @@ export type StringCompareOps<C extends Context, Ignore, V extends Primitive> =
99103

100104
export type ExtendedCompareOp<C extends Context, Ignore, V extends Primitive> =
101105
EqualCompareOp<C, Ignore, V> | NotEqualCompareOp<C, Ignore, V> | InqCompareOp<C, Ignore, V> |
102-
NinCompareOp<C, Ignore, V> | NumberCompareOps<C, Ignore, V> | StringCompareOps<C, Ignore, V>;
106+
NinCompareOp<C, Ignore, V> | NumberCompareOps<C, Ignore, V> | StringCompareOps<C, Ignore, V> |
107+
ExistsCompareOp;
103108

104109
export type PropertyCompareOps<C extends Context, Ignore> = {
105110
[K in StringPaths<C, Ignore>]:

0 commit comments

Comments
 (0)