Skip to content
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
95 changes: 95 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,97 @@
# loopback-angular

## Status

The spike is fully functional and can be used in any demo applications we
are developing now. It's probably ok to commit the generated file into
public github repos, even though we are keeping the generator private for now.

Note that the functionality of the generated resources did not get enough
testing, expect bugs.

## Installation

1. Clone this repository and checkout the correct branch

```sh
$ git clone git@github.com:strongloop/loopback-angular.git
$ cd loopback-angular
$ git checkout feature/initial-spike
```

2. Link the module to global node_modules

```sh
loopback-angular$ npm link
```

## Generating services.js

In your LoopBack+Angular project, run the following command to generate
a file with Angular services providing access to LoopBack Models:

```sh
$ lb-ng server/app.js client/js/lb-services.js
```

The first argument is the path to the LoopBack app file as generated by `slc`.

The second argument is the path where the generated code should be saved.

> Note: You have to re-run this command after every change in the model
> definitions.

## Angular API

Follow these steps to use the generated services inside your Angular
application:

1. Include "lb-services.js" in your index.html file

```html
<script src="js/lb-services.js"></script>
```

2. Register the angular module `lbServices` as a dependency of your app.

```js
angular.module('my-app-module',
['ngRoute' /* etc */, 'lbServices', 'my-app.controllers'])
```

3. To call a method on a model from your controller, add the model name
as a dependency of the controller.

```js
// access User model
module.controller('LoginCtrl', function($scope, User, $location) {
$scope.login = function() {
$scope.loginResult = User.login($scope.credentials,
function() {
// success
}, function(res) {
// error
});
```

> Note: Angular model names start always with a capital letter,
> even if your server definition starts with a lower-case letter.

## API Docs

The auto-generated source include ngdoc directives. You can use your favourite
ngdoc tool to view this documentation, e.g. [docular](http://grunt-docular.com/)

If you don't have Grunt workflow configured yet, you can use the following
command as a replacement for `grunt docular-server`:

```sh
$ lb-ng-doc client/js/lb-services.js
```

> Note: this tool provides only a very limited subset of docular features.
> It was created as a quick hack to check whether ngdoc+docular is a viable
> solution for providing API docs on the services generated by lb-ng.
>
> There are no ambitions to extend it into a full-fledged thing - you should
> switch to grunt-docular if you need a feature not implemented by lb-ng-doc.
31 changes: 31 additions & 0 deletions bin/lb-ng
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env node

var fs = require('fs');
var path = require('path');
var generator = require('..');

if (process.argv.length < 3) {
console.error('Usage:');
console.error(' lb-ng server/app.js [client/js/lb-services.js]');
console.error('The first argument is path to LoopBack app file.');
console.error('The second argument is path where to save the generated file');
return;
}

var appFile = path.resolve(process.argv[2]);
console.error('Loading LoopBack app %j', appFile);
var app = require(appFile);

var result = generator(app);

var outputFile = process.argv[3];
if (outputFile) {
outputFile = path.resolve(outputFile);
console.error('Saving the generated services source to %j', outputFile);
fs.writeFileSync(outputFile, result.services);
} else {
console.error('Dumping to stdout');
process.stdout.write(result.services);
}

process.exit();
36 changes: 36 additions & 0 deletions bin/lb-ng-doc
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env node

var path = require('path');
var docular = require('docular');

if (process.argv.length < 3) {
console.error('Usage:');
console.error(' lb-ng-doc client/js/lb-services.js [port]');
console.error('The first argument is a path to the file generated by lb-ng.');
console.error('The second argument is a port number for the docs webserver.');
return;
}

var services = path.resolve(process.argv[2]);
var port = process.argv[3] || 3030;

console.error('Using lb-ng file %j', services);

docular.genDocs({
groups: [
{
groupTitle: 'LoopBack',
groupId: 'loopback',
sections: [
{
id: 'lbServices',
title: 'LoopBack Services',
scripts: [ path.resolve(services) ]
}
]
}
]
}, function() {
docular.server({ port: port });
console.log('Browse the documentation at http://localhost:%d/', port);
});
7 changes: 7 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
var services = require('./lib/services');

exports = module.exports = function angularResources(app) {
return {
services: services(app)
};
};
89 changes: 89 additions & 0 deletions lib/services.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
var fs = require('fs');
var ejs = require('ejs');

ejs.filters.q = function(obj) {
return JSON.stringify(obj, null, 2 );
};

exports = module.exports = function generateServices(app) {
var models = describeModels(app);

var servicesTemplate = fs.readFileSync(
require.resolve('../templates/services.template'),
{ encoding: 'utf-8' }
);

return ejs.render(servicesTemplate, {
models: models
});
};

function describeModels(app) {
var apiPath = app.get('apiPathRoot') || '/api';
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
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),
_accepts: route.accepts,
_returns: route.returns,
_description: route.description
};
});

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;
}
34 changes: 34 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "loopback-angular",
"version": "0.0.1",
"description": "Service for auto-generating Angular $resource services for LoopBack",
"main": "index.js",
"bin": {
"lb-ng": "bin/lb-ng",
"lb-ng-doc": "bin/lb-ng-doc"
},
"scripts": {
"test": "mocha"
},
"repository": {
"type": "git",
"url": "git://github.com/strongloop/loopback-angular.git"
},
"keywords": [
"loopback",
"angular"
],
"author": "Miroslav Bajtos <miroslav@strongloop.com>",
"license": "StrongLoop License",
"bugs": {
"url": "https://github.com/strongloop/loopback-angular/issues"
},
"dependencies": {
"ejs": "~0.8.5",
"docular": "~0.6.3"
},
"devDependencies": {
"chai": "*",
"mocha": "*"
}
}
73 changes: 73 additions & 0 deletions templates/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));
}
return resource;
}]);
})(modelName, models[modelName]);
}
Loading