Comparing version 0.6.2 to 0.8.1
@@ -0,4 +1,7 @@ | ||
'use strict'; | ||
//-------------------------------------------------------------------------- | ||
// Private | ||
//-------------------------------------------------------------------------- | ||
var getFeatures, | ||
@@ -9,11 +12,3 @@ getCriteria, | ||
function setCriteria(configVal) { | ||
if(typeof configVal == 'function') { | ||
getCriteria = configVal; | ||
updateCriteria(); | ||
} else { | ||
getCriteria = undefined; | ||
} | ||
if(typeof configVal == 'object') { | ||
self._criteria = configVal; | ||
} | ||
self._criteria = configVal; | ||
} | ||
@@ -33,16 +28,2 @@ | ||
function updateCriteria() { | ||
if(!getCriteria) { | ||
return; | ||
} else if(getCriteria.length === 0) { | ||
self._criteria = getCriteria() || self._criteria; | ||
return; | ||
} else if(getCriteria.length === 1) { | ||
getCriteria(getCriteriaCallback); | ||
return; | ||
} else if(getCriteria.length > 1) { | ||
throw new Error('Too Many Arguments!'); | ||
} | ||
} | ||
function updateFeatures() { | ||
@@ -93,21 +74,21 @@ if(!getFeatures) { | ||
reload: function() { | ||
updateCriteria(); | ||
updateFeatures(); | ||
}, | ||
userHasFeature: function(user, f_name) { | ||
var featureCriteria = self._features[f_name]; | ||
if(typeof featureCriteria == 'undefined') { | ||
userHasFeature: function(user, featureName) { | ||
var feature = self._features[featureName]; | ||
if(typeof feature != 'object') { | ||
return null; | ||
} | ||
if(Object.keys(featureCriteria).length === 0) { | ||
var featureCriteria = feature.criteria || {}; | ||
var criteriaArray = Object.keys(featureCriteria); | ||
var isEnabled = true; | ||
if(criteriaArray.length == 0) { | ||
return false; | ||
} | ||
var isEnabled = true; | ||
Object.keys(featureCriteria).forEach(function(ckey) { | ||
criteriaArray.forEach(function(cKey) { | ||
if(isEnabled) { | ||
var c_name = ckey; | ||
var c_data = featureCriteria[ckey]; | ||
var c_func = self._criteria[c_name]; | ||
isEnabled = c_func(user, c_data); | ||
var c_data = featureCriteria[cKey]; | ||
var c_func = self._criteria[cKey]; | ||
isEnabled = c_func(user, c_data); | ||
} | ||
@@ -121,7 +102,7 @@ }); | ||
var user_features = {}; | ||
Object.keys(self._features).forEach(function(f_name) { | ||
if(flags[f_name] !== undefined) { | ||
user_features[f_name] = flags[f_name]; | ||
Object.keys(self._features).forEach(function(featureName) { | ||
if(flags[featureName] !== undefined) { | ||
user_features[featureName] = flags[featureName]; | ||
} else { | ||
user_features[f_name] = self.userHasFeature(user, f_name); | ||
user_features[featureName] = self.userHasFeature(user, featureName); | ||
} | ||
@@ -137,15 +118,19 @@ }); | ||
_isSet: false, | ||
features: {}, | ||
flags: req.cookies.fflip | ||
features: {} | ||
}; | ||
req.fflip.setFeatures = function(user) { | ||
if(req.cookies) { | ||
req.fflip.flags = req.cookies.fflip; | ||
} else { | ||
req.fflip.flags = {}; | ||
} | ||
req.fflip.setForUser = function(user) { | ||
req.fflip.features = self.featuresForUser(user, req.fflip.flags); | ||
req.fflip._isSet = true; | ||
}; | ||
req.fflip.hasFeature = function(fName) { | ||
req.fflip.has = function(featureName) { | ||
if(!req.fflip._isSet) { | ||
console.error('FFlip: features not set - call setFeatures() before checking for features (and consider adding middleware to always set features)'); | ||
console.error('FFlip: features not set - call setForUser() before checking for features (and consider adding middleware to always set features)'); | ||
return null; | ||
} | ||
return req.fflip.features[fName]; | ||
return req.fflip.features[featureName]; | ||
}; | ||
@@ -152,0 +137,0 @@ |
{ | ||
"name": "fflip", | ||
"description": "Highly Customizable & Über Powerful Feature Flipping", | ||
"description": "Feature Flipping Across the Server and Client", | ||
"author": "Fred Schott <fredkschott@gmail.com>", | ||
"licence": "MIT", | ||
"version": "0.6.2", | ||
"version": "0.8.1", | ||
"main": "lib/fflip", | ||
"repository": { | ||
"type": "git", | ||
"url": "http://github.com/FredKSchott/fflip" | ||
}, | ||
"repository": "FredKSchott/fflip", | ||
"bugs": "https://github.com/FredKSchott/fflip/issues", | ||
@@ -23,10 +20,9 @@ "scripts": { | ||
"keywords": [ | ||
"feature flipping", | ||
"feature toggling", | ||
"feature", | ||
"flip", | ||
"toggle", | ||
"continuous integration", | ||
"ci" | ||
"feature flipping", | ||
"feature toggling", | ||
"continuous integration" | ||
] | ||
} |
@@ -7,3 +7,3 @@ [![Build Status](https://travis-ci.org/FredKSchott/fflip.png)](https://travis-ci.org/FredKSchott/fflip) | ||
Working on an experimental new design? Starting a closed beta? Rolling out a new feature over the next few weeks? Fa-fa-fa-flip it! __fflip__ gives you complete control over releasing new functionality to your users based on thier user id, join date, membership status, and whatever else you can think of. __fflip's__ goal is to be the most extendable and customizable feature flipping/toggling module out there. | ||
Working on an experimental new design? Starting a closed beta? Rolling out a new feature over the next few weeks? Fa-fa-fa-flip it! __fflip__ gives you complete control over releasing new functionality to your users based on thier user id, join date, membership status, and whatever else you can think of. __fflip's__ goal is to be the most powerful and extendable feature flipping/toggling module out there. | ||
@@ -49,3 +49,4 @@ - Describes __custom criteria and features__ using easy-to-read JSON | ||
for(var id in idArr) { | ||
if(user.id == idArr[id]) return true; | ||
if(user.id == idArr[id]) | ||
return true; | ||
} | ||
@@ -58,14 +59,24 @@ return false; | ||
###Features | ||
Features are sets of criteria to test users against. The value associated with the criteria is passed in as the data argument of the criteria function. A user will have a featured enabled if they match all listed criteria, otherwise the feature is disabled. Features are described as follows: | ||
Features contain sets of criteria to test users against. The value associated with the criteria is passed in as the data argument of the criteria function. A user will have a featured enabled if they match all listed criteria, otherwise the feature is disabled. Features can include other optional properties for context. Features are described as follows: | ||
```javascript | ||
var ExampleFeaturesObject = { | ||
paidFeature: { | ||
isPaidUser: true | ||
criteria: { | ||
isPaidUser: true | ||
} | ||
}, | ||
closedBeta: { | ||
allowUserIDs: [20,30,80,181], | ||
name: "A Closed Beta", | ||
criteria: { | ||
allowUserIDs: [20,30,80,181] | ||
} | ||
}, | ||
newFeatureRollout: { | ||
isPaidUser: false, | ||
percentageOfUsers: 0.50, | ||
name: "A New Feature Rollout", | ||
description: "Rollout of that new feature over the next month", | ||
owner: "FredKSchott", // Remember: These are all optional, only criteria is required | ||
criteria: { | ||
isPaidUser: false, | ||
percentageOfUsers: 0.50 | ||
} | ||
} | ||
@@ -80,3 +91,3 @@ } | ||
Bool userHasFeature(user, featureName) // Return true/false if featureName is enabled for user | ||
void reload() // Force a reload of criteria/features | ||
void reload() // Force a reload (if loading features dynamically) | ||
void __express(app) // Connect with an Express app (see below) | ||
@@ -88,17 +99,17 @@ ``` | ||
fflip.config({ | ||
criteria: {}, // Object (see above) or Function (see below) | ||
features: {}, // Object or Function | ||
reload: 30, // Time between refreshing features/criteria, in seconds | ||
criteria: {}, // Criteria Object | ||
features: {}, // Features Object | Function (see below) | ||
reload: 30, // Interval for refreshing features, in seconds | ||
}); | ||
``` | ||
###Loading Features & Criteria Dynamically | ||
__fflip__ also accepts functions for loading criteria and features. If __fflip__ is passed a funciton with no arguments it will call the function and accept the return value. To load asyncronously, pass a function that sends a features/criteria data object to a callback. __fflip__ will recieve the callback and set the data accordingly. In both cases, __fflip__ will save these functions and call them again every X seconds, as set by the reload parameter. | ||
###Loading Features Dynamically | ||
__fflip__ also accepts functions for loading features. If __fflip__ is passed a funciton with no arguments it will call the function and accept the return value. To load asyncronously, pass a function that sends a features object to a callback. __fflip__ will recieve the callback and set the data accordingly. In both cases, __fflip__ will save the function and call it again every X seconds, as set by the reload parameter. | ||
```javascript | ||
// Load Criteria Syncronously | ||
var getCriteriaSync = function() { | ||
var collection = db.collection('criteria'); | ||
var criteriaArr = collection.find().toArray(); | ||
/* Proccess criteriaArr -> criteriaObj (format described above) */ | ||
return criteriaObj; | ||
var getFeaturesSync = function() { | ||
var collection = db.collection('features'); | ||
var featuresArr = collection.find().toArray(); | ||
/* Proccess featuresArr -> featuresObj (format described above) */ | ||
return featuresObj; | ||
} | ||
@@ -117,5 +128,4 @@ | ||
fflip.config({ | ||
criteria: getCriteriaSync, | ||
features: getFeaturesAsync, | ||
reload: 60 /* Call each function again and update features every 60 secondss */ | ||
features: getFeaturesAsync, // or: getFeaturesSync | ||
reload: 60 /* Call the function again and update every 60 secondss */ | ||
}); | ||
@@ -126,3 +136,4 @@ ``` | ||
##Express Integration | ||
__fflip__ provides easy integration with the popular web framework [Express](https://github.com/visionmedia/express). Just call ``fflip.__express(app)`` wherever you set up your server to enable the following: | ||
__fflip__ provides easy integration with the popular web framework [Express](https://github.com/visionmedia/express). | ||
Just call ``fflip.__express(app)`` wherever you set up your express application to enable the following: | ||
@@ -137,8 +148,7 @@ ####__A route for manually flipping on/off features__ | ||
flags: Any override flags set by the fflip cookie | ||
features: A user's fflip features object. Undefined until setFeatures() is called. | ||
setFeatures(user): Given a user, attaches the features object to the request (at req.fflip.features) | ||
hasFeature(featureName): Given a feature name, returns the feature boolean, or null if setFeatures() has't been called | ||
features: A user's fflip features object. Empty until setForUser() is called. | ||
setForUser(user): Given a user, attaches the features object to the request (at req.fflip.features) | ||
has(featureName): Given a feature name, returns the feature boolean, undefined if feature doesn't exist, or null if setForUser() has't been called | ||
} | ||
``` | ||
To avoid polluting the request object, All fflip functionality is contained within req.fflip. But (If your implementation allows it) you can add aliases directly onto the request object. | ||
@@ -145,0 +155,0 @@ ####Automatically deliver Features to the client |
@@ -19,9 +19,17 @@ var assert = require('assert'), | ||
fOpen: { | ||
c1: true | ||
name: 'fOpen', | ||
description: 'true for all users', | ||
criteria: { | ||
c1: true | ||
} | ||
}, | ||
fClosed: { | ||
c1: false | ||
fClosed: { | ||
criteria: { | ||
c1: false | ||
} | ||
}, | ||
fEval: { | ||
c2: 'abc' | ||
criteria: { | ||
c2: 'abc' | ||
} | ||
} | ||
@@ -87,19 +95,2 @@ }, | ||
test('should set criteria if given a syncronous loading function', function(){ | ||
var loadSyncronously = function() { | ||
return configData.criteria; | ||
}; | ||
fflip.config({criteria: loadSyncronously}); | ||
assert.equal(configData.criteria, fflip._criteria); | ||
}); | ||
test('should set criteria if given an asyncronous loading function', function(done){ | ||
var loadAsyncronously = function(callback) { | ||
callback(configData.criteria); | ||
assert.equal(configData.criteria, fflip._criteria); | ||
done(); | ||
}; | ||
fflip.config({criteria: loadAsyncronously}); | ||
}); | ||
test('should set reloadRate if given reload', function(){ | ||
@@ -120,27 +111,21 @@ fflip._reloadRate = 0; | ||
this.timeout(205); | ||
var count = -99; | ||
var loadAsyncronously = function(callback) { | ||
count++; | ||
if(count >= 2) { | ||
done(); | ||
} | ||
callback({}); | ||
done(); | ||
}; | ||
fflip.config({features: loadAsyncronously, criteria: loadAsyncronously, reload: 0.2}); | ||
fflip.config({features: loadAsyncronously, reload: 0.2}); | ||
count = 0; | ||
}); | ||
test('should update criteria and features', function(done){ | ||
var count; | ||
test('should update features', function(done){ | ||
this.timeout(100); | ||
var testReady = false; | ||
var loadAsyncronously = function(callback) { | ||
count++; | ||
callback({}); | ||
if(testReady) | ||
done(); | ||
}; | ||
fflip.config({features: loadAsyncronously, criteria: loadAsyncronously}); | ||
count = 0; | ||
fflip.config({features: loadAsyncronously}); | ||
testReady = true; | ||
fflip.reload(); | ||
setTimeout(function() { | ||
assert.equal(count, 2); | ||
done(); | ||
}, 100); | ||
}); | ||
@@ -160,3 +145,3 @@ | ||
test('should return false if features has no criteria', function(){ | ||
test('should return false if no criteria set', function(){ | ||
assert.equal(false, fflip.userHasFeature(userABC, 'fEmpty')); | ||
@@ -241,3 +226,3 @@ }); | ||
fflip._express_middleware(this.reqMock, this.resMock, function() { | ||
me.reqMock.fflip.setFeatures(userXYZ); | ||
me.reqMock.fflip.setForUser(userXYZ); | ||
assert(fflip.featuresForUser.calledOnce); | ||
@@ -250,9 +235,9 @@ assert(fflip.featuresForUser.calledWith(userXYZ, {fClosed: false})); | ||
test('req.fflip.hasFeatures() should get the correct features', function(done) { | ||
test('req.fflip.has() should get the correct features', function(done) { | ||
var me = this; | ||
fflip._express_middleware(this.reqMock, this.resMock, function() { | ||
me.reqMock.fflip.setFeatures(userXYZ); | ||
assert.strictEqual(me.reqMock.fflip.hasFeature('fOpen'), true); | ||
assert.strictEqual(me.reqMock.fflip.hasFeature('fClosed'), false); | ||
assert.strictEqual(me.reqMock.fflip.hasFeature('notafeature'), undefined); | ||
me.reqMock.fflip.setForUser(userXYZ); | ||
assert.strictEqual(me.reqMock.fflip.has('fOpen'), true); | ||
assert.strictEqual(me.reqMock.fflip.has('fClosed'), false); | ||
assert.strictEqual(me.reqMock.fflip.has('notafeature'), undefined); | ||
done(); | ||
@@ -262,9 +247,9 @@ }); | ||
test('req.fflip.hasFeatures() should return null if features have not been set', function(done) { | ||
test('req.fflip.has() should return null if features have not been set', function(done) { | ||
var me = this; | ||
var consoleErrorStub = sinon.stub(console, 'error'); // Supress Error Output | ||
fflip._express_middleware(this.reqMock, this.resMock, function() { | ||
assert.strictEqual(me.reqMock.fflip.hasFeature('fOpen'), null); | ||
assert.strictEqual(me.reqMock.fflip.hasFeature('fClosed'), null); | ||
assert.strictEqual(me.reqMock.fflip.hasFeature('notafeature'), null); | ||
assert.strictEqual(me.reqMock.fflip.has('fOpen'), null); | ||
assert.strictEqual(me.reqMock.fflip.has('fClosed'), null); | ||
assert.strictEqual(me.reqMock.fflip.has('notafeature'), null); | ||
done(); | ||
@@ -271,0 +256,0 @@ consoleErrorStub.restore(); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
153
24247
472
1