mongoose-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.
|
exports.create = function (app, config) {
config = config || {};
config.default_limit = config.default_limit || 20
config.max_limit = config.max_limit || 100;
models.getTopLevel().forEach(function (resource) {
var singular = lingo.singularize(resource)
, plural = lingo.pluralize(resource)
, top_prefix = (config.path || '/') + plural
, routes;
autoloadTopLevelResource(app, resource, config);
routes = topLevelRoutes(app, top_prefix, resource, config);
addRoutes(app, routes, top_prefix, singular);
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
|
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();
}
}
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).
|
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.
|
function topLevelRoutes (app, prefix, resource, config) {
var model = models.mongoose.model(resource)
, singular = lingo.singularize(resource)
, plural = lingo.pluralize(resource)
, routes = {};
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');
});
}
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);
}
}
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);
});
}
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');
}
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);
});
}
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 (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.
|
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;
routes.index = function (request, response, next) {
var parent = request.resource(parent_singular)
, children = [];
if (parent[attribute] && parent[attribute].length) {
parent[attribute].forEach(function (child) {
var obj = child.toJSON();
obj.id = obj._id;
delete obj._id;
children.push(obj);
});
}
return response.send(children);
}
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());
});
}
routes.read = function (request, response, next) {
var instance = request.resource(singular);
response.send(instance);
}
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);
});
}
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);
});
}
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;
}
|
| 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 && 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 && name in this.resources;
}
|
| 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 = {};
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;
}
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]) && 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));
}
}
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.
|
exports.isTopLevel = function (resource) {
return exports.getTopLevel().indexOf(resource) !== -1;
}
|
Check whether a resource is embedded in another.
|
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.
|
exports.hasChildren = function (resource) {
return exports.getChildMap()[resource] && exports.getChildMap()[resource].length;
}
|
Check whether the resource is embedded in others.
|
exports.hasParents = function (resource) {
return exports.getParentMap()[resource] && exports.getParentMap()[resource].length;
}
|
| 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.
|
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.
|
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 < 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.
|
exports.generateFile = function (app, file, namespace) {
fs.writeFileSync(file, exports.generate(app, namespace));
}
|
Generate common backbone code.
|
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.
|
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.
|
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;
}
|