-
Notifications
You must be signed in to change notification settings - Fork 167
RFC: dynamic model resources in Angular [DO NOT MERGE] #5
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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]); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: should be
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Schoonology Are you sure? Link?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. text/javascript is obsolete. application/javascript should be used.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Express uses There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-SO: http://tools.ietf.org/html/rfc4329 I stand (rather, sit) corrected! |
||
| 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; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: I haven't run this code to verify that the hack solves the problem as expected. When we start implementing the Angular client properly, we should add an automated test to cover this part.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't depend on
idbeing the property name that guarantees the model has been saved.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How comes that? Could you provide an example (scenario), when it does not work? Is there any other way how to detect that a model has been saved?
BTW my understanding is that LB Models are hard-coded to use
id, e.g. for the purpose of REST routing - see loopback/models/model.js and loopback-datasource-juggler/lib/dao.js.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the example.
Just for the record, our iOS and Android client SDKs don't support custom name of the id property either. (discussion).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moving the discussion about custom ids to strongloop/loopback#126.