diff --git a/app.js b/app.js
index f26691d066..e520d5b383 100644
--- a/app.js
+++ b/app.js
@@ -5,6 +5,7 @@ var express = require('express'),
routes = require('./lib/routes'),
eventApi = require('./lib/eventApi'),
featureApi = require('./lib/featureApi'),
+ featureArchiveApi = require('./lib/featureArchiveApi'),
strategyApi = require('./lib/strategyApi'),
validator = require('express-validator'),
app = express(),
@@ -39,6 +40,7 @@ app.use(bodyParser.json({strict: false}));
eventApi(router);
featureApi(router);
+featureArchiveApi(router);
strategyApi(router);
routes(router);
app.use(baseUriPath, router);
diff --git a/lib/eventType.js b/lib/eventType.js
index 029516a712..60d06788f4 100644
--- a/lib/eventType.js
+++ b/lib/eventType.js
@@ -1,6 +1,8 @@
module.exports = {
featureCreated : 'feature-created',
featureUpdated : 'feature-updated',
+ featureArchive : 'feature-archive',
+ featureRevive : 'feature-revive',
strategyCreated: 'strategy-created',
strategyDeleted: 'strategy-deleted'
};
\ No newline at end of file
diff --git a/lib/featureApi.js b/lib/featureApi.js
index fe9bd715b8..61ff4cfc1d 100644
--- a/lib/featureApi.js
+++ b/lib/featureApi.js
@@ -6,7 +6,7 @@ var featureDb = require('./featureDb');
var NameExistsError = require('./error/NameExistsError');
var NotFoundError = require('./error/NotFoundError');
var ValidationError = require('./error/ValidationError');
-var validateRequest = require('./error/validateRequest');
+var validateRequest = require('./error/validateRequest');
module.exports = function (app) {
@@ -41,7 +41,9 @@ module.exports = function (app) {
res.status(201).end();
})
.catch(NameExistsError, function() {
- res.status(403).json([{msg: "A feature named '" + req.body.name + "' already exists."}]).end();
+ res.status(403).json([{
+ msg: "A feature named '" + req.body.name + "' already exists. It could be archived."
+ }]).end();
})
.catch(ValidationError, function() {
res.status(400).json(req.validationErrors());
@@ -79,6 +81,32 @@ module.exports = function (app) {
});
});
+ app.delete('/features/:featureName', function (req, res) {
+ var featureName = req.params.featureName;
+ var userName = req.connection.remoteAddress;
+
+ featureDb.getFeature(featureName)
+ .then(function () {
+ return eventStore.create({
+ type: eventType.featureArchive,
+ createdBy: userName,
+ data: {
+ name: featureName
+ }
+ });
+ })
+ .then(function () {
+ res.status(200).end();
+ })
+ .catch(NotFoundError, function () {
+ res.status(404).end();
+ })
+ .catch(function (err) {
+ logger.error("Could not archive feature="+featureName, err);
+ res.status(500).end();
+ });
+ });
+
function validateUniqueName(req) {
return new Promise(function(resolve, reject) {
featureDb.getFeature(req.body.name)
diff --git a/lib/featureArchiveApi.js b/lib/featureArchiveApi.js
new file mode 100644
index 0000000000..19c12bd09d
--- /dev/null
+++ b/lib/featureArchiveApi.js
@@ -0,0 +1,37 @@
+var logger = require('./logger');
+var eventStore = require('./eventStore');
+var eventType = require('./eventType');
+var featureDb = require('./featureDb');
+var ValidationError = require('./error/ValidationError');
+var validateRequest = require('./error/validateRequest');
+
+module.exports = function (app) {
+
+ app.get('/archive/features', function (req, res) {
+ featureDb.getArchivedFeatures().then(function (archivedFeatures) {
+ res.json({'features': archivedFeatures});
+ });
+ });
+
+ app.post('/archive/revive', function (req, res) {
+ req.checkBody('name', 'Name is required').notEmpty();
+
+ validateRequest(req)
+ .then(function() {
+ return eventStore.create({
+ type: eventType.featureRevive,
+ createdBy: req.connection.remoteAddress,
+ data: req.body
+ });
+ }).then(function() {
+ res.status(200).end();
+ }).catch(ValidationError, function() {
+ res.status(400).json(req.validationErrors());
+ })
+ .catch(function(err) {
+ logger.error("Could not revive feature toggle", err);
+ res.status(500).end();
+ });
+ });
+};
+
diff --git a/lib/featureDb.js b/lib/featureDb.js
index f1f5334b35..9baf329c86 100644
--- a/lib/featureDb.js
+++ b/lib/featureDb.js
@@ -13,10 +13,19 @@ eventStore.on(eventType.featureUpdated, function (event) {
return updateFeature(event.data);
});
+eventStore.on(eventType.featureArchive, function (event) {
+ return archiveFeature(event.data);
+});
+
+eventStore.on(eventType.featureRevive, function (event) {
+ return reviveFeature(event.data);
+});
+
function getFeatures() {
return knex
.select(FEATURE_COLUMNS)
.from('features')
+ .where({archived: 0})
.orderBy('name', 'asc')
.map(rowToFeature);
}
@@ -29,6 +38,15 @@ function getFeature(name) {
.then(rowToFeature);
}
+function getArchivedFeatures() {
+ return knex
+ .select(['name', 'description'])
+ .from('features')
+ .where({archived: 1})
+ .orderBy('name', 'asc');
+}
+
+
function rowToFeature(row) {
if (!row) {
throw new NotFoundError('No feature toggle found');
@@ -48,6 +66,7 @@ function eventDataToRow(data) {
name: data.name,
description: data.description,
enabled: data.enabled ? 1 : 0,
+ archived: data.archived ? 1 :0,
strategy_name: data.strategy, // jshint ignore: line
parameters: data.parameters
};
@@ -70,9 +89,29 @@ function updateFeature(data) {
});
}
+function archiveFeature(data) {
+ return knex('features')
+ .where({name: data.name})
+ .update({archived: 1})
+ .catch(function (err) {
+ logger.error('Could not archive feature, error was: ', err);
+ });
+}
+
+function reviveFeature(data) {
+ return knex('features')
+ .where({name: data.name})
+ .update({archived: 0, enabled: 0})
+ .catch(function (err) {
+ logger.error('Could not archive feature, error was: ', err);
+ });
+}
+
+
module.exports = {
getFeatures: getFeatures,
getFeature: getFeature,
+ getArchivedFeatures: getArchivedFeatures,
_createFeature: createFeature, // visible for testing
_updateFeature: updateFeature // visible for testing
};
diff --git a/migrations/20141215210141-005-archived-flag-to-features.js b/migrations/20141215210141-005-archived-flag-to-features.js
new file mode 100644
index 0000000000..12a3995472
--- /dev/null
+++ b/migrations/20141215210141-005-archived-flag-to-features.js
@@ -0,0 +1 @@
+module.exports = require('../lib/migrationRunner').create('005-archived-flag-to-features');
diff --git a/migrations/sql/005-archived-flag-to-features.down.sql b/migrations/sql/005-archived-flag-to-features.down.sql
new file mode 100644
index 0000000000..4330a640ed
--- /dev/null
+++ b/migrations/sql/005-archived-flag-to-features.down.sql
@@ -0,0 +1 @@
+ALTER TABLE features DROP COLUMN "archived";
\ No newline at end of file
diff --git a/migrations/sql/005-archived-flag-to-features.up.sql b/migrations/sql/005-archived-flag-to-features.up.sql
new file mode 100644
index 0000000000..6676bed910
--- /dev/null
+++ b/migrations/sql/005-archived-flag-to-features.up.sql
@@ -0,0 +1 @@
+ALTER TABLE features ADD archived integer DEFAULT 0;
\ No newline at end of file
diff --git a/public/js/__tests__/components/feature/ArchiveFeatureComponent-test.js b/public/js/__tests__/components/feature/ArchiveFeatureComponent-test.js
new file mode 100644
index 0000000000..9391e19fae
--- /dev/null
+++ b/public/js/__tests__/components/feature/ArchiveFeatureComponent-test.js
@@ -0,0 +1,51 @@
+jest.dontMock("../../../components/feature/ArchiveFeatureComponent");
+
+var React = require("react/addons");
+var TestUtils = React.addons.TestUtils;
+var FeatureArchive = require("../../../components/feature/ArchiveFeatureComponent");
+var FeatureStore = require("../../../stores/FeatureStore");
+
+describe("FeatureForm", function () {
+ var Component;
+ beforeEach(function() {
+ FeatureStore.getArchivedFeatures.mockImplementation(function() {
+ return {
+ then: function (callback) {
+ return callback({
+ features: [
+ { name: "featureX" },
+ { name: "featureY" }
+ ]
+ });
+ }
+ };
+ });
+ FeatureStore.reviveFeature.mockImplementation(function() {
+ return {
+ then: function (callback) {return callback();}
+ };
+ });
+
+ Component = TestUtils .renderIntoDocument(
Name | ++ |
---|