@baropublic/feature-toggle v1.0.4
feature-toggle
Provides a centralized place for a SPA to specify and evaluate rules used to decide what functionality is available for the current app session.
Initialization
In order to initialize the context you should call the init(data) method.
import $ft from '@baropublic/feature-toggle';
$ft.init({
permissions: ['account:create', 'account:delete', 'invoice:read_all'],
profile: {
name: 'John',
familyName: 'Snow'
},
features: {
monthlyReport: true,
}
}, {env: 'dev'});
Now the context have some information against what we can evaluate assertions.
API
- init(initialData: Object, options: ContextOptions): void Initialize the context with some initial data and options. - ContextOptions: {env: 'dev' | 'prod', strict: boolean} env Affect the properties "$env.dev" and "$env.prod" created automatically by the init() method in the context. $env.isDev = options.env === "dev" $env.isProd = options.env === "prod" strict indicate if the library should throw an error when a property is not found in the context when an assertion is executed.
set(name: string, value: *, sealed: boolean): void Set or update a property in the context. If sealed is true then the property is readOnly. Notice that if value is an object all its properties will be read only as well, the same applies for deep nested objects.
```javascript $ft.set('profile.name', 'Albert'); $ft.set('features', {enableMonthlyReports:true, enableProfileEdition:false}, true); //none of this will have any effect because the prop "features" is sealed. $ft.set('features',{}); $ft.set('features.enableMonthlyReports', false); ```
evalFn(fn(q: Queryable): *): boolean Execute fn passing a queryable in order to allow access to the data in the context, the boolean representation of the value returned by fn will be returned by evalFn.
```javascript $ft.evalFn(q => q.get('features.monthlyReport') ? 1 : 0) //return true ```
ec(alias: Object): EvalContext Creates a new EvalContext in order to evaluate assertions against the context. Accept an object with aliases useful to avoid the repetition of long prop paths.
```javascript $ft.ec().eq('profile.familyName', 'Snow').eval(); //alias example let alias = {pfn:'profile.familyName'}; $ft.ec(alias).eq('${pfn}', 'Snow').eval() //return true //the alias is extended to all the chained assertions alias = {p:'profile.'}; $ft.ec(alias).eq('${p}name', 'John').eq('${p}familyName', 'Snow').eval() //return true ```
rel(path: string): EvalContext Change the scope of evaluation to the path specified. This is useful if you have very nested objects and want to avoid writing in every assertion the full property path.
```javascript //this will eval very.nested.object.prop1 and very.nested.object.array1 $ft.rel('very.nested.object').eq('prop1', 3).some('array1', ['a', 'b']).eval() ```
select(...descriptors: Selectable): * Select the value of the first selectable evaluated to true.
```javascript let s1 = $ft.ec().some('permissions', ['account:read']).selector(1); let s2 = $ft.ec().some('permissions', ['account:delete']).selector(2); //Will return the value of the first selector evaluated to true $ft.select(s1, s2) //return 2 ```
selectAll(...descriptors: Selectable): Array Similar to select() but returns an array with the values of all the selectables evaluated to true.
selector(value: *, evalCb(q: Queryable): boolean): Selectable Creates a new Selectable without specifying any assertion. This is useful to generate a default Selectable to be used in case all the others evaluation failed. In order to do that you must ignore the second argument. In case evalCb is passed the result of it indicate if the Selectable should be considered satisfied.
```javascript const mreSelector = $ft.selector('monthlyReportDisabled', q => q.get('features.monthlyReport') === false); const defSelector = $ft.selector('monthlyReportEnabled'); $ft.select(mreSelector, defSelector); //return "monthlyReportDisabled" ```
runtimeSelector(...descriptors: Selectable): RuntimeProxy Creates a proxy that determine in runtime to what object forward a method invocation.
```javascript const s1 = $ft.ec().eq('profile.lang', 'en').selector({ sayHello: () => 'Hello' }); const s2 = $ft.ec().eq('profile.lang', 'es').selector({ sayHello: () => 'Hola' }); const greeting = $ft.runtimeSelector(s1, s2); $ft.set('lang', 'en'); greeting.sayHello(); // return 'Hello' $ft.set('lang', 'es'); greeting.sayHello(); // return 'Hola' ```
forRule(id: string): Rule Retrieve a rule registered in the context so we can evaluate it. If not rule is found with the specified id a noop rule is returned so we dont have to check if the rule exist to invoke methods on it.
```javascript $ft.forRule('notExistent').eval() // returns false $ft.forRule('notExistent').runIf(()=> console.log('Hi !')); // will not print $ft.forRule('notExistent').selector('falsie selectable'); // will not be selected //valid rule $ft.ec().eq('profile.familyName', 'Snow').saveAsRule('isASnow'); $ft.forRule('isASnow').runIf(() => console.log('A king in the north !')); // will print the message ```
EvalContext
In order to do assertions in the context we can use the methods present in the object EvalContext obtained by calling $ft.ec(). Those methods can be chained because all of them returns a EvalContext instance.
API
Assertions
eq(propName: string, expectedValue: *): EvalContext Asserts that the value of the property specified by propName is equal to the expectedValue.
neq(propName: string, expectedValue: *): EvalContext Asserts that the value of the property specified by propName is not equal to the expectedValue.
all(propName: string, contained: Array): EvalContext Asserts that the value of the property specified by propName contains all the items in contained. In other words test if contained is a subgroup of propName. Both values must be arrays.
some(propName: string, coincidence: Array): EvalContext Asserts that the value of the property specified by propName contains at least one of the items in coincidence. The property specified by propName is expected to be an array, in case of other value type it will be converted to an array.
none(propName: string, excluded: Array): EvalContext Asserts that the value of the property specified by propName doesnt contain none of the items in excluded. Both values must be arrays.
or: EvalContext Is an evaluation context that returns the OR logical operation between two EvalContext.
```javascript let result = $ft.ec() .eq('profile.name','Adrian') .or .some('permissions',['account:delete']) .eval(); //result === true ```
assertFn(assertion(q:Queryable): *): EvalContext Adds assertion to the evaluation context.
```javascript $ft.ec() .assertFn(q => q.get('permissions').includes('account:create')) .saveAsRule('allowEdition'); $ft.forRule('allowEdition').eval(); //return true ```
in(propName: string, coincidence: Array): EvalContext Assert that the property referenced by propName is present in coincidence.
```javascript $ft.ec().in('profile.name', ['Albert', 'John']).eval() //return true ```
Evaluation
After defining the assertions that form an evaluation context, we should be able to execute them. In order to do that we will use some of the following methods:
eval(): boolean Executes the assertions (in the EvalContext) against the context data and return the evaluation result. An example ca be seen in the or explanation of the previous section.
evalFn(fn(q: Queryable): *): boolean Similar to the evalFn in the context.
```javascript $ft.evalFn(q => q.get('features.monthlyReport') ? 1 : 0) //return true ```
runIf(fn(q:Queryable): void): void Executes a function only if the EvalContext evaluation is true. The function received as argument will be called with a Queryable object as argument in order to allow access to the context information.
```javascript $ft.ec().some('permissions', ['account:delete']) .runIf(q => `The user ${q.get('profile.name')} can delete an account`); //the previous statement is similar to if($ft.ec().some('permissions', ['account:delete']).eval()){ $ft.run(q => `The user ${q.get('profile.name')} can delete an account`); } ```
saveAsRule(id: string): void Saves the evaluation context as a Rule identified by id. Then we can access and evaluate the rule from the global context using the method forRule(id: string): Rule. Should be used to define rules used across the project, for example in a configuration file executed during the application initialization, and be able to access those rules every where in our code.
```javascript //app.config.js $ft.ec().some('permissions', ['account:delete']).saveAsRule('canDeleteAccount'); //profile.list.js //... if($ft.forRule('canDeleteAccount').eval()){ deleteProfile(profileId); } //... ```
selector(wrappedValue:*): Selectable Creates a wrapper around wrappedValue using the current evaluation context as condition, this is useful when we need to chose between different objects based in different assertions.
```javascript let s1 = $ft.ec().some('permissions', ['account:read']).selector(1); let s2 = $ft.ec().some('permissions', ['account:delete']).selector(2); let defaultSelector = $ft.ec().selector(3); //Will return the value of the first selector evaluated to true $ft.select(s1, s2, defaultSelector) //return 2 ```