@launchdarkly/js-server-sdk-common
Version:
LaunchDarkly Server SDK for JavaScript - common code
171 lines • 8.69 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const createStreamListeners_1 = require("../../data_sources/createStreamListeners");
const AsyncStoreFacade_1 = require("../../store/AsyncStoreFacade");
const serialization_1 = require("../../store/serialization");
const VersionedDataKinds_1 = require("../../store/VersionedDataKinds");
const TestDataFlagBuilder_1 = require("./TestDataFlagBuilder");
const TestDataSource_1 = require("./TestDataSource");
/**
* A mechanism for providing dynamically updatable feature flag state in a simplified form to an SDK
* client in test scenarios.
*
* Unlike `FileData`, this mechanism does not use any external resources. It provides only the
* data that the application has put into it using the {@link TestData.update} method.
*
* ```
* // Import TestData from the integrations package of the SDK you are using.
* // This is a common implementation and may be used in multiple SDKs.
*
* const td = TestData();
* testData.update(td.flag("flag-key-1").booleanFlag().variationForAll(true));
* // Use the initialization mechanism specified by your SDK.
* const client = LDClient.init(sdkKey, { updateProcessor: td.getFactory() });
*
* // flags can be updated at any time:
* td.update(td.flag("flag-key-2")
* .variationForContext("user", "some-user-key", true)
* .fallthroughVariation(false));
* ```
*
* The above example uses a simple boolean flag, but more complex configurations are possible using
* the methods of the {@link TestDataFlagBuilder} that is returned by {@link TestData.flag}.
* {@link TestDataFlagBuilder} supports many of the ways a flag can be configured on the
* LaunchDarkly dashboard, but does not currently support
* 1. rule operators other than "in" and "not in", or
* 2. percentage rollouts.
*
* If the same `TestData` instance is used to configure multiple `LDClient` instances,
* any changes made to the data will propagate to all of the `LDClient`s.
*/
class TestData {
constructor() {
this._currentFlags = {};
this._currentSegments = {};
this._dataSources = [];
this._flagBuilders = {};
}
/**
* Get a factory for update processors that will be attached to this TestData instance.
* @returns An update processor factory.
*/
getFactory() {
// Provides an arrow function to prevent needed to bind the method to
// maintain `this`.
return (clientContext, featureStore, initSuccessHandler, _errorHandler) => {
const listeners = (0, createStreamListeners_1.createStreamListeners)(featureStore, clientContext.basicConfiguration.logger, {
put: initSuccessHandler,
});
const newSource = new TestDataSource_1.default(new AsyncStoreFacade_1.default(featureStore), this._currentFlags, this._currentSegments, (tds) => {
this._dataSources.splice(this._dataSources.indexOf(tds));
}, listeners);
this._dataSources.push(newSource);
return newSource;
};
}
/**
* Creates or copies a {@link TestDataFlagBuilder} for building a test flag configuration.
*
* If the flag key has already been defined in this `TestData` instance,
* then the builder starts with the same configuration that was last
* provided for this flag.
*
* Otherwise, it starts with a new default configuration in which the flag
* has `true` and `false` variations, is `true` for all users when targeting
* is turned on and `false` otherwise, and currently has targeting turned on.
* You can change any of those properties and provide more complex behavior
* using the `TestDataFlagBuilder` methods.
*
* Once you have set the desired configuration, pass the builder to
* {@link TestData.update}.
*
* @param key the flag key
* @returns a flag configuration builder
*
*/
flag(key) {
if (this._flagBuilders[key]) {
return this._flagBuilders[key].clone();
}
return new TestDataFlagBuilder_1.default(key).booleanFlag();
}
/**
* Updates the test data with the specified flag configuration.
*
* This has the same effect as if a flag were added or modified in the
* LaunchDarkly dashboard. It immediately propagates the flag changes to
* any `LDClient` instance(s) that you have already configured to use
* this `TestData`. If no `LDClient` has been started yet, it simply adds
* this flag to the test data which will be provided to any `LDClient`
* that you subsequently configure.
*
* Any subsequent changes to this `TestDataFlagBuilder` instance do not affect
* the test data unless you call `update` again.
*
* @param flagBuilder a flag configuration builder
* @return a promise that will resolve when the feature stores are updated
*/
update(flagBuilder) {
const flagKey = flagBuilder.getKey();
const oldItem = this._currentFlags[flagKey];
const oldVersion = oldItem ? oldItem.version : 0;
const newFlag = flagBuilder.build(oldVersion + 1);
this._currentFlags[flagKey] = newFlag;
this._flagBuilders[flagKey] = flagBuilder.clone();
return Promise.all(this._dataSources.map((impl) => impl.upsert(VersionedDataKinds_1.default.Features, newFlag)));
}
/**
* Copies a full feature flag data model object into the test data.
*
* It immediately propagates the flag change to any `LDClient` instance(s) that you have already
* configured to use this `TestData`. If no `LDClient` has been started yet, it simply adds this
* flag to the test data which will be provided to any LDClient that you subsequently configure.
*
* Use this method if you need to use advanced flag configuration properties that are not
* supported by the simplified {@link TestDataFlagBuilder} API. Otherwise it is recommended to use
* the regular {@link flag}/{@link update} mechanism to avoid dependencies on details of the data
* model.
*
* You cannot make incremental changes with {@link flag}/{@link update} to a flag that has been
* added in this way; you can only replace it with an entirely new flag configuration.
*
* @param flagConfig the flag configuration as a JSON object
* @return a promise that will resolve when the feature stores are updated
*/
usePreconfiguredFlag(inConfig) {
// We need to do things like process attribute reference, and
// we do not want to modify the passed in value.
const flagConfig = JSON.parse(JSON.stringify(inConfig));
const oldItem = this._currentFlags[flagConfig.key];
const newItem = Object.assign(Object.assign({}, flagConfig), { version: oldItem ? oldItem.version + 1 : flagConfig.version });
(0, serialization_1.processFlag)(newItem);
this._currentFlags[flagConfig.key] = newItem;
return Promise.all(this._dataSources.map((impl) => impl.upsert(VersionedDataKinds_1.default.Features, newItem)));
}
/**
* Copies a full segment data model object into the test data.
*
* It immediately propagates the change to any `LDClient` instance(s) that you have already
* configured to use this `TestData`. If no `LDClient` has been started yet, it simply adds
* this segment to the test data which will be provided to any LDClient that you subsequently
* configure.
*
* This method is currently the only way to inject segment data, since there is no builder
* API for segments. It is mainly intended for the SDK's own tests of segment functionality,
* since application tests that need to produce a desired evaluation state could do so more easily
* by just setting flag values.
*
* @param segmentConfig the segment configuration as a JSON object
* @return a promise that will resolve when the feature stores are updated
*/
usePreconfiguredSegment(inConfig) {
const segmentConfig = JSON.parse(JSON.stringify(inConfig));
const oldItem = this._currentSegments[segmentConfig.key];
const newItem = Object.assign(Object.assign({}, segmentConfig), { version: oldItem ? oldItem.version + 1 : segmentConfig.version });
(0, serialization_1.processSegment)(newItem);
this._currentSegments[segmentConfig.key] = newItem;
return Promise.all(this._dataSources.map((impl) => impl.upsert(VersionedDataKinds_1.default.Segments, newItem)));
}
}
exports.default = TestData;
//# sourceMappingURL=TestData.js.map
;