mongoose-rest

rest

lib/rest.js

Module dependencies.

var models = require('./models')
  , lingo = require('lingo').en;

The MongoDB ID format.

var id_format = /^[0-9a-f]{24}$/;

Add RESTful routes.

  • param: HTTPServer app

  • param: object routes

  • param: string prefix

  • param: string singular

  • api: private

function addRoutes (app, routes, prefix, singular) {
    app.get    ( prefix + '.:format?'                   , routes.index  );
    app.post   ( prefix                                 , routes.create );
    app.get    ( prefix + '/:' + singular + '.:format?' , routes.read   );
    app.put    ( prefix + '/:' + singular               , routes.update );
    app.delete ( prefix + '/:' + singular               , routes.destroy);
}

Create resource-based, RESTful routes for mongoose models. All models must be defined before calling this method.

  • param: HTTPServer app

  • param: object config (optional)

  • api: public

exports.create = function (app, config) {
    config = config || {};

    //Set config defaults
    config.default_limit = config.default_limit || 20
    config.max_limit = config.max_limit || 100;

    //Add routes for each top level model
    models.getTopLevel().forEach(function (resource) {

        var singular = lingo.singularize(resource)
          , plural = lingo.pluralize(resource)
          , top_prefix = (config.path || '/') + plural
          , routes;

        //Autoload a resource when the param is part of a route
        autoloadTopLevelResource(app, resource, config);

        //Add RESTful routes
        routes = topLevelRoutes(app, top_prefix, resource, config);

        addRoutes(app, routes, top_prefix, singular);

        //Add routes for embedded documents
        models.getChildren(resource).forEach(function (embedded) {

            var prefix = top_prefix + '/:' + singular;

            autoloadEmbeddedResource(app, resource,
                embedded.resource, embedded.attribute, config);

            routes = embeddedRoutes(app, prefix, resource, embedded.resource,
                                        embedded.attribute);

            addRoutes(app, routes, prefix + '/' + embedded.plural, embedded.singular);

        });

    });

    app.dynamicHelpers({resource: function (request, response) {
        return request.resource;
    }});
}

Autoload a resource when a route contains it as a parameter, e.g. /posts/:post will automatically load the requested post, where :post is either an ID or unique slug, e.g. /posts/my-test-post or /posts/23

  • param: HTTPServer app

  • param: string resource

  • param: object config (optional)

  • api: private

function autoloadTopLevelResource (app, resource, config) {
    var model = models.mongoose.model(resource)
      , singular = lingo.singularize(resource);

    app.param(singular, function (request, response, next) {
        var id = request.params[singular];

        function handleResource (err, obj) {
            if (err) {
                return next(new Error(err));
            } else if (null == obj) {
                if (request.xhr || request.format) {
                    response.send(404);
                } else {
                    request.flash('error', 'The %s could not be found.', singular);
                    response.redirect('back');
                }
            } else {
                request.resource(singular, obj);
                next();
            }
        }

        //Is there a unique slug attribute we can lookup by? If not, lookup by ID
        if (model.schema.tree.slug && !id_format.test(id)) {
            model.findOne({slug: id}, handleResource);
        } else {
            model.findById(id, handleResource);
        }
    });
}

Autoload an embedded resource when a route contains it as a parameter, e.g. /posts/:post/commments/:comment will automatically load the requested comment (assuming the post has already been loaded).

  • param: HTTPServer app

  • param: string parent

  • param: string resource

  • param: string attribute - the attribute name of the embedded resource

  • param: object config (optional)

  • api: private

function autoloadEmbeddedResource (app, parent, resource, attribute, config) {
    var model = models.mongoose.model(resource)
      , singular = lingo.singularize(resource)
      , parent_singular = lingo.singularize(parent);

    app.param(singular, function (request, response, next) {
        var parent = request.resource(parent_singular)
          , id = request.params[singular];
        if (parent && attribute in parent) {
            parent[attribute].forEach(function (child) {
                if (child.get('id') == id) {
                    request.resource(singular, child);
                    return next();
                }
            });
        } else if (request.xhr || request.format) {
            response.send([]);
        } else {
            request.flash('error', 'The %s could not be found.', singular);
            response.redirect('back');
        }
    });
}

