UNPKG

featurehub-repository

Version:

Core package of API that exposes FeatureHub feature flags, values and configuration to client applications written in Typescript or Javascript.

313 lines 14.1 kB
import { RolloutStrategyAttributeConditional, RolloutStrategyFieldType } from './models/models'; import { eq, gt, gte, lt, lte } from 'semver'; import { createCIDR, parse as parseIp } from 'ip6addr'; import { v3 as murmur3 } from 'murmurhash'; export class Murmur3PercentageCalculator { constructor() { this.MAX_PERCENTAGE = 1000000; } determineClientPercentage(percentageText, featureId) { const result = murmur3(percentageText + featureId); return Math.floor(result / Math.pow(2, 32) * this.MAX_PERCENTAGE); } } export class Applied { constructor(matched, value) { this.matched = matched; this.value = value; } } export class ApplyFeature { constructor(percentageCalculator, matcherRepository) { this._percentageCalculator = percentageCalculator || new Murmur3PercentageCalculator(); this._matcherRepository = matcherRepository || new MatcherRegistry(); } apply(strategies, key, featureValueId, context) { if (context != null && strategies != null && strategies.length > 0) { let percentage = null; let percentageKey = null; let basePercentage = new Map(); const defaultPercentageKey = context.defaultPercentageKey(); for (let rsi of strategies) { if (rsi.percentage !== 0 && (defaultPercentageKey != null || (rsi.percentageAttributes !== undefined && rsi.percentageAttributes.length > 0))) { let newPercentageKey = this.determinePercentageKey(context, rsi.percentageAttributes); if (!basePercentage.has(newPercentageKey)) { basePercentage.set(newPercentageKey, 0); } let basePercentageVal = basePercentage.get(newPercentageKey); if (percentage === null || newPercentageKey !== percentageKey) { percentageKey = newPercentageKey; percentage = this._percentageCalculator.determineClientPercentage(percentageKey, featureValueId); } let useBasePercentage = (rsi.attributes === undefined || rsi.attributes.length === 0) ? basePercentageVal : 0; if (percentage <= (useBasePercentage + rsi.percentage)) { if (rsi.attributes != null && rsi.attributes.length > 0) { if (this.matchAttribute(context, rsi)) { return new Applied(true, rsi.value); } } else { return new Applied(true, rsi.value); } } if (rsi.attributes !== undefined && rsi.attributes.length > 0) { basePercentage.set(percentageKey, basePercentage.get(percentageKey) + rsi.percentage); } } if ((rsi.percentage === 0 || rsi.percentage === undefined) && rsi.attributes != undefined && rsi.attributes.length > 0 && this.matchAttribute(context, rsi)) { return new Applied(true, rsi.value); } } } return new Applied(false, null); } determinePercentageKey(context, percentageAttributes) { if (percentageAttributes == null || percentageAttributes.length === 0) { return context.defaultPercentageKey(); } return percentageAttributes.filter((pa) => context.getAttr(pa, '<none>')).join('$'); } matchAttribute(context, rsi) { for (let attr of rsi.attributes) { let suppliedValue = context.getAttr(attr.fieldName, null); if (suppliedValue === null && attr.fieldName.toLowerCase() === 'now') { switch (attr.type) { case RolloutStrategyFieldType.Date: suppliedValue = new Date().toISOString().substring(0, 10); break; case RolloutStrategyFieldType.Datetime: suppliedValue = new Date().toISOString(); break; } } if (attr.values == null && suppliedValue == null) { if (attr.conditional !== RolloutStrategyAttributeConditional.Equals) { return false; } continue; } if (attr.values == null || suppliedValue == null) { return false; } if (!this._matcherRepository.findMatcher(attr).match(suppliedValue, attr)) { return false; } } return true; } } export class MatcherRegistry { findMatcher(attr) { switch (attr === null || attr === void 0 ? void 0 : attr.type) { case RolloutStrategyFieldType.String: return new StringMatcher(); case RolloutStrategyFieldType.SemanticVersion: return new SemanticVersionMatcher(); case RolloutStrategyFieldType.Number: return new NumberMatcher(); case RolloutStrategyFieldType.Date: return new DateMatcher(); case RolloutStrategyFieldType.Datetime: return new DateTimeMatcher(); case RolloutStrategyFieldType.Boolean: return new BooleanMatcher(); case RolloutStrategyFieldType.IpAddress: return new IPNetworkMatcher(); } return new FallthroughMatcher(); } } class FallthroughMatcher { match(suppliedValue, attr) { return false; } } class BooleanMatcher { match(suppliedValue, attr) { const val = 'true' === suppliedValue; if (attr.conditional === RolloutStrategyAttributeConditional.Equals) { return val === (attr.values[0].toString() === 'true'); } if (attr.conditional === RolloutStrategyAttributeConditional.NotEquals) { return val !== (attr.values[0].toString() === 'true'); } return false; } } class StringMatcher { match(suppliedValue, attr) { const vals = this.attrToStringValues(attr); switch (attr.conditional) { case RolloutStrategyAttributeConditional.Equals: return vals.findIndex((v) => v === suppliedValue) >= 0; case RolloutStrategyAttributeConditional.EndsWith: return vals.findIndex((v) => suppliedValue.endsWith(v)) >= 0; case RolloutStrategyAttributeConditional.StartsWith: return vals.findIndex((v) => suppliedValue.startsWith(v)) >= 0; case RolloutStrategyAttributeConditional.Greater: return vals.findIndex((v) => suppliedValue > v) >= 0; case RolloutStrategyAttributeConditional.GreaterEquals: return vals.findIndex((v) => suppliedValue >= v) >= 0; case RolloutStrategyAttributeConditional.Less: return vals.findIndex((v) => suppliedValue < v) >= 0; case RolloutStrategyAttributeConditional.LessEquals: return vals.findIndex((v) => suppliedValue <= v) >= 0; case RolloutStrategyAttributeConditional.NotEquals: return vals.findIndex((v) => v === suppliedValue) === -1; case RolloutStrategyAttributeConditional.Includes: return vals.findIndex((v) => suppliedValue.includes(v)) >= 0; case RolloutStrategyAttributeConditional.Excludes: return vals.findIndex((v) => suppliedValue.includes(v)) === -1; case RolloutStrategyAttributeConditional.Regex: return vals.findIndex((v) => suppliedValue.match(v)) >= 0; } return false; } attrToStringValues(attr) { return attr.values.filter((v) => v != null).map((v) => v.toString()); } } class DateMatcher extends StringMatcher { match(suppliedValue, attr) { try { const parsedDate = new Date(suppliedValue); if (parsedDate == null) { return false; } return super.match(parsedDate.toISOString().substring(0, 10), attr); } catch (e) { return false; } } attrToStringValues(attr) { return attr.values.filter((v) => v != null) .map((v) => (v instanceof Date) ? v.toISOString().substring(0, 10) : v.toString()); } } class DateTimeMatcher extends StringMatcher { match(suppliedValue, attr) { try { const parsedDate = new Date(suppliedValue); if (parsedDate == null) { return false; } return super.match(parsedDate.toISOString().substr(0, 19) + 'Z', attr); } catch (e) { return false; } } attrToStringValues(attr) { return attr.values.filter((v) => v != null) .map((v) => (v instanceof Date) ? (v.toISOString().substr(0, 19) + 'Z') : v.toString()); } } class NumberMatcher { match(suppliedValue, attr) { try { const isFloat = suppliedValue.indexOf('.') >= 0; const num = isFloat ? parseFloat(suppliedValue) : parseInt(suppliedValue, 10); const conv = (v) => isFloat ? parseFloat(v) : parseInt(v, 10); const vals = attr.values.filter((v) => v != null).map((v) => v.toString()); switch (attr.conditional) { case RolloutStrategyAttributeConditional.Equals: return vals.findIndex((v) => conv(v) === num) >= 0; case RolloutStrategyAttributeConditional.EndsWith: return vals.findIndex((v) => suppliedValue.endsWith(v)) >= 0; case RolloutStrategyAttributeConditional.StartsWith: return vals.findIndex((v) => suppliedValue.startsWith(v)) >= 0; case RolloutStrategyAttributeConditional.Greater: return vals.findIndex((v) => num > conv(v)) >= 0; case RolloutStrategyAttributeConditional.GreaterEquals: return vals.findIndex((v) => num >= conv(v)) >= 0; case RolloutStrategyAttributeConditional.Less: return vals.findIndex((v) => num < conv(v)) >= 0; case RolloutStrategyAttributeConditional.LessEquals: return vals.findIndex((v) => num <= conv(v)) >= 0; case RolloutStrategyAttributeConditional.NotEquals: return vals.findIndex((v) => conv(v) === num) === -1; case RolloutStrategyAttributeConditional.Includes: return vals.findIndex((v) => suppliedValue.includes(v)) >= 0; case RolloutStrategyAttributeConditional.Excludes: return vals.findIndex((v) => suppliedValue.includes(v)) === -1; case RolloutStrategyAttributeConditional.Regex: return vals.findIndex((v) => suppliedValue.match(v)) >= 0; } } catch (e) { return false; } return false; } } class SemanticVersionMatcher { match(suppliedValue, attr) { const vals = attr.values.filter((v) => v != null).map((v) => v.toString()); switch (attr.conditional) { case RolloutStrategyAttributeConditional.Includes: case RolloutStrategyAttributeConditional.Equals: return vals.findIndex((v) => eq(suppliedValue, v)) >= 0; case RolloutStrategyAttributeConditional.EndsWith: break; case RolloutStrategyAttributeConditional.StartsWith: break; case RolloutStrategyAttributeConditional.Greater: return vals.findIndex((v) => gt(suppliedValue, v)) >= 0; case RolloutStrategyAttributeConditional.GreaterEquals: return vals.findIndex((v) => gte(suppliedValue, v)) >= 0; case RolloutStrategyAttributeConditional.Less: return vals.findIndex((v) => lt(suppliedValue, v)) >= 0; case RolloutStrategyAttributeConditional.LessEquals: return vals.findIndex((v) => lte(suppliedValue, v)) >= 0; case RolloutStrategyAttributeConditional.NotEquals: case RolloutStrategyAttributeConditional.Excludes: return vals.findIndex((v) => !eq(suppliedValue, v)) >= 0; case RolloutStrategyAttributeConditional.Regex: break; } return false; } } class IPNetworkMatcher { match(suppliedValue, attr) { const ip = new IPNetworkProxy(suppliedValue); const vals = attr.values.filter((v) => v != null).map((v) => new IPNetworkProxy(v.toString())); switch (attr.conditional) { case RolloutStrategyAttributeConditional.Equals: case RolloutStrategyAttributeConditional.Includes: return vals.findIndex((v) => v.contains(ip)) >= 0; case RolloutStrategyAttributeConditional.NotEquals: case RolloutStrategyAttributeConditional.Excludes: return vals.findIndex((v) => v.contains(ip)) === -1; } return false; } } class IPNetworkProxy { constructor(addr) { this._original = addr; if (addr.includes('/')) { this._isAddress = false; this._network = createCIDR(addr); } else { this._isAddress = true; this._address = parseIp(addr); } } contains(proxy) { if (proxy._isAddress && this._isAddress) { return proxy._address.compare(this._address) === 0; } if (!proxy._isAddress && !this._isAddress) { return this._network.compare(proxy._network) === 0; } if (proxy._isAddress && !this._isAddress) { return this._network.contains(proxy._original); } } } //# sourceMappingURL=strategy_matcher.js.map