resourcejs
Version:
A simple Express library to reflect Mongoose models to a REST interface.
1,075 lines (977 loc) • 32 kB
JavaScript
'use strict';
const paginate = require('node-paginate-anything');
const jsonpatch = require('fast-json-patch');
const mongodb = require('mongodb');
const moment = require('moment');
const parseRange = require('range-parser');
const debug = {
query: require('debug')('resourcejs:query'),
index: require('debug')('resourcejs:index'),
get: require('debug')('resourcejs:get'),
put: require('debug')('resourcejs:put'),
post: require('debug')('resourcejs:post'),
patch: require('debug')('resourcejs:patch'),
delete: require('debug')('resourcejs:delete'),
virtual: require('debug')('resourcejs:virtual'),
respond: require('debug')('resourcejs:respond'),
};
const utils = require('./utils');
class Resource {
constructor(app, route, modelName, model, options) {
this.app = app;
this.options = options || {};
if (this.options.convertIds === true) {
this.options.convertIds = /(^|\.)_id$/;
}
this.name = modelName.toLowerCase();
this.model = model;
this.modelName = modelName;
this.route = `${route}/${this.name}`;
this.methods = [];
this._swagger = null;
}
/**
* Maintain reverse compatibility.
*
* @param app
* @param method
* @param path
* @param callback
* @param last
* @param options
*/
register(app, method, path, callback, last, options) {
this.app = app;
return this._register(method, path, callback, last, options);
}
/**
* Add a stack processor to be able to execute the middleware independently of ExpressJS.
* Taken from https://github.com/randymized/composable-middleware/blob/master/lib/composable-middleware.js#L27
*
* @param stack
* @return {function(...[*]=)}
*/
stackProcessor(stack) {
return (req, res, done) => {
let layer = 0;
(function next(err) {
const fn = stack[layer++];
if (fn == null) {
done(err);
}
else {
if (err) {
switch (fn.length) {
case 4:
fn(err, req, res, next);
break;
case 2:
fn(err, next);
break;
default:
next(err);
break;
}
}
else {
switch (fn.length) {
case 3:
fn(req, res, next);
break;
case 1:
fn(next);
break;
default:
next();
break;
}
}
}
})();
};
}
/**
* Register a new callback but add before and after options to the middleware.
*
* @param method
* @param path
* @param callback
* @param last
* @param options
*/
_register(method, path, callback, last, options) {
let routeStack = [];
// The before middleware.
if (options && options.before) {
const before = options.before.map((m) => m.bind(this));
routeStack = [...routeStack, ...before];
}
routeStack = [...routeStack, callback.bind(this)];
// The after middleware.
if (options && options.after) {
const after = options.after.map((m) => m.bind(this));
routeStack = [...routeStack, ...after];
}
routeStack = [...routeStack, last.bind(this)];
// Add a fallback error handler.
const error = (err, req, res, next) => {
if (err) {
const status = err.status ? err.status : 400;
res.status(status).json({
status,
message: err.message || err,
});
}
else {
return next();
}
};
routeStack = [...routeStack, error.bind(this)]
// Declare the resourcejs object on the app.
if (!this.app.resourcejs) {
this.app.resourcejs = {};
}
if (!this.app.resourcejs[path]) {
this.app.resourcejs[path] = {};
}
// Add a stack processor so this stack can be executed independently of Express.
this.app.resourcejs[path][method] = this.stackProcessor(routeStack);
// Apply these callbacks to the application.
switch (method) {
case 'get':
this.app.get(path, routeStack);
break;
case 'post':
this.app.post(path, routeStack);
break;
case 'put':
this.app.put(path, routeStack);
break;
case 'patch':
this.app.patch(path, routeStack);
break;
case 'delete':
this.app.delete(path, routeStack);
break;
}
}
/**
* Sets the different responses and calls the next middleware for
* execution.
*
* @param res
* The response to send to the client.
* @param next
* The next middleware
*/
static respond(req, res, next) {
if (req.noResponse || res.headerSent || res.headersSent) {
debug.respond('Skipping');
return next();
}
if (res.resource) {
switch (res.resource.status) {
case 404:
res.status(404).json({
status: 404,
errors: ['Resource not found'],
});
break;
case 400:
case 500:
const errors = {};
for (let property in res.resource.error.errors) {
if (res.resource.error.errors.hasOwnProperty(property)) {
const error = res.resource.error.errors[property];
const { path, name, message } = error;
res.resource.error.errors[property] = { path, name, message };
}
}
res.status(res.resource.status).json({
status: res.resource.status,
message: res.resource.error.message,
errors: res.resource.error.errors,
});
break;
case 204:
// Convert 204 into 200, to preserve the empty result set.
// Update the empty response body based on request method type.
debug.respond(`204 -> ${req.__rMethod}`);
switch (req.__rMethod) {
case 'index':
res.status(200).json([]);
break;
default:
res.status(200).json({});
break;
}
break;
default:
res.status(res.resource.status).json(res.resource.item);
break;
}
}
next();
}
static ObjectId(id) {
try {
return (new mongodb.ObjectId(id));
}
catch (e) {
return id;
}
}
/**
* Sets the response that needs to be made and calls the next middleware for
* execution.
*
* @param res
* @param resource
* @param next
*/
static setResponse(res, resource, next) {
res.resource = resource;
next();
}
/**
* Returns the method options for a specific method to be executed.
* @param method
* @param options
* @returns {{}}
*/
static getMethodOptions(method, options) {
if (!options) {
options = {};
}
// If this is already converted to method options then return.
if (options.methodOptions) {
return options;
}
// Uppercase the method.
method = method.charAt(0).toUpperCase() + method.slice(1).toLowerCase();
const methodOptions = { methodOptions: true };
// Find all of the options that may have been passed to the rest method.
const beforeHandlers = options.before ?
(
Array.isArray(options.before) ? options.before : [options.before]
) :
[];
const beforeMethodHandlers = options[`before${method}`] ?
(
Array.isArray(options[`before${method}`]) ? options[`before${method}`] : [options[`before${method}`]]
) :
[];
methodOptions.before = [...beforeHandlers, ...beforeMethodHandlers];
const afterHandlers = options.after ?
(
Array.isArray(options.after) ? options.after : [options.after]
) :
[];
const afterMethodHandlers = options[`after${method}`] ?
(
Array.isArray(options[`after${method}`]) ? options[`after${method}`] : [options[`after${method}`]]
) :
[];
methodOptions.after = [...afterHandlers, ...afterMethodHandlers];
// Expose mongoose hooks for each method.
['before', 'after'].forEach((type) => {
const path = `hooks.${method.toString().toLowerCase()}.${type}`;
utils.set(
methodOptions,
path,
utils.get(options, path, (req, res, item, next) => next())
);
});
// Return the options for this method.
return methodOptions;
}
/**
* _register the whole REST api for this resource.
*
* @param options
* @returns {*|null|HttpPromise}
*/
rest(options) {
return this
.index(options)
.get(options)
.virtual(options)
.put(options)
.patch(options)
.post(options)
.delete(options);
}
/**
* Returns a query parameters fields.
*
* @param req
* @param name
* @returns {*}
*/
static getParamQuery(req, name) {
if (!Object.prototype.hasOwnProperty.call(req.query, name)) {
switch (name) {
case 'populate':
return '';
default:
return null;
}
}
if (name === 'populate' && utils.isObjectLike(req.query[name])) {
return req.query[name];
}
else {
const query = ( Array.isArray(req.query[name]) ? req.query[name].join(',') : req.query[name] );
// Generate string of spaced unique keys
return (query && typeof query === 'string') ? [...new Set(query.match(/[^, ]+/g))].join(' ') : null;
}
}
static getQueryValue(name, value, param, options, selector) {
if (selector && (selector === 'eq' || selector === 'ne') && (typeof value === 'string')) {
const lcValue = value.toLowerCase();
if (lcValue === 'null') {
return null;
}
if (lcValue === '"null"') {
return 'null';
}
if (lcValue === 'true') {
return true;
}
if (lcValue === '"true"') {
return 'true';
}
if (lcValue === 'false') {
return false;
}
if (lcValue === '"false"') {
return 'false';
}
}
if (param.instance === 'Number') {
return parseInt(value, 10);
}
if (param.instance === 'Date') {
const date = moment.utc(value, ['YYYY-MM-DD', 'YYYY-MM', 'YYYY', 'x', moment.ISO_8601], true);
if (date.isValid()) {
return date.toDate();
}
}
// If this is an ID, and the value is a string, convert to an ObjectId.
if (
options.convertIds &&
name.match(options.convertIds) &&
(typeof value === 'string') &&
(mongodb.ObjectId.isValid(value))
) {
try {
value = Resource.ObjectId(value);
}
catch (err) {
console.warn(`Invalid ObjectId: ${value}`);
}
}
return value;
}
/**
* Get the range of items from headers
*
* @param req
* @param size
* @returns {Object}
*/
static getRangeFromHeaders(req, size) {
if (!req.headers.range) {
return null;
}
const range = parseRange(size, req.headers.range);
if (range.type !== 'items') {
return null;
}
return range[0];
}
/**
* Get the find query for the index.
*
* @param req
* @returns {Object}
*/
getFindQuery(req, options, existing) {
const findQuery = {};
options = options || this.options;
// Get the filters and omit the limit, skip, select, sort and populate.
const {limit, skip, select, sort, populate, ...filters} = req.query;
// Sets the findQuery property.
const setFindQuery = function(name, value) {
// Ensure we do not override any existing query parameters.
if (!existing || !existing.hasOwnProperty(name)) {
findQuery[name] = value;
}
};
// Iterate through each filter.
Object.entries(filters).forEach(([name, value]) => {
// Get the filter object.
const filter = utils.zipObject(['name', 'selector'], name.split('__'));
// See if this parameter is defined in our model.
const param = this.model.schema.paths[filter.name.split('.')[0]];
if (param) {
// See if this selector is a regular expression.
if (filter.selector === 'regex') {
// Set the regular expression for the filter.
const parts = value.match(/\/?([^/]+)\/?([^/]+)?/);
let regex = null;
try {
regex = new RegExp(parts[1], (parts[2] || 'i'));
}
catch (err) {
debug.query(err);
regex = null;
}
if (regex) {
setFindQuery(filter.name, regex);
}
return;
} // See if there is a selector.
else if (filter.selector) {
var filterQuery = findQuery[filter.name];
// Init the filter.
if (!filterQuery) {
filterQuery = {};
}
if (filter.selector === 'exists') {
value = ((value === 'true') || (value === '1')) ? true : value;
value = ((value === 'false') || (value === '0')) ? false : value;
value = !!value;
}
// Special case for in filter with multiple values.
else if (['in', 'nin', 'all'].includes(filter.selector)) {
value = Array.isArray(value) ? value : value.split(',');
value = value.map((item) => Resource.getQueryValue(filter.name, item, param, options, filter.selector));
}
else {
// Set the selector for this filter name.
value = Resource.getQueryValue(filter.name, value, param, options, filter.selector);
}
filterQuery[`$${filter.selector}`] = value;
setFindQuery(filter.name, filterQuery);
return;
}
else {
// Set the find query to this value.
value = Resource.getQueryValue(filter.name, value, param, options, filter.selector);
setFindQuery(filter.name, value);
return;
}
}
if (!options.queryFilter) {
// Set the find query to this value.
setFindQuery(filter.name, value);
}
});
// Return the findQuery.
return findQuery;
}
countQuery(query, pipeline) {
// We cannot use aggregation if mongoose special options are used... like populate.
if (!utils.isEmpty(query._mongooseOptions) || !pipeline) {
return query;
}
const stages = [
{ $match: query.getQuery() },
...pipeline,
{
$group: {
_id : null,
count : { $sum : 1 },
},
},
];
return {
async countDocuments() {
const items = await query.model.aggregate(stages).exec()
return items.length ? items[0].count : 0;
},
};
}
indexQuery(query, pipeline) {
// We cannot use aggregation if mongoose special options are used... like populate.
if (!utils.isEmpty(query._mongooseOptions) || !pipeline) {
return query.lean();
}
const stages = [
{ $match: query.getQuery() },
...pipeline,
];
if (query.options && query.options.sort && !utils.isEmpty(query.options.sort)) {
stages.push({ $sort: query.options.sort });
}
if (query.options && query.options.skip) {
stages.push({ $skip: query.options.skip });
}
if (query.options && query.options.limit) {
stages.push({ $limit: query.options.limit });
}
if (!utils.isEmpty(query._fields)) {
stages.push({ $project: query._fields });
}
return query.model.aggregate(stages);
}
/**
* The index for a resource.
*
* @param options
*/
index(options) {
options = Resource.getMethodOptions('index', options);
this.methods.push('index');
this._register('get', this.route, async (req, res, next) => {
// Store the internal method for response manipulation.
req.__rMethod = 'index';
// Allow before handlers the ability to disable resource CRUD.
if (req.skipResource) {
debug.index('Skipping Resource');
return next();
}
// Get the query object.
let countQuery = req.countQuery || req.modelQuery || req.model || this.model;
const query = req.modelQuery || req.model || this.model;
// Make sure to clone the count query if it is available.
if (typeof countQuery.clone === 'function') {
countQuery = countQuery.clone();
}
// Get the find query.
const findQuery = this.getFindQuery(req, null, query._conditions);
// First get the total count.
try {
const count = await this.countQuery( countQuery.find(findQuery), query.pipeline).countDocuments();
// Get the default limit.
const defaults = { limit: 10, skip: 0 };
const range = Resource.getRangeFromHeaders(req, count);
if (range) {
req.query.limit = req.query.limit || (range.end - range.start + 1);
req.query.skip = req.query.skip || range.start;
// Delete Range header to recreate it below for 'node-paginate-anything' compatibility
delete req.headers.range;
}
let { limit, skip } = req.query
limit = parseInt(limit, 10)
limit = (isNaN(limit) || (limit < 0)) ? defaults.limit : limit
skip = parseInt(skip, 10)
skip = (isNaN(skip) || (skip < 0)) ? defaults.skip : skip
const reqQuery = { limit, skip };
// If a skip is provided, then set the range headers.
if (reqQuery.skip && !req.headers.range) {
req.headers['range-unit'] = 'items';
req.headers.range = `${reqQuery.skip}-${reqQuery.skip + (reqQuery.limit - 1)}`;
}
// Get the page range.
const pageRange = paginate(req, res, count, reqQuery.limit) || {
limit: reqQuery.limit,
skip: reqQuery.skip,
};
// Make sure that if there is a range provided in the headers, it takes precedence.
if (req.headers.range) {
reqQuery.limit = pageRange.limit;
reqQuery.skip = pageRange.skip;
}
// Next get the items within the index.
const queryExec = query
.find(findQuery)
.limit(reqQuery.limit)
.skip(reqQuery.skip)
.select(Resource.getParamQuery(req, 'select'))
.sort(Resource.getParamQuery(req, 'sort'));
// Only call populate if they provide a populate query.
const populate = Resource.getParamQuery(req, 'populate');
if (populate) {
debug.index(`Populate: ${populate}`);
queryExec.populate(populate);
}
options.hooks.index.before.call(
this,
req,
res,
findQuery,
async () => {
try {
const items = await this.indexQuery(queryExec, query.pipeline).exec();
debug.index(items);
options.hooks.index.after.call(
this,
req,
res,
items,
Resource.setResponse.bind(Resource, res, { status: res.statusCode, item: items }, next)
);
}
catch (err) {
debug.index(err);
debug.index(err.name);
if (err.name === 'CastError' && populate) {
err.message = `Cannot populate "${populate}" as it is not a reference in this resource`;
debug.index(err.message);
}
return Resource.setResponse(res, { status: 400, error: err }, next);
}
}
);
}
catch (err) {
debug.index(err);
return Resource.setResponse(res, { status: 400, error: err }, next);
}
}, Resource.respond, options);
return this;
}
/**
* Register the GET method for this resource.
*/
get(options) {
options = Resource.getMethodOptions('get', options);
this.methods.push('get');
this._register('get', `${this.route}/:${this.name}Id`, (req, res, next) => {
// Store the internal method for response manipulation.
req.__rMethod = 'get';
if (req.skipResource) {
debug.get('Skipping Resource');
return next();
}
const query = (req.modelQuery || req.model || this.model).findOne();
const search = { '_id': req.params[`${this.name}Id`] };
// Only call populate if they provide a populate query.
const populate = Resource.getParamQuery(req, 'populate');
if (populate) {
debug.get(`Populate: ${populate}`);
query.populate(populate);
}
options.hooks.get.before.call(
this,
req,
res,
search,
async () => {
try {
let item = await query.where(search).lean().exec()
if (!item) return Resource.setResponse(res, { status: 404 }, next);
return options.hooks.get.after.call(
this,
req,
res,
item,
() => {
// Allow them to only return specified fields.
const select = Resource.getParamQuery(req, 'select');
if (select) {
const newItem = {};
// Always include the _id.
if (item._id) {
newItem._id = item._id;
}
select.split(' ').map(key => {
key = key.trim();
if (item.hasOwnProperty(key)) {
newItem[key] = item[key];
}
});
item = newItem;
}
Resource.setResponse(res, { status: 200, item: item }, next)
}
);
}
catch (err) {
return Resource.setResponse(res, { status: 400, error: err }, next);
}
}
);
}, Resource.respond, options);
return this;
}
/**
* Virtual (GET) method. Returns a user-defined projection (typically an aggregate result)
* derived from this resource
* The virtual method expects at least the path and the before option params to be set.
*/
virtual(options) {
if (!options || !options.path || !options.before) return this;
const path = options.path;
options = Resource.getMethodOptions('virtual', options);
this.methods.push(`virtual/${path}`);
this._register('get', `${this.route}/virtual/${path}`, async (req, res, next) => {
// Store the internal method for response manipulation.
req.__rMethod = 'virtual';
if (req.skipResource) {
debug.virtual('Skipping Resource');
return next();
}
const query = req.modelQuery || req.model;
if (!query) return Resource.setResponse(res, { status: 404 }, next);
try {
const item = await query.exec()
if (!item) return Resource.setResponse(res, { status: 404 }, next);
return Resource.setResponse(res, { status: 200, item }, next);
}
catch (err) {
return Resource.setResponse(res, { status: 400, error: err }, next)
}
}, Resource.respond, options);
return this;
}
/**
* Post (Create) a new item
*/
post(options) {
options = Resource.getMethodOptions('post', options);
this.methods.push('post');
this._register('post', this.route, (req, res, next) => {
// Store the internal method for response manipulation.
req.__rMethod = 'post';
if (req.skipResource) {
debug.post('Skipping Resource');
return next();
}
const Model = req.model || this.model;
const model = new Model(req.body);
options.hooks.post.before.call(
this,
req,
res,
req.body,
async () => {
try {
const writeOptions = req.writeOptions || {};
const item = await model.save(writeOptions)
debug.post(item);
// Trigger any after hooks before responding.
return options.hooks.post.after.call(
this,
req,
res,
item,
Resource.setResponse.bind(Resource, res, { status: 201, item }, next)
);
}
catch (err) {
debug.post(err);
return Resource.setResponse(res, { status: 400, error: err }, next);
}
}
);
}, Resource.respond, options);
return this;
}
/**
* Put (Update) a resource.
*/
put(options) {
options = Resource.getMethodOptions('put', options);
this.methods.push('put');
this._register('put', `${this.route}/:${this.name}Id`, async (req, res, next) => {
// Store the internal method for response manipulation.
req.__rMethod = 'put';
if (req.skipResource) {
debug.put('Skipping Resource');
return next();
}
// Remove __v field
const { __v, ...update} = req.body;
const query = req.modelQuery || req.model || this.model;
try {
const item = await query.findOne({ _id: Resource.ObjectId(req.params[`${this.name}Id`]) })
if (!item) {
debug.put(`No ${this.name} found with ${this.name}Id: ${req.params[`${this.name}Id`]}`);
return Resource.setResponse(res, { status: 404 }, next);
}
item.set(update);
options.hooks.put.before.call(
this,
req,
res,
item,
async () => {
const writeOptions = req.writeOptions || {};
try {
const savedItem = await item.save(writeOptions);
return options.hooks.put.after.call(
this,
req,
res,
savedItem,
Resource.setResponse.bind(Resource, res, { status: 200, item: savedItem }, next)
);
}
catch (err) {
debug.put(err);
return Resource.setResponse(res, { status: 400, error: err }, next);
}
});
}
catch (err) {
debug.put(err);
return Resource.setResponse(res, { status: 400, error: err }, next);
}
}, Resource.respond, options);
return this;
}
/**
* Patch (Partial Update) a resource.
*/
patch(options) {
options = Resource.getMethodOptions('patch', options);
this.methods.push('patch');
this._register('patch', `${this.route}/:${this.name}Id`, async (req, res, next) => {
// Store the internal method for response manipulation.
req.__rMethod = 'patch';
if (req.skipResource) {
debug.patch('Skipping Resource');
return next();
}
const query = req.modelQuery || req.model || this.model;
const writeOptions = req.writeOptions || {};
try {
const item = await query.findOne({ '_id': req.params[`${this.name}Id`] });
if (!item) return Resource.setResponse(res, { status: 404, error: err }, next);
// Ensure patches is an array
const patches = [].concat(req.body);
let patchFail = null;
try {
patches.forEach((patch) => {
if (patch.op === 'test') {
patchFail = patch;
const success = jsonpatch.applyOperation(item, patch, true);
if (!success || !success.test) {
return Resource.setResponse(res, {
status: 412,
name: 'Precondition Failed',
message: 'A json-patch test op has failed. No changes have been applied to the document',
item,
patch,
}, next);
}
}
});
jsonpatch.applyPatch(item, patches, true);
}
catch (err) {
switch (err.name) {
// Check whether JSON PATCH error
case 'TEST_OPERATION_FAILED':
return Resource.setResponse(res, {
status: 412,
name: 'Precondition Failed',
message: 'A json-patch test op has failed. No changes have been applied to the document',
item,
patch: patchFail,
}, next);
case 'SEQUENCE_NOT_AN_ARRAY':
case 'OPERATION_NOT_AN_OBJECT':
case 'OPERATION_OP_INVALID':
case 'OPERATION_PATH_INVALID':
case 'OPERATION_FROM_REQUIRED':
case 'OPERATION_VALUE_REQUIRED':
case 'OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED':
case 'OPERATION_PATH_CANNOT_ADD':
case 'OPERATION_PATH_UNRESOLVABLE':
case 'OPERATION_FROM_UNRESOLVABLE':
case 'OPERATION_PATH_ILLEGAL_ARRAY_INDEX':
case 'OPERATION_VALUE_OUT_OF_BOUNDS':
err.errors = [{
name: err.name,
message: err.toString(),
}];
return Resource.setResponse(res, {
status: 400,
item,
error: err,
}, next);
// Something else than JSON PATCH
default:
return Resource.setResponse(res, { status: 400, item, error: err }, next);
}
}
try {
const savedItem = await item.save(writeOptions);
return Resource.setResponse(res, { status: 200, item: savedItem }, next);
}
catch (err) {
return Resource.setResponse(res, { status: 400, error: err }, next);
}
}
catch(err) {
return Resource.setResponse(res, { status: 400, error: err }, next)
}
}, Resource.respond, options);
return this;
}
/**
* Delete a resource.
*/
delete(options) {
options = Resource.getMethodOptions('delete', options);
this.methods.push('delete');
this._register('delete', `${this.route}/:${this.name}Id`, async (req, res, next) => {
// Store the internal method for response manipulation.
req.__rMethod = 'delete';
if (req.skipResource) {
debug.delete('Skipping Resource');
return next();
}
const query = req.modelQuery || req.model || this.model;
try {
const item = await query.findOne({ '_id': req.params[`${this.name}Id`] });
if (!item) {
debug.delete(`No ${this.name} found with ${this.name}Id: ${req.params[`${this.name}Id`]}`);
return Resource.setResponse(res, { status: 404 }, next);
}
if (req.skipDelete) {
return Resource.setResponse(res, { status: 204, item, deleted: true }, next);
}
options.hooks.delete.before.call(
this,
req,
res,
item,
async () => {
const writeOptions = req.writeOptions || {};
try {
await item.deleteOne(writeOptions);
debug.delete(item);
options.hooks.delete.after.call(
this,
req,
res,
item,
Resource.setResponse.bind(Resource, res, { status: 204, item, deleted: true }, next)
);
}
catch (err) {
debug.delete(err);
return Resource.setResponse(res, { status: 400, error: err }, next);
}
}
);
}
catch (err) {
debug.delete(err);
return Resource.setResponse(res, { status: 400, error: err }, next);
}
}, Resource.respond, options);
return this;
}
/**
* Returns the swagger definition for this resource.
*/
swagger(resetCache) {
resetCache = resetCache || false;
if (!this.__swagger || resetCache) {
this.__swagger = require('./Swagger')(this);
}
return this.__swagger;
}
}
// Make sure to create a new instance of the Resource class.
function ResourceFactory(app, route, modelName, model, options) {
return new Resource(app, route, modelName, model, options);
}
ResourceFactory.Resource = Resource;
module.exports = ResourceFactory;