Generate routes for top level models.

  • param: HTTPServer app

  • param: string resource

  • param: object config (optional)

  • return: object routes

  • api: private

function topLevelRoutes (app, prefix, resource, config) {

    var model = models.mongoose.model(resource)
      , singular = lingo.singularize(resource)
      , plural = lingo.pluralize(resource)
      , routes = {};

    //GET /<resource>
    routes.index = function (request, response, next) {
        var page = request.query.page || 1
          , limit = Math.min(config.max_limit, request.query.limit
                                                   || config.default_limit)
          , offset = (page - 1) * limit
          , locals = {}
          , query;

        function doQuery(query) {
            query.skip(offset).limit(limit).run(function (err, results) {
                if (err) {
                    return next(new Error(err));
                } else if (request.xhr || request.format === 'json') {
                    return response.send(results || []);
                }
                var locals = {
                    limit  : limit
                  , page   : page
                  , offset : offset
                  , query  : request.query
                }
                locals[plural] = results || [];
                response.locals(locals);
                request.resource(plural, results);
                response.render(plural + '/index');
            });
        }

        //Use Model.search() if it's defined
        if (model.search) {
            model.search(request.query, request.user, function (err, query) {
                if (err) {
                    return next(new Error(err));
                }
                doQuery(query);
            });
        } else {
            query = model.find();
            if (request.query.order) {
                query = query.sort([[
                    request.query.order, request.query.desc ? 'descending'
                                                            : 'ascending'
                ]]);
            }
            doQuery(query);
        }
    }

    //POST /<resource>
    routes.create = function (request, response, next) {
        var attr, instance = new model();
        for (attr in request.body) {
            if (!(attr in model.schema.tree)) {
                delete request.body[attr];
            }
        }
        if (model.filter) {
            request.body = model.filter(request.body);
        }
        for (attr in request.body) {
            instance[attr] = request.body[attr];
        }
        instance.save(function (err) {
            if (err) {
                    return next(new Error(err));
            } else if (request.xhr || request.format === 'json') {
                return response.send(instance);
            }
            request.flash('info', 'The %s was created successfully', singular);
            response.redirect('/' + plural);
        });
    }

    //GET /<resource>/:id
    routes.read = function (request, response, next) {
        if (request.xhr || request.format === 'json') {
            return response.send(request.resource(singular).toJSON());
        }
        response.local('instance', request.resource(singular));
        response.render(plural + '/read');
    }

    //PUT /<resource>/:id
    routes.update = function (request, response, next) {
        var attr, instance = request.resource(singular);
        for (attr in request.body) {
            if (!(attr in model.schema.tree)) {
                delete request.body[attr];
            }
        }
        if (model.filter) {
            request.body = model.filter(request.body);
        }
        for (attr in request.body) {
            instance[attr] = request.body[attr];
        }
        instance.save(function (err) {
            if (err) {
                return next(new Error(err));
            } else if (request.xhr || request.format === 'json') {
                return response.send(instance);
            }
            request.flash('info', 'The %s was updated successfully', singular);
            response.redirect('/' + plural + '/' + request.params.id);
        });
    }

    //DELETE /<resource>/:id
    routes.destroy = function (request, response, next) {
        request.resource(singular).remove(function (err) {
            if (err) {
                return next(new Error(err));
            } else if (request.xhr || request.format === 'json') {
                return response.send(200);
            }
            request.flash('info', 'The %s was removed successfully', singular);
            response.redirect('/' + plural);
        });
    }

    //If there's a static acl() method, patch each action to route through it
    if (model.acl) {
        for (var action in routes) {
            (function (action, handle) {
                routes[action] = function (request, response, next) {
                    var user = request.user || null
                      , obj = request.resource(singular) || request.body;
                    model.acl(user, action, obj, function (ok) {
                        if (!ok) next(new Error('auth'));
                        else handle(request, response, next);
                    });
                }
            })(action, routes[action]);
        }
    }

    return routes;
}

Generate routes for embedded documents.

  • param: HTTPServer app

  • param: string prefix

  • param: string parent

  • param: string resource

  • param: string attribute

  • param: object config (optional)

  • return: object routes

  • api: private

