Repository: stormpath/express-stormpath (js)

Issue:
Implement JSON view model for GET on /register
Implement JSON view model for GET on `/register` according to the [framework spec](https://github.com/stormpath/stormpath-framework-spec):

> ## View Models
> 
> The login an registration views are dynamic.  The registration form has
> configurable fields, and both login and registration need to dynamically render
> buttons for any providers (Google, Facebook, SAML) that are mapped to the
> specified Stormpath application.
> 
> For that reason, we support JSON GET requests of the `/login` and `/register`
> views.  The response is a view model that the client can use to render the
> form appropriately.
> 
> Please see [login](https://github.com/stormpath/stormpath-framework-spec/blob/master/login.md) and [registration](https://github.com/stormpath/stormpath-framework-spec/blob/master/registration.md) for specific information on each
> model.

Part of #298.

Pull request:
JSON view model for GET on /register
Fixes #307.

This PR implements a JSON view model for `GET` on `/register` according to the [spec](https://github.com/stormpath/stormpath-framework-spec/blob/master/registration.md#registration-view-model).

This is done by refactoring the `helper/get-login-view-model.js` to be able to take a form for which it generates a view model for (f6c911b).

### Notes

* This PR is going to be merged into `framework-spec-upgrade`.

### How to verify

1. Checkout this branch
2. Clone [express-stormpath-sample-project](https://github.com/stormpath/express-stormpath-sample-project)
3. Use `npm link` and `npm link express-stormpath` to link the library to the example project
4. Make a `GET` request to `/login` with the `Accept` header set to `application/json`
5. Verify that you get a JSON model back according to the [spec](https://github.com/stormpath/stormpath-framework-spec/blob/master/registration.md#registration-view-model)
6. Try to edit the config (`web.register.fields` and `web.register.fieldOrder`) and verify that the fields and its order are correct
7. Verify the content of `accountStores`
8. Try the same for `/login`

Patch:
diff --git a/lib/controllers/login.js b/lib/controllers/login.js
index ae9d1e5..2970d60 100644
--- a/lib/controllers/login.js
+++ b/lib/controllers/login.js
@@ -69,7 +69,7 @@ module.exports = function (req, res, next) {
     'application/json': function () {
       switch (req.method) {
         case 'GET':
-          helpers.getLoginViewModel(config, function (err, viewModel) {
+          helpers.getFormViewModel('login', config, function (err, viewModel) {
             if (err) {
               return helpers.writeJsonError(res, err);
             }
diff --git a/lib/controllers/register.js b/lib/controllers/register.js
index 9e82fd2..1658fe9 100644
--- a/lib/controllers/register.js
+++ b/lib/controllers/register.js
@@ -125,6 +125,20 @@ module.exports = function (req, res) {
   var postRegistrationHandler = config.postRegistrationHandler;
   var view = config.web.register.view;
 
+  // If a GET request is made with the Accept header as json,
+  // respond with the view model for this form.
+  // TODO: When `stormpath.web.produces` is implemented,
+  // this `if` must check that it is set to `application/json`.
+  if (req.method === 'GET' && accepts === 'json') {
+    return helpers.getFormViewModel('register', config, function (err, viewModel) {
+      if (err) {
+        return res.status(400).json({ error: err.userMessage || err.message });
+      }
+
+      res.status(200).json(viewModel);
+    });
+  }
+
   // Handle incoming POST requests from an API-like clients (something like
   // Angular / React / REST).
   if (req.method === 'POST' && accepts === 'json') {
diff --git a/lib/helpers/get-login-view-model.js b/lib/helpers/get-form-view-model.js
similarity index 76%
rename from lib/helpers/get-login-view-model.js
rename to lib/helpers/get-form-view-model.js
index ea52dee..f1cb937 100644
--- a/lib/helpers/get-login-view-model.js
+++ b/lib/helpers/get-form-view-model.js
@@ -3,8 +3,7 @@
 var async = require('async');
 var _ = require('lodash');
 
-var cachedViewModel;
-var cacheTimestamp = 0;
+var cache = {};
 
 /**
  * Returns the cached model if it's not older than `ttl`.
@@ -12,13 +11,20 @@ var cacheTimestamp = 0;
  *
  * @method
  *
+ * @param {string} formName - The name of the form (e.g. 'login' or 'register').
  * @param {number} ttl - The time-to-live value for the cached object.
  */
-function getCachedModel(ttl) {
-  var age = (new Date().getTime() - cacheTimestamp) / 1000; // In seconds.
+function getCachedModel(formName, ttl) {
+  var cacheItem = cache[formName];
 
-  if (cachedViewModel && age <= ttl) {
-    return cachedViewModel;
+  if (!cacheItem) {
+    return null;
+  }
+
+  var age = (new Date().getTime() - cacheItem.timestamp) / 1000; // In seconds.
+
+  if (age <= ttl) {
+    return cacheItem.model;
   }
 
   return null;
@@ -29,25 +35,28 @@ function getCachedModel(ttl) {
  *
  * @method
  *
+ * @param {string} formName - The name of the form (e.g. 'login' or 'register').
  * @param {Object} model - The model to cache.
  */
-function setCachedModel(model) {
-  cachedViewModel = model;
-  cacheTimestamp = new Date().getTime();
+function setCachedModel(formName, model) {
+  cache[formName] = {
+    model: model,
+    timestamp: new Date().getTime()
+  };
 }
 
 /**
- * Returns list of all enabled fields, as defined by `stormpath.web.login.fields`,
- * and ordered by `stormpath.web.login.fieldOrder`.
+ * Returns list of all enabled fields, as defined by `form.fields`,
+ * and ordered by `form.fieldOrder`.
  *
  * @method
  *
- * @param {Object} config - The Stormpath configuration.
+ * @param {Object} form - The form from the Stormpath configuration.
  */
-function getFormFields(config) {
+function getFormFields(form) {
   // Get fields by fieldOrder.
-  var fields = config.web.login.fieldOrder.map(function (fieldName) {
-    return config.web.login.fields[fieldName];
+  var fields = form.fieldOrder.map(function (fieldName) {
+    return form.fields[fieldName];
   });
 
   // Filter out only the enabled fields.
@@ -165,20 +174,22 @@ function getProviders(config, callback) {
 /**
  * Returns an object containing:
  *
- * - A list of all enabled fields, as defined by `stormpath.web.login.fields`,
- *   and ordered by `stormpath.web.login.fieldOrder`.
+ * - A list of all enabled fields, as defined by `form.fields`,
+ *   and ordered by `form.fieldOrder`.
  * - A list of providers, such as social providers or SAML providers.
  *
  * <https://github.com/stormpath/stormpath-framework-spec/blob/master/login.md#login-view-model>
+ * <https://github.com/stormpath/stormpath-framework-spec/blob/master/registration.md#registration-view-model>
  *
  * @method
  *
+ * @param {string} formName - The name of the form (e.g. 'login' or 'register').
  * @param {Object} config - The Stormpath configuration.
  * @param {Function} callback - Callback function (error, model).
  */
-function getLoginViewModel(config, callback) {
+function getFormViewModel(formName, config, callback) {
   var cacheTtl = config.client.cacheManager.defaultTtl;
-  var cachedModel = getCachedModel(cacheTtl);
+  var cachedModel = getCachedModel(formName, cacheTtl);
 
   if (cachedModel) {
     return process.nextTick(function () {
@@ -186,7 +197,8 @@ function getLoginViewModel(config, callback) {
     });
   }
 
-  var fields = getFormFields(config);
+  var form = config.web[formName];
+  var fields = getFormFields(form);
 
   var model = {
     form: {
@@ -202,14 +214,14 @@ function getLoginViewModel(config, callback) {
 
     model.accountStores = providers;
 
-    setCachedModel(model);
+    setCachedModel(formName, model);
 
     callback(null, model);
   });
 }
 
 module.exports = {
-  default: getLoginViewModel,
+  default: getFormViewModel,
 
   // Expose private functions for testing.
   getFormFields: getFormFields,
diff --git a/lib/helpers/index.js b/lib/helpers/index.js
index 55efc6c..fd4101a 100644
--- a/lib/helpers/index.js
+++ b/lib/helpers/index.js
@@ -23,5 +23,5 @@ module.exports = {
   validateAccount: require('./validate-account'),
   xsrfValidator: require('./xsrf-validator'),
   revokeToken: require('./revoke-token'),
-  getLoginViewModel: require('./get-login-view-model').default
+  getFormViewModel: require('./get-form-view-model').default
 };


Test patch:
diff --git a/test/helpers/test-get-login-view-model.js b/test/helpers/test-get-form-view-model.js
similarity index 91%
rename from test/helpers/test-get-login-view-model.js
rename to test/helpers/test-get-form-view-model.js
index 8b913f0..7844006 100644
--- a/test/helpers/test-get-login-view-model.js
+++ b/test/helpers/test-get-form-view-model.js
@@ -1,7 +1,7 @@
 'use strict';
 
 var assert = require('assert');
-var getLoginViewModel = require('../../lib/helpers/get-login-view-model');
+var getFormViewModel = require('../../lib/helpers/get-form-view-model');
 
 function getMockConfig() {
   return {
@@ -48,20 +48,20 @@ function getMockAccountStore() {
   };
 }
 
-describe('helpers/get-login-view-model.js', function () {
+describe('helpers/get-form-view-model.js', function () {
   var config;
 
   beforeEach(function () {
     config = getMockConfig();
   });
 
-  describe('getFormFields(config)', function () {
+  describe('getFormFields(form)', function () {
     var getFormFields;
     var returnValue;
 
     beforeEach(function () {
-      getFormFields = getLoginViewModel.getFormFields;
-      returnValue = getFormFields(config);
+      getFormFields = getFormViewModel.getFormFields;
+      returnValue = getFormFields(config.web.login);
     });
 
     it('should return a non-empty array', function () {
@@ -103,7 +103,7 @@ describe('helpers/get-login-view-model.js', function () {
     var returnValue;
 
     beforeEach(function () {
-      getAccountStoreModel = getLoginViewModel.getAccountStoreModel;
+      getAccountStoreModel = getFormViewModel.getAccountStoreModel;
       accountStore = getMockAccountStore();
       returnValue = getAccountStoreModel(accountStore);
     });