Skip to content

An ORM for Angular with support for caching and batch operations

License

Notifications You must be signed in to change notification settings

bendrucker/convex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

convex Stand With Ukraine

The convex package name has been acquired Convex, Inc. through a donation to 🇺🇦 Razom. Existing versions remain available.


A powerful, opionated ORM for Angular with support for caching and batch operations. I built convex to scratch my own itch and use it actively in production at Valet.io. It makes a number of assumptions about how you build your APIs:

  • Primary keys are UUIDs
  • The batch endpoint handles the batch-me-if-you-can protocol
  • Querystrings are parsed according to the qs spec
  • Nested objects can be expanded a la Stripe
  • Foreign keys are named {{object}}_id

If convex is missing something you need or you can't abide by these assumptions, I welcome issues and PRs.

Requirements

Because convex uses Object.defineProperty for relations, it is not compatible with IE 8. Angular <1.3 and earlier are not supported but Angular 1.2 should work.

Sauce Test Status

Setup

angular.module('myApp', [
  require('convex')
]);

ConvexModel is the core service in convex. It provides access to all convex features and creates the objects that will store your data.

Creating Models

ConvexModel.extend(prototype, constructor) -> ConvexModel

Creates a new model contructor, adding methods to the prototype from prototype and to the constructor from constructor. $name is a required property in prototype and should be the lowercase, singular name of the object.

var User = ConvexModel.extend({
  $name: 'user'
});

You can optionally pass a $plural property for generating plural keys. Otherwise, an 's' will be appended to model.$name.

Because data is set directly on the instances of ConvexModel, you need to avoid collisions by ensuring that prototype properties never share the same name with your data properties. The easiest way to accomplish this is by prefixing methods with $ and never using that prefix in your API responses.

new ConvexModel([attributes]) -> model

Accepts an optional attributes objects with initial data to set on the model. If one of those attributes is an id, convex will look for an existing model with the same id. If one is found, convex will add any new attributes to that cached model and return it. If not, the new model will be cached. When attributes.id is omitted, it will be assigned as a new UUID.

var uuid = require('uuid').v4();
var user1 = new User({id: uuid});
var user2 = new User({id: uuid});
user1 === user2 // true

Model API

$set([attributes]) -> model

Special setter method for handling related data. You can set data normally, but $set will automatically handle delegating nested objects to a related model where appropriate. It plays an important internal role but you may never actually need to use it.

$path([instance]) -> string

Pluralizes the $name and adds the id to generate the path to the resource. If instance is false the path omits the id.

user.$path(); // '/users'
user.$path(user.id); '/users/5d6b6...'

$reset() -> model

Removes all data

$clone() -> model

Copies the original model's data to a new model with a new id.

toJSON() -> object

Creates a copy of the model data without properties beginning in $ or embedded relations. It does include foreign keys as well as properties inherited from the prototype.

$fetch([options]) -> promise(model)

Fetches the model data from the remote server. Sets received data on the model. This is a noop unless an id was passed when creating the model or it has already been saved.

GET /users/5d6b6... responds with:

{
  "id": "5d6b6...",
  "name": "Ben"
}
user.$fetch().then(function (user) {
  user.name === 'Ben' // true
});

$save([options]) -> promise(model)

Saves the model to the server. Intelligently chooses between a POST and PUT request based on whether the model has been saved before or provided an id to the constructor. Related data will be automatically excluded from the request payload.

user.name = 'Ben Drucker';
user.$save().then(function (user) {
  // Successful request:
  // PUT /users/5d6b6...
});

$delete([options]) -> promise(model)

Deletes the model if it has been saved and removes all other references. Also sets model.$deleted = true in case you're referencing the model directly anywhere in your application.

user.$delete().then(function (user) {
  user.$deleted === true; // true
});

Model.$where([query], [options]) -> promise(collection)

Gets an array of models using the query to construct a querystring to filter the results.

GET /users?admin=true responds with:

[
  {
    "id": "5d6b6...",
    "name": "Ben",
    "admin": true
  }
]
User.$where({admin: true}).then(function (users) {
  users.length === 1 // true
  users[0] instanceof User // true
  users[0].name === 'Ben' // true
});

Model.$all([options]) -> promise(collection)

Gets an array of models. Equivalent to calling Model.$where(undefined).

Collection API

The Collection API implements a decorated array. It can be passed to directives like ngRepeat but also provides special methods for working with models.

new ConvexCollection(Model, [models]) -> collection

