From 1422709d57474c40653801f8413ddae44af89d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 3 Jan 2014 13:35:39 +0100 Subject: [PATCH 1/2] Refactor accessToken handling into User service Introduce a new service `LoopBack` that holds accessToken and can provide other LoopBack-related functionality in the future. --- client/js/controllers.js | 10 ++++------ client/js/services.js | 31 ++++++++++++++++++++++++------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/client/js/controllers.js b/client/js/controllers.js index b6f230c..8bdfb6c 100644 --- a/client/js/controllers.js +++ b/client/js/controllers.js @@ -1,7 +1,7 @@ angular.module('starter.controllers', []) .controller('AppCtrl', function($rootScope, $scope, User, $location) { - $scope.currentUser = + $scope.currentUser = $rootScope.currentUser = User.get({id: $rootScope.currentUserId}, function() { // success }, function() { @@ -10,11 +10,10 @@ angular.module('starter.controllers', []) $scope.options = [ {text: 'Logout', action: function() { - User.logout({token: $rootScope.accessToken}, function() { - $scope.currentUser = + User.logout(function() { + $scope.currentUser = $rootScope.currentUser = - $rootScope.currentUserId = - $rootScope.accessToken = null; + $rootScope.currentUserId = null; $location.path('/'); }); }} @@ -35,7 +34,6 @@ angular.module('starter.controllers', []) $scope.login = function() { $scope.loginResult = User.login($scope.credentials, function() { - $rootScope.accessToken = $scope.loginResult.id; $rootScope.currentUserId = $scope.loginResult.userId; $location.path('/'); }, diff --git a/client/js/services.js b/client/js/services.js index 846693a..1387d46 100644 --- a/client/js/services.js +++ b/client/js/services.js @@ -1,26 +1,43 @@ angular.module('starter.services', ['ngResource']) - .factory('User', ['$resource', function($resource) { + .factory('LoopBack', function() { + return { accessToken: null }; + }) + .factory('User', ['$q', '$resource', 'LoopBack', function($q, + $resource, + LoopBack) { return $resource('/api/users/:id', {id: '@id'}, { login: { method: 'POST', - url: '/api/users/login' + url: '/api/users/login', + interceptor: { + response: function(response) { + var loginResult = response.data; + LoopBack.accessToken = loginResult.id; + return response || $q.when(response); + } + } }, logout: { method: 'POST', - url: '/api/users/logout?sid=:token', - params: {token: '@token'} + url: '/api/users/logout', + interceptor: { + response: function(response) { + LoopBack.accessToken = null; + return response || $q.when(response); + } + } } }); }]) .config(function ($httpProvider) { $httpProvider.interceptors.push('requestInterceptor'); }) - .factory('requestInterceptor', function ($q, $rootScope) { + .factory('requestInterceptor', function ($q, LoopBack) { return { 'request': function (config) { console.log('config', config); - if($rootScope.accessToken) { - config.headers.authorization = $rootScope.accessToken; + if(LoopBack.accessToken) { + config.headers.authorization = LoopBack.accessToken; } return config || $q.when(config); } From 07e82d3a7b58f7974ca4c94bb92abfe2602d195b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 3 Jan 2014 20:19:26 +0100 Subject: [PATCH 2/2] Spike: dynamic model resources in Angular The solutions has several parts: 1. Server-side module server/lib/angular-resources.client.js server/lib/angular-resources.js This module provides a middleware (request handler) that returns Angular module defining $resource objects for all public models defined in the loopback application. It should be probably extracted to a new module, e.g. loopback-angular. 2. Server-side integration One-liner in server/app.js to install the middleware defined above. 3. Client-side changes Replace 'client/js/services.js' with the script provided by the new middleware, rename 'starter.services' to 'lbModels' in depedency configurations. --- client/index.html | 2 +- client/js/app.js | 3 +- client/js/services.js | 45 ------------- server/app.js | 4 ++ server/lib/angular-resources.client.js | 73 ++++++++++++++++++++ server/lib/angular-resources.js | 93 ++++++++++++++++++++++++++ 6 files changed, 172 insertions(+), 48 deletions(-) delete mode 100644 client/js/services.js create mode 100644 server/lib/angular-resources.client.js create mode 100644 server/lib/angular-resources.js diff --git a/client/index.html b/client/index.html index 3061474..63a939a 100644 --- a/client/index.html +++ b/client/index.html @@ -25,8 +25,8 @@ + - diff --git a/client/js/app.js b/client/js/app.js index d9de4e8..09e7025 100644 --- a/client/js/app.js +++ b/client/js/app.js @@ -1,9 +1,8 @@ // angular.module is a global place for creating, registering and retrieving Angular modules // 'starter' is the name of this angular module example (also set in a attribute in index.html) // the 2nd parameter is an array or 'requires' -// 'starter.services' is found in services.js // 'starter.controllers' is found in controllers.js -angular.module('starter', ['ionic', 'ngRoute', 'ngAnimate', 'starter.services', 'starter.controllers']) +angular.module('starter', ['ionic', 'ngRoute', 'ngAnimate', 'lbModels', 'starter.controllers']) .config(function ($compileProvider){ // Needed for routing to work diff --git a/client/js/services.js b/client/js/services.js deleted file mode 100644 index 1387d46..0000000 --- a/client/js/services.js +++ /dev/null @@ -1,45 +0,0 @@ -angular.module('starter.services', ['ngResource']) - .factory('LoopBack', function() { - return { accessToken: null }; - }) - .factory('User', ['$q', '$resource', 'LoopBack', function($q, - $resource, - LoopBack) { - return $resource('/api/users/:id', {id: '@id'}, { - login: { - method: 'POST', - url: '/api/users/login', - interceptor: { - response: function(response) { - var loginResult = response.data; - LoopBack.accessToken = loginResult.id; - return response || $q.when(response); - } - } - }, - logout: { - method: 'POST', - url: '/api/users/logout', - interceptor: { - response: function(response) { - LoopBack.accessToken = null; - return response || $q.when(response); - } - } - } - }); - }]) - .config(function ($httpProvider) { - $httpProvider.interceptors.push('requestInterceptor'); - }) - .factory('requestInterceptor', function ($q, LoopBack) { - return { - 'request': function (config) { - console.log('config', config); - if(LoopBack.accessToken) { - config.headers.authorization = LoopBack.accessToken; - } - return config || $q.when(config); - } - } - }); diff --git a/server/app.js b/server/app.js index db72958..61ea4b2 100644 --- a/server/app.js +++ b/server/app.js @@ -64,6 +64,10 @@ try { // ignore errors, explorer stays disabled } +// TODO(bajtos) move the implementation to loopback or loopback-angular +// Nice to have: move this initialization out to boot/ fold +app.use('/angular-resources.js', require('./lib/angular-resources')(app, apiPath)); + /* * EXTENSION POINT * Add your custom request-handling middleware here. diff --git a/server/lib/angular-resources.client.js b/server/lib/angular-resources.client.js new file mode 100644 index 0000000..2b5f295 --- /dev/null +++ b/server/lib/angular-resources.client.js @@ -0,0 +1,73 @@ +// Note: this is a partial file that expects there is a `models` variable +// already defined. `models` should contain a definition of all models +// and shared methods +var module = angular.module('lbModels', ['ngResource']); +module + .factory('LoopBackAuth', function() { + return { + accessToken: null + }; + }) + .config(function($httpProvider) { + $httpProvider.interceptors.push('loopbackAuthRequestInterceptor'); + }) + .factory('loopbackAuthRequestInterceptor', function($q, LoopBackAuth) { + return { + 'request': function(config) { + console.log('config', config); + if (LoopBackAuth.accessToken) { + config.headers.authorization = LoopBackAuth.accessToken; + } + return config || $q.when(config); + } + } + }); + +for (var modelName in models) { + (function defineFactory(name, meta) { + module.factory( + name, + ['$q', '$resource', 'LoopBackAuth', function($q, $resource, LoopBackAuth) { + var actions = angular.extend(meta.actions, {}); + if (name === 'User') { + if (actions.login) { + actions.login = angular.extend(actions.login, { + interceptor: { + response: function(response) { + var loginResult = response.data; + LoopBackAuth.accessToken = loginResult.id; + return response || $q.when(response); + } + } + }); + } + + if (actions.logout) { + actions.logout = angular.extend(actions.logout, { + interceptor: { + response: function(response) { + LoopBackAuth.accessToken = null; + return response || $q.when(response); + } + } + }); + } + } + + console.log('creating resource', name, meta.url, meta.paramDefaults, actions); + + var resource = $resource(meta.url, meta.paramDefaults, actions); + + // Angular always calls POST on $save() + // This hack is based on + // http://kirkbushell.me/angular-js-using-ng-resource-in-a-more-restful-manner/ + resource.prototype.$save = function() { + var fn = this.id === undefined ? + this.$create : + this.$prototype$updateAttributes; + fn.apply(this, Array.prototype.slice.call(arguments)); + } + return resource; + }]); + })(modelName, models[modelName]); +} diff --git a/server/lib/angular-resources.js b/server/lib/angular-resources.js new file mode 100644 index 0000000..e106126 --- /dev/null +++ b/server/lib/angular-resources.js @@ -0,0 +1,93 @@ +var fs = require('fs'); +var format = require('util').format; + +var clientFileName = require.resolve('./angular-resources.client.js'); +var clientScript = fs.readFileSync(clientFileName, { encoding: 'utf8' }); + +var scriptFormat = + '(function() {\n' + + '"use strict";\n\n' + + 'var models = %s\n' + + '%s\n' + + '})();\n'; + +exports = module.exports = function angularResources(app, apiPath) { + return function(req, res, next) { + var models = describeModels(app, apiPath); + + var script = format( + scriptFormat, + JSON.stringify(models, null, 2), + clientScript + ); + + res.set('Content-Type', 'application/javascript'); + res.send(script); + } +} + +function describeModels(app, apiPath) { + var remotes = app.remotes(); + var allClasses = remotes.classes(); + var allRoutes = remotes.handler('rest').adapter.allRoutes(); + + var result = {}; + + allRoutes.forEach(function(route) { + var methodParts = route.method.split('.'); + var classPart = methodParts[0]; + var methodName = methodParts.slice(1).join('$'); + + var classDef = allClasses.filter(function (item) { + return item.name === classPart; + })[0]; + + + var className = classDef && classDef.ctor.definition && classDef.ctor.definition.name; + if (!className) { + return; // not a LoopBack model + } + + // Ensure the first letter is upper-case + var className = className[0].toUpperCase() + className.slice(1); + + var modelDesc = result[className]; + if (!modelDesc) { + modelDesc = result[className] = { + url: undefined, + paramDefaults: undefined, + actions: {} + }; + } + + var fullPath = apiPath + route.path; + + + if (methodName == 'findById') { + // findById should be mounted at the base REST path, e.g. /users/:id + modelDesc.url = fullPath; + // TODO - defaults should come from `route.accepts` or even class data + modelDesc.paramDefaults = { id: '@id' }; + } + + modelDesc.actions[methodName] = { + url: apiPath + route.path, + method: getMethodFromVerb(route.verb), + // TODO(bajtos) convert route accepts to angular params (?) + isArray: isReturningArray(route.returns) + }; + }); + + return result; +} + +function getMethodFromVerb(verb) { + if (verb === 'all') return 'POST'; + return verb.toUpperCase(); +} + +function isReturningArray(routeReturns) { + return routeReturns && routeReturns.length == 1 && + routeReturns[0].root && + routeReturns[0].type === 'array' ? true : undefined; +}