diff --git a/lib/client-metrics/index.js b/lib/client-metrics/index.js index be25954b9b..3067faf45c 100644 --- a/lib/client-metrics/index.js +++ b/lib/client-metrics/index.js @@ -54,7 +54,7 @@ module.exports = class UnleashClientMetrics { if (!toggles[seenToggleName]) { toggles[seenToggleName] = []; } - toggles[seenToggleName].push(appName); + toggles[seenToggleName].push({ appName }); }); }); return toggles; diff --git a/lib/db/client-applications-store.js b/lib/db/client-applications-store.js new file mode 100644 index 0000000000..31ba7c5bf3 --- /dev/null +++ b/lib/db/client-applications-store.js @@ -0,0 +1,74 @@ +'use strict'; + +const COLUMNS = ['app_name', 'created_at', 'updated_at', 'description', 'url', 'color', 'icon']; +const TABLE = 'client_applications'; + +const mapRow = (row) => ({ + appName: row.app_name, + createdAt: row.created_at, + updatedAt: row.updated_at, + description: row.description, + url: row.url, + color: row.color, + icon: row.icon, +}); + +const remapRow = (input, old = {}) => ({ + app_name: input.appName, + updated_at: input.updatedAt, + description: input.description || old.description, + url: input.url || old.url, + color: input.color || old.color, + icon: input.icon || old.icon, +}); + + +class ClientApplicationsDb { + constructor (db) { + this.db = db; + } + + updateRow (details, prev) { + details.updatedAt = 'now()'; + return this.db(TABLE) + .where('app_name', details.appName) + .update(remapRow(details, prev)); + } + + insertNewRow (details) { + return this.db(TABLE).insert(remapRow(details)); + } + + upsert (data) { + if (!data) { + throw new Error('Missing data to add / update'); + } + return this.db(TABLE) + .select(COLUMNS) + .where('app_name', data.appName) + .then(result => { + if (result && result[0]) { + return this.updateRow(data, result[0]); + } else { + return this.insertNewRow(data); + } + }); + } + + getApplicationMetaData (appName) { + if (appName) { + return this.db + .select(COLUMNS) + .where('app_name', appName) + .from(TABLE) + .map(mapRow); + } + return this.db + .select(COLUMNS) + .from(TABLE) + .orderBy('created_at', 'asc') + .map(mapRow); + } +}; + +module.exports = ClientApplicationsDb; diff --git a/lib/db/index.js b/lib/db/index.js index 738fc9acea..7f880d27c9 100644 --- a/lib/db/index.js +++ b/lib/db/index.js @@ -8,6 +8,7 @@ const ClientInstanceStore = require('./client-instance-store'); const ClientMetricsDb = require('./client-metrics-db'); const ClientMetricsStore = require('./client-metrics-store'); const ClientStrategyStore = require('./client-strategy-store'); +const ClientApplicationsStore = require('./client-applications-store'); module.exports.createStores = (config) => { const db = createDb(config); @@ -19,6 +20,7 @@ module.exports.createStores = (config) => { eventStore, featureToggleStore: new FeatureToggleStore(db, eventStore), strategyStore: new StrategyStore(db, eventStore), + clientApplicationsStore: new ClientApplicationsStore(db), clientInstanceStore: new ClientInstanceStore(db), clientMetricsStore: new ClientMetricsStore(clientMetricsDb), clientStrategyStore: new ClientStrategyStore(db), diff --git a/lib/routes/metrics.js b/lib/routes/metrics.js index 1a575c94c4..2c5bd471fb 100644 --- a/lib/routes/metrics.js +++ b/lib/routes/metrics.js @@ -11,6 +11,7 @@ module.exports = function (app, config) { clientMetricsStore, clientStrategyStore, clientInstanceStore, + clientApplicationsStore, } = config.stores; const metrics = new ClientMetrics(clientMetricsStore); @@ -22,7 +23,19 @@ module.exports = function (app, config) { app.get('/client/seen-apps', (req, res) => { const seenApps = metrics.getSeenAppsPerToggle(); - res.json(seenApps); + clientApplicationsStore.getApplicationMetaData() + .then(toLookup) + .then(metaData => { + Object.keys(seenApps).forEach(key => { + seenApps[key] = seenApps[key].map(entry => { + if (metaData[entry.appName]) { + entry.data = metaData[entry.appName]; + } + return entry; + }); + }); + res.json(seenApps); + }); }); app.get('/client/metrics/feature-toggles', (req, res) => { @@ -99,27 +112,43 @@ module.exports = function (app, config) { } }); + app.post('/client/applications/:appName', (req, res) => { + const input = Object.assign({}, req.body, { + appName: req.params.appName, + }); + clientApplicationsStore + .upsert(input) + .then(() => res.status(202).end()) + .catch((e) => { + logger.error(e); + res.status(500).end(); + }); + }); + + function toLookup (metaData) { + return metaData.reduce((result, entry) => { + result[entry.appName] = entry; + return result; + }, {}); + } + app.get('/client/applications/', (req, res) => { const strategyName = req.query.strategyName; - let appsPromise; - - if (strategyName) { - appsPromise = clientStrategyStore.getAppsForStrategy(strategyName); - } else { - appsPromise = clientStrategyStore.getApplications(); - } - - appsPromise - .then(apps => { - const applications = apps.map(appName => ({ - appName, - links: { - appDetails: `/api/client/applications/${appName}`, - }, - })); - res.json({ applications }); - }) - .catch(err => catchLogAndSendErrorResponse(err, res)); + Promise.all([ + strategyName ? clientStrategyStore.getAppsForStrategy(strategyName) : clientStrategyStore.getApplications(), + clientApplicationsStore.getApplicationMetaData().then(toLookup), + ]) + .then(([apps, metaData]) => { + const applications = apps.map(({ appName }) => ({ + appName, + data: metaData[appName], + links: { + appDetails: `/api/client/applications/${appName}`, + }, + })); + res.json({ applications }); + }) + .catch(err => catchLogAndSendErrorResponse(err, res)); }); app.get('/client/applications/:appName', (req, res) => { @@ -128,8 +157,11 @@ module.exports = function (app, config) { Promise.all([ clientInstanceStore.getByAppName(appName), clientStrategyStore.getByAppName(appName), + clientApplicationsStore.getApplicationMetaData(appName), ]) - .then(([instances, strategies]) => res.json({ appName, instances, strategies, seenToggles })) + .then(([instances, strategies, [metaData]]) => + res.json({ appName, instances, strategies, seenToggles, data: metaData }) + ) .catch(err => catchLogAndSendErrorResponse(err, res)); }); }; diff --git a/migrations/20161205203516-create-client-applications.js b/migrations/20161205203516-create-client-applications.js new file mode 100644 index 0000000000..fe15cbefe1 --- /dev/null +++ b/migrations/20161205203516-create-client-applications.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('../scripts/migration-runner').create('011-create-client-applications'); diff --git a/migrations/sql/011-create-client-applications.down.sql b/migrations/sql/011-create-client-applications.down.sql new file mode 100644 index 0000000000..2dabc37181 --- /dev/null +++ b/migrations/sql/011-create-client-applications.down.sql @@ -0,0 +1 @@ +DROP TABLE client_applications; diff --git a/migrations/sql/011-create-client-applications.up.sql b/migrations/sql/011-create-client-applications.up.sql new file mode 100644 index 0000000000..0b6aa15134 --- /dev/null +++ b/migrations/sql/011-create-client-applications.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE client_applications ( + app_name varchar(255) PRIMARY KEY NOT NULL, + created_at timestamp default now(), + updated_at timestamp default now(), + description varchar(255), + icon varchar(255), + url varchar(255), + color varchar(255) +);