UNPKG

featurehub-repository

Version:

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

320 lines 14.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MatcherRegistry = exports.ApplyFeature = exports.Applied = exports.Murmur3PercentageCalculator = void 0; const models_1 = require("./models/models"); const semver_1 = require("semver"); const ip6addr_1 = require("ip6addr"); const murmurhash_1 = require("murmurhash"); class Murmur3PercentageCalculator { constructor() { this.MAX_PERCENTAGE = 1000000; } determineClientPercentage(percentageText, featureId) { const result = murmurhash_1.v3(percentageText + featureId); return Math.floor(result / Math.pow(2, 32) * this.MAX_PERCENTAGE); } } exports.Murmur3PercentageCalculator = Murmur3PercentageCalculator; class Applied { constructor(matched, value) { this.matched = matched; this.value = value; } } exports.Applied = Applied; 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 models_1.RolloutStrategyFieldType.Date: suppliedValue = new Date().toISOString().substring(0, 10); break; case models_1.RolloutStrategyFieldType.Datetime: suppliedValue = new Date().toISOString(); break; } } if (attr.values == null && suppliedValue == null) { if (attr.conditional !== models_1.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; } } exports.ApplyFeature = ApplyFeature; class MatcherRegistry { findMatcher(attr) { switch (attr === null || attr === void 0 ? void 0 : attr.type) { case models_1.RolloutStrategyFieldType.String: return new StringMatcher(); case models_1.RolloutStrategyFieldType.SemanticVersion: return new SemanticVersionMatcher(); case models_1.RolloutStrategyFieldType.Number: return new NumberMatcher(); case models_1.RolloutStrategyFieldType.Date: return new DateMatcher(); case models_1.RolloutStrategyFieldType.Datetime: return new DateTimeMatcher(); case models_1.RolloutStrategyFieldType.Boolean: return new BooleanMatcher(); case models_1.RolloutStrategyFieldType.IpAddress: return new IPNetworkMatcher(); } return new FallthroughMatcher(); } } exports.MatcherRegistry = MatcherRegistry; class FallthroughMatcher { match(suppliedValue, attr) { return false; } } class BooleanMatcher { match(suppliedValue, attr) { const val = 'true' === suppliedValue; if (attr.conditional === models_1.RolloutStrategyAttributeConditional.Equals) { return val === (attr.values[0].toString() === 'true'); } if (attr.conditional === models_1.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 models_1.RolloutStrategyAttributeConditional.Equals: return vals.findIndex((v) => v === suppliedValue) >= 0; case models_1.RolloutStrategyAttributeConditional.EndsWith: return vals.findIndex((v) => suppliedValue.endsWith(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.StartsWith: return vals.findIndex((v) => suppliedValue.startsWith(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.Greater: return vals.findIndex((v) => suppliedValue > v) >= 0; case models_1.RolloutStrategyAttributeConditional.GreaterEquals: return vals.findIndex((v) => suppliedValue >= v) >= 0; case models_1.RolloutStrategyAttributeConditional.Less: return vals.findIndex((v) => suppliedValue < v) >= 0; case models_1.RolloutStrategyAttributeConditional.LessEquals: return vals.findIndex((v) => suppliedValue <= v) >= 0; case models_1.RolloutStrategyAttributeConditional.NotEquals: return vals.findIndex((v) => v === suppliedValue) === -1; case models_1.RolloutStrategyAttributeConditional.Includes: return vals.findIndex((v) => suppliedValue.includes(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.Excludes: return vals.findIndex((v) => suppliedValue.includes(v)) === -1; case models_1.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 models_1.RolloutStrategyAttributeConditional.Equals: return vals.findIndex((v) => conv(v) === num) >= 0; case models_1.RolloutStrategyAttributeConditional.EndsWith: return vals.findIndex((v) => suppliedValue.endsWith(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.StartsWith: return vals.findIndex((v) => suppliedValue.startsWith(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.Greater: return vals.findIndex((v) => num > conv(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.GreaterEquals: return vals.findIndex((v) => num >= conv(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.Less: return vals.findIndex((v) => num < conv(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.LessEquals: return vals.findIndex((v) => num <= conv(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.NotEquals: return vals.findIndex((v) => conv(v) === num) === -1; case models_1.RolloutStrategyAttributeConditional.Includes: return vals.findIndex((v) => suppliedValue.includes(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.Excludes: return vals.findIndex((v) => suppliedValue.includes(v)) === -1; case models_1.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 models_1.RolloutStrategyAttributeConditional.Includes: case models_1.RolloutStrategyAttributeConditional.Equals: return vals.findIndex((v) => semver_1.eq(suppliedValue, v)) >= 0; case models_1.RolloutStrategyAttributeConditional.EndsWith: break; case models_1.RolloutStrategyAttributeConditional.StartsWith: break; case models_1.RolloutStrategyAttributeConditional.Greater: return vals.findIndex((v) => semver_1.gt(suppliedValue, v)) >= 0; case models_1.RolloutStrategyAttributeConditional.GreaterEquals: return vals.findIndex((v) => semver_1.gte(suppliedValue, v)) >= 0; case models_1.RolloutStrategyAttributeConditional.Less: return vals.findIndex((v) => semver_1.lt(suppliedValue, v)) >= 0; case models_1.RolloutStrategyAttributeConditional.LessEquals: return vals.findIndex((v) => semver_1.lte(suppliedValue, v)) >= 0; case models_1.RolloutStrategyAttributeConditional.NotEquals: case models_1.RolloutStrategyAttributeConditional.Excludes: return vals.findIndex((v) => !semver_1.eq(suppliedValue, v)) >= 0; case models_1.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 models_1.RolloutStrategyAttributeConditional.Equals: case models_1.RolloutStrategyAttributeConditional.Includes: return vals.findIndex((v) => v.contains(ip)) >= 0; case models_1.RolloutStrategyAttributeConditional.NotEquals: case models_1.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 = ip6addr_1.createCIDR(addr); } else { this._isAddress = true; this._address = ip6addr_1.parse(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