/angular-restmod

Rails inspired REST-API ORM for Angular

Primary LanguageJavaScriptMIT LicenseMIT

Angular Restmod Build Status Stories in Ready

Restmod creates objects that you can use from within Angular to interact with your RESTful API.

Saving Bikes on your serverside database would be as easy as:

var Bike = restmod.model('/bikes');
var newBike = Bike.$build({ brand: 'Trek' });
newBike.model = '5200';
newBike.$save(); // bike is persisted sending a POST to /bikes

It also supports collections, relations, lifecycle hooks, attribute renaming and much more. Continue reading for a quick start or check the API Reference for more: http://platanus.github.io/angular-restmod

Why Restmod?

Restmod brings Rails ActiveRecord's ease of use to the Angular Framework. It succesfuly combines Angular's encapsulated design with Active Record's opinionated style. There are other alternatives available though:

$resource: Might be enough for small projects, included as an Angular opt-in. It only provides a basic model type layer, with limited features.

Restangular: very complete library, but does not propose a model layer.

angular-activerecord: Nice alternative to $resource, still very limited in its functionality.

ModelCore: Inspired in angular-activerecord, provides a more complete set of features but lacks testing.

Getting Started

1. Get the code

You can get it straight from the repository

git clone git@github.com:platanus/angular-restmod.git

but we recommend you to use bower to retrieve the Restmod package

bower install angular-restmod --save

2. Include it on your project

Make sure the restmod source is required in your code.

<script type="text/javascript" src="js/angular-restmod-bundle.min.js"></script>

Next, include angular module as one of your app's dependencies

module = angular.module('MyApp', ['plRestmod'])

Basic usage

You begin by creating a new model using the restmod.model method. We recommend you to put each model on a separate factory. The first argument for model is the resource URL.

module.factory('Bike', function(restmod) {
	return restmod.model('/bikes');
});

The generated model type provides basic CRUD operations to interact with the API:

To retrieve an object by ID use $find, the returned object will be filled with the response data when the server response is received.

Let's say you have a REST API that responds JSON to a GET REQUEST on /bikes/1

{
	"id": 1,
	"brand": "Trek",
	"created_at": "2014-05-23"
}

Then, on your code you would call

bike = Bike.$find(1);

The bike object will be populated as soon as the API returns some data. You can use $then to do something when data becomes available.

bike.$then(function() {
	console.log(bike.brand); // will output 'Trek'
});

IMPORTANT: RestMod will rename attributes from under_score to camelCase by default, refer to the building docs if you need to disable this feature. In the example above you should use bike.createdAt to refer to the value of the created_at returned by the API.

To reload an object use $fetch. WARNING: This will overwrite modified properties.

bike.$fetch();

To retrieve an object collection $collection or $search can be used.

bikes = Bike.$search({ category: 'enduro' });
// same as
bikes = Bike.$collection({ category: 'enduro' }); // server request not yet sent
bikes.$refresh();

To reload a collection use $refresh. To append more results use $fetch.

bikes = Bike.$collection({ category: 'enduro' });
bikes.$refresh({ page: 1 }); // clear collection and load page 1
bikes.$fetch({ page: 2 }); // page 2 is appended to page 1, usefull for infinite scrolls...
bikes.$refresh({ page: 3 }); // collection is reset, page 3 is loaded on response

To update an object, just modify the properties and call $save.

bike = Bike.$find(1);
bike.brand = 'Trek';
bike.$save();

To create a new object use $build and then call $save. This will send a POST request to the server.

var newBike = Bike.$build({ brand: 'Comencal' });
newBike.model = 'Meta';
newBike.$save(); // bike is persisted

Or use $create

var newBike = Bike.$create({ brand: 'Comencal', model: 'Meta' });

If called on a collection, $build and $create will return a collection-bound object that will be added when saved successfully.

newBike = bikes.$create({ brand: 'Comencal', model: 'Meta' });
// after server returns, 'bikes' will contain 'newBike'.

To show a non saved object on the bound collection use $reveal

var newBike = bikes.$create({ brand: 'Comencal', model: 'Meta' }).$reveal();
// 'newBike' is inmediatelly available at 'bikes'

