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
JavaScript
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