diff --git a/server-sdk-common/__tests__/integrations/test_data/TestData.test.ts b/server-sdk-common/__tests__/integrations/test_data/TestData.test.ts new file mode 100644 index 0000000000..ce41ce75b8 --- /dev/null +++ b/server-sdk-common/__tests__/integrations/test_data/TestData.test.ts @@ -0,0 +1,398 @@ +import { AttributeReference } from '../../../src'; +import { Flag } from '../../../src/evaluation/data/Flag'; +import { FlagRule } from '../../../src/evaluation/data/FlagRule'; +import TestData from '../../../src/integrations/test_data/TestData'; +import Configuration from '../../../src/options/Configuration'; +import AsyncStoreFacade from '../../../src/store/AsyncStoreFacade'; +import InMemoryFeatureStore from '../../../src/store/InMemoryFeatureStore'; +import VersionedDataKinds from '../../../src/store/VersionedDataKinds'; + +const basicBooleanFlag: Flag = { + fallthrough: { + variation: 0, + }, + key: 'new-flag', + offVariation: 1, + on: true, + variations: [true, false], + version: 1, +}; + +it('initializes the data store with flags configured the data store is created', async () => { + const td = new TestData(); + td.update(td.flag('new-flag').variationForAll(true)); + + const store = new InMemoryFeatureStore(); + const processor = td.getFactory()(new Configuration({ + featureStore: store, + })); + + processor.start(); + const facade = new AsyncStoreFacade(store); + + const res = await facade.get(VersionedDataKinds.Features, 'new-flag'); + + expect(res).toEqual(basicBooleanFlag); +}); + +it('updates the data store when update is called', async () => { + const td = new TestData(); + const store = new InMemoryFeatureStore(); + const processor = td.getFactory()(new Configuration({ + featureStore: store, + })); + + processor.start(); + const facade = new AsyncStoreFacade(store); + + // In this test the update is after initialization. + await td.update(td.flag('new-flag').variationForAll(true)); + const res = await facade.get(VersionedDataKinds.Features, 'new-flag'); + expect(res).toEqual(basicBooleanFlag); +}); + +it('can include pre-configured items', async () => { + const td = new TestData(); + td.usePreconfiguredFlag({ key: 'my-flag', version: 1000, on: true }); + td.usePreconfiguredSegment({ key: 'my-segment', version: 2000 }); + + const store = new InMemoryFeatureStore(); + const processor = td.getFactory()(new Configuration({ + featureStore: store, + })); + + processor.start(); + + td.usePreconfiguredFlag({ key: 'my-flag', on: false }); + td.usePreconfiguredFlag({ key: 'my-flag-2', version: 1000, on: true }); + td.usePreconfiguredSegment({ key: 'my-segment', included: ['x'] }); + td.usePreconfiguredSegment({ key: 'my-segment-2', version: 2000 }); + + const facade = new AsyncStoreFacade(store); + const allFlags = await facade.all(VersionedDataKinds.Features); + const allSegments = await facade.all(VersionedDataKinds.Segments); + + expect(allFlags).toEqual({ + 'my-flag': { + key: 'my-flag', + on: false, + version: 1001, + }, + 'my-flag-2': { + key: 'my-flag-2', + on: true, + version: 1000, + }, + }); + + expect(allSegments).toEqual({ + 'my-segment': { + included: [ + 'x', + ], + key: 'my-segment', + version: 2001, + }, + 'my-segment-2': { + key: 'my-segment-2', + version: 2000, + }, + }); +}); + +it.each([true, false])('does not update the store after stop/close is called', async (stop) => { + const td = new TestData(); + + const store = new InMemoryFeatureStore(); + const processor = td.getFactory()(new Configuration({ + featureStore: store, + })); + + processor.start(); + td.update(td.flag('new-flag').variationForAll(true)); + if (stop) { + processor.stop(); + } else { + processor.close(); + } + td.update(td.flag('new-flag-2').variationForAll(true)); + + const facade = new AsyncStoreFacade(store); + + const flag1 = await facade.get(VersionedDataKinds.Features, 'new-flag'); + const flag2 = await facade.get(VersionedDataKinds.Features, 'new-flag-2'); + + expect(flag1).toBeDefined(); + expect(flag2).toBeNull(); +}); + +it('can update a flag that already exists in the store', async () => { + const td = new TestData(); + + const store = new InMemoryFeatureStore(); + + const processor = td.getFactory()(new Configuration({ + featureStore: store, + })); + + processor.start(); + td.update(td.flag('new-flag').variationForAll(true)); + td.update(td.flag('new-flag').variationForAll(false)); + + const facade = new AsyncStoreFacade(store); + const res = (await facade.get(VersionedDataKinds.Features, 'new-flag')) as Flag; + expect(res.version).toEqual(2); + expect(res.fallthrough.variation).toEqual(1); +}); + +describe('given a TestData instance', () => { + let td: TestData; + beforeEach(() => { + td = new TestData(); + }); + + it('doesn\'t provide the same reference when updating an existing builder', () => { + const flag = td.flag('test-flag'); + td.update(flag); + const flagCopy = td.flag('test-flag'); + flagCopy.on(false); + expect(flagCopy).not.toEqual(flag); + }); + + it('can clone a complex flag configuration', () => { + const flag = td.flag('test-flag') + .ifMatch('user', 'name', 'ben', 'christian') + .andNotMatch('user', 'country', 'fr') + .thenReturn(true); + + td.update(flag); + const flagCopy = td.flag('test-flag'); + + const flagRules: FlagRule[] = [{ + id: 'rule0', + variation: 0, + clauses: [ + { + attribute: 'name', + attributeReference: new AttributeReference('name'), + contextKind: 'user', + negate: false, + op: 'in', + values: [ + 'ben', + 'christian', + ], + }, + { + contextKind: 'user', + attribute: 'country', + attributeReference: new AttributeReference('country'), + negate: true, + op: 'in', + values: [ + 'fr', + ], + }, + ], + }]; + + expect(flagCopy.build(1).rules).toEqual(flagRules); + }); + + it('defaults a new flag to on', () => { + expect(td.flag('whatever').build(0).on).toBe(true); + }); + + it('defaults a new flag builder to a boolean flag', () => { + const flag = td.flag('test-flag-booleanFlags').build(1); + flag.variations.every((val) => expect(typeof val).toBe('boolean')); + expect(flag.variations[1]).not.toEqual(flag.variations[0]); + expect(flag.variations.length).toBe(2); + }); + + it('can set variations on the flag builder', () => { + const flag = td.flag('test-flag'); + flag.variations('a', 'b'); + expect(flag.build(0).variations).toEqual(['a', 'b']); + }); + + it('can set a value for all', () => { + const flag = td.flag('test-flag'); + flag.valueForAll('potato'); + const built = flag.build(1); + expect(built.variations).toEqual(['potato']); + expect(built.fallthrough.variation).toEqual(0); + }); + + it('can handle boolean values for *Variation setters', () => { + const flag = td.flag('test-flag').fallthroughVariation(false); + expect(flag.build(0).fallthrough).toEqual({ variation: 1 }); + + const offFlag = td.flag('off-flag').offVariation(true); + expect(offFlag.build(0).fallthrough).toEqual({ variation: 0 }); + }); + + it('can set boolean values for a specific user target', () => { + const flag = td.flag('test-flag').variationForContext('user', 'potato', false); + const flag2 = td.flag('test-flag').variationForUser('potato', true); + expect(flag.build(0).contextTargets).toEqual([ + { + contextKind: 'user', + variation: 1, + values: ['potato'], + }, + ]); + expect(flag2.build(0).contextTargets).toEqual([ + { + contextKind: 'user', + variation: 0, + values: ['potato'], + }, + ]); + }); + + it('can clear targets', () => { + const flag = td.flag('test-flag').variationForContext('user', 'potato', false); + const clearedFlag = flag.clone().clearAllTargets(); + expect(clearedFlag.build(0)).not.toHaveProperty('targets'); + }); + + it('can make not matching rules', () => { + const flag = td.flag('flag') + .ifNotMatch('user', 'name', 'Saffron', 'Bubble') + .thenReturn(true); + + expect(flag.build(1)).toEqual({ + fallthrough: { + variation: 0, + }, + key: 'flag', + offVariation: 1, + on: true, + rules: [ + { + clauses: [ + { + attribute: 'name', + attributeReference: { + components: [ + 'name', + ], + isValid: true, + redactionName: 'name', + }, + contextKind: 'user', + negate: true, + op: 'in', + values: [ + 'Saffron', + 'Bubble', + ], + }, + ], + id: 'rule0', + variation: 0, + }, + ], + variations: [ + true, + false, + ], + version: 1, + }); + }); + + it('can add and remove a rule', () => { + const flag = td.flag('test-flag') + .ifMatch('user', 'name', 'ben', 'christian') + .andNotMatch('user', 'country', 'fr') + .thenReturn(true); + + const flagRules: FlagRule[] = [{ + id: 'rule0', + variation: 0, + clauses: [ + { + attribute: 'name', + attributeReference: new AttributeReference('name'), + contextKind: 'user', + negate: false, + op: 'in', + values: [ + 'ben', + 'christian', + ], + }, + { + contextKind: 'user', + attribute: 'country', + attributeReference: new AttributeReference('country'), + negate: true, + op: 'in', + values: [ + 'fr', + ], + }, + ], + }]; + + expect(flag.build(1).rules).toEqual(flagRules); + + const clearedRulesFlag = flag.clearRules(); + expect(clearedRulesFlag.build(0)).not.toHaveProperty('rules'); + }); + + it('can move a targeted context from one variation to another', () => { + const flag = td.flag('test-flag').variationForContext('user', 'ben', false).variationForContext('user', 'ben', true); + // Because there was only one target in the first variation there will be only + // a single variation after that target is removed. + expect(flag.build(1).contextTargets).toEqual([ + { + contextKind: 'user', + variation: 0, + values: ['ben'], + }, + ]); + }); + + it('if a targeted context is moved from one variation to another, then other targets remain for that variation', () => { + const flag = td.flag('test-flag') + .variationForContext('user', 'ben', false) + .variationForContext('user', 'joe', false) + .variationForContext('user', 'ben', true); + + expect(flag.build(1).contextTargets).toEqual([ + { + contextKind: 'user', + variation: 0, + values: ['ben'], + }, + { + contextKind: 'user', + variation: 1, + values: ['joe'], + }, + ]); + }); + + it('should allow targets from multiple contexts in the same variation', () => { + const flag = td.flag('test-flag') + .variationForContext('user', 'ben', false) + .variationForContext('potato', 'russet', false) + .variationForContext('potato', 'yukon', false); + // Because there was only one target in the first variation there will be only + // a single variation after that target is removed. + expect(flag.build(0).contextTargets).toEqual([ + { + contextKind: 'user', + variation: 1, + values: ['ben'], + }, + { + contextKind: 'potato', + variation: 1, + values: ['russet', 'yukon'], + }, + ]); + }); +}); diff --git a/server-sdk-common/src/api/integrations/TestData.ts b/server-sdk-common/src/api/integrations/TestData.ts deleted file mode 100644 index 1ff6279f49..0000000000 --- a/server-sdk-common/src/api/integrations/TestData.ts +++ /dev/null @@ -1,377 +0,0 @@ -/** - * 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 [[TestData.update]] method. - * - * @example - * const { TestData } = require('launchdarkly-node-server-sdk/interfaces'); - * - * const td = TestData(); - * testData.update(td.flag("flag-key-1").booleanFlag().variationForAll(true)); - * const client = new LDClient(sdkKey, { updateProcessor: td }); - * - * // 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 [[TestDataFlagBuilder]] that is returned by [[TestData.flag]]. - * [[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. - * - * @see [[FileDataSource]] - */ -export interface TestData { - /** - * Creates or copies a [[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 - * [[TestData.update]]. - * - * @param key the flag key - * @returns a flag configuration builder - * - */ - flag(key: string): TestDataFlagBuilder; - - /** - * 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: TestDataFlagBuilder): Promise; - - /** - * 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 [[TestDataFlagBuilder]] API. Otherwise it is recommended to use the - * regular [[flag]]/[[update]] mechanism to avoid dependencies on details of the data model. - * - * You cannot make incremental changes with [[flag]]/[[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(flagConfig: any): Promise; - - /** - * 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(segmentConfig: any): Promise; -} - -/** - * A builder for feature flag configurations to be used with [[TestData]]. - */ - -export interface TestDataFlagBuilder { - /** - * A shortcut for setting the flag to use the standard boolean configuration. - * - * This is the default for all new flags created with [[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(): TestDataFlagBuilder; - - /** - * 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: any[]): TestDataFlagBuilder; - - /** - * 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 [[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: boolean): TestDataFlagBuilder; - - /** - * 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: boolean | number): TestDataFlagBuilder; - - /** - * 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: boolean | number): TestDataFlagBuilder; - - /** - * 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: boolean | number): TestDataFlagBuilder; - - /** - * 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: any): TestDataFlagBuilder; - - /** - * 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: string, variation: boolean | number): TestDataFlagBuilder; - - /** - * 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: string, contextKey: string, - variation: boolean | number): TestDataFlagBuilder; - - /** - * Removes any existing rules from the flag. This undoes the effect of methods - * like [[ifMatch]]. - * - * @return the same flag builder - */ - clearRules(): TestDataFlagBuilder; - - /** - * Removes any existing targets from the flag. This undoes the effect of - * methods like [[variationForContext]]. - * - * @return the same flag builder - */ - clearAlltargets(): TestDataFlagBuilder; - - /** - * 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: string, attribute: string, ...values: any): TestDataRuleBuilder; - - /** - * Starts defining a flag rule using the "is not one of" operator. - * - * For example, this creates a rule that returnes `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: string, attribute: string, ...values: any): TestDataRuleBuilder; -} - -/** - * A builder for feature flag rules to be used with [[TestDataFlagBuilder]]. - * - * In the LaunchDarkly model, a flag can have any number of rules, and - * a rule can have any number of clauses. A clause is an individual test - * such as "name is 'X'". A rule matches a user if all of the rule's - * clauses match the user. - * - * To start defining a rule, use one of the flag builder's matching methods - * such as `ifMatch`. This defines the first clause for the rule. Optionally, - * you may add more clauses with the rule builder's methods such as `andMatch`. - * Finally, call `thenReturn` to finish defining the rule. - */ - -export interface TestDataRuleBuilder { - /** - * Adds another clause using the "is one of" operator. - * - * For example, this creates a rule that returns `true` if the name is - * "Patsy" and the country is "gb": - * - * testData.flag('flag') - * .ifMatch('name', 'Patsy') - * .andMatch('country', 'gb') - * .thenReturn(true) - * - * @param contextKind the kind of the context - * @param attribute the user attribute to match against - * @param values values to compare to - * @return the flag rule builder - */ - andMatch(contextKind: string, attribute: string, ...values: any): TestDataRuleBuilder; - - /** - * Adds another clause using the "is not one of" operator. - * - * For example, this creates a rule that returns `true` if the name is - * "Patsy" and the country is not "gb": - * - * testData.flag('flag') - * .ifMatch('name', 'Patsy') - * .andNotMatch('country', 'gb') - * .thenReturn(true) - * - * @param contextKind the kind of the context - * @param attribute the user attribute to match against - * @param values values to compare to - * @return the flag rule builder - */ - andNotMatch(contextKind: string, attribute: string, ...values: any): TestDataRuleBuilder; - - /** - * Finishes defining the rule, specifying the result value as either a boolean or an index - * - * 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 variation - * either `true` or `false` or the index of the desired variation: - * 0 for the first, 1 for the second, etc. - * @return the flag rule builder - */ - thenReturn(variation: boolean | number): TestDataFlagBuilder; -} diff --git a/server-sdk-common/src/api/integrations/index.ts b/server-sdk-common/src/api/integrations/index.ts index cff538a563..fdf3c3574d 100644 --- a/server-sdk-common/src/api/integrations/index.ts +++ b/server-sdk-common/src/api/integrations/index.ts @@ -1,2 +1 @@ export * from './FileDataSourceOptions'; -export * from './TestData'; diff --git a/server-sdk-common/src/integrations/test_data/TestData.ts b/server-sdk-common/src/integrations/test_data/TestData.ts new file mode 100644 index 0000000000..c1a44e3489 --- /dev/null +++ b/server-sdk-common/src/integrations/test_data/TestData.ts @@ -0,0 +1,194 @@ +import { LDStreamProcessor } from '../../api'; +import { Flag } from '../../evaluation/data/Flag'; +import { Segment } from '../../evaluation/data/Segment'; +import Configuration from '../../options/Configuration'; +import AsyncStoreFacade from '../../store/AsyncStoreFacade'; +import { processFlag, processSegment } from '../../store/serialization'; +import VersionedDataKinds from '../../store/VersionedDataKinds'; +import TestDataFlagBuilder from './TestDataFlagBuilder'; +import TestDataSource from './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 [[TestData.update]] method. + * + * @example + * TKTK: Needs updating when we know the package name. + * const { TestData } = require('launchdarkly-node-server-sdk/interfaces'); + * + * const td = TestData(); + * testData.update(td.flag("flag-key-1").booleanFlag().variationForAll(true)); + * TKTK: May need to update this, depending on how we show importing the client. + * 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 [[TestDataFlagBuilder]] that is returned by [[TestData.flag]]. + * [[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. + * + * @see [[FileDataSource]] + */ +export default class TestData { + private currentFlags: Record = {}; + + private currentSegments: Record = {}; + + private dataSources: TestDataSource[] = []; + + private flagBuilders: Record = {}; + + /** + * Get a factory for update processors that will be attached to this TestData instance. + * @returns An update processor factory. + */ + getFactory(): (config: Configuration) => LDStreamProcessor { + // Provides an arrow function to prevent needed to bind the method to + // maintain `this`. + return (config: Configuration) => { + const newSource = new TestDataSource( + new AsyncStoreFacade(config.featureStore), + this.currentFlags, + + this.currentSegments, + (tds) => { + this.dataSources.splice(this.dataSources.indexOf(tds)); + }, + ); + + this.dataSources.push(newSource); + return newSource; + }; + } + + /** + * Creates or copies a [[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 + * [[TestData.update]]. + * + * @param key the flag key + * @returns a flag configuration builder + * + */ + flag(key: string): TestDataFlagBuilder { + if (this.flagBuilders[key]) { + return this.flagBuilders[key].clone(); + } + return new TestDataFlagBuilder(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: TestDataFlagBuilder): Promise { + 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.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 [[TestDataFlagBuilder]] API. Otherwise it is recommended to use the + * regular [[flag]]/[[update]] mechanism to avoid dependencies on details of the data model. + * + * You cannot make incremental changes with [[flag]]/[[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: any): Promise { + // 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 = { ...flagConfig, version: oldItem ? oldItem.version + 1 : flagConfig.version }; + processFlag(newItem); + this.currentFlags[flagConfig.key] = newItem; + + return Promise.all( + this.dataSources.map((impl) => impl.upsert(VersionedDataKinds.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: any): Promise { + const segmentConfig = JSON.parse(JSON.stringify(inConfig)); + + const oldItem = this.currentSegments[segmentConfig.key]; + const newItem = { + ...segmentConfig, + version: oldItem ? oldItem.version + 1 : segmentConfig.version, + }; + processSegment(newItem); + this.currentSegments[segmentConfig.key] = newItem; + + return Promise.all( + this.dataSources.map((impl) => impl.upsert(VersionedDataKinds.Segments, newItem)), + ); + } +} diff --git a/server-sdk-common/src/integrations/test_data/TestDataFlagBuilder.ts b/server-sdk-common/src/integrations/test_data/TestDataFlagBuilder.ts new file mode 100644 index 0000000000..fc39d67f09 --- /dev/null +++ b/server-sdk-common/src/integrations/test_data/TestDataFlagBuilder.ts @@ -0,0 +1,421 @@ +import { TypeValidators } from '@launchdarkly/js-sdk-common'; +import { Flag } from '../../evaluation/data/Flag'; +import { Target } from '../../evaluation/data/Target'; +import { TRUE_VARIATION_INDEX, FALSE_VARIATION_INDEX, variationForBoolean } from './booleanVariation'; +import TestDataRuleBuilder from './TestDataRuleBuilder'; + +interface BuilderData { + on: boolean; + variations: any[]; + offVariation?: number; + fallthroughVariation?: number; + // For a given variation, what are the targets of that variation. + // Each target being a context kind and a list of keys for that kind. + targetsByVariation?: Record>; + rules?: TestDataRuleBuilder[]; +} + +/** + * A builder for feature flag configurations to be used with [[TestData]]. + */ +export default class TestDataFlagBuilder { + private data: BuilderData = { + on: true, + variations: [], + }; + + /** + * @internal + */ + constructor(private readonly key: string, data?: BuilderData) { + 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.offVariation = data.offVariation; + } + if (data.targetsByVariation) { + this.data.targetsByVariation = JSON.parse(JSON.stringify(data.targetsByVariation)); + } + if (data.rules) { + this.data.rules = []; + data.rules.forEach((rule) => { + this.data.rules?.push(rule.clone()); + }); + } + } + } + + private get isBooleanFlag(): boolean { + return this.data.variations.length === 2 + && this.data.variations[TRUE_VARIATION_INDEX] === true + && this.data.variations[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 [[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(): TestDataFlagBuilder { + if (this.isBooleanFlag) { + return this; + } + // Change this flag into a boolean flag. + return this.variations(true, false) + .fallthroughVariation(TRUE_VARIATION_INDEX) + .offVariation(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: any[]): TestDataFlagBuilder { + 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 [[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: boolean): TestDataFlagBuilder { + 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: number | boolean): TestDataFlagBuilder { + if (TypeValidators.Boolean.is(variation)) { + return this.booleanFlag().fallthroughVariation(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: number | boolean): TestDataFlagBuilder { + if (TypeValidators.Boolean.is(variation)) { + return this.booleanFlag().offVariation(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: number | boolean): TestDataFlagBuilder { + 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: any): TestDataFlagBuilder { + 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: string, variation: number | boolean): TestDataFlagBuilder { + 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: string, + contextKey: string, + variation: number | boolean, + ): TestDataFlagBuilder { + if (TypeValidators.Boolean.is(variation)) { + return this.booleanFlag() + .variationForContext(contextKind, contextKey, 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 [[ifMatch]]. + * + * @return the same flag builder + */ + clearRules(): TestDataFlagBuilder { + delete this.data.rules; + return this; + } + + /** + * Removes any existing targets from the flag. This undoes the effect of + * methods like [[variationForContext]]. + * + * @return the same flag builder + */ + clearAllTargets(): TestDataFlagBuilder { + 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: string, + attribute: string, + ...values: any + ): TestDataRuleBuilder { + const flagRuleBuilder = new TestDataRuleBuilder(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: string, + attribute: string, + ...values: any + ): TestDataRuleBuilder { + const flagRuleBuilder = new TestDataRuleBuilder(this); + return flagRuleBuilder.andNotMatch(contextKind, attribute, ...values); + } + + /** + * @internal + */ + addRule(flagRuleBuilder: TestDataRuleBuilder) { + if (!this.data.rules) { + this.data.rules = []; + } + this.data.rules.push(flagRuleBuilder as TestDataRuleBuilder); + } + + /** + * @internal + */ + build(version: number) { + const baseFlagObject: Flag = { + key: this.key, + version, + on: this.data.on, + offVariation: this.data.offVariation, + fallthrough: { + variation: this.data.fallthroughVariation, + }, + variations: [...this.data.variations], + }; + + if (this.data.targetsByVariation) { + const contextTargets: Target[] = []; + Object.entries( + this.data.targetsByVariation, + ).forEach(([variation, contextTargetsForVariation]) => { + Object.entries(contextTargetsForVariation).forEach(([contextKind, values]) => { + contextTargets.push({ + contextKind, + values, + // Iterating the object it will be a string. + variation: parseInt(variation, 10), + }); + }); + }); + baseFlagObject.contextTargets = contextTargets; + } + + if (this.data.rules) { + baseFlagObject.rules = this.data.rules.map( + (rule, i) => (rule as TestDataRuleBuilder).build(String(i)), + ); + } + + return baseFlagObject; + } + + /** + * @internal + */ + clone(): TestDataFlagBuilder { + return new TestDataFlagBuilder(this.key, this.data); + } + + /** + * @internal + */ + getKey(): string { + return this.key; + } +} diff --git a/server-sdk-common/src/integrations/test_data/TestDataRuleBuilder.ts b/server-sdk-common/src/integrations/test_data/TestDataRuleBuilder.ts new file mode 100644 index 0000000000..3c2785595e --- /dev/null +++ b/server-sdk-common/src/integrations/test_data/TestDataRuleBuilder.ts @@ -0,0 +1,149 @@ +import { AttributeReference, TypeValidators } from '@launchdarkly/js-sdk-common'; +import { variationForBoolean } from './booleanVariation'; +import { Clause } from '../../evaluation/data/Clause'; + +/** + * A builder for feature flag rules to be used with [[TestDataFlagBuilder]]. + * + * In the LaunchDarkly model, a flag can have any number of rules, and + * a rule can have any number of clauses. A clause is an individual test + * such as "name is 'X'". A rule matches a user if all of the rule's + * clauses match the user. + * + * To start defining a rule, use one of the flag builder's matching methods + * such as `ifMatch`. This defines the first clause for the rule. Optionally, + * you may add more clauses with the rule builder's methods such as `andMatch`. + * Finally, call `thenReturn` to finish defining the rule. + */ + +export default class TestDataRuleBuilder { + private clauses: Clause[] = []; + + private variation?: number; + + /** + * @internal + */ + constructor( + private readonly flagBuilder: BuilderType & { + addRule: (rule: TestDataRuleBuilder) => void, + booleanFlag: () => BuilderType, + }, + clauses?: Clause[], + variation?: number, + ) { + if (clauses) { + this.clauses = [...clauses]; + } + if (variation !== undefined) { + this.variation = variation; + } + } + + /** + * Adds another clause using the "is one of" operator. + * + * For example, this creates a rule that returns `true` if the name is + * "Patsy" and the country is "gb": + * + * testData.flag('flag') + * .ifMatch('name', 'Patsy') + * .andMatch('country', 'gb') + * .thenReturn(true) + * + * @param contextKind the kind of the context + * @param attribute the user attribute to match against + * @param values values to compare to + * @return the flag rule builder + */ + andMatch( + contextKind: string, + attribute: string, + ...values: any + ): TestDataRuleBuilder { + this.clauses.push({ + contextKind, + attribute, + attributeReference: new AttributeReference(attribute), + op: 'in', + values, + negate: false, + }); + return this; + } + + /** + * Adds another clause using the "is not one of" operator. + * + * For example, this creates a rule that returns `true` if the name is + * "Patsy" and the country is not "gb": + * + * testData.flag('flag') + * .ifMatch('name', 'Patsy') + * .andNotMatch('country', 'gb') + * .thenReturn(true) + * + * @param contextKind the kind of the context + * @param attribute the user attribute to match against + * @param values values to compare to + * @return the flag rule builder + */ + andNotMatch( + contextKind: string, + attribute: string, + ...values: any + ): TestDataRuleBuilder { + this.clauses.push({ + contextKind, + attribute, + attributeReference: new AttributeReference(attribute), + op: 'in', + values, + negate: true, + }); + return this; + } + + /** + * Finishes defining the rule, specifying the result value as either a boolean or an index + * + * 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 variation + * either `true` or `false` or the index of the desired variation: + * 0 for the first, 1 for the second, etc. + * @return the flag rule builder + */ + thenReturn(variation: number | boolean): BuilderType { + if (TypeValidators.Boolean.is(variation)) { + this.flagBuilder.booleanFlag(); + return this.thenReturn(variationForBoolean(variation)); + } + + this.variation = variation; + this.flagBuilder.addRule(this); + return this.flagBuilder; + } + + /** + * @internal + */ + build(id: string) { + return { + id: `rule${id}`, + variation: this.variation, + clauses: this.clauses, + }; + } + + /** + * @internal + */ + clone(): TestDataRuleBuilder { + return new TestDataRuleBuilder(this.flagBuilder, this.clauses, this.variation); + } +} diff --git a/server-sdk-common/src/integrations/test_data/TestDataSource.ts b/server-sdk-common/src/integrations/test_data/TestDataSource.ts new file mode 100644 index 0000000000..6eccfa09b2 --- /dev/null +++ b/server-sdk-common/src/integrations/test_data/TestDataSource.ts @@ -0,0 +1,40 @@ +import { LDStreamProcessor } from '../../api'; +import { DataKind } from '../../api/interfaces'; +import { LDKeyedFeatureStoreItem } from '../../api/subsystems'; +import { Flag } from '../../evaluation/data/Flag'; +import { Segment } from '../../evaluation/data/Segment'; +import AsyncStoreFacade from '../../store/AsyncStoreFacade'; +import VersionedDataKinds from '../../store/VersionedDataKinds'; + +/** + * @internal + */ +export default class TestDataSource implements LDStreamProcessor { + constructor( + private readonly featureStore: AsyncStoreFacade, + private readonly flags: Record, + private readonly segments: Record, + private readonly onStop: (tfs: TestDataSource) => void, + ) { + } + + async start(fn?: ((err?: any) => void) | undefined) { + await this.featureStore.init({ + [VersionedDataKinds.Features.namespace]: { ...this.flags }, + [VersionedDataKinds.Segments.namespace]: { ...this.segments }, + }); + fn?.(); + } + + stop() { + this.onStop(this); + } + + close() { + this.stop(); + } + + async upsert(kind: DataKind, value: LDKeyedFeatureStoreItem) { + return this.featureStore.upsert(kind, value); + } +} diff --git a/server-sdk-common/src/integrations/test_data/booleanVariation.ts b/server-sdk-common/src/integrations/test_data/booleanVariation.ts new file mode 100644 index 0000000000..079a86a2a2 --- /dev/null +++ b/server-sdk-common/src/integrations/test_data/booleanVariation.ts @@ -0,0 +1,9 @@ +export const TRUE_VARIATION_INDEX = 0; +export const FALSE_VARIATION_INDEX = 1; + +/** + * @internal + */ +export function variationForBoolean(val: boolean) { + return val ? TRUE_VARIATION_INDEX : FALSE_VARIATION_INDEX; +} diff --git a/server-sdk-common/src/options/Configuration.ts b/server-sdk-common/src/options/Configuration.ts index be211480a4..cc037c7634 100644 --- a/server-sdk-common/src/options/Configuration.ts +++ b/server-sdk-common/src/options/Configuration.ts @@ -215,13 +215,6 @@ export default class Configuration { ); this.eventsCapacity = validatedOptions.capacity; this.timeout = validatedOptions.timeout; - if (TypeValidators.Function.is(validatedOptions.featureStore)) { - // @ts-ignore - this.featureStore = validatedOptions.featureStore(options); - } else { - // @ts-ignore - this.featureStore = validatedOptions.featureStore; - } // TODO: bigSegments this.updateProcessor = validatedOptions.updateProcessor; @@ -244,5 +237,13 @@ export default class Configuration { this.wrapperVersion = validatedOptions.wrapperVersion; this.tags = new ApplicationTags(validatedOptions); this.diagnosticRecordingInterval = validatedOptions.diagnosticRecordingInterval; + + if (TypeValidators.Function.is(validatedOptions.featureStore)) { + // @ts-ignore + this.featureStore = validatedOptions.featureStore(this); + } else { + // @ts-ignore + this.featureStore = validatedOptions.featureStore; + } } } diff --git a/server-sdk-common/src/store/serialization.ts b/server-sdk-common/src/store/serialization.ts index 1021f3b582..641b0581e5 100644 --- a/server-sdk-common/src/store/serialization.ts +++ b/server-sdk-common/src/store/serialization.ts @@ -68,7 +68,10 @@ function processRollout(rollout?: Rollout) { } } -function processFlag(flag: Flag) { +/** + * @internal + */ +export function processFlag(flag: Flag) { if (flag.fallthrough && flag.fallthrough.rollout) { const rollout = flag.fallthrough.rollout!; processRollout(rollout); @@ -86,7 +89,10 @@ function processFlag(flag: Flag) { }); } -function processSegment(segment: Segment) { +/** + * @internal + */ +export function processSegment(segment: Segment) { segment?.rules?.forEach((rule) => { if (rule.bucketBy) { // Rules before U2C would have had literals for attributes.