UNPKG

meanify

Version:

Node.js Express middleware that uses your Mongoose schema to generate SCRUD API routes compatible with AngularJS and ngResource.

461 lines (354 loc) • 16.2 kB
# meanify Node.js Express middleware that uses your Mongoose schema to generate SCRUD API routes compatible with AngularJS and ngResource. ## Implementation Before you begin, be sure [MongoDB is installed](http://docs.mongodb.org/manual/installation/) and `mongod` is running. Install **meanify** as a dependency and add it to your `package.json` file. ```bash npm install meanify --save ``` First, define your Mongoose models and any necessary validations and indexes. ```javascript // models.js var mongoose = require('mongoose'); var Schema = mongoose.Schema; var userSchema = new Schema({ name: { type: String, required: true }, email: { type: String, required: true }, password: { type: String, required: true } }); mongoose.model('User', userSchema); var postSchema = new Schema({ title: { type: String, required: true }, contents: { type: String, required: true }, author: { type: Schema.Types.ObjectId, ref: 'User', index: true } }); mongoose.model('Post', postSchema); ``` Initialize *meanify's* router middleware after your Mongoose models. ```javascript // server.js var express = require('express'); var app = express(); require('./models'); var mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/meanify'); var meanify = require('meanify')({ path: '/api', pluralize: true }); app.use(meanify()); app.listen(3001); ``` Start up your express app using the `DEBUG=meanify` param to verify your routes. ```bash āžœ DEBUG=meanify node server.js ``` The **meanify** log will show the newly created endpoints. ```bash meanify GET /api/users +0ms meanify POST /api/users +0ms meanify GET /api/users/:id +0ms meanify POST /api/users/:id +0ms meanify DELETE /api/users/:id +0ms meanify GET /api/posts +0ms meanify POST /api/posts +0ms meanify GET /api/posts/:id +0ms meanify POST /api/posts/:id +0ms meanify DELETE /api/posts/:id +0ms ``` The middleware functions powering these routes may also be accessed directly for more control over route creation. For example: ```javascript // app.use(meanify()); // Disable automatic routing. app.get('/api/posts', meanify.posts.search); app.post('/api/posts', meanify.posts.create); app.get('/api/posts/:id', meanify.posts.read); app.put('/api/posts/:id', meanify.posts.update); // Support PUT instead of POST. // app.delete('/api/posts/:id', meanify.posts.delete); // Disable this route. ``` ## Options **Meanify** expects options to be passed in on require of the module as seen below. This builds an Express middleware router object, which is accessed by invoking the returned function. ```javascript var meanify = require('meanify')({ path: '/api', exclude: ['Counter'], lowercase: false, pluralize: true, caseSensitive: false, strict: false, puts: true, relate: true }); app.use(meanify()); ``` ### path The root path to be used as a base for the generated routes. Default: `'/'` ### exclude Array of models to exclude from middleware generation. Default: `undefined` ### lowercase Prevents **meanify** from lowercasing generated route name. Default: `true` ### pluralize Pluralizes the model name when used in the route, i.e. "user" becomes "users". Default: `false` ### caseSensitive Enable case sensitivity, treating "/Foo" and "/foo" as different routes. Default: `true` ### strict Enable strict routing, treating "/foo" and "/foo/" differently by the router. Default `true` ### puts By default, ngResource does not support PUT for updates without [making it more RESTful](http://kirkbushell.me/angular-js-using-ng-resource-in-a-more-restful-manner/). This option adds PUT routes in addition to the POST routes for resource creation and update. ### relate Experimental feature that automatically populates references on create and removes them on delete. Default: `false` ### hooks Object containing registration hooks for `search`, `create`, `read`, `update`, and `delete` operations. [See below for full hooks documentation.](#hooks) ## Usage For each model, five endpoints are created that handle resource search, create, read, update and delete (SCRUD) functions. ### Search ```bash GET /{path}/{model}?{fields}{options} ``` The search route returns an array of resources that match the fields and values provided in the query parameters. For example: ```bash GET /api/posts?author=544bbbceecd047be03d0e0f7&__limit=1 ``` If no query parameters are present, it returns the entire data set. No results will be an empty array (`[]`). Options are passed in as query parameters in the format of `&__{option}={value}` in the query string, and unlock the power of MongoDB's `find()` API. Option | Description -------- | ------------- limit | Limits the result set count to the supplied value. skip | Number of records to skip (offset). distinct | Finds the distinct values for a specified field across the current collection. sort | Sorts the record according to provided [shorthand sort syntax](http://mongoosejs.com/docs/api.html#query_Query-sort) (e.g. `&__sort=-name`). populate | Populates object references with the full resource (e.g. `&__populate=users`). count | When present, returns the resulting count in an array (e.g. `[38]`). near | Performs a geospatial query on given coordinates and an optional range (in meters), sorted by distance by default. Required format: `{longitude},{latitude},{range}` **Meanify** also supports range queries. To perform a range query, pass in a stringified JSON object into the field on the request. ```bash GET /api/posts?createdAt={"$gt":"2013-01-01T00:00:00.000Z"} ``` Using `ngResource` in AngularJS, performing a range query is easy: ```javascript // Find posts created on or after 1/1/2013. Posts.query({ createdAt: JSON.stringify({ $gte: new Date('2013-01-01') }) }); ``` ### Create ```bash POST /{path}/{model} ``` Posting (or putting, if enabled) to the create route validates the incoming data and creates a new resource in the collection. Upon validation failure, a `400` error with details will be returned to the client. On success, a status code of `201` will be issued and the new resource will be returned. ### Read ```bash GET /{path}/{model}/{id} ``` The read path returns a single resource object in the collection that matches a given id. If the resource does not exist, a `404` is returned. ### Update ```bash POST /{path}/{model}/{id} ``` Posting (or putting, if enabled) to the update route will validate the incoming data and update the existing resource in the collection and respond with `204` if successful. Upon validation failure, a `400` error with details will be returned to the client. A `404` will be returned if the resource did not exist. ### Delete ```bash DELETE /{path}/{model}/{id} ``` Issuing a delete request to this route will result in the deletion of the resource and a `204` response if successful. If there was no resource, a `404` will be returned. ## Sub-documents [Mongoose sub-documents](http://mongoosejs.com/docs/subdocs.html) let you define schemas inside schemas, allowing for nested data structures that [can be validated](http://mongoosejs.com/docs/validation.html) and [hooked into via middleware](http://mongoosejs.com/docs/middleware.html). ```javascript var commentSchema = new Schema({ message: { type: String, required: true } }); var postSchema = new Schema({ title: { type: String, required: true }, author: { type: Schema.Types.ObjectId, ref: 'User', index: true }, comments: [ commentSchema ], createdAt: Date }); mongoose.model('Post', postSchema); ``` **Meanify** will detect sub-documents and expose SCRUD routes beneath the top-level models. ```bash meanify GET /api/posts/:id/comments +1ms meanify POST /api/posts/:id/comments +0ms meanify GET /api/posts/:id/comments/:commentsId +0ms meanify POST /api/posts/:id/comments/:commentsId +0ms meanify DELETE /api/posts/:id/comments/:commentsId +0ms ``` These routes work similar to the model SCRUD routes defined above, with the exception of the Search route. Currently, options are not supported so a `GET` to the sub-document collection will simply return the entire array set. The sub-document middleware is made available underneath the parent middleware. ```javascript app.post('/api/posts/:id/comments/:commentsId', meanify.posts.comments.update); ``` ## Validation **Meanify** will handle [validation described by your models](http://mongoosejs.com/docs/validation.html), as well as any [error handling defined](http://mongoosejs.com/docs/middleware.html) in your `pre` middleware hooks. ```javascript postSchema.path('type').validate(function (value) { return /article|review/i.test(value); }, 'InvalidType'); ``` The above custom validator example will return a validation error if a value other than "article" or "review" exists in the `type` field upon creation or update. Example response: ```javascript { message: 'Validation failed', name: 'ValidationError', errors: { type: { message: 'InvalidType', name: 'ValidatorError', path: 'type', type: 'user defined', value: 'poop' } } } } ``` Advanced validation for the create and update routes may be achieved using the `pre` hook, for example: ```javascript commentSchema.pre('save', function (next) { if (this.message.length <= 5) { var error = new Error(); error.name = 'ValidateLength' error.message = 'Comments must be longer than 5 characters.'; return next(error); } next(); }); ``` **Meanify** will return a `400` with the error object passed by your middleware. ```javascript { name: 'ValidateLength', message: 'Comments must be longer than 5 characters.' } ``` ## <a name="hooks"></a>Hooks Using hooks, you can access Express' `request`, `response` objects and `next` function for maximum control over the response to the client. This is especially useful for authorization. Hooks can be registered via `options` and passed into **meanify** on initialization. For example: ```javascript // Options Registration var meanify = require('../meanify')({ path: '/api', hooks: { // Include the Schema name registered in Mongoose. Post: { // Key should be `search`, `create`, // `read`, `update`, or `delete` read: function (req, res, done, next) { // Do some stuff. done(); }, delete: function (req, res, done, next) { // Do different stuff. done(); } } } }); app.use(meanify()); ``` Alternatively, you can add hooks via the middleware object created after initialization. For example: ```javascript // Dynamic Registration meanify.posts.hook('create', function (req, res, done, next) { var doc = this; if (doc.user === req.user._id) { // Proceed as normal. done(); } else if (badDog) { res.status(401).send({ name: 'Unauthorized', message: 'You can`t do that, Dave.' }); } else if (goodDog) { // Tell `meanify` to send a `400` response with this JSON body. done({ name: 'HookError', message: 'This is a completely custom error!' }); } else { // Bypass `meanify` and generate a `500` for Express to handle. // 500 error handled by Express next(err); } }); ``` **Meanify** exposes hooks for the `search`, `create`, `read`, `update` and `delete` operations on primary resources. Sub-documents are not currently supported. The `search` hook executes _after_ the results have been retrieved from the database, just _before_ sending to the client. This is useful for manipulating search results or tailoring them to users. `this` refers to the search results. The `create` hook takes place immediately _after_ receiving the request, right _before_ Mongoose attempts validation. `this` refers to the newly initialized Mongoose object (i.e. `Model.create(req.body)`). The `read` hook fires _after_ Mongoose successfully finds the resource, just _before_ returning the data to the client. `this` refers to the retrieved Mongoose resource. The `update` hook fires _after_ Mongoose successfully finds the resource to update, right _before_ it attempts validation to save it. `this` refers to the retrieved Mongoose resource with updated properties. The `delete` hook executes _after_ **meanify** successfully locates the resource to delete, right _before_ attempting to remove the document from the collection. `this` refers to the retrieved Mongoose resource to delete. Hook callbacks are invoked with the `req`, `res`, `done` and `next` arguments: * `req` - The Express `request` object. * `res` - The Express `response` object. * `done` - The **Meanify** callback. Sending with an object generates a `400` HTTP error and returns the object as JSON to the client. * `next` - The Express `next` function. Calling `next()` skips to subsequent middleware or `404` handler. Calling `next(err)` invokes Express' `500` error handler. ## Instance Methods [Instance methods](http://mongoosejs.com/docs/guide.html#methods) can also be defined on models in Mongoose and accessed via the **meanify** API routes. ```javascript postSchema.method('mymethod', function (req, res, done) { var error = null; var query = req.query; var body = req.body; if (query.foo) { // Do some stuff with query params. body.foo = query.foo; // Return new object in the response. done(error, body); } else { error = { name: 'NoFoo', message: 'Foo not found.' }; // Send error. done(error); } }); ``` Instance methods defined on schemas should expect three arguments: the `request` object, `response` object, and a `done` callback. If the `done` callback sees `null` as the first argument and the modified resource object second, a successful `200` response will be returned along with the resource object as the body. If the callback sees the error defined as the first argument, a failure (i.e. a `400`) response is returned. The `response` object gives you more control over the response and status code sent. For example, you could send a `401` or `403` response status (i.e. `res.status(401).send()`) instead of the standard `400` using the callback. Note that inside the schema method context, `this` refers to the resource document found in the database. **Note:** When naming your instance methods, choose something other than the [built-in instance methods in Mongoose](http://mongoosejs.com/docs/api.html#document-js). Methods are invoked by adding it after the `id` segment of the `POST` request. ```bash POST /{path}/{model}/{id}/{method}?foo=bar ``` The query parameters and body of the `POST` are passed to the instance method, returning a `200` status and resource body if successful, or a `400` status and error object if not. Custom instance methods are made available under the `meanify.update` property for greater control in your routes. Note that the `:id` parameter is required. ```javascript app.post('/api/posts/:id/mymethod', meanify.posts.update.mymethod); ``` **Note:** Instance methods are not supported on sub-documents. ## Roadmap * Nested sub-documents. * Sub-document instance methods. * Static methods. * Generation of AngularJS ngResource service via `/api/?ngResource` endpoint. * Examples and documentation on integration in AngularJS. ## Changelog ### 0.1.8 | 2/1/2016 * Hooks for `search`, `create`, `read`, `update`, and `delete` operations. ### 0.1.7 | 11/3/2015 * Support for Mongo's [distinct aggregation command](https://docs.mongodb.org/manual/reference/command/distinct/) via the `__distinct` option. ### 0.1.6 | 12/31/2014 * Instance method constructor supports `req`, `res`, `next` interface. ### 0.1.5 | 12/8/2014 * Generated routes and middleware for schema sub-documents. * Validation and error handling in middleware `pre` hooks. ### 0.1.4 | 12/7/2014 * Generated routes and middleware for model instance methods. ### 0.1.3 | 12/5/2014 * `exclude` option excludes models from router but retains middleware functions. ### 0.1.2 | 11/23/2014 * JSON object support in query parameters, enabling range queries. * `body-parser` middleware is bundled in **meanify** router. * Started unit test framework and added `.jshintrc`. ### 0.1.1 | 10/28/2014 * Basic example of a service using **meanify**. ### 0.1.0 | 10/28/2014 * Alpha release ready for publish to npm and testing.