function embeddedRoutes (app, prefix, parent_resource, resource, attribute) {

    var model = models.mongoose.model(resource)
      , plural = lingo.pluralize(resource)
      , singular = lingo.singularize(resource)
      , parent_model = models.mongoose.model(parent_resource)
      , parent_plural = lingo.pluralize(parent_resource)
      , parent_singular = lingo.singularize(parent_resource)
      , routes = {};

    prefix += '/' + plural;

    //GET /<parent_resource>/:parent_id/<resource>
    routes.index = function (request, response, next) {
        var parent = request.resource(parent_singular)
          , children = [];

        if (parent[attribute] &amp;&amp; parent[attribute].length) {
            parent[attribute].forEach(function (child) {
                //Convert each child to a JSON string
                var obj = child.toJSON();
                obj.id = obj._id;
                delete obj._id;
                children.push(obj);
            });
        }
        return response.send(children);
    }

    //POST /<parent_resource>/:parent_id/<resource>/:id
    routes.create = function (request, response, next) {
        var parent = request.resource(parent_singular)
          , child = new model();
        if (!parent[attribute]) {
            parent[attribute] = [];
        }
        for (attr in request.body) {
            if (attr in model.schema.tree) {
                child[attr] = request.body[attr];
            }
        }
        parent[attribute].push(child);
        parent.save(function (err) {
            if (err) {
                return next(new Error(err));
            }
            response.send(child.toJSON());
        });
    }

    //GET /<parent_resource>/:parent_id/<resource>/:id
    routes.read = function (request, response, next) {
        var instance = request.resource(singular);
        response.send(instance);
    }

    //PUT /<parent_resource>/:parent_id/<resource>/:id
    routes.update = function (request, response, next) {
        var instance = request.resource(singular)
          , parent = request.resource(parent_singular);
        for (attr in request.body) {
            if (attr in model.schema.tree) {
                instance[attr] = request.body[attr];
            }
        }
        parent.save(function (err) {
            if (err) {
                return next(new Error(err));
            }
            response.send(200);
        });
    }

    //DELETE /<parent_resource>/:parent_id/<resource>/:id
    routes.destroy = function (request, response, next) {
        var instance = request.resource(singular)
          , parent = request.resource(parent_singular);
        instance.remove();
        parent.save(function (err) {
            if (err) {
                return next(new Error(err));
            }
            response.send(200);
        });
    }

    //Run each embedded document route through the parent's acl() method
    if (parent_model.acl) {
        for (var action in routes) {
            (function (action, handle) {
                routes[action] = function (request, response, next) {
                    var user = request.user || null
                      , obj = request.resource(singular) || request.body;
                    parent_model.acl(user, action, obj, function (ok) {
                        if (!ok) next(new Error('auth'));
                        else handle(request, response, next);
                    });
                }
            })(action, routes[action]);
        }
    }

    return routes;
}

request

lib/request.js

Module dependencies.

var http = require('http')
  , request = http.IncomingMessage.prototype;

Bind a resource to the request.

  • param: string name

  • param: object resource

  • api: public

request.resource = function (name, resource) {
    if (arguments.length == 2) {
        this.resources = this.resource || {};
        return this.resources[name] = resource;
    } else if (this.resources &amp;&amp; name in this.resources) {
        return this.resources[name]
    }
    return null;
}

Check if the request has the specified resource.

  • param: string name

  • api: public

request.hasResource = function (name) {
    return this.resources &amp;&amp; name in this.resources;
}

models

lib/models.js

Module dependencies.

var lingo = require('lingo').en;

The schema map.

var embedded = [], top_level = []
  , child_map = {}
  , parent_map = {};

The mongoose instance in use.

exports.mongoose = null;

A hacky way of mapping attributes => names.

  • param: Schema schema

  • return: string key

  • api: private

function attrKey(schema) {
    return Object.keys(schema.tree).join('-');
}

Build a schema map.

  • param: Mongoose mongoose

  • api: public

