Skip to content
This repository was archived by the owner on Apr 18, 2020. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
<script src="cordova.js"></script>

<!-- your app's script -->
<script src="angular-resources.js"></script>
<script src="js/app.js"></script>
<script src="js/services.js"></script>
<script src="js/controllers.js"></script>
</head>

Expand Down
3 changes: 1 addition & 2 deletions client/js/app.js
Original file line number Diff line number Diff line change
@@ -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 <body> 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
Expand Down
10 changes: 4 additions & 6 deletions client/js/controllers.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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('/');
});
}}
Expand All @@ -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('/');
},
Expand Down
28 changes: 0 additions & 28 deletions client/js/services.js

This file was deleted.

4 changes: 4 additions & 0 deletions server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
73 changes: 73 additions & 0 deletions server/lib/angular-resources.client.js
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));
}

Copy link
Copy Markdown
Member Author

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.

Copy link
Copy Markdown
Member

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 id being the property name that guarantees the model has been saved.

Copy link
Copy Markdown
Member Author

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 id being the property name that guarantees the model has been saved.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app.model('foo', {properties: {myId: {id: true, type: 'string'}});

Copy link
Copy Markdown
Member Author

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).

Copy link
Copy Markdown
Member Author

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.

return resource;
}]);
})(modelName, models[modelName]);
}
93 changes: 93 additions & 0 deletions server/lib/angular-resources.js
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');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: should be text/javascript.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Schoonology Are you sure? Link?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Express uses application/javascript, our template should use the same content type for the sake of consistency.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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;
}