Creates a new collection that uses the provided Model to cast data. The optional models argument is an array of models or model data that will be passed to collection.$push.

collection.$new(modelOrObject...) -> model

Returns a new model bound to the belongsTo relation shared by the members of the collection.

collection.$push(modelOrObject...) -> collection

Similar to Array.prototype.push but can handle either Model instances or plain objects that will be cast as models.

collection.$fetch([query], [options]) -> promise(collection)

Requests an array of data and casts is to Model instances, filtering it by setting query as the querystring. Model.$where uses this method.

Batch API

Convex can construct batch requests that wrap multiple request configurations into a single payload. This is especially useful for applications designed for mobile use since high latency makes multiple requests particularly costly. Convex will automatically assemble this payload and handle fulfilling the requests as if they were each made individually.

For more detail on how your server should handle batch requests, refer to the batch-me-if-you-can protocol.

model.$batch(callback) -> promise(requests)

Calls the provided callback with batch (a ConvexBatch instance). All REST methods can pass this as batch in options. $batch returns a promise that is resolved with the return value (or resolution if a promise is returned) of callback. All individual requests and promises are also resolved/rejected directly.

var user1 = new User({name: 'Ben'});
var user2 = new User({name: 'Ben2'});

user1.$batch(function (batch) {
  return $q.all([
    user1.$save({batch: batch})
      .then(function (user) {
        console.log('User 1 saved', user);
      }),
    user2.$save({batch: batch})
      .then(function (user) {
        console.log('User 2 saved', user);
      })
  ]);
})
.then(function (users) {
  console.log('All users saved successfully', users);
})
.catch(function (err) {
  console.log('One or more users failed to save:', err);
});

batch.parallel([setting]) -> boolean

When called with no arguments, returns the setting (defaults to true). When an argument is provided, it sets the parallel setting for the batch.

batch.all(array) -> promise(array)

Sugar method for calling $q.all without needing to inject it separately.

Relations API

Convex can help managed related data and cast it into real models. Combined with the Batch and Cache APIs, relations provide a powerful means to minimize requests and keep data in sync within your application.

Model.belongsTo(Target, key, [options]) -> Model

Creates a new belongsTo relation on Model where model instances are expected to have a {{key}}_id (or options.foreignKey) foreign key. Target can be a ConvexModel instance or a string that represents an injectable service.

GET /users/5d6b6... responds with:

{
  "name": "Ben",
  "family_id": "9f8mc..."
}
app
  .factory('User', function (ConvexModel) {
    return ConvexModel.extend({
      $name: 'family'
    })
    .belongsTo('Family');
  })
  .factory('Family', function (ConvexModel) {
    return ConvexModel.extend({
      $name: 'family',
      $plural: 'families'
    });
  })
  .run(function (User, Family) {
    new User({id: '5d6b6...'})
      .$fetch()
      .then(function (user) {
        user.family instanceof Family // true
        user.family_id === user.family.id // true
      });
  });

Model.hasOne(Target, key, [options]) -> Model

Creates a new hasOne relation on Model where one Target instance is expected to have a {{key}}_id that references each model.

Model.hasMany(Target, key, [options]) -> Model

Creates a new hasMany relation on Model where many Target instances are expected to have a {{key}}_id that references each model.

Requesting Related Data

All requests can accept an array in options.expand that will be added to the query object before it is formatted with qs. Nested expansion can be requested using dot syntax. Your API is expected to handle the expand querystring array and return the requested related data.

Expansion is only supported for GET requests. If you wish to save multiple objects without multiple requests to your remote API, you should use the Batch API.

GET /families/9f8mc... responds with:

{
  "id": "9f8mc...",
  "surname": "Drucker"
}
user.$fetch({
  expand: ['family']
})
.then(function (user) {
  user.family.surname === 'Drucker'; // true
});

Cache API

Any GET request can be cached either for the duration of the page session or permanently in local storage. options.cache controls caching behavior for requests. Passing true will cache in memory while 'persist' will cache to local storage and memory.

model.$fetch({
  cache: 'persist'
});

Even after restarting the browser, rerunning the above example will retrieve the data from local storage instead of performing a new request. Convex's own in-memory cache will always be checked before falling back to local storage. To request fresh data, just omit the cache option. To force a refresh of the cache, pass the force option and set the cache option to the desired cache level. A remote request will be performed and the response will overwrite the old cache.

About

An ORM for Angular with support for caching and batch operations

Resources

License

Stars

Watchers

Forks

Packages

No packages published