Finally, to destroy an object just call $destroy. Destroying an object bound to a collection will remove it from the collection.

bike.$destroy();

As with $create, calling $destroy on a record bound to a collection will remove it from the collection on server response.

All operations described above will set the $promise property. This property is a regular $q promise that is resolved when operation succeds or fail. It can be used directly or using the $then method.

bike.$fetch().$then(function(_bike) {
	doSomething(_bike.brand);
});
// same as:
bike.$fetch().$promise.then(function(_bike) {
	doSomething(_bike.brand);
});

Customizing model behaviour

When defining a model, you can pass a definition object

Bike = restmod.model('api/bikes',
// This is the definition object:
{
	createdAt: { encode: 'date' },
	owner: { belongsTo: 'User' }
}
);

The definition object allows you to:

  • Define relations between models
  • Customize an attribute's serialization and default values
  • Add custom methods
  • Add lifecycle hooks

Relations

Relations are defined like this:

Bike = restmod.model('/bikes', {
	parts: { hasMany: 'Part' },
	owner: { belongsTo: 'User' }
});

There are three types of relations:

HasMany

Let's say you have the following 'Part' model:

module.factory('Part', function() {
	return restmod.model('/parts');
});

The HasMany relation allows you to access parts of a specific bike directly from a bike object. In other words, HasMany is a hirearchical relation between a model instance (bike) and a model collection (parts).

Bike = restmod.model('/bikes', {
	parts: { hasMany: 'Part' }
});

bike = Bike.$new(1); 			// no request are made to the server yet.
parts = bike.parts.$fetch(); 	// sends a GET to /bikes/1/parts

Later on, after 'parts' has already been resolved,

parts[0].$fetch(); // updates the part at index 0. This will do a GET /parts/:id

Calling $create on the collection will POST to the collection nested url.

var part = bike.parts.$create({ serialNo: 'XX123', category: 'wheels' }); // sends POST /bikes/1/parts

If the child collection model is anonymous (no url given to model) then all CRUD routes for the collection items are bound to the parent.

So if 'Part' was defined like:

restmod.model(null);

The example above would behave like this:

console.log(bike.parts[0].$url())
bike.parts[0].$fetch();

Will send GET to /bikes/1/parts/:id instead of /parts/:id

HasOne

This is a hirearchical relation between one model's instance and another model's instance. The child instance url is bound to the parent url. The child instance is created at the same time as the parent, so its available even if the parent is not resolved.

Let's say you have the following 'User' model:

module.factory('User', function() {
	return restmod.model('/users');
});

That relates to a 'Bike' through a hasOne relation:

Bike = restmod.model('/bikes', {
	owner: { hasOne: 'User' }
});

Then a bike's owner data can then be retrieved just by knowing the bike primary key (id):

owner = Bike.$new(1).owner.$fetch();

will send GET /bikes/1/owner

Since the user resource has its own resource url defined:

owner.name = 'User';
owner.$save();

will send PUT /user/X.

If 'User' was to be defined like an anonymous resource:

module.factory('User', function() {
	return restmod.model(null); // note that the url is null
});

Then calling:

owner.name = 'User';
owner.$save();

will send a PUT to /bikes/1/owner

BelongsTo

This is a reference relation between a model instance and another model instance. The child instance is not bound to the parent and is generated after server response to a parent's $fetch is received. A key is used by default to bind child to parent. The key property name can be optionally selected using the key attribute.

Let's say you have the same 'User' model as before:

module.factory('User', function() {
	return restmod.model('/users');
});

That relates to a 'Bike' through a belongsTo relation this time:

Bike = restmod.model('/bikes', {
	owner: { belongsTo: 'User', key: 'last_owner_id' } // default key would be 'owner_id'
});

Also you have the following bike resource:

GET /bikes/1

{
	id: 1,
	brand: 'Transition',
	last_owner_id: 2
}

Then retrieving the resource:

bike = Bike.$find(1);

Will produce a bike object with its owner property initialized to a user with id=2, the owner property will only be available AFTER server response arrives.

Then calling

bike.owner.$fetch();

Will send a GET to /users/2 and populate the owner property with the user data.

