@bucketco/flag-evaluation
Advanced tools
Comparing version 0.0.4 to 0.0.5
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.evaluate = exports.evaluateRuleWithContext = exports.rejectedDueToPartialRollout = exports.hashInt = exports.evaluateFlag = exports.unflattenJSON = exports.flattenJSON = void 0; | ||
exports.evaluate = exports.hashInt = exports.evaluateFlag = exports.unflattenJSON = exports.flattenJSON = void 0; | ||
const node_crypto_1 = require("node:crypto"); | ||
@@ -52,20 +52,6 @@ function flattenJSON(data) { | ||
function evaluateFlag({ context, flag, }) { | ||
var _a; | ||
const flatContext = flattenJSON(context); | ||
const missingContextFieldsSet = new Set(); | ||
for (const rule of flag.rules) { | ||
(_a = rule.filter) === null || _a === void 0 ? void 0 : _a.map((r) => r.field).filter((field) => !(field in flatContext)).forEach((field) => missingContextFieldsSet.add(field)); | ||
if (rule.partialRolloutAttribute && | ||
!(rule.partialRolloutAttribute in flatContext)) { | ||
missingContextFieldsSet.add(rule.partialRolloutAttribute); | ||
} | ||
} | ||
const ruleEvaluationResults = flag.rules.map((rule) => evaluateRecursively(rule.filter, flatContext, missingContextFieldsSet)); | ||
const missingContextFields = Array.from(missingContextFieldsSet); | ||
const ruleEvaluationResults = flag.rules.map((rule) => { | ||
return evaluateRuleWithContext({ | ||
context: flatContext, | ||
rule, | ||
key: flag.key, | ||
}); | ||
}); | ||
const firstIdx = ruleEvaluationResults.findIndex(Boolean); | ||
@@ -93,33 +79,35 @@ return { | ||
exports.hashInt = hashInt; | ||
function rejectedDueToPartialRollout({ rule, context, key, }) { | ||
if (rule.partialRolloutAttribute === undefined || | ||
rule.partialRolloutThreshold === undefined) { | ||
return false; | ||
} | ||
// reject if the partial rollout attribute is not present in the context | ||
if (!(rule.partialRolloutAttribute in context)) { | ||
return true; | ||
} | ||
// not included in the partial rollout if the hash is above the threshold | ||
return (hashInt(`${key}.${context[rule.partialRolloutAttribute]}`) > | ||
rule.partialRolloutThreshold); | ||
} | ||
exports.rejectedDueToPartialRollout = rejectedDueToPartialRollout; | ||
function evaluateRuleWithContext({ context, key, rule, }) { | ||
const match = (rule.filter || []).every((filter) => { | ||
var _a; | ||
if (!(filter.field in context)) { | ||
function evaluateRecursively(filter, context, missingContextFieldsSet) { | ||
switch (filter.type) { | ||
case "constant": | ||
return filter.value; | ||
case "context": | ||
if (!(filter.field in context)) { | ||
missingContextFieldsSet.add(filter.field); | ||
return false; | ||
} | ||
return evaluate(context[filter.field], filter.operator, filter.values || []); | ||
case "rolloutPercentage": | ||
if (filter.partialRolloutThreshold === 100000) { | ||
return true; | ||
} | ||
missingContextFieldsSet.add(filter.partialRolloutAttribute); | ||
if (!(filter.partialRolloutAttribute in context)) { | ||
return false; | ||
} | ||
return (hashInt(`${filter.flagKey}.${context[filter.partialRolloutAttribute]}`) > filter.partialRolloutThreshold); | ||
case "group": | ||
return filter.filters.reduce((acc, current) => { | ||
if (filter.operator === "and") { | ||
return (acc && | ||
evaluateRecursively(current, context, missingContextFieldsSet)); | ||
} | ||
return (acc || evaluateRecursively(current, context, missingContextFieldsSet)); | ||
}, filter.operator === "and"); | ||
case "negation": | ||
return !evaluateRecursively(filter.filter, context, missingContextFieldsSet); | ||
default: | ||
return false; | ||
} | ||
return evaluate(context[filter.field], filter.operator, ((_a = filter === null || filter === void 0 ? void 0 : filter.values) === null || _a === void 0 ? void 0 : _a.length) ? filter.values : [""]); | ||
}); | ||
if (!match) { | ||
return false; | ||
} | ||
if (rejectedDueToPartialRollout({ rule, context, key })) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
exports.evaluateRuleWithContext = evaluateRuleWithContext; | ||
function evaluate(fieldValue, op, values) { | ||
@@ -126,0 +114,0 @@ const value = values[0]; |
@@ -0,5 +1,16 @@ | ||
export type FilterClass = { | ||
type: string; | ||
}; | ||
export type FilterGroup<T extends FilterClass> = { | ||
type: "group"; | ||
operator: "and" | "or"; | ||
filters: FilterTree<T>[]; | ||
}; | ||
export type FilterNegation<T extends FilterClass> = { | ||
type: "negation"; | ||
filter: FilterTree<T>; | ||
}; | ||
export type FilterTree<T extends FilterClass> = FilterGroup<T> | FilterNegation<T> | T; | ||
export interface Rule { | ||
filter?: ContextFilter[]; | ||
partialRolloutThreshold?: number; | ||
partialRolloutAttribute?: string; | ||
filter: RuleFilter; | ||
} | ||
@@ -24,2 +35,3 @@ export type FlagData = { | ||
export type ContextFilter = { | ||
type: "context"; | ||
field: string; | ||
@@ -29,2 +41,13 @@ operator: ContextFilterOp; | ||
}; | ||
export type PercentageRolloutFilter = { | ||
type: "rolloutPercentage"; | ||
flagKey: string; | ||
partialRolloutAttribute: string; | ||
partialRolloutThreshold: number; | ||
}; | ||
export type ConstantFilter = { | ||
type: "constant"; | ||
value: boolean; | ||
}; | ||
export type RuleFilter = FilterTree<ContextFilter | PercentageRolloutFilter | ConstantFilter>; | ||
export declare function flattenJSON(data: object): Record<string, string>; | ||
@@ -34,13 +57,3 @@ export declare function unflattenJSON(data: Record<string, any>): Record<string, any>; | ||
export declare function hashInt(hashInput: string): number; | ||
export declare function rejectedDueToPartialRollout({ rule, context, key, }: { | ||
rule: Rule; | ||
context: Record<string, string>; | ||
key: string; | ||
}): boolean; | ||
export declare function evaluateRuleWithContext({ context, key, rule, }: { | ||
key: string; | ||
context: Record<string, string>; | ||
rule: Rule; | ||
}): boolean; | ||
export declare function evaluate(fieldValue: string, op: ContextFilterOp, values: string[]): boolean; | ||
export {}; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.evaluate = exports.evaluateRuleWithContext = exports.rejectedDueToPartialRollout = exports.hashInt = exports.evaluateFlag = exports.unflattenJSON = exports.flattenJSON = void 0; | ||
exports.evaluate = exports.hashInt = exports.evaluateFlag = exports.unflattenJSON = exports.flattenJSON = void 0; | ||
const node_crypto_1 = require("node:crypto"); | ||
@@ -52,20 +52,6 @@ function flattenJSON(data) { | ||
function evaluateFlag({ context, flag, }) { | ||
var _a; | ||
const flatContext = flattenJSON(context); | ||
const missingContextFieldsSet = new Set(); | ||
for (const rule of flag.rules) { | ||
(_a = rule.filter) === null || _a === void 0 ? void 0 : _a.map((r) => r.field).filter((field) => !(field in flatContext)).forEach((field) => missingContextFieldsSet.add(field)); | ||
if (rule.partialRolloutAttribute && | ||
!(rule.partialRolloutAttribute in flatContext)) { | ||
missingContextFieldsSet.add(rule.partialRolloutAttribute); | ||
} | ||
} | ||
const ruleEvaluationResults = flag.rules.map((rule) => evaluateRecursively(rule.filter, flatContext, missingContextFieldsSet)); | ||
const missingContextFields = Array.from(missingContextFieldsSet); | ||
const ruleEvaluationResults = flag.rules.map((rule) => { | ||
return evaluateRuleWithContext({ | ||
context: flatContext, | ||
rule, | ||
key: flag.key, | ||
}); | ||
}); | ||
const firstIdx = ruleEvaluationResults.findIndex(Boolean); | ||
@@ -93,33 +79,35 @@ return { | ||
exports.hashInt = hashInt; | ||
function rejectedDueToPartialRollout({ rule, context, key, }) { | ||
if (rule.partialRolloutAttribute === undefined || | ||
rule.partialRolloutThreshold === undefined) { | ||
return false; | ||
} | ||
// reject if the partial rollout attribute is not present in the context | ||
if (!(rule.partialRolloutAttribute in context)) { | ||
return true; | ||
} | ||
// not included in the partial rollout if the hash is above the threshold | ||
return (hashInt(`${key}.${context[rule.partialRolloutAttribute]}`) > | ||
rule.partialRolloutThreshold); | ||
} | ||
exports.rejectedDueToPartialRollout = rejectedDueToPartialRollout; | ||
function evaluateRuleWithContext({ context, key, rule, }) { | ||
const match = (rule.filter || []).every((filter) => { | ||
var _a; | ||
if (!(filter.field in context)) { | ||
function evaluateRecursively(filter, context, missingContextFieldsSet) { | ||
switch (filter.type) { | ||
case "constant": | ||
return filter.value; | ||
case "context": | ||
if (!(filter.field in context)) { | ||
missingContextFieldsSet.add(filter.field); | ||
return false; | ||
} | ||
return evaluate(context[filter.field], filter.operator, filter.values || []); | ||
case "rolloutPercentage": | ||
if (filter.partialRolloutThreshold === 100000) { | ||
return true; | ||
} | ||
missingContextFieldsSet.add(filter.partialRolloutAttribute); | ||
if (!(filter.partialRolloutAttribute in context)) { | ||
return false; | ||
} | ||
return (hashInt(`${filter.flagKey}.${context[filter.partialRolloutAttribute]}`) > filter.partialRolloutThreshold); | ||
case "group": | ||
return filter.filters.reduce((acc, current) => { | ||
if (filter.operator === "and") { | ||
return (acc && | ||
evaluateRecursively(current, context, missingContextFieldsSet)); | ||
} | ||
return (acc || evaluateRecursively(current, context, missingContextFieldsSet)); | ||
}, filter.operator === "and"); | ||
case "negation": | ||
return !evaluateRecursively(filter.filter, context, missingContextFieldsSet); | ||
default: | ||
return false; | ||
} | ||
return evaluate(context[filter.field], filter.operator, ((_a = filter === null || filter === void 0 ? void 0 : filter.values) === null || _a === void 0 ? void 0 : _a.length) ? filter.values : [""]); | ||
}); | ||
if (!match) { | ||
return false; | ||
} | ||
if (rejectedDueToPartialRollout({ rule, context, key })) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
exports.evaluateRuleWithContext = evaluateRuleWithContext; | ||
function evaluate(fieldValue, op, values) { | ||
@@ -126,0 +114,0 @@ const value = values[0]; |
{ | ||
"name": "@bucketco/flag-evaluation", | ||
"version": "0.0.4", | ||
"version": "0.0.5", | ||
"license": "MIT", | ||
@@ -28,10 +28,10 @@ "repository": { | ||
"devDependencies": { | ||
"@bucketco/eslint-config": "0.0.2", | ||
"@bucketco/tsconfig": "0.0.2", | ||
"@bucketco/eslint-config": "^0.0.2", | ||
"@bucketco/tsconfig": "^0.0.2", | ||
"@types/jest": "^29.5.12", | ||
"@types/node": "^20.11.0", | ||
"eslint": "^8.56.0", | ||
"eslint": "^8.57.0", | ||
"jest": "^29.7.0", | ||
"jest-junit": "^16.0.0", | ||
"prettier": "^3.1.1", | ||
"prettier": "^3.3.3", | ||
"ts-jest": "^29.1.4", | ||
@@ -41,3 +41,3 @@ "ts-node": "^10.9.2", | ||
}, | ||
"gitHead": "bd0171e757944d78ea0815d5155dc89ddf37f21f" | ||
"gitHead": "9cf8f0887db8dc51e335e1260b5b77b730f5d0df" | ||
} |
Sorry, the diff of this file is not supported yet
21996
376