Comparing version 0.3.5 to 0.4.0
531
index.js
@@ -8,50 +8,18 @@ // Dependencies | ||
// Private Members | ||
// --------------- | ||
var app = express(); | ||
var exec = require('./middleware/exec'); | ||
var headers = require('./middleware/headers'); | ||
var configure = require('./middleware/configure'); | ||
var query = require('./middleware/query'); | ||
var send = require('./middleware/send'); | ||
var validation = require('./middleware/validation'); | ||
function applyOptions(options, callback) { | ||
applyControllerOptions(options, function (error) { | ||
if (error) return next(error); | ||
applyQueryOptions(options, callback); | ||
}); | ||
} | ||
// Apply various options based on controller parameters | ||
function applyControllerOptions (options, callback) { | ||
if (options.controller.select) query.select(options.controller.select); | ||
if (options.controller.restrict) { | ||
options.controller.restrict(options.query, options.request); | ||
} | ||
callback(); | ||
} | ||
// Apply various options based on request query parameters | ||
function applyQueryOptions (options, callback) { | ||
var populate; | ||
if (options.request.query.skip) options.query.skip(options.request.query.skip); | ||
if (options.request.query.limit) options.query.limit(options.request.query.limit); | ||
if (options.request.query.populate) { | ||
populate = JSON.parse(options.request.query.populate); | ||
if (!Array.isArray(populate)) populate = [ populate ]; | ||
populate.forEach(function (field) { | ||
// Don't allow selecting +field from client | ||
if (field.select && field.select.indexOf('+') !== -1) { | ||
callback(new Error('Including fields excluded at schema level (using +) is not permitted')); | ||
return false; | ||
} | ||
options.query.populate(field) | ||
}); | ||
} | ||
callback(); | ||
} | ||
// Module Definition | ||
// ----------------- | ||
var app = express(); | ||
var baucis = module.exports = function (options) { | ||
options = options || {}; | ||
options || (options = {}); | ||
//if (options.prefixUrl) app.set('urlPrefix', options.urlPrefix); | ||
Object.keys(options).forEach(function (key) { | ||
app.set(key, options[key]); | ||
}); | ||
@@ -61,435 +29,53 @@ return app; | ||
// Default Settings | ||
// ---------------- | ||
//app.set('urlPrefix', '/api'); | ||
// Middleware | ||
// ---------- | ||
// Functions to return middleware for HTTP verbs | ||
// Retrieve header for the addressed document | ||
function head (options) { | ||
var f = function (request, response, next) { | ||
var id = request.params.id; | ||
var query = mongoose.model(options.singular).findById(id); | ||
var userOptions = { | ||
query: query, | ||
controller: options, | ||
request: request | ||
}; | ||
applyOptions(userOptions, function (error) { | ||
if (error) return next(error); | ||
query.count(function (error, count) { | ||
if (error) return next(error); | ||
if (count === 0) return response.send(404); | ||
response.send(200); | ||
}); | ||
}); | ||
}; | ||
return f; | ||
} | ||
// Retrieve the addressed document | ||
function get (options) { | ||
var f = function (request, response, next) { | ||
var id = request.params.id; | ||
var query = mongoose.model(options.singular).findById(id); | ||
var userOptions = { | ||
query: query, | ||
controller: options, | ||
request: request | ||
}; | ||
applyOptions(userOptions, function (error) { | ||
if (error) return next(error); | ||
query.exec(function (error, doc) { | ||
if (error) return next(error); | ||
if (!doc) return response.send(404); | ||
response.json(doc); | ||
}); | ||
}); | ||
}; | ||
return f; | ||
} | ||
// Treat the addressed document as a collection, and push | ||
// the addressed object to it | ||
function post (options) { | ||
var f = function (request, response, next) { | ||
response.send(405); // method not allowed (as of yet unimplemented) | ||
}; | ||
return f; | ||
} | ||
// Replace the addressed document, or create it if it doesn't exist | ||
function put (options) { | ||
var f = function (request, response, next) { | ||
// Can't send id for update, even if unchanged | ||
delete request.body._id; | ||
var id = request.params.id || null; | ||
var create = (id === null); | ||
var query = mongoose.model(options.singular).findByIdAndUpdate(id, request.body, {upsert: true}); | ||
var userOptions = { | ||
query: query, | ||
controller: options, | ||
request: request | ||
}; | ||
applyControllerOptions(userOptions, function (error) { | ||
if (error) return next(error); | ||
query.exec(function (error, doc) { | ||
if (error) return next(error); | ||
if (create) response.status(201); | ||
else response.status(200); | ||
response.set('Location', path.join(options.basePath, doc.id)); | ||
response.json(doc); | ||
}); | ||
}); | ||
}; | ||
return f; | ||
} | ||
// Delete the addressed object | ||
function del (options) { | ||
var f = function (request, response, next) { | ||
var id = request.params.id; | ||
var query = mongoose.model(options.singular).remove({ _id: id }); | ||
var userOptions = { | ||
query: query, | ||
controller: options, | ||
request: request | ||
}; | ||
applyControllerOptions(userOptions, function (error) { | ||
if (error) return next(error); | ||
query.exec(function (error, count) { | ||
if (error) return next(error); | ||
response.json(count); | ||
}); | ||
}); | ||
}; | ||
return f; | ||
} | ||
// Retrieve documents matching conditions | ||
function headCollection (options) { | ||
var f = function (request, response, next) { | ||
var conditions; | ||
var query; | ||
var userOptions = { | ||
query: query, | ||
controller: options, | ||
request: request | ||
}; | ||
if (request.query && request.query.conditions) { | ||
conditions = JSON.parse(request.query.conditions); | ||
} | ||
query = mongoose.model(options.singular).find(conditions); | ||
applyOptions(userOptions, function (error) { | ||
if (error) return next(error); | ||
query.count(function (error, count) { | ||
if (error) return next(error); | ||
response.send(200); | ||
}); | ||
}); | ||
}; | ||
return f; | ||
} | ||
// retrieve documents matching conditions | ||
function getCollection (options) { | ||
var f = function (request, response, next) { | ||
var firstWasProcessed = false; | ||
var conditions; | ||
if (request.query && request.query.conditions) { | ||
conditions = JSON.parse(request.query.conditions); | ||
} | ||
var query = mongoose.model(options.singular).find(conditions); | ||
var userOptions = { | ||
query: query, | ||
controller: options, | ||
request: request | ||
}; | ||
applyOptions(userOptions, function (error) { | ||
if (error) return next(error); | ||
// If `count` is set, return the number of documents the query matches | ||
if (request.query.count === true) { | ||
query.count(function (error, count) { | ||
if (error) return next(error); | ||
response.json(count); | ||
}); | ||
return; | ||
} | ||
// Otherwise, stream the array to the client | ||
response.set('Content-Type', 'application/json'); | ||
response.write('['); | ||
query.stream() | ||
.on('data', function (doc) { | ||
if (firstWasProcessed) response.write(', '); | ||
response.write(JSON.stringify(doc.toJSON())); | ||
firstWasProcessed = true; | ||
}) | ||
.on('error', next) | ||
.on('close', function () { | ||
response.write(']'); | ||
response.send(); | ||
}); | ||
}); | ||
}; | ||
return f; | ||
} | ||
// Create a new document and return its ID | ||
function postCollection (options) { | ||
var f = function (request, response, next) { | ||
var body = request.body; | ||
// Must be object or array | ||
if (!body || typeof body !== 'object') { | ||
return next(new Error('Must supply a document or array to POST')); | ||
} | ||
response.status(201); | ||
// If just one object short circuit | ||
if (!Array.isArray(body)) { | ||
return mongoose.model(options.singular).create(body, function (error, doc) { | ||
if (error) return next(error); | ||
response.set('Location', path.join(options.basePath, doc.id)); | ||
response.json(doc); | ||
}); | ||
} | ||
// No empty arrays | ||
if (body.length === 0) return next(new Error('Array was empty.')); | ||
// Create and save given documents | ||
var promises = body.map(mongoose.model(options.singular).create); | ||
var ids = []; | ||
var processedCount = 0; | ||
var location; | ||
// Stream the response JSON array | ||
response.set('Content-Type', 'application/json'); | ||
response.write('['); | ||
promises.forEach(function (promise) { | ||
promise.then(function (doc) { | ||
response.write(JSON.stringify(doc.toJSON())); | ||
ids.push(doc.id); | ||
// Still more to process? | ||
if (processedCount < body.length) return response.write(', '); | ||
// Last one was processed | ||
response.write(']'); | ||
location = options.basePath + '?conditions={ _id: { $in: [' + ids.join() + '] } }'; | ||
response.set('Location', location); | ||
response.send(); | ||
}); | ||
promise.error(function (error) { | ||
next(error); | ||
}); | ||
}); | ||
}; | ||
return f; | ||
} | ||
// Replace all docs with given docs ... | ||
function putCollection (options) { | ||
var f = function (request, response, next) { | ||
response.send(405); // method not allowed (as of yet unimplemented) | ||
}; | ||
return f; | ||
} | ||
// Delete all documents matching conditions | ||
function delCollection (options) { | ||
var f = function (request, response, next) { | ||
var conditions = request.body || {}; | ||
var query = mongoose.model(options.singular).remove(conditions); | ||
var userOptions = { | ||
query: query, | ||
controller: options, | ||
request: request | ||
}; | ||
applyOptions(userOptions, function (error) { | ||
if (error) return next(error); | ||
query.exec(function (error, count) { | ||
if (error) return next(error); | ||
response.json(count); | ||
}); | ||
}); | ||
}; | ||
return f; | ||
} | ||
// Add "Link" header field, with some basic defaults | ||
function addLinkRelations (options) { | ||
var f = function (request, response, next) { | ||
response.links({ | ||
collection: options.basePath, | ||
search: options.basePath, | ||
edit: path.join(options.basePath, request.params.id), | ||
self: path.join(options.basePath, request.params.id), | ||
'latest-version': path.join(options.basePath, request.params.id) | ||
}); | ||
next(); | ||
}; | ||
return f; | ||
} | ||
// Add "Link" header field, with some basic defaults (for collection routes) | ||
function addLinkRelationsCollection (options) { | ||
var f = function (request, response, next) { | ||
response.links({ | ||
search: options.basePath, | ||
self: options.basePath, | ||
'latest-version': options.basePath | ||
}); | ||
next(); | ||
}; | ||
return f; | ||
} | ||
// Build the "Allow" response header | ||
function addAllowResponseHeader (options) { | ||
var f = function (request, response, next) { | ||
var allowed = []; | ||
if (options.head !== false) allowed.push('HEAD'); | ||
if (options.get !== false) allowed.push('GET'); | ||
if (options.post !== false) allowed.push('POST'); | ||
if (options.put !== false) allowed.push('PUT'); | ||
if (options.del !== false) allowed.push('DELETE'); | ||
response.set('Allow', allowed.join()); | ||
next(); | ||
}; | ||
return f; | ||
} | ||
// Build the "Accept" response header | ||
function addAcceptResponseHeader (options) { | ||
var f = function (request, response, next) { | ||
response.set('Accept', 'application/json'); | ||
next(); | ||
}; | ||
return f; | ||
} | ||
// Validation | ||
// ---------- | ||
// var validation = function (options) { | ||
// var validators = {}; | ||
// var f = function (request, response, next) { | ||
// response.json(validators); | ||
// }; | ||
// Object.keys(s.paths).forEach(function (path) { | ||
// var pathValidators = []; | ||
// if (path.enumValues.length > 0) { | ||
// // TODO | ||
// pathValidators.push( ); | ||
// } | ||
// if (path.regExp !== null) { | ||
// // TODO | ||
// pathValidators.push( ); | ||
// } | ||
// // test path.instance TODO or path.options.type | ||
// // TODO use any path.validators? | ||
// // TODO other path.options? | ||
// validators[path.path] = pathValidators; | ||
// }); | ||
// return f; | ||
// }; | ||
// Public Methods | ||
// -------------- | ||
baucis.rest = function (options) { | ||
options || (options = {}); // TODO clone, defaults | ||
if (!options.singular) throw new Error('Must provide the Mongoose schema name'); | ||
if (!options.plural) options.plural = lingo.en.pluralize(options.singular); | ||
if (!options.basePath) options.basePath = '/'; | ||
var basePath = options.basePath = path.join('/', options.basePath); | ||
var basePathWithId = options.basePathWithId = path.join(basePath, ':id'); | ||
var basePathWithOptionalId = options.basePathWithOptionalId = path.join(basePath, ':id?'); | ||
var controller = express(); | ||
var basePath = path.join('/', options.basePath || '/'); | ||
var basePathWithId = path.join(basePath, ':id'); | ||
var basePathWithOptionalId = path.join(basePath, ':id?'); | ||
controller.set('model', mongoose.model(options.singular)); | ||
controller.set('plural', options.plural || lingo.en.pluralize(options.singular)); | ||
controller.set('basePath', basePath); | ||
controller.set('basePathWithId', basePathWithId); | ||
controller.set('basePathWithOptionalId', basePathWithOptionalId); | ||
Object.keys(options).forEach(function (key) { | ||
controller.set(key, options[key]); | ||
}); | ||
controller.use(express.json()); | ||
controller.all(basePathWithId, addAllowResponseHeader(options)); | ||
controller.all(basePathWithId, addAcceptResponseHeader(options)); | ||
controller.all(basePath, addAllowResponseHeader(options)); | ||
controller.all(basePath, addAcceptResponseHeader(options)); | ||
// Initialize baucis state | ||
controller.all(basePathWithOptionalId, function (request, response, next) { | ||
request.baucis = {}; | ||
next(); | ||
}); | ||
// Allow/Accept headers | ||
controller.all(basePathWithId, headers.allow); | ||
controller.all(basePathWithId, headers.accept); | ||
controller.all(basePath, headers.allow); | ||
controller.all(basePath, headers.accept); | ||
if (options.configure) options.configure(controller); | ||
// Set Link header if desired | ||
if (options.relations === true) { | ||
controller.head(basePathWithId, addLinkRelations(options)); | ||
controller.get(basePathWithId, addLinkRelations(options)); | ||
controller.post(basePathWithId, addLinkRelations(options)); | ||
controller.put(basePathWithId, addLinkRelations(options)); | ||
controller.head(basePathWithId, headers.link); | ||
controller.get(basePathWithId, headers.link); | ||
controller.post(basePathWithId, headers.link); | ||
controller.put(basePathWithId, headers.link); | ||
controller.head(basePath, addLinkRelationsCollection(options)); | ||
controller.get(basePath, addLinkRelationsCollection(options)); | ||
controller.post(basePath, addLinkRelationsCollection(options)); | ||
controller.put(basePath, addLinkRelationsCollection(options)); | ||
controller.head(basePath, headers.linkCollection); | ||
controller.get(basePath, headers.linkCollection); | ||
controller.post(basePath, headers.linkCollection); | ||
controller.put(basePath, headers.linkCollection); | ||
} | ||
// Add all pre-query middleware | ||
if (options.all) controller.all(basePathWithOptionalId, options.all); | ||
@@ -502,17 +88,20 @@ if (options.head) controller.head(basePathWithOptionalId, options.head); | ||
if (options.head !== false) controller.head(basePathWithId, head(options)); | ||
if (options.get !== false) controller.get(basePathWithId, get(options)); | ||
if (options.post !== false) controller.post(basePathWithId, post(options)); | ||
if (options.put !== false) controller.put(basePathWithId, put(options)); | ||
if (options.del !== false) controller.del(basePathWithId, del(options)); | ||
// Add routes for singular documents | ||
if (options.head !== false) controller.head(basePathWithId, query.head, configure.controller, configure.query, exec.count, send.count); | ||
if (options.get !== false) controller.get(basePathWithId, query.get, configure.controller, configure.query, exec.exec, send.exec); | ||
if (options.post !== false) controller.post(basePathWithId, query.post, configure.controller, configure.query, exec.exec, send.exec); | ||
if (options.put !== false) controller.put(basePathWithId, query.put, configure.controller, configure.query, exec.exec, send.exec); | ||
if (options.del !== false) controller.del(basePathWithId, query.del, configure.controller, configure.query, exec.exec, send.exec); | ||
if (options.head !== false) controller.head(basePath, headCollection(options)); | ||
if (options.get !== false) controller.get(basePath, getCollection(options)); | ||
if (options.post !== false) controller.post(basePath, postCollection(options)); | ||
if (options.put !== false) controller.put(basePath, putCollection(options)); | ||
if (options.del !== false) controller.del(basePath, delCollection(options)); | ||
// Add routes for collections of documents | ||
if (options.head !== false) controller.head(basePath, configure.conditions, query.headCollection, configure.controller, configure.query, exec.count, send.count); | ||
if (options.get !== false) controller.get(basePath, configure.conditions, query.getCollection, configure.controller, configure.query, exec.stream, send.stream); | ||
if (options.post !== false) controller.post(basePath, query.postCollection /*, exec.promises, send.promises */); | ||
if (options.put !== false) controller.put(basePath, query.putCollection, configure.controller, configure.query, exec.exec, send.exec); | ||
if (options.del !== false) controller.del(basePath, configure.conditions, query.delCollection, configure.controller, configure.query, exec.exec, send.exec); | ||
if (options.publish !== false) app.use(path.join('/', options.plural), controller); | ||
// Publish unless told not to | ||
if (options.publish !== false) app.use(path.join('/', controller.get('plural')), controller); | ||
return controller; | ||
}; |
{ | ||
"name": "baucis", | ||
"version": "0.3.5", | ||
"version": "0.4.0", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "mocha --globals vegetables" | ||
"test": "mocha --bail --globals vegetables" | ||
}, | ||
@@ -8,0 +8,0 @@ "repository": { |
@@ -1,2 +0,2 @@ | ||
baucis v0.3.5 | ||
baucis v0.4.0 | ||
============= | ||
@@ -3,0 +3,0 @@ |
@@ -19,3 +19,3 @@ var expect = require('expect.js'); | ||
request.get(options, function (error, response, body) { | ||
request.del(options, function (error, response, body) { | ||
if (error) return done(error); | ||
@@ -29,22 +29,10 @@ | ||
expect(response).to.have.property('statusCode', 200); | ||
expect(body).to.have.property('name', 'Shitake'); | ||
expect(body).to.be(1); // count of deleted objects | ||
request.del(options, function (error, response, body) { | ||
if (error) return done(error); | ||
request.del(options, function (error, response, body) { | ||
if (error) return done(error); | ||
var options = { | ||
url: 'http://localhost:8012/api/v1/vegetables/' + shitake._id, | ||
json: true | ||
}; | ||
expect(response).to.have.property('statusCode', 200); | ||
expect(body).to.be(1); // count of deleted objects | ||
request.get(options, function (error, response, body) { | ||
if (error) return done(error); | ||
expect(response).to.have.property('statusCode', 404); | ||
done(); | ||
}); | ||
}); | ||
expect(response).to.have.property('statusCode', 404); | ||
done(); | ||
}); | ||
}); | ||
@@ -51,0 +39,0 @@ |
@@ -14,14 +14,12 @@ var expect = require('expect.js'); | ||
url: 'http://localhost:8012/api/v1/vegetables/', | ||
json: { | ||
name: 'Tomato' | ||
} | ||
json: { name: 'Tomato' } | ||
}; | ||
request.post(options, function (error, response, body) { | ||
if (error) return done(error); | ||
var id = body._id; | ||
expect(response.statusCode).to.equal(201); | ||
expect(id).not.to.be.empty(); // TODO check it's an ObjectID | ||
expect(body._id).not.to.be.empty(); // TODO check it's an ObjectID | ||
var options = { | ||
url: 'http://localhost:8012/api/v1/vegetables/' + id, | ||
url: 'http://localhost:8012/api/v1/vegetables/' + body._id, | ||
json: true | ||
@@ -28,0 +26,0 @@ }; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
28
272807
1031