diff --git a/packages/flag-evaluation/src/index.ts b/packages/flag-evaluation/src/index.ts index 293d43f6..49c7e9af 100644 --- a/packages/flag-evaluation/src/index.ts +++ b/packages/flag-evaluation/src/index.ts @@ -116,6 +116,7 @@ export interface ContextFilter { field: string; operator: ContextFilterOperator; values?: string[]; + valueSet?: Set; } /** @@ -286,6 +287,7 @@ export function evaluate( fieldValue: string, operator: ContextFilterOperator, values: string[], + valueSet?: Set, ): boolean { const value = values[0]; @@ -331,9 +333,11 @@ export function evaluate( case "IS_NOT": return fieldValue !== value; case "ANY_OF": - return values.includes(fieldValue); + return valueSet ? valueSet.has(fieldValue) : values.includes(fieldValue); case "NOT_ANY_OF": - return !values.includes(fieldValue); + return valueSet + ? !valueSet.has(fieldValue) + : !values.includes(fieldValue); case "IS_TRUE": return fieldValue == "true"; case "IS_FALSE": @@ -362,6 +366,7 @@ function evaluateRecursively( context[filter.field], filter.operator, filter.values || [], + filter.valueSet, ); case "rolloutPercentage": { if (!(filter.partialRolloutAttribute in context)) { @@ -463,3 +468,47 @@ export function evaluateFeatureRules({ missingContextFields, }; } + +export function newEvaluator(rules: Rule[]) { + function translateRule(rule: RuleFilter): RuleFilter { + if (rule.type === "group") { + return { + ...rule, + filters: rule.filters.map(translateRule), + }; + } + + if ( + rule.type === "context" && + (rule.operator === "ANY_OF" || rule.operator === "NOT_ANY_OF") + ) { + return { + ...rule, + valueSet: new Set(rule.values ?? []), + }; + } + + return { ...rule }; + } + + const translatedRules = rules.map((rule) => { + const { filter } = rule; + const translatedFilter = translateRule(filter); + + return { + ...rule, + filter: translatedFilter, + }; + }); + + return function evaluateOptimized( + context: Record, + featureKey: string, + ) { + return evaluateFeatureRules({ + context, + featureKey, + rules: translatedRules, + }); + }; +} diff --git a/packages/flag-evaluation/test/index.test.ts b/packages/flag-evaluation/test/index.test.ts index 7739f009..cb87eca5 100644 --- a/packages/flag-evaluation/test/index.test.ts +++ b/packages/flag-evaluation/test/index.test.ts @@ -6,6 +6,7 @@ import { EvaluationParams, flattenJSON, hashInt, + newEvaluator, unflattenJSON, } from "../src"; @@ -239,6 +240,115 @@ describe("evaluate feature targeting integration ", () => { ruleEvaluationResults: [false], }); }); + + it("evaluates optimized rule evaluations correctly", async () => { + const res = newEvaluator([ + { + value: true, + filter: { + type: "group", + operator: "and", + filters: [ + { + type: "context", + field: "company.id", + operator: "IS", + values: ["company1"], + }, + { + type: "rolloutPercentage", + key: "flag", + partialRolloutAttribute: "company.id", + partialRolloutThreshold: 99999, + }, + { + type: "group", + operator: "or", + filters: [ + { + type: "context", + field: "company.id", + operator: "ANY_OF", + values: ["company2"], + }, + { + type: "negation", + filter: { + type: "context", + field: "company.id", + operator: "IS", + values: ["company3"], + }, + }, + ], + }, + { + type: "negation", + filter: { + type: "constant", + value: false, + }, + }, + ], + }, + }, + ])( + { + "company.id": "company1", + }, + "feature", + ); + + expect(res).toEqual({ + value: true, + context: { + "company.id": "company1", + }, + featureKey: "feature", + missingContextFields: [], + reason: "rule #0 matched", + ruleEvaluationResults: [true], + }); + }); + + it.each([ + { + context: { "company.id": "company1" }, + expected: true, + }, + { + context: { "company.id": "company2" }, + expected: true, + }, + { + context: { "company.id": "company3" }, + expected: false, + }, + ])( + "%#: evaluates optimized rule evaluations correctly", + async ({ context, expected }) => { + const evaluator = newEvaluator([ + { + value: true, + filter: { + type: "group", + operator: "and", + filters: [ + { + type: "context", + field: "company.id", + operator: "ANY_OF", + values: ["company1", "company2"], + }, + ], + }, + }, + ]); + + const res = evaluator(context, "feature"); + expect(res.value ?? false).toEqual(expected); + }, + ); }); describe("operator evaluation", () => {