@bucketco/flag-evaluation
Advanced tools
Comparing version 0.0.7 to 0.1.0
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.evaluate = exports.hashInt = exports.evaluateTargeting = exports.unflattenJSON = exports.flattenJSON = void 0; | ||
exports.evaluateFeatureRules = exports.evaluate = exports.hashInt = exports.unflattenJSON = exports.flattenJSON = void 0; | ||
const node_crypto_1 = require("node:crypto"); | ||
/** | ||
* Flattens a nested JSON object into a single-level object, with keys indicating the nesting levels. | ||
* Keys in the resulting object are represented in a dot notation to reflect the nesting structure of the original data. | ||
* | ||
* @param {object} data - The nested JSON object to be flattened. | ||
* @return {Record<string, string>} A flattened JSON object with "stringified" keys and values. | ||
*/ | ||
function flattenJSON(data) { | ||
@@ -34,2 +41,8 @@ if (Object.keys(data).length === 0) | ||
exports.flattenJSON = flattenJSON; | ||
/** | ||
* Converts a flattened JSON object with dot-separated keys into a nested JSON object. | ||
* | ||
* @param {Record<string, any>} data - The flattened JSON object where keys are dot-separated representing nested levels. | ||
* @return {Record<string, any>} The unflattened JSON object with nested structure restored. | ||
*/ | ||
function unflattenJSON(data) { | ||
@@ -41,3 +54,5 @@ const result = {}; | ||
if (index === keys.length - 1) { | ||
acc[key] = data[i]; | ||
if (typeof acc === "object") { | ||
acc[key] = data[i]; | ||
} | ||
} | ||
@@ -53,18 +68,9 @@ else if (!acc[key]) { | ||
exports.unflattenJSON = unflattenJSON; | ||
function evaluateTargeting({ context, feature, }) { | ||
const flatContext = flattenJSON(context); | ||
const missingContextFieldsSet = new Set(); | ||
const ruleEvaluationResults = feature.targeting.rules.map((rule) => evaluateRecursively(rule.filter, flatContext, missingContextFieldsSet)); | ||
const missingContextFields = Array.from(missingContextFieldsSet); | ||
const firstIdx = ruleEvaluationResults.findIndex(Boolean); | ||
return { | ||
value: firstIdx > -1, | ||
feature, | ||
context: flatContext, | ||
ruleEvaluationResults, | ||
reason: firstIdx > -1 ? `rule #${firstIdx} matched` : "no matched rules", | ||
missingContextFields, | ||
}; | ||
} | ||
exports.evaluateTargeting = evaluateTargeting; | ||
/** | ||
* Generates a hashed integer based on the input string. The method extracts 20 bits from the hash, | ||
* scales it to a range between 0 and 100000, and returns the resultant integer. | ||
* | ||
* @param {string} hashInput - The input string used to generate the hash. | ||
* @return {number} A number between 0 and 100000 derived from the hash of the input string. | ||
*/ | ||
function hashInt(hashInput) { | ||
@@ -81,37 +87,13 @@ // 1. hash the key and the partial rollout attribute | ||
exports.hashInt = hashInt; | ||
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.partialRolloutAttribute in context)) { | ||
missingContextFieldsSet.add(filter.partialRolloutAttribute); | ||
return false; | ||
} | ||
const hashVal = hashInt(`${filter.key}.${context[filter.partialRolloutAttribute]}`); | ||
return hashVal < 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; | ||
} | ||
} | ||
function evaluate(fieldValue, op, values) { | ||
/** | ||
* Evaluates a field value against a specified operator and comparison values. | ||
* | ||
* @param {string} fieldValue - The value to be evaluated. | ||
* @param {ContextFilterOperator} operator - The operator used for the evaluation (e.g., "CONTAINS", "GT"). | ||
* @param {string[]} values - An array of comparison values for evaluation. | ||
* @return {boolean} The result of the evaluation based on the operator and comparison values. | ||
*/ | ||
function evaluate(fieldValue, operator, values) { | ||
const value = values[0]; | ||
switch (op) { | ||
switch (operator) { | ||
case "CONTAINS": | ||
@@ -140,3 +122,3 @@ return fieldValue.toLowerCase().includes(value.toLowerCase()); | ||
const fieldValueDate = new Date(fieldValue).getTime(); | ||
return op === "AFTER" | ||
return operator === "AFTER" | ||
? fieldValueDate > daysAgo.getTime() | ||
@@ -160,3 +142,3 @@ : fieldValueDate < daysAgo.getTime(); | ||
default: | ||
console.error(`unknown operator: ${op}`); | ||
console.error(`unknown operator: ${operator}`); | ||
return false; | ||
@@ -166,2 +148,53 @@ } | ||
exports.evaluate = evaluate; | ||
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.partialRolloutAttribute in context)) { | ||
missingContextFieldsSet.add(filter.partialRolloutAttribute); | ||
return false; | ||
} | ||
const hashVal = hashInt(`${filter.key}.${context[filter.partialRolloutAttribute]}`); | ||
return hashVal < 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; | ||
} | ||
} | ||
function evaluateFeatureRules({ context, featureKey, rules, }) { | ||
const flatContext = flattenJSON(context); | ||
const missingContextFieldsSet = new Set(); | ||
const ruleEvaluationResults = rules.map((rule) => evaluateRecursively(rule.filter, flatContext, missingContextFieldsSet)); | ||
const missingContextFields = Array.from(missingContextFieldsSet); | ||
const firstMatchedRuleIndex = ruleEvaluationResults.findIndex(Boolean); | ||
const firstMatchedRule = firstMatchedRuleIndex > -1 ? rules[firstMatchedRuleIndex] : undefined; | ||
return { | ||
value: firstMatchedRule === null || firstMatchedRule === void 0 ? void 0 : firstMatchedRule.value, | ||
featureKey, | ||
context: flatContext, | ||
ruleEvaluationResults, | ||
reason: firstMatchedRuleIndex > -1 | ||
? `rule #${firstMatchedRuleIndex} matched` | ||
: "no matched rules", | ||
missingContextFields, | ||
}; | ||
} | ||
exports.evaluateFeatureRules = evaluateFeatureRules; | ||
//# sourceMappingURL=index.js.map |
@@ -0,4 +1,21 @@ | ||
/** | ||
* Represents a filter class with a specific type property. | ||
* | ||
* This type is intended to define the structure for objects | ||
* that classify or categorize based on a particular filter type. | ||
* | ||
* Properties: | ||
* - type: Specifies the classification type as a string. | ||
*/ | ||
export type FilterClass = { | ||
type: string; | ||
}; | ||
/** | ||
* Represents a group of filters that can be combined with a logical operator. | ||
* | ||
* @template T The type of filter class that defines the criteria within the filter group. | ||
* @property type The fixed type indicator for this filter structure, always "group". | ||
* @property operator The logical operator used to combine the filters in the group. It can be either "and" (all conditions must pass) or "or" (at least one condition must pass). | ||
* @property filters An array of filter trees containing individual filters or nested groups of filters. | ||
*/ | ||
export type FilterGroup<T extends FilterClass> = { | ||
@@ -9,2 +26,12 @@ type: "group"; | ||
}; | ||
/** | ||
* Represents a filter negation structure for use within filtering systems. | ||
* | ||
* A `FilterNegation` is used to encapsulate a negation operation, | ||
* which negates the conditions defined in the provided `filter`. | ||
* | ||
* @template T - A generic type that extends FilterClass, indicating the type of the filter. | ||
* @property type - Specifies the type of this filter operation as "negation". | ||
* @property filter - A `FilterTree` structure of type `T` that defines the filter conditions to be negated. | ||
*/ | ||
export type FilterNegation<T extends FilterClass> = { | ||
@@ -14,31 +41,70 @@ type: "negation"; | ||
}; | ||
/** | ||
* Represents a tree structure for filters that can be composed of filter groups, | ||
* filter negations, or individual filter instances of a specified type. | ||
* | ||
* @template T - A type that extends the `FilterClass`. | ||
*/ | ||
export type FilterTree<T extends FilterClass> = FilterGroup<T> | FilterNegation<T> | T; | ||
export interface Rule { | ||
filter: RuleFilter; | ||
} | ||
export type FeatureData = { | ||
key: string; | ||
targeting: { | ||
rules: Rule[]; | ||
}; | ||
}; | ||
export interface EvaluateTargetingParams { | ||
context: Record<string, unknown>; | ||
feature: FeatureData; | ||
} | ||
export interface EvaluateTargetingResult { | ||
value: boolean; | ||
feature: FeatureData; | ||
context: Record<string, any>; | ||
ruleEvaluationResults: boolean[]; | ||
reason?: string; | ||
missingContextFields?: string[]; | ||
} | ||
type ContextFilterOp = "IS" | "IS_NOT" | "ANY_OF" | "NOT_ANY_OF" | "CONTAINS" | "NOT_CONTAINS" | "GT" | "LT" | "AFTER" | "BEFORE" | "SET" | "NOT_SET" | "IS_TRUE" | "IS_FALSE"; | ||
export type ContextFilter = { | ||
/** | ||
* Represents a set of predefined operators that can be used to filter a specific context. | ||
* These operators can express various conditions, including equality checks, comparison, | ||
* set membership, and boolean evaluations. | ||
* | ||
* Possible values: | ||
* - "IS": Specifies exact match. | ||
* - "IS_NOT": Specifies a negation of exact match. | ||
* - "ANY_OF": Checks if a value is present in a set of specified values. | ||
* - "NOT_ANY_OF": Checks if a value is not present in a set of specified values. | ||
* - "CONTAINS": Verifies if a value contains a specific substring or element. | ||
* - "NOT_CONTAINS": Verifies if a value does not contain a specific substring or element. | ||
* - "GT": Greater than comparison. | ||
* - "LT": Less than comparison. | ||
* - "AFTER": Compares if a value is after a specified point (e.g., time, rank). | ||
* - "BEFORE": Compares if a value is before a specified point (e.g., time, rank). | ||
* - "SET": Checks if a value is set or exists. | ||
* - "NOT_SET": Checks if a value is not set or does not exist. | ||
* - "IS_TRUE": Checks if a boolean value is true. | ||
* - "IS_FALSE": Checks if a boolean value is false. | ||
*/ | ||
type ContextFilterOperator = "IS" | "IS_NOT" | "ANY_OF" | "NOT_ANY_OF" | "CONTAINS" | "NOT_CONTAINS" | "GT" | "LT" | "AFTER" | "BEFORE" | "SET" | "NOT_SET" | "IS_TRUE" | "IS_FALSE"; | ||
/** | ||
* Represents a filter configuration used to filter data based on specific context. | ||
* | ||
* This interface defines the structure of a context filter, containing a field, | ||
* an operator, and optional values to control the filtering behavior. | ||
* | ||
* The `type` property must always have the value "context" to classify filters | ||
* of this type. | ||
* | ||
* The `field` property specifies the name of the context field to filter. | ||
* | ||
* The `operator` property defines the filtering operation to perform on the | ||
* specified field (e.g., equals, contains, etc.). | ||
* | ||
* The optional `values` property is an array of strings that lists the values | ||
* to be used in conjunction with the operator for filtering. | ||
* | ||
* This interface is typically utilized in contexts where data needs to be | ||
* dynamically filtered based on specific criteria derived from contextual | ||
* attributes. | ||
*/ | ||
export interface ContextFilter { | ||
type: "context"; | ||
field: string; | ||
operator: ContextFilterOp; | ||
operator: ContextFilterOperator; | ||
values?: string[]; | ||
}; | ||
} | ||
/** | ||
* Represents a filter configuration to enable percentage-based rollout of a feature or functionality. | ||
* | ||
* This type defines the necessary parameters to control access to a feature | ||
* by evaluating a specific attribute and applying it against a defined percentage threshold. | ||
* | ||
* Properties: | ||
* - `type` - Indicates the type of the filter. For this filter type, it will always be "rolloutPercentage". | ||
* - `key` - A unique key or identifier that distinguishes this rollout filter. | ||
* - `partialRolloutAttribute` - Specifies the attribute used to evaluate eligibility for the rollout. | ||
* - `partialRolloutThreshold` - A numeric value representing the upper-bound threshold (0-100) for the percentage-based rollout. | ||
*/ | ||
export type PercentageRolloutFilter = { | ||
@@ -50,2 +116,11 @@ type: "rolloutPercentage"; | ||
}; | ||
/** | ||
* Represents a constant filter configuration. | ||
* | ||
* The ConstantFilter type is used to define a filter configuration with a fixed, | ||
* immutable value. It always evaluates to the specified boolean `value`. | ||
* | ||
* @property {string} type - Indicates the type of filter, which is always "constant". | ||
* @property {boolean} value - The fixed boolean value for the filter. | ||
*/ | ||
export type ConstantFilter = { | ||
@@ -55,8 +130,107 @@ type: "constant"; | ||
}; | ||
/** | ||
* A composite type for representing a rule-based filter system. | ||
* | ||
* This type is constructed using a `FilterTree` structure that consists of | ||
* nested filters of the following types: | ||
* - `ContextFilter`: A filter that evaluates based on specified context criteria. | ||
* - `PercentageRolloutFilter`: A filter that performs a percentage-based rollout. | ||
* - `ConstantFilter`: A filter that evaluates based on fixed conditions or constants. | ||
* | ||
* `RuleFilter` is typically used in scenarios where a hierarchical filtering mechanism | ||
* is needed to determine outcomes based on multiple layered conditions. | ||
*/ | ||
export type RuleFilter = FilterTree<ContextFilter | PercentageRolloutFilter | ConstantFilter>; | ||
/** | ||
* Represents a value that can be used in a rule configuration. | ||
* | ||
* RuleValue can take on different types, allowing flexibility based on the | ||
* specific rule's requirements. This can include: | ||
* - A boolean value: to represent true/false conditions. | ||
* - A string: typically used for textual or keyword-based rules. | ||
* - A number: for numerical rules or thresholds. | ||
* - An object: for more complex rule definitions or configurations. | ||
* | ||
* This type is useful for accommodating various rule structures in applications | ||
* that work with dynamic or user-defined regulations. | ||
*/ | ||
type RuleValue = boolean | string | number | object; | ||
/** | ||
* Represents a rule that defines a filtering criterion and an associated value. | ||
* | ||
* @template T - Specifies the type of the associated value that extends RuleValue. | ||
* @property {RuleFilter} filter - The filtering criterion used by the rule. | ||
* @property {T} value - The value associated with the rule. | ||
*/ | ||
export interface Rule<T extends RuleValue> { | ||
filter: RuleFilter; | ||
value: T; | ||
} | ||
/** | ||
* Flattens a nested JSON object into a single-level object, with keys indicating the nesting levels. | ||
* Keys in the resulting object are represented in a dot notation to reflect the nesting structure of the original data. | ||
* | ||
* @param {object} data - The nested JSON object to be flattened. | ||
* @return {Record<string, string>} A flattened JSON object with "stringified" keys and values. | ||
*/ | ||
export declare function flattenJSON(data: object): Record<string, string>; | ||
/** | ||
* Converts a flattened JSON object with dot-separated keys into a nested JSON object. | ||
* | ||
* @param {Record<string, any>} data - The flattened JSON object where keys are dot-separated representing nested levels. | ||
* @return {Record<string, any>} The unflattened JSON object with nested structure restored. | ||
*/ | ||
export declare function unflattenJSON(data: Record<string, any>): Record<string, any>; | ||
export declare function evaluateTargeting({ context, feature, }: EvaluateTargetingParams): EvaluateTargetingResult; | ||
/** | ||
* Generates a hashed integer based on the input string. The method extracts 20 bits from the hash, | ||
* scales it to a range between 0 and 100000, and returns the resultant integer. | ||
* | ||
* @param {string} hashInput - The input string used to generate the hash. | ||
* @return {number} A number between 0 and 100000 derived from the hash of the input string. | ||
*/ | ||
export declare function hashInt(hashInput: string): number; | ||
export declare function evaluate(fieldValue: string, op: ContextFilterOp, values: string[]): boolean; | ||
/** | ||
* Evaluates a field value against a specified operator and comparison values. | ||
* | ||
* @param {string} fieldValue - The value to be evaluated. | ||
* @param {ContextFilterOperator} operator - The operator used for the evaluation (e.g., "CONTAINS", "GT"). | ||
* @param {string[]} values - An array of comparison values for evaluation. | ||
* @return {boolean} The result of the evaluation based on the operator and comparison values. | ||
*/ | ||
export declare function evaluate(fieldValue: string, operator: ContextFilterOperator, values: string[]): boolean; | ||
/** | ||
* Represents the parameters required for evaluating rules against a specific feature in a given context. | ||
* | ||
* @template T - The type of the rule value used in evaluation. | ||
* | ||
* @property {string} featureKey - The key that identifies the specific feature to be evaluated. | ||
* @property {Rule<T>[]} rules - An array of rules used for evaluation. | ||
* @property {Record<string, unknown>} context - The contextual data used during the evaluation process. | ||
*/ | ||
export interface EvaluationParams<T extends RuleValue> { | ||
featureKey: string; | ||
rules: Rule<T>[]; | ||
context: Record<string, unknown>; | ||
} | ||
/** | ||
* Represents the result of an evaluation process for a specific feature and its associated rules. | ||
* | ||
* @template T - The type of the rule value being evaluated. | ||
* | ||
* @property {string} featureKey - The unique key identifying the feature being evaluated. | ||
* @property {T | undefined} value - The resolved value of the feature, if the evaluation is successful. | ||
* @property {Record<string, any>} context - The contextual information used during the evaluation process. | ||
* @property {boolean[]} ruleEvaluationResults - Array indicating the success or failure of each rule evaluated. | ||
* @property {string} [reason] - Optional field providing additional explanation regarding the evaluation result. | ||
* @property {string[]} [missingContextFields] - Optional array of context fields that were required but not provided during the evaluation. | ||
*/ | ||
export interface EvaluationResult<T extends RuleValue> { | ||
featureKey: string; | ||
value: T | undefined; | ||
context: Record<string, any>; | ||
ruleEvaluationResults: boolean[]; | ||
reason?: string; | ||
missingContextFields?: string[]; | ||
} | ||
export declare function evaluateFeatureRules<T extends RuleValue>({ context, featureKey, rules, }: EvaluationParams<T>): EvaluationResult<T>; | ||
export {}; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.evaluate = exports.hashInt = exports.evaluateTargeting = exports.unflattenJSON = exports.flattenJSON = void 0; | ||
exports.evaluateFeatureRules = exports.evaluate = exports.hashInt = exports.unflattenJSON = exports.flattenJSON = void 0; | ||
const node_crypto_1 = require("node:crypto"); | ||
/** | ||
* Flattens a nested JSON object into a single-level object, with keys indicating the nesting levels. | ||
* Keys in the resulting object are represented in a dot notation to reflect the nesting structure of the original data. | ||
* | ||
* @param {object} data - The nested JSON object to be flattened. | ||
* @return {Record<string, string>} A flattened JSON object with "stringified" keys and values. | ||
*/ | ||
function flattenJSON(data) { | ||
@@ -34,2 +41,8 @@ if (Object.keys(data).length === 0) | ||
exports.flattenJSON = flattenJSON; | ||
/** | ||
* Converts a flattened JSON object with dot-separated keys into a nested JSON object. | ||
* | ||
* @param {Record<string, any>} data - The flattened JSON object where keys are dot-separated representing nested levels. | ||
* @return {Record<string, any>} The unflattened JSON object with nested structure restored. | ||
*/ | ||
function unflattenJSON(data) { | ||
@@ -41,3 +54,5 @@ const result = {}; | ||
if (index === keys.length - 1) { | ||
acc[key] = data[i]; | ||
if (typeof acc === "object") { | ||
acc[key] = data[i]; | ||
} | ||
} | ||
@@ -53,18 +68,9 @@ else if (!acc[key]) { | ||
exports.unflattenJSON = unflattenJSON; | ||
function evaluateTargeting({ context, feature, }) { | ||
const flatContext = flattenJSON(context); | ||
const missingContextFieldsSet = new Set(); | ||
const ruleEvaluationResults = feature.targeting.rules.map((rule) => evaluateRecursively(rule.filter, flatContext, missingContextFieldsSet)); | ||
const missingContextFields = Array.from(missingContextFieldsSet); | ||
const firstIdx = ruleEvaluationResults.findIndex(Boolean); | ||
return { | ||
value: firstIdx > -1, | ||
feature, | ||
context: flatContext, | ||
ruleEvaluationResults, | ||
reason: firstIdx > -1 ? `rule #${firstIdx} matched` : "no matched rules", | ||
missingContextFields, | ||
}; | ||
} | ||
exports.evaluateTargeting = evaluateTargeting; | ||
/** | ||
* Generates a hashed integer based on the input string. The method extracts 20 bits from the hash, | ||
* scales it to a range between 0 and 100000, and returns the resultant integer. | ||
* | ||
* @param {string} hashInput - The input string used to generate the hash. | ||
* @return {number} A number between 0 and 100000 derived from the hash of the input string. | ||
*/ | ||
function hashInt(hashInput) { | ||
@@ -81,37 +87,13 @@ // 1. hash the key and the partial rollout attribute | ||
exports.hashInt = hashInt; | ||
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.partialRolloutAttribute in context)) { | ||
missingContextFieldsSet.add(filter.partialRolloutAttribute); | ||
return false; | ||
} | ||
const hashVal = hashInt(`${filter.key}.${context[filter.partialRolloutAttribute]}`); | ||
return hashVal < 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; | ||
} | ||
} | ||
function evaluate(fieldValue, op, values) { | ||
/** | ||
* Evaluates a field value against a specified operator and comparison values. | ||
* | ||
* @param {string} fieldValue - The value to be evaluated. | ||
* @param {ContextFilterOperator} operator - The operator used for the evaluation (e.g., "CONTAINS", "GT"). | ||
* @param {string[]} values - An array of comparison values for evaluation. | ||
* @return {boolean} The result of the evaluation based on the operator and comparison values. | ||
*/ | ||
function evaluate(fieldValue, operator, values) { | ||
const value = values[0]; | ||
switch (op) { | ||
switch (operator) { | ||
case "CONTAINS": | ||
@@ -140,3 +122,3 @@ return fieldValue.toLowerCase().includes(value.toLowerCase()); | ||
const fieldValueDate = new Date(fieldValue).getTime(); | ||
return op === "AFTER" | ||
return operator === "AFTER" | ||
? fieldValueDate > daysAgo.getTime() | ||
@@ -160,3 +142,3 @@ : fieldValueDate < daysAgo.getTime(); | ||
default: | ||
console.error(`unknown operator: ${op}`); | ||
console.error(`unknown operator: ${operator}`); | ||
return false; | ||
@@ -166,2 +148,53 @@ } | ||
exports.evaluate = evaluate; | ||
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.partialRolloutAttribute in context)) { | ||
missingContextFieldsSet.add(filter.partialRolloutAttribute); | ||
return false; | ||
} | ||
const hashVal = hashInt(`${filter.key}.${context[filter.partialRolloutAttribute]}`); | ||
return hashVal < 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; | ||
} | ||
} | ||
function evaluateFeatureRules({ context, featureKey, rules, }) { | ||
const flatContext = flattenJSON(context); | ||
const missingContextFieldsSet = new Set(); | ||
const ruleEvaluationResults = rules.map((rule) => evaluateRecursively(rule.filter, flatContext, missingContextFieldsSet)); | ||
const missingContextFields = Array.from(missingContextFieldsSet); | ||
const firstMatchedRuleIndex = ruleEvaluationResults.findIndex(Boolean); | ||
const firstMatchedRule = firstMatchedRuleIndex > -1 ? rules[firstMatchedRuleIndex] : undefined; | ||
return { | ||
value: firstMatchedRule === null || firstMatchedRule === void 0 ? void 0 : firstMatchedRule.value, | ||
featureKey, | ||
context: flatContext, | ||
ruleEvaluationResults, | ||
reason: firstMatchedRuleIndex > -1 | ||
? `rule #${firstMatchedRuleIndex} matched` | ||
: "no matched rules", | ||
missingContextFields, | ||
}; | ||
} | ||
exports.evaluateFeatureRules = evaluateFeatureRules; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@bucketco/flag-evaluation", | ||
"version": "0.0.7", | ||
"version": "0.1.0", | ||
"license": "MIT", | ||
@@ -37,3 +37,3 @@ "repository": { | ||
}, | ||
"gitHead": "a4f39af2ab2aa92e134a4f70d12c6abaeec7f591" | ||
"gitHead": "4d51b5dbce8f6f1bb9b6b2b3e12a586512409ed8" | ||
} |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
34879
616
1