UNPKG

flagsmith-nodejs

Version:

Flagsmith lets you manage features flags and remote config across web, mobile and server side applications. Deliver true Continuous Integration. Get builds out faster. Control who has access to new features.

150 lines (149 loc) 6.19 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getContextValue = exports.traitsMatchSegmentCondition = exports.getIdentitySegments = void 0; const jsonpathModule = require("jsonpath"); const index_js_1 = require("../utils/hashing/index.js"); const models_js_1 = require("./models.js"); const constants_js_1 = require("./constants.js"); // Handle ESM/CJS interop - jsonpath exports default in ESM const jsonpath = jsonpathModule.default || jsonpathModule; /** * Returns all segments that the identity belongs to based on segment rules evaluation. * * An identity belongs to a segment if it matches ALL of the segment's rules. * If the context has no identity or segments, returns an empty array. * * @param context - Evaluation context containing identity and segment definitions * @returns Array of segments that the identity matches */ function getIdentitySegments(context) { if (!context.identity || !context.segments) return []; return Object.values(context.segments).filter(segment => { if (segment.rules.length === 0) return false; return segment.rules.every(rule => traitsMatchSegmentRule(rule, segment.key, context)); }); } exports.getIdentitySegments = getIdentitySegments; /** * Evaluates whether a segment condition matches the identity's traits or context values. * * Handles different types of conditions: * - PERCENTAGE_SPLIT: Deterministic percentage-based bucketing using identity key * - IS_SET/IS_NOT_SET: Checks for trait existence * - Standard operators: EQUAL, NOT_EQUAL, etc. via SegmentConditionModel * - JSONPath expressions: $.identity.identifier, $.environment.name, etc. * * @param condition - The condition to evaluate (property, operator, value) * @param segmentKey - Key of the segment (used for percentage split hashing) * @param context - Evaluation context containing identity, traits, and environment * @returns true if the condition matches */ function traitsMatchSegmentCondition(condition, segmentKey, context) { if (condition.operator === constants_js_1.PERCENTAGE_SPLIT) { let splitKey; if (!condition.property) { splitKey = context?.identity?.key; } else { splitKey = getContextValue(condition.property, context); } if (!splitKey) { return false; } const hashedPercentage = (0, index_js_1.getHashedPercentageForObjIds)([segmentKey, splitKey]); return hashedPercentage <= parseFloat(String(condition.value)); } if (!condition.property) { return false; } const traitValue = getTraitValue(condition.property, context); if (condition.operator === constants_js_1.IS_SET) { return traitValue !== undefined && traitValue !== null; } if (condition.operator === constants_js_1.IS_NOT_SET) { return traitValue === undefined || traitValue === null; } if (traitValue !== undefined && traitValue !== null) { const segmentCondition = new models_js_1.SegmentConditionModel(condition.operator, condition.value, condition.property); return segmentCondition.matchesTraitValue(traitValue); } return false; } exports.traitsMatchSegmentCondition = traitsMatchSegmentCondition; function traitsMatchSegmentRule(rule, segmentKey, context) { const matchesConditions = evaluateConditions(rule, segmentKey, context); const matchesSubRules = evaluateSubRules(rule, segmentKey, context); return matchesConditions && matchesSubRules; } function evaluateConditions(rule, segmentKey, context) { if (!rule.conditions || rule.conditions.length === 0) return true; const conditionResults = rule.conditions.map((condition) => traitsMatchSegmentCondition(condition, segmentKey, context)); return evaluateRuleConditions(rule.type, conditionResults); } function evaluateSubRules(rule, segmentKey, context) { if (!rule.rules || rule.rules.length === 0) return true; return rule.rules.every((subRule) => traitsMatchSegmentRule(subRule, segmentKey, context)); } function evaluateRuleConditions(ruleType, conditionResults) { switch (ruleType) { case 'ALL': return conditionResults.length === 0 || conditionResults.every(result => result); case 'ANY': return conditionResults.length > 0 && conditionResults.some(result => result); case 'NONE': return conditionResults.length === 0 || conditionResults.every(result => !result); default: return false; } } function getTraitValue(property, context) { if (property.startsWith('$.')) { const contextValue = getContextValue(property, context); if (contextValue !== undefined && isPrimitive(contextValue)) { return contextValue; } } const traits = context?.identity?.traits || {}; return traits[property]; } function isPrimitive(value) { if (value === null || value === undefined) { return true; } // Objects and arrays are non-primitive return typeof value !== 'object'; } /** * Evaluates JSONPath expressions against the evaluation context. * * Supports accessing nested context values using JSONPath syntax. * Commonly used paths: * - $.identity.identifier - User's unique identifier * - $.identity.key - User's internal key * - $.environment.name - Environment name * - $.environment.key - Environment key * * @param jsonPath - JSONPath expression starting with '$.' * @param context - Evaluation context to query against * @returns The resolved value, or undefined if path doesn't exist or is invalid */ function getContextValue(jsonPath, context) { if (!context || !jsonPath?.startsWith('$.')) return undefined; try { const normalizedPath = normalizeJsonPath(jsonPath); const results = jsonpath.query(context, normalizedPath); return results.length > 0 ? results[0] : undefined; } catch (error) { return undefined; } } exports.getContextValue = getContextValue; function normalizeJsonPath(jsonPath) { return jsonPath.replace(/\.([^.\[\]]+)$/, "['$1']"); }