This relation can be optionally defined as inline, this means that it is expected that the child object data comes inlined in the parent object server data. The inline property name can be optionally selected using the source attribute.

Lets redefine the Bike model as:

var Bike = restmod.model('/bikes', {
	owner: { belongsTo: 'User', inline: true, source: 'last_owner' } // source would default to *owner*
});

And suppose that the last bike resource looks like:

GET /bikes/1

{
	id: 1,
	brand: 'Transition',
	last_owner: {
		id: 2
		name: 'Juanito'
	}
}

Then retrieving the bike resource:

var bike = Bike.$find(1);

Will produce a bike object with its owner property initialized to a user with id=2 and name=Juanito. As before, the owner property will only be available AFTER server response arrives.

Serialization, masking and default values.

When you communicate with an API, some attribute types require special treatment (like a date, for instance)

Decode

You can specify a way of decoding an attribute when it arrives from the server.

Let's say you have defined a filter like this:

Angular.factory('DateParseFilter', function() {
	return function(_value) {
		date = new Date();
		date.setTime(Date.parse(_value));
		return date;
	}
})

then you use it as a standard decoder like this:

var Bike = restmod.model('/bikes', {
	createdAt: {decode:'date_parse'}
});

Encode

To specify a way of encoding an attribute before you send it back to the server: Just as with the previous example (decode), you use an Angular Filter. In this example we use the built in 'date' filter.

var Bike = restmod.model('/bikes', {
	createdAt: {encode:'date', param:'yyyy-MM-dd'}
});

On both encode and decode you can use an inline function instead of the filter's name. It is also possible to bundle an encoder and decoder together using a Serializer object, check the API Reference for more.

Attribute masking

Following the Angular conventions, attributes that start with a '$' symbol are considered private and never sent to the server. Furthermore, you can define a mask that allows you to specify a more advanced behaviour for other attributes:

var Bike = restmod.model('/bikes', {
	createdAt: {mask:'CU'}, // won't send this attribute on Create or Update
	viewCount: {mask:'R'}, // won't load this attribute on Read (fetch)
	opened: {mask:true}, // will ignore this attribute in relation to the API
});

Default value

You can define default values for your attributes, both static and dynamic. Dynamic defaults are defined using a function that will be called on record creation.

var Bike = restmod.model('/bikes', {
	wheels: { init: 2 }, // every new bike will have 2 wheels by default
	createdAt: { init: function() {
	 return new Date();
	}}
});

Explicit attribute mapping

You can explicitly tell restmod to map a given server attribute to one of the model's attributs:

var Bike = restmod.model('/bikes', {
	created: { map: 'stats.created_at' }
});

Custom methods

You can add a custom instance method to a Model

var Bike = restmod.model('/bikes', {
	pedal: function() {
	 this.strokes += 1;
	}
});

You can also add a class method to the Model type

var Bike = restmod.model('/bikes', {
	'@searchByBrand': function(_brand) {
	 return this.$search({ brand: _brand });
	}
});

Methods added to the class are available to the Model's collections.

var xc_bikes = Bike.$search({category:'xc'}); //$search returns a collection
xc_treks = xc_bikes.searchByBrand('Trek');

Hooks (callbacks)

Just like you do with ActiveRecord, you can add triggers on certain steps of the object lifecycle

var Bike = restmod.model('/bikes', {
	'~beforeSave': function() {
		this.partCount = this.parts.length;
	}
});

Note that a hook can be defined for a type, a collection or a record. Also, hooks can also be defined for a given execution context using $decorate. Check the hooks advanced documentation.

Mixins

To ease up the definition of models, and keep thing DRY, Restmod provides you with mixin capabilities. For example, say you already defined a Vehicle model as a factory:

Angular.factory('Vehicle', function() {
	return restmod.model('/vehicle', {
	createdAt: {encode:'date', param:'yyyy-MM-dd'}
	});
})

You can then define your Bike model that inherits from the Vehicle model, and also sets additional functionality.

var Bike = restmod.model('/bikes', 'Vehicle', {
	pedal: function (){
		alert('pedaling')
	}
});

Some links:

REST api designs guidelines: https://github.com/interagent/http-api-design REST json api standard: http://jsonapi.org