epilogue

Create REST resources and controllers with Sequelize and Express or Restify

Epilogue

Create flexible REST endpoints and controllers from Sequelize models in your Express app in Node.

var Sequelize = require('sequelize'),
    restify = require('restify'),
    epilogue = require('epilogue');
 
// Define your models 
var database = new Sequelize('database', 'root', 'password');
var User = database.define('User', {
  username: Sequelize.STRING,
  birthday: Sequelize.DATE
});
 
// Initialize server 
var server = restify.createServer();
server.use(restify.queryParser());
server.use(restify.bodyParser());
 
// Initialize epilogue 
epilogue.initialize({
  app: server,
  sequelize: database
});
 
// Create REST resource 
var userResource = epilogue.resource({
  model: User,
  endpoints: ['/users', '/users/:id']
});
 
// Create database and listen 
database
  .sync({ force: true })
  .then(function() {
    server.listen(function() {
      console.log('%s listening at %s', server.name, server.url);
    });
  });

On the server we now have the following controllers and endpoints:

ControllerEndpointDescription
users.createPOST /usersCreate a user
users.listGET /usersGet a listing of users
users.readGET /users/:idGet details about a user
users.updatePUT /users/:idUpdate a user
users.deleteDELETE /users/:idDelete a user

Of course it's likely that we'll want more flexibility. Our users resource has properties for each of the controller actions. Controller actions in turn have hooks for setting and overriding behavior at each step of the request. We have these milestones to work with: start, auth, fetch, data, write, send, and complete.

var ForbiddenError = require('epilogue').Errors.ForbiddenError;
 
// disallow deletes on users 
users.delete.auth(function(reqrescontext) {
    throw new ForbiddenError("can't delete a user");
    // optionally: 
    // return context.error(403, "can't delete a user"); 
})

We can set behavior for milestones directly as above, or we can add functionality before and after milestones too:

// check the cache first 
users.list.fetch.before(function(reqrescontext) {
    var instance = cache.get(context.criteria);
 
    if (instance) {
        // keep a reference to the instance and skip the fetch 
        context.instance = instance;
        return context.skip;
    } else {
        // cache miss; we continue on 
        return context.continue;
    }
})

Milestones can also be defined in a declarative fashion, and used as middleware with any resource. For example:

// my-middleware.js 
module.exports = {
  create: {
    fetchfunction(reqrescontext) {
      // manipulate the fetch call 
      return context.continue;
    }
  },
  list: {
    write: {
      beforefunction(reqrescontext) {
        // modify data before writing list data 
        return context.continue;
      },
      actionfunction(reqrescontext) {
        // change behavior of actually writing the data 
        return context.continue;
      },
      afterfunction(reqrescontext) {
        // set some sort of flag after writing list data 
        return context.continue;
      }
    }
  }
};
 
// my-app.js 
var rest = require('epilogue'),
    restMiddleware = require('my-middleware');
 
rest.initialize({
    app: app,
    sequelize: sequelize
});
 
var users = rest.resource({
    model: User,
    endpoints: ['/users', '/users/:id']
});
 
users.use(restMiddleware);

Epilogue middleware also supports bundling in extra resource configuration by specifying an "extraConfiguration" member of the middleware like so:

// my-middleware.js 
module.exports = {
  extraConfigurationfunction(resource) {
    // support delete for plural form of a resource 
    var app = resource.app;
    app.del(resource.endpoints.plural, function(reqres) {
      resource.controllers.delete._control(req, res);
    });
  }
};

To show an error and halt execution of milestone functions you can throw an error:

var ForbiddenError = require('epilogue').Errors.ForbiddenError;
 
beforefunction(reqrescontext) {
    return authenticate.then(function(authed) {
        if(!authed) throw new ForbiddenError();
 
        return context.continue;
    });
}

Listing resources support filtering, searching, sorting, and pagination as described below.

Add query parameters named after fields to limit results.

$ curl http://localhost/users?name=James+Conrad
 
HTTP/1.1 200 OK
Content-Type: application/json
 
[
  {
    "name": "James Conrad",
    "email": "jamesconrad@fastmail.fm"
  }
]

Use the q parameter to perform a substring search across all fields.

$ curl http://localhost/users?q=james
 
HTTP/1.1 200 OK
Content-Type: application/json
 
[
  {
    "name": "James Conrad",
    "email": "jamesconrad@fastmail.fm"
  }, {
    "name": "Jim Huntington",
    "email": "jamesh@huntington.mx"
  }
]

Search behavior can be customized to change the parameter used for searching, as well as which attributes are included in the search, like so:

var users = rest.resource({
    model: User,
    endpoints: ['/users', '/users/:id'],
    search: {
      param: 'searchOnlyUsernames',
      attributes: [ 'username' ]
    }
});

This would restrict substring searches to the username attribute of the User model, and the search parameter would be 'searchOnlyUsernames':

$ curl http://localhost/users?searchOnlyUsernames=james

Specify the sort parameter to sort results. Values are field names, optionally preceded by a - to indicate descending order. Multiple sort values may be separated by ,.

$ curl http://localhost/users?sort=-name
 
HTTP/1.1 200 OK
Content-Type: application/json
 
[
  {
    "name": "Jim Huntington",
    "email": "jamesh@huntington.mx"
  }, {
    "name": "James Conrad",
    "email": "jamesconrad@fastmail.fm"
  }
]

Sort behavior can be customized to change the parameter used for sorting, as well as which attributes are allowed to be used for sorting like so:

var users = rest.resource({
    model: User,
    endpoints: ['/users', '/users/:id'],
    sort: {
      param: 'orderby',
      attributes: [ 'username' ]
    }
});

This would restrict sorting to only the username attribute of the User model, and the sort parameter would be 'orderby':

$ curl http://localhost/users?orderby=username

By default all attributes defined on the model are allowed to be sorted on. Sorting on a attribute not allowed will cause a 400 error to be returned with errors in the format:

$ curl http://localhost/users?sortby=invalid,-otherinvalid,valid
 
HTTP/1.1 400 BAD REQUEST
Content-Type: application/json
 
{
  "message": "Sorting not allowed on given attributes",
  "errors": ["invalid", "otherinvalid"]
}

List routes support pagination via offset or page and count query parameters. Find metadata about pagination and number of results in the Content-Range response header. Pagination defaults to a default of 100 results per page, and a maximum of 1000 results per page.

# get the third page of results
$ curl http://localhost/users?offset=200&count=100
 
HTTP/1.1 200 OK
Content-Type: application/json
Content-Range: items 200-299/3230
 
[
  { "name": "James Conrad", ... },
  ...
]

Alternatively, you can specify that pagination is disabled for a given resource by passing false to the pagination property like so:

var users = rest.resource({
    model: User,
    endpoints: ['/users', '/users/:id'],
    pagination: false
});

Set defaults and give epilouge a reference to your express app. Send the following parameters:

A reference to the Express application

Prefix to prepend to resource endpoints

HTTP method to use for update routes, one of POST, PUT, or PATCH

Create a resource and CRUD actions given a Sequelize model and endpoints. Accepts these parameters:

Reference to a Sequelize model

Specify endpoints as an array with two sinatra-style URL paths in plural and singular form (e.g., ['/users', '/users/:id']).

Create only the specified list of actions for the resource. Options include create, list, read, update, and delete. Defaults to all.

Check out the Milestone docs

Copyright (C) 2012-2015 David Chester
Copyright (C) 2014-2015 Matt Broadstone

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.