@launchdarkly/js-server-sdk-common
Version:
LaunchDarkly Server SDK for JavaScript - common code
388 lines • 15.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const js_sdk_common_1 = require("@launchdarkly/js-sdk-common");
const booleanVariation_1 = require("./booleanVariation");
const TestDataRuleBuilder_1 = require("./TestDataRuleBuilder");
/**
* A builder for feature flag configurations to be used with {@link TestData}.
*/
class TestDataFlagBuilder {
/**
* @internal
*/
constructor(_key, data) {
this._key = _key;
this._data = {
on: true,
variations: [],
};
if (data) {
// Not the fastest way to deep copy, but this is a testing mechanism.
this._data = {
on: data.on,
variations: [...data.variations],
};
if (data.offVariation !== undefined) {
this._data.offVariation = data.offVariation;
}
if (data.fallthroughVariation !== undefined) {
this._data.fallthroughVariation = data.fallthroughVariation;
}
if (data.targetsByVariation) {
this._data.targetsByVariation = JSON.parse(JSON.stringify(data.targetsByVariation));
}
if (data.rules) {
this._data.rules = [];
data.rules.forEach((rule) => {
var _a;
(_a = this._data.rules) === null || _a === void 0 ? void 0 : _a.push(rule.clone());
});
}
}
}
get _isBooleanFlag() {
return (this._data.variations.length === 2 &&
this._data.variations[booleanVariation_1.TRUE_VARIATION_INDEX] === true &&
this._data.variations[booleanVariation_1.FALSE_VARIATION_INDEX] === false);
}
/**
* A shortcut for setting the flag to use the standard boolean configuration.
*
* This is the default for all new flags created with {@link TestData.flag}. The
* flag will have two variations, `true` and `false` (in that order). It
* will return `false` whenever targeting is off and `true` when targeting
* is on unless other settings specify otherwise.
*
* @return the flag builder
*/
booleanFlag() {
if (this._isBooleanFlag) {
return this;
}
// Change this flag into a boolean flag.
return this.variations(true, false)
.fallthroughVariation(booleanVariation_1.TRUE_VARIATION_INDEX)
.offVariation(booleanVariation_1.FALSE_VARIATION_INDEX);
}
/**
* Sets the allowable variation values for the flag.
*
* The values may be of any JSON-compatible type: boolean, number, string, array,
* or object. For instance, a boolean flag normally has `variations(true, false)`;
* a string-valued flag might have `variations("red", "green")`; etc.
*
* @param values any number of variation values
* @return the flag builder
*/
variations(...values) {
this._data.variations = [...values];
return this;
}
/**
* Sets targeting to be on or off for this flag.
*
* The effect of this depends on the rest of the flag configuration, just
* as it does on the real LaunchDarkly dashboard. In the default configuration
* that you get from calling {@link TestData.flag} with a new flag key, the flag
* will return `false` whenever targeting is off and `true` when targeting
* is on.
*
* @param targetingOn true if targeting should be on
* @return the flag builder
*/
on(targetingOn) {
this._data.on = targetingOn;
return this;
}
/**
* Specifies the fallthrough variation for a flag. The fallthrough is
* the value that is returned if targeting is on and the user was not
* matched by a more specific target or rule.
*
* If a boolean is supplied, and the flag was previously configured with
* other variations, this also changes it to a boolean flag.
*
* @param variation
* either `true` or `false` or the index of the desired fallthrough
* variation: 0 for the first, 1 for the second, etc.
* @return the flag builder
*/
fallthroughVariation(variation) {
if (js_sdk_common_1.TypeValidators.Boolean.is(variation)) {
return this.booleanFlag().fallthroughVariation((0, booleanVariation_1.variationForBoolean)(variation));
}
this._data.fallthroughVariation = variation;
return this;
}
/**
* Specifies the off variation for a flag. This is the variation that is
* returned whenever targeting is off.
*
* If a boolean is supplied, and the flag was previously configured with
* other variations, this also changes it to a boolean flag.
*
* @param variation
* either `true` or `false` or the index of the desired off
* variation: 0 for the first, 1 for the second, etc.
* @return the flag builder
*/
offVariation(variation) {
if (js_sdk_common_1.TypeValidators.Boolean.is(variation)) {
return this.booleanFlag().offVariation((0, booleanVariation_1.variationForBoolean)(variation));
}
this._data.offVariation = variation;
return this;
}
/**
* Sets the flag to always return the specified variation for all contexts.
*
* Targeting is switched on, any existing targets or rules are removed,
* and the fallthrough variation is set to the specified value. The off
* variation is left unchanged.
*
* If a boolean is supplied, and the flag was previously configured with
* other variations, this also changes it to a boolean flag.
*
* @param varation
* either `true` or `false` or the index of the desired variation:
* 0 for the first, 1 for the second, etc.
* @return the flag builder
*/
variationForAll(variation) {
return this.on(true).clearRules().clearAllTargets().fallthroughVariation(variation);
}
/**
* Sets the flag to always return the specified variation value for all contexts.
*
* The value may be of any valid JSON type. This method changes the flag to have
* only a single variation, which is this value, and to return the same variation
* regardless of whether targeting is on or off. Any existing targets or rules
* are removed.
*
* @param value The desired value to be returned for all contexts.
* @return the flag builder
*/
valueForAll(value) {
return this.variations(value).variationForAll(0);
}
/**
* Sets the flag to return the specified variation for a specific context key
* when targeting is on. The context kind for contexts created with this method
* will be 'user'.
*
* This has no effect when targeting is turned off for the flag.
*
* If the variation is a boolean value and the flag was not already a boolean
* flag, this also changes it to be a boolean flag.
*
* If the variation is an integer, it specifies a variation out of whatever
* variation values have already been defined.
*
* @param contextKey a context key
* @param variation
* either `true` or `false` or the index of the desired variation:
* 0 for the first, 1 for the second, etc.
* @return the flag builder
*/
variationForUser(contextKey, variation) {
return this.variationForContext('user', contextKey, variation);
}
/**
* Sets the flag to return the specified variation for a specific context key
* when targeting is on.
*
* This has no effect when targeting is turned off for the flag.
*
* If the variation is a boolean value and the flag was not already a boolean
* flag, this also changes it to be a boolean flag.
*
* If the variation is an integer, it specifies a variation out of whatever
* variation values have already been defined.
*
* @param contextKind a context kind
* @param contextKey a context key
* @param variation
* either `true` or `false` or the index of the desired variation:
* 0 for the first, 1 for the second, etc.
* @return the flag builder
*/
variationForContext(contextKind, contextKey, variation) {
if (js_sdk_common_1.TypeValidators.Boolean.is(variation)) {
return this.booleanFlag().variationForContext(contextKind, contextKey, (0, booleanVariation_1.variationForBoolean)(variation));
}
if (!this._data.targetsByVariation) {
this._data.targetsByVariation = {};
}
this._data.variations.forEach((_, i) => {
if (i === variation) {
// If there is nothing set at the current variation then set it to the empty array
const targetsForVariation = this._data.targetsByVariation[i] || {};
if (!(contextKind in targetsForVariation)) {
targetsForVariation[contextKind] = [];
}
const exists = targetsForVariation[contextKind].indexOf(contextKey) !== -1;
// Add context to current variation set if they aren't already there
if (!exists) {
targetsForVariation[contextKind].push(contextKey);
}
this._data.targetsByVariation[i] = targetsForVariation;
}
else {
// remove user from other variation set if necessary
const targetsForVariation = this._data.targetsByVariation[i];
if (targetsForVariation) {
const targetsForContextKind = targetsForVariation[contextKind];
if (targetsForContextKind) {
const targetIndex = targetsForContextKind.indexOf(contextKey);
if (targetIndex !== -1) {
targetsForContextKind.splice(targetIndex, 1);
if (!targetsForContextKind.length) {
delete targetsForVariation[contextKind];
}
}
}
if (!Object.keys(targetsForVariation).length) {
delete this._data.targetsByVariation[i];
}
}
}
});
return this;
}
/**
* Removes any existing rules from the flag. This undoes the effect of methods
* like {@link ifMatch}.
*
* @return the same flag builder
*/
clearRules() {
delete this._data.rules;
return this;
}
/**
* Removes any existing targets from the flag. This undoes the effect of
* methods like {@link variationForContext}.
*
* @return the same flag builder
*/
clearAllTargets() {
delete this._data.targetsByVariation;
return this;
}
/**
* Starts defining a flag rule using the "is one of" operator.
*
* For example, this creates a rule that returnes `true` if the name is
* "Patsy" or "Edina":
*
* testData.flag('flag')
* .ifMatch('user', name', 'Patsy', 'Edina')
* .thenReturn(true)
*
* @param contextKind the kind of the context
* @param attribute the context attribute to match against
* @param values values to compare to
* @return
* a flag rule builder; call `thenReturn` to finish the rule
* or add more tests with another method like `andMatch`
*/
ifMatch(contextKind, attribute, ...values) {
const flagRuleBuilder = new TestDataRuleBuilder_1.default(this);
return flagRuleBuilder.andMatch(contextKind, attribute, ...values);
}
/**
* Starts defining a flag rule using the "is not one of" operator.
*
* For example, this creates a rule that returns `true` if the name is
* neither "Saffron" nor "Bubble":
*
* testData.flag('flag')
* .ifNotMatch('user', 'name', 'Saffron', 'Bubble')
* .thenReturn(true)
*
* @param contextKind the kind of the context
* @param attribute the user attribute to match against
* @param values values to compare to
* @return
* a flag rule builder; call `thenReturn` to finish the rule
* or add more tests with another method like `andNotMatch`
*/
ifNotMatch(contextKind, attribute, ...values) {
const flagRuleBuilder = new TestDataRuleBuilder_1.default(this);
return flagRuleBuilder.andNotMatch(contextKind, attribute, ...values);
}
checkRatio(ratio) {
var _a;
this._data.migration = (_a = this._data.migration) !== null && _a !== void 0 ? _a : {};
this._data.migration.checkRatio = ratio;
return this;
}
samplingRatio(ratio) {
this._data.samplingRatio = ratio;
return this;
}
/**
* @internal
*/
addRule(flagRuleBuilder) {
if (!this._data.rules) {
this._data.rules = [];
}
this._data.rules.push(flagRuleBuilder);
}
/**
* @internal
*/
build(version) {
const baseFlagObject = {
key: this._key,
version,
on: this._data.on,
offVariation: this._data.offVariation,
fallthrough: {
variation: this._data.fallthroughVariation,
},
variations: [...this._data.variations],
migration: this._data.migration,
samplingRatio: this._data.samplingRatio,
};
if (this._data.targetsByVariation) {
const contextTargets = [];
const userTargets = [];
Object.entries(this._data.targetsByVariation).forEach(([variation, contextTargetsForVariation]) => {
Object.entries(contextTargetsForVariation).forEach(([contextKind, values]) => {
const numberVariation = parseInt(variation, 10);
contextTargets.push({
contextKind,
values: contextKind === 'user' ? [] : values,
// Iterating the object it will be a string.
variation: numberVariation,
});
if (contextKind === 'user') {
userTargets.push({ values, variation: numberVariation });
}
});
});
baseFlagObject.targets = userTargets;
baseFlagObject.contextTargets = contextTargets;
}
if (this._data.rules) {
baseFlagObject.rules = this._data.rules.map((rule, i) => rule.build(String(i)));
}
return baseFlagObject;
}
/**
* @internal
*/
clone() {
return new TestDataFlagBuilder(this._key, this._data);
}
/**
* @internal
*/
getKey() {
return this._key;
}
}
exports.default = TestDataFlagBuilder;
//# sourceMappingURL=TestDataFlagBuilder.js.map