Define a translation "schema" between mongoose models and API resources. Once you do this, you can:
- Call methods to convert your models to resources and resources to models.
- Generate express middleware to handle GET, POST, PUT, and DELETE requests for that resource.
- Why ResourceSchema?
- Install
- Creating a Resource
- Defining a Schema
- Options
- Converting With Methods
- Generating Middleware
- Query Parameters
- Contributing
- Code of Conduct
- License
ResourceSchema abstracts a lot of the boilerplate when creating API endpoints for RESTful resources. It helps you:
- translate models to and from their corresponding resource representation
- validate values
- handle for malformed data
- automatically convert url query parameters to their corresponding mongoose query parameters.
All of which allows you to focus on higher-level resource design.
Create a schema translation:
Product = require './models/product'
var schema = {
'_id': '_id',
// Get resource field 'name' from model field 'name'
// Convert the name to lowercase whenever saved
'name': {
field: 'name',
set: function (productResource) { return productResource.name.toLowerCase(); }
},
// make sure the day matches the specified format before saving
'day': {
field: 'day',
match: /[0-9]{4}-[0-9]{2}-[0-9]{2}/
},
// Model field 'active' renamed to resource field 'isActive'
'isActive': 'active',
// Dynamically get field 'code' whenever the resource is requested:
'code': {
get: function (productModel) { productModel.letter + productModel.number }
},
// Dynamically get totalQuantitySold whenever the resource is requested.
// Resolve 'totalQuantitySoldByProductId' before applying the getter.
'totalQuantitySold': {
resolve: {
totalQuantitySoldByProductId: function ({models}, done) {
getTotalQuantitySoldById(models, done)
}
},
get: function (productModel, {totalQuantitySoldByProductId}) {
totalQuantitySoldByProductId[productModel._id]
}
}
// field soldOn allows you to query for products sold on the specified days
// e.g. api/products?soldOn=2014-10-01&soldOn=2014-10-05
'soldOn': {
type: String,
isArray: true,
match: /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/,
find: function (days) { return { 'day': $in: days } }
},
// field fromLastWeek allows you to query for products sold in the last week
// e.g. api/products?fromLastWeek=true
'fromLastWeek': {
type: Boolean,
find: (days) -> { 'day': $gt: '2014-10-12' }
}
};
module.exports = new ResourceSchema(Product, schema);
Then generate middleware to automatically handle requests for the resource:
resourceConverter = require './resource-converter'
// generate express middleware that automatically handles GET, POST, PUT, and DELETE requests:
app.get('/products', resourceConverter.get(), resourceConverter.send);
app.post('/products', resourceConverter.post(), resourceConverter.send);
app.put('products/:_id', resourceConverter.put('_id'), resourceConverter.send);
app.get('products/:_id', resourceConverter.get('_id'), resourceConverter.send);
app.delete('products/:_id', resourceConverter.delete('_id'), resourceConverter.send);
npm install resource-schema --save
- model - mongoose model to generate the resource from
- [schema] - optional object to configure custom resource fields. If no schema is provided, the resource schema is automatically generated from the model schema.
- [options] - optional object to configure schema options, like document limits and default mongoose queries.
The schema allows define the shape of your resource. If you do not provide a schema, the resource will look exactly like the model.
We can define the schema using these properties:
- field - string that maps a resource field to a mongoose model field.
- get - function that dynamically gets the value whenever a resource is requested.
- set - function that dynamically sets the value whenever a resource is PUT or POSTed
- resolve - function for getting async data needed to build resource
- find - function that dynamically builds a mongoose query whenever querying by this field
- findAsync - TODO asynchronous version of find
- optional - do not include this field in the resource unless specifically requested with the $add query parameter
- validate - function that validates the field before saving or updating
- match - regexp to validate field before saving
- type - convert the type of the field before saving/querying. This is especially for converting query parameters, which default to a string.
- isArray - convert value to array before saving/querying. This is especially for converting query parameters, which will not be an array of only querying by on value.
Maps a resource field to a mongoose model field.
schema = {
'name': { field: 'name' }
}
We can also define this with a shorthand notation:
schema = {
'name': 'name'
}
Or even simpler with coffeescript:
schema = {
'name'
}
Note, this can be used to rename a model field to a new field on the resource:
schema = {
'category.name': 'categoryName'
}
// => {
// category: {
// name: 'value'
// }
// }
- model - corresponding POJO model for requested resource (use the
fat: true
option on the ResourceSchema to get a mongoose model) - context - object containing req, res, next, and resolved values (see "resolve" for details)
Dynamically get the value whenever a resource is requested.
var schema = {
'fullName': {
get: function (resource, context) {
resource.firstName + ' ' + resource.lastName
}
}
}
- resource - resource saved by client
- context - object containing req, res, next, and resolved values (see "resolve" for details)
Function that dynamically sets the value whenever a resource is saved or updated.
var schema = {
'name': {
set: function (resource, context) {
return resource.name.toLowerCase()
}
}
}
Key value object where the key is the name of the variable to resolve, and the value is an asynchronous function that returns the value. The function accepts one argument:
- context - object containing req, res, next, models, and resources associated with this request
Once a variable is resolved, it is attached to the "context" object, and is available to all the getters and setters for that field.
var schema = {
'note': {
resolve:
userNoteByUserId: function(context, done) {
var userIds = context.models.map(function(user) { return user._id });
UserNote.find({userId: $in: userIds}).then(function(notes) {
var userNoteByUserId = _(notes).indexBy('userId');
done(null, userNoteByUserId);
});
})
},
// userNoteByUserId now available on context object
get: function (user, context) {
var userNoteByUserId = context.userNoteByUserId;
return userNoteByUserId[user._id];
}
}
}
- value - value of query parameter from client
- context - object containing req, res, next, and resolved values (see "resolve" for details)
Function that dynamically builds a mongoose query whenever querying by this field. Return an object that will extend the mongoose query.
var schema = {
'soldOn': {
find: function (days, context) {
return { 'day': $in: days }
}
}
}
If true, do not include this field in the resource unless specifically requested with the $add query parameter
// GET /api/products?$add=name
var schema = {
'name': {
optional: true,
field: 'name'
}
}
- value - value of query parameter from client, or value on object
Return a 400 invalid request if the provided value does not pass the validation test.
var schema = {
'date': {
validate: function(value) {
return /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/.test(value)
}
}
}
Return a 400 invalid request if the provided value does match the given regular expression.
var schema = {
'date': {
match: /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/
}
}
Convert the type of the value.
Valid types include
- String
- Date
- Number
- Boolean
- and any other "newable" class
schema = {
'active': {
type: Boolean
}
}
This is especially useful for query parameters, which are a string by default
Convert value to array before saving/querying. This is especially for converting query parameters, which will not be an array if only querying by on value.
schema = {
'daysToSelect': {
isArray: true
find: function(days, context) { ... }
}
}
Options allow you to make configurations for the entire resource.
- models - all models found from the query
Filter limits resources returned from every GET request.
new ResourceSchema(Model, schema, {
filter: function(models) {
models.filter(function(model) {
return model.isActive
})
}
})
Limit the number of returned documents for GET requests. Defaults to 1000. 0 signifies unlimited.
new ResourceSchema(Model, schema, {
limit: 100
})
- context - object containing req, res, next, and resolved values (see "resolve" for details)
Function returns an object that will be used at as starting point to build every query.
new ResourceSchema(Product, schema, {
find: function(context) {
active: true,
createdAt: $gt: '2013-01-01'
}
})
Like resolve on schema, but resolved variable available to every getter and setter on the resource.
Define query parameters for this resource. Note, you could define these directly on the schema, but some people prefer to separate query parameters from all other fields.
new ResourceSchema(Product, schema, {
queryParams: {
'soldOn': {
type: String,
isArray: true,
match: /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/,
find: function(days) {
return { 'day': $in: days }
}
},
'fromLastWeek': {
type: Boolean,
find: function(days) {
return { 'day': $gt: '2014-10-12' }
}
}
}
})
Convert from resource representation to model representation
Convert from multiple resource representations to their corresponding model representations
Convert from model representation to resource representation
Convert from multiple model representations to their corresponding resource representations
Once you've defined a new resource, call .get(), .post(), .put(), or .delete() to generate the appropriate middleware to handle the request.
var resource = new ResourceSchema(Model, schema, options);
app.get('/products', resource.get(), function(req, res, next) {
# resources on res.body
});
The middleware will attach the resources to res.body, which can be used by other middleware, or sent immediately back to the client.
Handle bulk GET requests. Results can by filtered by query parameters. Limits response to 1000 resources by default.
var resource = new ResourceSchema(Model, schema, options);
app.get('/products', resource.get(), function(req, res, next) {
// resources on res.body
});
// GET /products?name=magicbox
- idField - field to use as resource identifier. Note, this must match the field name defined on the resource and the name on req.params.
Handle GET requests for single resource.
var resource = new ResourceSchema(Model, schema, options);
app.get('/products/:_id', resource.get('_id'), function(req, res, next) {
// resources on res.body
});
// GET /products/1234
// => {
// _id: 1234
// name: 'banana bread'
// }
Handle POST requests. Can take a single resource or an array of resources.
var resource = new ResourceSchema(Model, schema, options);
app.post('/products', resource.post(), function(req, res, next) {
// resources on res.body
});
// POST /products
// {
// _id: 1234
// name: 'banana bread'
// }
//
// or
//
// POST /products
// [
// {
// _id: 1234
// name: 'banana bread'
// },
// {
// _id: 4567
// name: 'apples'
// }
// ]
- idField - field to use as resource identifier. Note, this must match the field name defined on the resource and the name on req.params.
Generate middleware to handle PUT requests to a resource. This does an upsert, so if the resource does not exist, it will create one.
This will handle bulk PUT requests as well, automatically reading the idField and upserting for each resource.
var resource = new ResourceSchema(Model, schema, options);
app.put('/products/:_id', resource.put('_id'), function(req, res, next) {
// resources on res.body
});
// PUT /products/1234
// {
// _id: 1234
// name: 'banana bread'
// }
//
// or
//
// PUT /products
// [
// {
// _id: 1234
// name: 'banana bread'
// },
// {
// _id: 4567
// name: 'apples'
// }
// ]
- idField - field to use as resource identifier. Note, this must match the field name defined on the resource and the name on req.params.
Generate middleware to handle DELETE requests to a single resource.
var resource = new ResourceSchema(Model, schema, options);
app.delete('/products/:_id', resource.delete('_id'), function(req, res, next) {
// resources on res.body
});
// DELETE /products/1234
Convenience method for sending the resources on res.body back to the client.
var resource = new ResourceSchema(Model, schema, options);
app.get('/products', resource.get(), resource.send);
ResourceSchema allows you to use a variety of query parameters to interact with your resources.
Select fields to return on the resource. Similar to mongoose select.
GET /products?$select=name&$select=active
GET /products?$select[]=name&$select[]=active
GET /products?$select=name active
Limit the number of resources to return in the response
GET /products?$limit=10
Skip number of documents
GET /products?$skip=5
Sort the returned documents
GET /products?$sort=name&$sort=-price
Count total number of resources that would be available for this query if results were not limited. The result is added in the response header as 'x-resource-count'. This is useful for calculating total number of pages when paginating.
GET /products?$addResourceCount=true
Add an optional field to the resource. See optional schema field for more details.
GET /products?$add=quantitySold
You can query by any resource field with a 'field', 'find', or 'filter' attribute.
GET /products?name=strawberry
If the querying against one with a 'field' attribute, it will automatically perform an $in query.
GET /products?name=strawberry&name=apple
=>
Product.find({ 'name': $in: ['strawberry', 'apple'] })
Note that you can query nested fields with Express' [bracket] notation.
GET /products?categrory[name]=fruit
$ git clone https://github.com/goodeggs/resource-schema && cd resource-schema
$ npm install
$ npm test
Code of Conduct for contributing to or participating in this project.