exports.use = function (mongoose) {

    exports.mongoose = mongoose;

    var key, model, attr, schema, attr_map = {};

    //Create a map of attributes => model name
    for (model in mongoose.modelSchemas) {
        schema = mongoose.modelSchemas[model];
        key = attrKey(schema);
        if (key in attr_map) {
            console.error('WARNING: Two or more schemas have the same attributes.');
        }
        attr_map[key] = model;
    }

    //Work out which document schemas are embedded inside others
    for (model in mongoose.modelSchemas) {
        schema = mongoose.modelSchemas[model];
        child_map[model] = [];
        for (attr in schema.tree) {
            (function (attr) {
                if (Array.isArray(schema.tree[attr]) &amp;&amp; schema.tree[attr][0].path) {
                    key = attrKey(schema.tree[attr][0]);
                    resource = attr_map[key];
                    child_map[model].push({
                        attribute : attr
                      , resource  : resource
                      , singular  : lingo.singularize(resource)
                      , plural    : lingo.pluralize(resource)
                    });
                    embedded.push(resource);
                    if (!(resource in parent_map)) {
                        parent_map[resource] = [];
                    }
                    parent_map[resource].push({
                        attribute : attr
                      , resource  : model
                      , singular  : lingo.singularize(model)
                      , plural    : lingo.pluralize(model)
                    });
                }
            }(attr));
        }
    }

    //Work out which resources are top level
    Object.keys(mongoose.modelSchemas).forEach(function (resource) {
        if (!~embedded.indexOf(resource)) {
            top_level.push(resource);
        }
    });

}

Get top level resources.

  • return: array resources

  • api: public

exports.getTopLevel = function () {
    return top_level;
}

Get embedded resources.

  • return: array resources

  • api: public

exports.getEmbedded = function () {
    return embedded;
}

Get a map of parent => children resources.

  • return: array map

  • api: public

exports.getChildMap = function () {
    return child_map;
}

Get a map of child => parent resources.

  • return: array map

  • api: public

exports.getParentMap = function () {
    return parent_map;
}

Check whether a resource is top level.

  • return: boolean istoplevel

  • api: public

exports.isTopLevel = function (resource) {
    return exports.getTopLevel().indexOf(resource) !== -1;
}

Check whether a resource is embedded in another.

  • return: boolean is_embedded

  • api: public

exports.isEmbedded = function (resource) {
    return exports.getEmbedded().indexOf(resource) !== -1;
}

Get the children of the specified resource.

  • return: array children

  • api: public

exports.getChildren = function (resource) {
    return exports.getChildMap()[resource] || [];
}

Get the parents of the specified resource.

  • return: array children

  • api: public

exports.getParents = function (resource) {
    return exports.getParentMap()[resource] || [];
}

Check whether the resource has embedded resources.

  • return: boolean has_children

  • api: public

exports.hasChildren = function (resource) {
    return exports.getChildMap()[resource] &amp;&amp; exports.getChildMap()[resource].length;
}

Check whether the resource is embedded in others.

  • return: boolean has_parents

  • api: public

exports.hasParents = function (resource) {
    return exports.getParentMap()[resource] &amp;&amp; exports.getParentMap()[resource].length;
}

backbone

lib/backbone.js

Module dependencies.

var models = require('./models')
  , fs = require('fs')
  , lingo = require('lingo').en;

Convert a lowercase, underscored string to a proper cased class name. e.g. "my_table" => "MyTable"

  • param: string table

  • return: string class

  • api: private

function classify (str) {
    return str.replace('_', ' ').replace(/(^| )[a-z]/g, function (str) {
        return str.toUpperCase();
    }).replace(' ', '');
}

Recursively replace _id with id in an object.

  • param: object instance

  • api: private

function convertUnderscoreId (instance) {
    for (var attr in instance) {
        if (attr == '_id') {
            instance.id = instance[attr];
            delete instance._id;
        } else if (Array.isArray(instance[attr])) {
            instance[attr].forEach(function (child) {
                if (typeof child === 'object') {
                    convertUnderscoreId(child);
                }
            });
        } else if (typeof instance[attr] === 'object') {
            convertUnderscoreId(instance[attr]);
        }
    }
}

Generate backbone models.

  • param: string namespace (optional)

  • return: string backbone_javascript

  • api: public

exports.generate = function (app, namespace) {
    namespace = namespace || '';

    var backbone = backboneCommon(namespace);

    models.getEmbedded().forEach(function (resource) {
        backbone += backboneEmbeddedModel(namespace, resource);
    });

    models.getTopLevel().forEach(function (resource) {
        backbone += backboneTopLevelModel(namespace,
                        resource, models.getChildren(resource));
    });

    return backbone;
}

Generate express view helpers for creating backbone models and collections.

  • param: HTTPServer app

  • param: string namespace (optional)

  • api: public

exports.helpers = function (app, namespace) {

    namespace = namespace || '';

    app.dynamicHelpers({

        backboneModel: function (request, response) {
            return function (resource, var_name) {
                var singular = resource
                  , class = namespace + classify(resource)
                  , instance = request.resource(resource);

                var_name = var_name || resource;

                if (instance.toJSONSafe) {
                    instance = instance.toJSONSafe();
                } else {
                    instance = instance.toJSON()
                }

                convertUnderscoreId(instance);

                var model = 'var '+var_name+' = '
                          + 'new '+class+'(' + JSON.stringify(instance) + ');';

                return model;
            }
        }

      , backboneCollection: function (request, response) {
            return function (resource, var_name) {
                var singular = lingo.singularize(resource)
                  , class = namespace + classify(singular)
                  , instances = request.resource(resource);

                var_name = var_name || resource;

                for (var i = 0, l = instances.length; i &lt; l; i++) {
                    if (instances[i].toJSONSafe) {
                        instances[i] = instances[i].toJSONSafe();
                    } else {
                        instances[i] = instances[i].toJSON()
                    }

                    convertUnderscoreId(instances[i]);
                }

                var collection = 'var '+var_name+' = '
                               + 'new '+class+'Collection(' + JSON.stringify(instances) + ');';

                return collection;
            }
        }

    });
}

Generate backbone models and write to a file.

  • param: string file

  • param: string namespace (optional)

  • api: public

exports.generateFile = function (app, file, namespace) {
    fs.writeFileSync(file, exports.generate(app, namespace));
}

Generate common backbone code.

  • param: string namespace (optional)

  • api: private

function backboneCommon (namespace) {
    return 'var '+namespace+'Model = Backbone.Model.extend({\n'
         + '    set: function (attributes, options) {\n'
         + '        Backbone.Model.prototype.set.call(this, '
         +              'attributes, options);\n'
         + '        this.pullEmbedded();\n'
         + '    }\n'
         + '  , pullEmbedded: function () {\n'
         + '        for (var attr in this.attributes) {\n'
         + '            if (this[attr] && this[attr] instanceof Backbone.Collection) {\n'
         + '                for (var i = 0, models = [], model = this[attr].model, '
         +                          'l = this.attributes[attr].length; i < l; i++) {\n'
         + '                    models.push(new model(this.attributes[attr][i]));\n'
         + '                }\n'
         + '                this[attr].reset(models);\n'
         + '                delete this.attributes[attr];\n'
         + '            }\n'
         + '        }\n'
         + '    }\n'
         + '});\n'
         + '\n\n'
         + 'var '+namespace+'Collection = Backbone.Collection.extend({});\n\n';
}

Generate backbone code for embedded models.

  • param: string namespace (optional)

  • api: private

function backboneEmbeddedModel (namespace, resource) {
    var singular = namespace + classify(lingo.singularize(resource));

    return 'var '+singular+' = '+namespace+'Model.extend({})\n'
         + '  , '+singular+'Collection = '
         + namespace+'Collection.extend({ model: '+singular+' });\n\n';
}

Generate backbone code for top level models.

  • param: string namespace (optional)

  • api: private

function backboneTopLevelModel (namespace, resource, children) {
    var singular = namespace + classify(lingo.singularize(resource))
      , plural = lingo.pluralize(resource)
      , backbone = '';

    backbone += 'var '+singular+' = Model.extend({\n'
              + '    urlRoot: \'/'+plural+'\'\n';

    if (models.hasChildren(resource)) {
        backbone += '  , initialize: function () {\n';
        models.getChildren(resource).forEach(function (em) {
            backbone += '        this.'+em.attribute+' = new '
                      + namespace + classify(em.singular) + 'Collection;\n'
                      + '        this.'+em.attribute+'.url = \'/'+plural
                      + '/\' + this.id + \'/'+em.plural+'\'\n';
        });
        backbone += '        this.pullEmbedded();\n'
                  + '    }\n';
    }

    backbone += '});\n\n';

    backbone += 'var ' + singular + 'Collection = Collection.extend({\n'
              + '    model: ' + singular + '\n'
              + '  , url: \'/' + plural + '\'\n'
              + '});\n\n';

    return backbone;
}